bep40 commited on
Commit
c146956
·
verified ·
1 Parent(s): a8d1356

Restore commit 30b624b1 - batch 1/171

Browse files
Files changed (37) hide show
  1. 404.html +13 -13
  2. ai-search-fix.js +331 -0
  3. ai-search.js +647 -0
  4. cart-fix.js +190 -0
  5. catalogue_ocr.json +1 -0
  6. catalogues.json +1 -0
  7. comprehensive-fix.js +386 -0
  8. discount-formula-fix.js +155 -0
  9. grob-img-fix.js +76 -0
  10. grob-price-fix.js +68 -0
  11. hafele-eurogold-img-fix.js +207 -0
  12. img-fix.js +4 -0
  13. order-store.js +377 -0
  14. product_seo_min.json +0 -0
  15. products_with_slugs.json +3 -0
  16. qr-payment.js +353 -0
  17. quote-ui.js +163 -0
  18. san-pham/02-03-grob/index.html +20 -0
  19. san-pham/02-45-grob/index.html +20 -0
  20. san-pham/02-50-grob/index.html +20 -0
  21. san-pham/02-60-grob/index.html +20 -0
  22. san-pham/02-70-grob/index.html +20 -0
  23. san-pham/02-80-grob/index.html +20 -0
  24. san-pham/02-90-grob/index.html +20 -0
  25. san-pham/1044p-malloca-chau-rua-chen-1-ho-inox-304-kich-thuoc-44cm-lap-am-hoac-duong-2/index.html +20 -0
  26. san-pham/28cc304-grob-ma-hang-28cc304/index.html +20 -0
  27. san-pham/28cf304-grob-phu-kien-grob/index.html +20 -0
  28. san-pham/30l2304-grob-ma-hang-28cc304/index.html +20 -0
  29. san-pham/30lh304-grob-ma-hang-28cc304/index.html +20 -0
  30. san-pham/400-51-122-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-122-40051122/index.html +20 -0
  31. san-pham/400-51-123-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-123-40051123/index.html +20 -0
  32. san-pham/401-30-000-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-000-40130000/index.html +20 -0
  33. san-pham/401-30-001-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-001-40130001/index.html +20 -0
  34. san-pham/401-30-002-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-002-40130002/index.html +20 -0
  35. san-pham/401-30-003-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-003-40130003/index.html +20 -0
  36. san-pham/401-30-004-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-004-40130004/index.html +20 -0
  37. san-pham/401-30-005-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-005-40130005/index.html +20 -0
404.html CHANGED
@@ -84,24 +84,24 @@
84
 
85
  if (isBot) {
86
  var html = '<!DOCTYPE html><html lang="vi"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
87
- + '<title>' + title.replace(/"/g,'"') + '</title>'
88
- + '<meta property="og:title" content="' + title.replace(/"/g,'"') + '">'
89
- + '<meta property="og:description" content="' + ogDesc.replace(/"/g,'"') + '">'
90
- + '<meta property="og:image" content="' + image.replace(/"/g,'"') + '">'
91
- + '<meta property="og:url" content="' + ogUrl.replace(/"/g,'"') + '">'
92
  + (price > 0 ? '<meta property="product:price:amount" content="' + price + '"><meta property="product:price:currency" content="VND">' : '')
93
  + '<meta name="twitter:card" content="summary_large_image">'
94
- + '<meta name="twitter:title" content="' + title.replace(/"/g,'"') + '">'
95
- + '<meta name="twitter:description" content="' + ogDesc.replace(/"/g,'"') + '">'
96
- + '<meta name="twitter:image" content="' + image.replace(/"/g,'"') + '">'
97
- + '<link rel="canonical" href="' + ogUrl.replace(/"/g,'"') + '">'
98
  + '<style>body{font-family:sans-serif;max-width:700px;margin:40px auto;padding:20px;color:#333}h1{font-size:1.5em;color:#003f62}.price{font-size:1.3em;color:#003f62;font-weight:bold}img{max-width:100%;max-height:400px;margin:16px 0}.btn{display:inline-block;background:#003f62;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold;margin-top:8px}.meta{color:#666;font-size:.85em}</style>'
99
  + '</head><body>'
100
- + '<h1>' + name.replace(/"/g,'"') + '</h1>'
101
- + (sku ? '<p class="meta">Mã: ' + sku.replace(/"/g,'"') + '</p>' : '')
102
  + (priceStr ? '<p class="price">' + priceStr + '</p>' : '')
103
- + '<img src="' + image.replace(/"/g,'"') + '" alt="' + name.replace(/"/g,'"') + '">'
104
- + '<p>' + desc.replace(/"/g,'"') + '</p>'
105
  + '<a class="btn" href="' + SITE + '/?product=' + encodeURIComponent(slug) + '">Xem chi tiết tại V.AI STUDIO</a>'
106
  + '</body></html>';
107
  document.open();
 
84
 
85
  if (isBot) {
86
  var html = '<!DOCTYPE html><html lang="vi"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
87
+ + '<title>' + title.replace(/"/g,'&quot;') + '</title>'
88
+ + '<meta property="og:title" content="' + title.replace(/"/g,'&quot;') + '">'
89
+ + '<meta property="og:description" content="' + ogDesc.replace(/"/g,'&quot;') + '">'
90
+ + '<meta property="og:image" content="' + image.replace(/"/g,'&quot;') + '">'
91
+ + '<meta property="og:url" content="' + ogUrl.replace(/"/g,'&quot;') + '">'
92
  + (price > 0 ? '<meta property="product:price:amount" content="' + price + '"><meta property="product:price:currency" content="VND">' : '')
93
  + '<meta name="twitter:card" content="summary_large_image">'
94
+ + '<meta name="twitter:title" content="' + title.replace(/"/g,'&quot;') + '">'
95
+ + '<meta name="twitter:description" content="' + ogDesc.replace(/"/g,'&quot;') + '">'
96
+ + '<meta name="twitter:image" content="' + image.replace(/"/g,'&quot;') + '">'
97
+ + '<link rel="canonical" href="' + ogUrl.replace(/"/g,'&quot;') + '">'
98
  + '<style>body{font-family:sans-serif;max-width:700px;margin:40px auto;padding:20px;color:#333}h1{font-size:1.5em;color:#003f62}.price{font-size:1.3em;color:#003f62;font-weight:bold}img{max-width:100%;max-height:400px;margin:16px 0}.btn{display:inline-block;background:#003f62;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold;margin-top:8px}.meta{color:#666;font-size:.85em}</style>'
99
  + '</head><body>'
100
+ + '<h1>' + name.replace(/"/g,'&quot;') + '</h1>'
101
+ + (sku ? '<p class="meta">Mã: ' + sku.replace(/"/g,'&quot;') + '</p>' : '')
102
  + (priceStr ? '<p class="price">' + priceStr + '</p>' : '')
103
+ + '<img src="' + image.replace(/"/g,'&quot;') + '" alt="' + name.replace(/"/g,'&quot;') + '">'
104
+ + '<p>' + desc.replace(/"/g,'&quot;') + '</p>'
105
  + '<a class="btn" href="' + SITE + '/?product=' + encodeURIComponent(slug) + '">Xem chi tiết tại V.AI STUDIO</a>'
106
  + '</body></html>';
107
  document.open();
ai-search-fix.js ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * FIX: AI Search Button - doAISearch()
3
+ * =====================================
4
+ * Root cause: doAISearch() in index.html depended on window._vaiSearchContext
5
+ * from search-plus-boot.js?v=27 which DOES NOT EXIST in the repo (404).
6
+ *
7
+ * Fix: Rewrite doAISearch() to use window.aiSearch() from ai-search.js which is
8
+ * properly loaded and contains full NL search logic.
9
+ *
10
+ * Additional fix: Handle multi-product search (comma/semicolon separated codes).
11
+ * Critical fix: All references to D must use window.D since let D in index.html
12
+ * does NOT create a global property on window object.
13
+ *
14
+ * FIXED: Added "Thêm giỏ" button to AI search results
15
+ */
16
+
17
+ window.doAISearch = function() {
18
+ var input = document.getElementById('aiSearch');
19
+ var resultsDiv = document.getElementById('aiResults');
20
+ if (!input || !resultsDiv) return;
21
+
22
+ var query = input.value.trim();
23
+ if (!query || query.length < 2) {
24
+ resultsDiv.style.display = 'block';
25
+ resultsDiv.innerHTML = '💡 Nhập mã SP hoặc mô tả: "bếp từ Grob dưới 10tr", "máy hút mùi Malloca"';
26
+ return;
27
+ }
28
+
29
+ resultsDiv.style.display = 'block';
30
+ resultsDiv.innerHTML = '⏳ Đang tìm...';
31
+
32
+ // Wait for data to be ready
33
+ function getD() { return (typeof window.D !== 'undefined' && window.D.length > 0) ? window.D : null; }
34
+
35
+ // Multi-product search: if query contains comma/semicolon, search by product codes
36
+ var queries = query.split(/[,;]+/).map(function(c) { return c.trim(); }).filter(function(c) { return c.length >= 2; });
37
+
38
+ var D = getD();
39
+
40
+ if (queries.length > 1 && D) {
41
+ // Multi-code search mode
42
+ var allResults = [];
43
+ var normSimple = function(s) { return String(s || '').toLowerCase().replace(/[.\/\-]/g, ''); };
44
+
45
+ queries.forEach(function(q) {
46
+ var qNorm = normSimple(q);
47
+ for (var i = 0; i < D.length && allResults.length < 30; i++) {
48
+ var p = D[i];
49
+ if (!p) continue;
50
+ var nameField = (p.name || '').toLowerCase();
51
+ var modField = (p.mod || '').toLowerCase();
52
+ var skuField = normSimple(p.sku || '');
53
+ var modelField = (p.model || '').toLowerCase();
54
+ var slugField = (p.slug || '').toLowerCase();
55
+
56
+ if (skuField === qNorm ||
57
+ modField.replace(/[.\/\- ]/g, '').includes(qNorm) ||
58
+ nameField.replace(/[.\/\- ]/g, '').includes(qNorm) ||
59
+ slugField.replace(/[.\/\- ]/g, '').includes(qNorm)) {
60
+ allResults.push(p);
61
+ }
62
+ }
63
+ });
64
+
65
+ if (allResults.length > 0) {
66
+ _renderMultiCodeResults(allResults, resultsDiv, query);
67
+ return;
68
+ }
69
+ }
70
+
71
+ if (typeof window.aiSearch === 'function' && D) {
72
+ var aiToken = (window.huggingface && window.huggingface.variables && window.huggingface.variables.VAISTUDIO) || '';
73
+ window.aiSearch(query, D, aiToken).then(function(aiResult) {
74
+ if (!aiResult || (!aiResult.results.length && !aiResult.aiAnswer)) {
75
+ _fallbackAISearch(query, resultsDiv);
76
+ return;
77
+ }
78
+ _renderAIResultsToDiv(aiResult, resultsDiv, query);
79
+ }).catch(function(err) {
80
+ _fallbackAISearch(query, resultsDiv);
81
+ console.error('AI Search error:', err);
82
+ });
83
+ } else {
84
+ _fallbackAISearch(query, resultsDiv);
85
+ }
86
+ };
87
+
88
+ // Render results for multi-product code search
89
+ function _renderMultiCodeResults(products, container, query) {
90
+ var html = '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><i class="fas fa-search" style="margin-right:4px"></i><strong>' + products.length + '</strong> sản phẩm tìm được cho "<strong>' + query + '</strong>"</div>';
91
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">';
92
+
93
+ products.forEach(function(p, i) {
94
+ var idx = window.D ? window.D.indexOf(p) : -1;
95
+ var brandColor = p.brand === 'Eurogold' ? '#c8102c' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
96
+ var imgSrc = p.image || p.i || '';
97
+ var priceStr = p.price || (p.pn ? p.pn : 'Liên hệ');
98
+ var catStr = p.cat || '';
99
+ var catIcon = p.catIcon || 'fa-tag';
100
+ var productId = p.sku || p.mod || p.slug || idx;
101
+ var safeName = (p.name || '').replace(/'/g, "\\'");
102
+
103
+ html += '<div class="pc fade" style="cursor:pointer;transition-delay:' + Math.min(i * 20, 300) + 'ms">' +
104
+ '<div class="pi" style="height:150px;position:relative"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
105
+ '<span class="pi-badge"><i class="fas ' + catIcon + '"></i> ' + catStr + '</span>' +
106
+ '<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
107
+ '<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:3px">' + (p.sku || p.mod || p.model || '') + '</div>' +
108
+ '<div class="pn" style="font-size:.76rem;margin-bottom:8px">' + p.name + '</div>' +
109
+ '<div style="display:flex;align-items:center;justify-content:space-between"><span class="pp" style="font-size:.92rem">' + priceStr + '</span>' +
110
+ '<button onclick="event.stopPropagation();addToCart({id:\'' + productId + '\',name:\'' + safeName + '\',price:' + (p.priceNum || 0) + ',sku:\'' + (p.sku || '') + '\',image:\'' + (p.image || '') + '\',brand:\'' + (p.brand || '') + '\'});this.style.background=\'#2e7d32\';this.innerHTML=\'✅ Đã thêm\';this.disabled=true;" ' +
111
+ 'style="background:#25d366;color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:.72rem;font-weight:600;cursor:pointer;transition:all .2s"><i class="fas fa-shopping-cart" style="margin-right:3px"></i> Thêm giỏ</button></div></div></div>';
112
+ });
113
+
114
+ html += '</div>';
115
+ container.innerHTML = html;
116
+ container.style.display = 'block';
117
+
118
+ // Make products clickable
119
+ container.querySelectorAll('.pc').forEach(function(el, i) {
120
+ el.onclick = function() { showDetail(products[i] && (window.D ? window.D.indexOf(products[i]) : -1)); };
121
+ });
122
+
123
+ if (window.requestAnimationFrame) {
124
+ requestAnimationFrame(function() {
125
+ var fadeEls = container.querySelectorAll('.fade');
126
+ for (var j = 0; j < fadeEls.length; j++) {
127
+ fadeEls[j].classList.add('vis');
128
+ }
129
+ });
130
+ }
131
+ }
132
+
133
+ function _renderAIResultsToDiv(aiResult, container, query) {
134
+ var badges = '';
135
+ if (aiResult.categories && aiResult.categories.length > 0) {
136
+ badges += '<span style="display:inline-block;background:rgba(0,63,98,.08);color:#003f62;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-layer-group" style="margin-right:4px"></i>' + aiResult.categories.map(function(c) { return c.replace(/_/g, ' '); }).join(', ') + '</span>';
137
+ }
138
+ if (aiResult.constraints && aiResult.constraints.length > 0) {
139
+ aiResult.constraints.forEach(function(c) {
140
+ badges += '<span style="display:inline-block;background:rgba(219,152,21,.15);color:#8a6d0e;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-sliders" style="margin-right:4px"></i>' + c.label + '</span>';
141
+ });
142
+ }
143
+ if (aiResult.budget && aiResult.budget > 0) {
144
+ badges += '<span style="display:inline-block;background:rgba(46,125,50,.1);color:#2e7d32;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-wallet" style="margin-right:4px"></i>Tối đa ' + (aiResult.budget / 1000000).toFixed(0) + ' triệu</span>';
145
+ }
146
+
147
+ var aiHtml = '';
148
+ if (aiResult.aiAnswer) {
149
+ aiHtml = '<div style="background:linear-gradient(135deg,rgba(0,63,98,.04),rgba(219,152,21,.06));border-radius:16px;padding:16px;margin-bottom:16px;border:2px solid #e2e8f0"><div style="display:flex;align-items:center;gap:8px;margin-bottom:10px"><div style="width:32px;height:32px;border-radius:50%;background:#003f62;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-robot"></i></div><span style="font-weight:700;color:#003f62;font-size:.85rem">V.AI STUDIO trả lời</span></div><div style="font-size:.85rem;color:#0f172a;line-height:1.7">' + aiResult.aiAnswer + '</div></div>';
150
+ }
151
+
152
+ var comboHtml = '';
153
+ if (aiResult.combos && aiResult.combos.length > 0) {
154
+ comboHtml = '<div style="background:linear-gradient(135deg,rgba(219,152,21,.06),rgba(46,125,50,.04));border-radius:16px;padding:16px;margin-bottom:16px;border:2px solid rgba(219,152,21,.2)"><div style="display:flex;align-items:center;gap:8px;margin-bottom:12px"><div style="width:32px;height:32px;border-radius:50%;background:#db9815;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-gift"></i></div><span style="font-weight:700;color:#0f172a;font-size:.85rem">Gợi ý combo trong ngân sách</span></div>';
155
+ aiResult.combos.forEach(function(combo, i) {
156
+ var totalStr = combo.total > 0 ? (Number(combo.total).toLocaleString('vi-VN').replace(/,/g, '.') + 'đ') : 'Liên hệ';
157
+ comboHtml += '<div style="background:#fff;border-radius:12px;padding:12px;margin-bottom:8px;border:1px solid #e2e8f0"><div style="font-weight:600;font-size:.8rem;color:#003f62;margin-bottom:6px">Combo ' + (i + 1) + ' — Tổng: ' + totalStr + '</div>';
158
+ combo.items.forEach(function(item) {
159
+ var p = item.p;
160
+ comboHtml += '<div style="display:flex;align-items:center;gap:8px;padding:5px 0;font-size:.76rem;color:#64748b"><i class="fas fa-check-circle" style="color:#2e7d32;font-size:.6rem"></i>' + p.name + ' <span style="color:#003f62;font-weight:600;margin-left:auto">' + (p.price || 'LH') + '</span></div>';
161
+ });
162
+ comboHtml += '</div>';
163
+ });
164
+ comboHtml += '</div>';
165
+ }
166
+
167
+ var items = aiResult.results || [];
168
+ var html = '<div style="margin-bottom:12px">' + badges + '</div>' + aiHtml + comboHtml;
169
+
170
+ if (items.length > 0) {
171
+ html += '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><i class="fas fa-search" style="margin-right:4px"></i><strong>' + items.length + '</strong> kết quả cho "<strong>' + query + '</strong>"</div>';
172
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">';
173
+ html += items.map(function(r, i) {
174
+ var p = r.p;
175
+ var idx = (r.idx !== undefined && r.idx >= 0) ? r.idx : (window.D ? window.D.indexOf(p) : -1);
176
+ var brandColor = p.brand === 'Eurogold' ? '#c8102c' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
177
+ var labels = (r.labels || []).map(function(l) {
178
+ return '<span style="display:inline-block;background:rgba(0,63,98,.06);padding:2px 6px;border-radius:4px;font-size:.58rem;color:#003f62;margin-right:3px;margin-bottom:3px">' + l + '</span>';
179
+ }).join('');
180
+ var imgSrc = p.image || p.i || '';
181
+ var priceStr = p.price || (window.formatPrice ? window.formatPrice(p.priceNum || p.pn) : (p.pn ? p.pn : 'Liên hệ'));
182
+ var catStr = p.cat || '';
183
+ var catIcon = p.catIcon || 'fa-tag';
184
+ var productId = p.sku || p.mod || p.slug || idx;
185
+
186
+ // Escape single quotes for onclick
187
+ var safeName = (p.name || '').replace(/'/g, "\\'");
188
+
189
+ return '<div class="pc fade" data-idx="' + idx + '" style="cursor:pointer;transition-delay:' + Math.min(i * 20, 300) + 'ms">' +
190
+ '<div class="pi" style="height:150px;position:relative"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
191
+ '<span class="pi-badge"><i class="fas ' + catIcon + '"></i> ' + catStr + '</span>' +
192
+ '<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
193
+ '<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:3px">' + (p.model || p.brand || '') + '</div>' +
194
+ '<div class="pn" style="font-size:.76rem;margin-bottom:8px">' + p.name + '</div>' +
195
+ (labels ? '<div style="margin-top:4px">' + labels + '</div>' : '') +
196
+ '<div style="display:flex;align-items:center;justify-content:space-between"><span class="pp" style="font-size:.92rem">' + priceStr + '</span>' +
197
+ '<button onclick="event.stopPropagation();addToCart({id:\'' + productId + '\',name:\'' + safeName + '\',price:' + (p.priceNum || 0) + ',sku:\'' + (p.sku || '') + '\',image:\'' + (p.image || '') + '\',brand:\'' + (p.brand || '') + '\'});this.style.background=\'#2e7d32\';this.innerHTML=\'✅ Đã thêm\';this.disabled=true;" ' +
198
+ 'style="background:#25d366;color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:.72rem;font-weight:600;cursor:pointer;transition:all .2s"><i class="fas fa-shopping-cart" style="margin-right:3px"></i> Thêm giỏ</button></div></div></div>';
199
+ }).join('');
200
+ html += '</div>';
201
+
202
+ // Bind click handlers after rendering
203
+ setTimeout(function() {
204
+ container.querySelectorAll('.pc[data-idx]').forEach(function(el) {
205
+ var idx = el.getAttribute('data-idx');
206
+ el.onclick = function() { showDetail(parseInt(idx)); };
207
+ });
208
+ }, 100);
209
+ }
210
+
211
+ container.innerHTML = html;
212
+ container.style.display = 'block';
213
+
214
+ if (window.requestAnimationFrame) {
215
+ requestAnimationFrame(function() {
216
+ var fadeEls = container.querySelectorAll('.fade');
217
+ for (var j = 0; j < fadeEls.length; j++) {
218
+ fadeEls[j].classList.add('vis');
219
+ }
220
+ });
221
+ }
222
+ }
223
+
224
+ function _fallbackAISearch(query, container) {
225
+ var D = (typeof window.D !== 'undefined') ? window.D : null;
226
+ if (!D || !D.length) {
227
+ container.innerHTML = '⏳ Đang tải dữ liệu sản phẩm... Vui lòng chờ.';
228
+ return;
229
+ }
230
+ var qNorm = query.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
231
+ var results = [];
232
+ for (var i = 0; i < D.length && results.length < 20; i++) {
233
+ var p = D[i];
234
+ if (!p) continue;
235
+ var nameNorm = (p.name || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
236
+ var skuNorm = (p.sku || p.mod || '').toLowerCase();
237
+ var catNorm = (p.cat || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
238
+ var brandNorm = (p.brand || '').toLowerCase();
239
+ if (nameNorm.indexOf(qNorm) !== -1 ||
240
+ (skuNorm && skuNorm.indexOf(qNorm) !== -1) ||
241
+ (catNorm && catNorm.indexOf(qNorm) !== -1) ||
242
+ (brandNorm && brandNorm.indexOf(qNorm) !== -1) ||
243
+ (p._idx && typeof p._idx === 'string' && p._idx.toLowerCase().indexOf(qNorm) !== -1)) {
244
+ results.push(p);
245
+ }
246
+ }
247
+ if (!results.length) {
248
+ container.innerHTML = '❌ Không tìm thấy "' + query + '". Thử: "bếp từ", "máy hút mùi", "Malloca", "Eurogold"';
249
+ return;
250
+ }
251
+ container.innerHTML = '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><strong>' + results.length + '</strong> kết quả</div>' +
252
+ '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">' +
253
+ results.map(function(p) {
254
+ var idx = window.D ? window.D.indexOf(p) : -1;
255
+ var brandColor = p.brand === 'Eurogold' ? '#c8102c' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
256
+ var imgSrc = p.image || p.i || '';
257
+ var priceStr = p.price || (p.pn ? p.pn : 'Liên hệ');
258
+ var productId = p.sku || p.mod || p.slug || idx;
259
+ var safeName = (p.name || '').replace(/'/g, "\\'");
260
+
261
+ return '<div class="pc fade" style="cursor:pointer">' +
262
+ '<div class="pi" style="height:150px;position:relative"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
263
+ '<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
264
+ '<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';margin-bottom:3px">' + (p.brand || '') + '</div>' +
265
+ '<div class="pn" style="font-size:.76rem;margin-bottom:8px">' + p.name + '</div>' +
266
+ '<div style="display:flex;align-items:center;justify-content:space-between"><span class="pp" style="font-size:.92rem">' + priceStr + '</span>' +
267
+ '<button onclick="event.stopPropagation();addToCart({id:\'' + productId + '\',name:\'' + safeName + '\',price:' + (p.priceNum || 0) + ',sku:\'' + (p.sku || '') + '\',image:\'' + (p.image || '') + '\',brand:\'' + (p.brand || '') + '\'});this.style.background=\'#2e7d32\';this.innerHTML=\'✅ Đã thêm\';this.disabled=true;" ' +
268
+ 'style="background:#25d366;color:#fff;border:none;border-radius:6px;padding:5px 10px;font-size:.72rem;font-weight:600;cursor:pointer;transition:all .2s"><i class="fas fa-shopping-cart" style="margin-right:3px"></i> Thêm giỏ</button></div></div></div>';
269
+ }).join('') + '</div>';
270
+
271
+ // Bind click handlers
272
+ container.querySelectorAll('.pc').forEach(function(el, i) {
273
+ el.onclick = function() { showDetail(window.D ? window.D.indexOf(results[i]) : -1); };
274
+ });
275
+
276
+ if (window.requestAnimationFrame) {
277
+ requestAnimationFrame(function() {
278
+ var fadeEls = container.querySelectorAll('.fade');
279
+ for (var j = 0; j < fadeEls.length; j++) {
280
+ fadeEls[j].classList.add('vis');
281
+ }
282
+ });
283
+ }
284
+ }
285
+
286
+ // ── ADD TO CART FUNCTION ─────────────────────────────────────────────────────
287
+ window.addToCart = function(product) {
288
+ // Normalize product object
289
+ var prod = {
290
+ id: product.id || product.sku || Date.now(),
291
+ name: product.name || 'Sản phẩm',
292
+ price: product.price || 0,
293
+ priceNum: product.price || product.priceNum || 0,
294
+ sku: product.sku || '',
295
+ image: product.image || '',
296
+ brand: product.brand || '',
297
+ quantity: product.quantity || 1
298
+ };
299
+
300
+ // Get existing cart
301
+ var cart = JSON.parse(localStorage.getItem('vai_cart') || '[]');
302
+
303
+ // Check if already in cart
304
+ var existingIdx = cart.findIndex(function(item) {
305
+ return item.id === prod.id || item.sku === prod.sku;
306
+ });
307
+
308
+ if (existingIdx >= 0) {
309
+ cart[existingIdx].quantity = (cart[existingIdx].quantity || 1) + 1;
310
+ } else {
311
+ cart.push(prod);
312
+ }
313
+
314
+ localStorage.setItem('vai_cart', JSON.stringify(cart));
315
+
316
+ // Update cart UI
317
+ var cartNum = document.getElementById('cartNum');
318
+ if (cartNum) {
319
+ cartNum.textContent = cart.reduce(function(sum, item) { return sum + (item.quantity || 1); }, 0);
320
+ cartNum.style.display = 'flex';
321
+ }
322
+
323
+ // Show notification
324
+ var note = document.createElement('div');
325
+ note.innerHTML = '<i class="fas fa-check-circle" style="color:#2e7d32;margin-right:6px"></i>✅ Đã thêm "' + prod.name.substring(0, 30) + '" vào giỏ!';
326
+ note.style.cssText = 'position:fixed;top:20px;right:20px;background:#fff;padding:12px 20px;border-radius:10px;box-shadow:0 4px 16px rgba(0,0,0,0.15);z-index:9999;font-size:.85rem;color:#0f172a;display:flex;align-items:center;border:2px solid #e2e8f0';
327
+ document.body.appendChild(note);
328
+ setTimeout(function() { note.remove(); }, 3000);
329
+
330
+ console.log('[AI Search] Added to cart:', prod);
331
+ };
ai-search.js ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AI Search Module for V.AI STUDIO
3
+ * ==================================
4
+ * Ported from bep40/vaistudio-zalo-bot (app.py) NL search logic.
5
+ * Provides: NL category detection, budget parsing, spec constraint extraction,
6
+ * constraint matching, category search with scoring, combo builder, AI fallback.
7
+ */
8
+
9
+ // ── Vietnamese normalization ──────────────────────────────────────────────────
10
+
11
+ function _norm(s) {
12
+ return s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd').replace(/Đ/g, 'D');
13
+ }
14
+
15
+ function _normC(s) {
16
+ return _norm(String(s || '')).replace(/[.\-_ /]+/g, '');
17
+ }
18
+
19
+ // ── Brand detection ─────────────────────────────────────────────────────────
20
+
21
+ const BRANDS = ['malloca', 'grob', 'canzy', 'eurogold', 'garis', 'demax', 'boss', 'hafele', 'teka', 'bosch', 'panasonic', 'electrolux', 'samsung', 'lg', 'toshiba', 'sharp'];
22
+
23
+ function detectBrand(text) {
24
+ const n = _norm(text || '');
25
+ for (const b of BRANDS) {
26
+ if (n.includes(b)) return b;
27
+ }
28
+ return '';
29
+ }
30
+
31
+ // ── NL Category Map ─────────────────────────────────────────────────────────
32
+
33
+ const NL_CATEGORY_MAP = {
34
+ 'bep tu': ['bep tu', 'bep dien tu', 'bep cam ung', 'bep tu doi', 'bep tu 2', 'bep tu 3', 'bep tu don', 'induction'],
35
+ 'bep gas': ['bep gas', 'bep ga', 'gas am', 'gas doi'],
36
+ 'bep hong ngoai': ['bep hong ngoai', 'hong ngoai'],
37
+ 'may hut mui': ['may hut mui', 'hut mui', 'hut khoi', 'khu mui', 'may hut khoi'],
38
+ 'may rua chen': ['may rua chen', 'may rua bat', 'rua chen', 'rua bat'],
39
+ 'lo nuong': ['lo nuong', 'lo am tu', 'lo nuong am'],
40
+ 'lo vi song': ['lo vi song', 'vi song'],
41
+ 'chau rua': ['chau rua', 'chau rua chen', 'bon rua', 'sink'],
42
+ 'voi rua': ['voi rua', 'voi rua chen', 'voi nong lanh'],
43
+ 'tu lanh': ['tu lanh', 'tu ruou'],
44
+ 'may say chen': ['may say chen', 'say chen', 'say bat'],
45
+ 'phu kien tu bep': ['phu kien tu bep', 'phu kien bep', 'ke bat', 'gia bat', 'gia dia', 'tu do kho', 'ke gia vi', 'thung rac', 'ke xoong', 'mam xoay'],
46
+ 'gia bat': ['gia bat', 'gia dia', 'ke bat', 'nang ha'],
47
+ 'tu do kho': ['tu do kho', 'tu kho', 'he kho'],
48
+ 'ray': ['ray am', 'ray bi', 'ray giam chan', 'ray hop'],
49
+ 'thung rac': ['thung rac', 'thung gao'],
50
+ 'khoa cua': ['khoa cua', 'khoa van tay', 'khoa thong minh', 'khoa dien tu'],
51
+ 'tay nam': ['tay nam', 'num tu', 'tay cam'],
52
+ 'may giat': ['may giat', 'may say'],
53
+ 'may loc nuoc': ['may loc nuoc', 'loc nuoc'],
54
+ 'tivi': ['tivi', 'tv'],
55
+ 'dieu hoa': ['dieu hoa', 'may lanh'],
56
+ 'noi chien': ['noi chien khong dau', 'chien khong dau'],
57
+ 'may hut am': ['may hut am', 'hut am']
58
+ };
59
+
60
+ function nlDetectCategories(text) {
61
+ const n = _norm(text || '');
62
+ const cats = [];
63
+ for (const [cat, kws] of Object.entries(NL_CATEGORY_MAP)) {
64
+ if (kws.some(kw => n.includes(kw))) cats.push(cat);
65
+ }
66
+ if (cats.length > 1 && cats.includes('phu kien tu bep') && !['phu kien tu bep', 'phu kien bep'].some(x => n.includes(x))) {
67
+ const idx = cats.indexOf('phu kien tu bep');
68
+ if (idx > -1) cats.splice(idx, 1);
69
+ }
70
+ return cats;
71
+ }
72
+
73
+ // ── Budget parsing ──────────────────────────────────────────────────────────
74
+
75
+ function nlParseBudget(text) {
76
+ const n = _norm(text || '');
77
+ let m = n.match(/(?:duoi|<|toi da|khong qua|nho hon)\s*([\d.,]+)\s*(trieu|tr|m)?/);
78
+ if (!m) m = n.match(/([\d.,]+)\s*(trieu|tr)\b/);
79
+ if (!m) return 0;
80
+ let val = parseFloat(m[1].replace('.', '').replace(',', '.'));
81
+ const unit = (m[2] || '').toLowerCase();
82
+ return (unit === 'trieu' || unit === 'tr' || unit === 'm' || val < 200) ? Math.round(val * 1000000) : Math.round(val);
83
+ }
84
+
85
+ // ── Spec constraint detection ───────────────────────────────────────────────
86
+
87
+ function nlDetectSpecConstraints(text) {
88
+ const n = _norm(text || '');
89
+ const cs = [];
90
+
91
+ // Diameter
92
+ let m = n.match(/(?:ong thoat|duong ong|ong khoi|phi|ø)\D{0,12}(\d{2,3})\s*(?:mm)?/) || n.match(/(\d{2,3})\s*(?:mm)?\D{0,8}(?:ong thoat|duong ong|phi|ø)/);
93
+ if (m && !n.match(RegExp('(?:' + m[1] + ')\\s*(?:trieu|tr|m)\\b'))) {
94
+ cs.push({ type: 'diameter', value: parseInt(m[1]), label: 'Ống thoát Ø' + m[1] });
95
+ }
96
+
97
+ // Suction power
98
+ m = n.match(/(?:cong suat hut|suc hut|hut manh|m3\/?h)\D{0,20}(\d{3,4})/);
99
+ if (m) {
100
+ const op = ['duoi', 'nho hon', 'toi da'].some(x => n.includes(x)) ? '<=' : '>=';
101
+ cs.push({ type: 'suction', value: parseInt(m[1]), op, label: 'Công suất hút ' + m[1] + 'm³/h' });
102
+ }
103
+
104
+ // Noise level
105
+ m = n.match(/(?:do on|db)\D{0,16}(\d{2})/);
106
+ if (m) {
107
+ const op = ['tren', 'lon hon'].some(x => n.includes(x)) ? '>=' : '<=';
108
+ cs.push({ type: 'noise', value: parseInt(m[1]), op, label: 'Độ ồn ' + m[1] + 'dB' });
109
+ }
110
+
111
+ // Width (only without explicit comparison words)
112
+ if (!n.match(/(?:duoi|nho hon|<|toi da|khong qua|tren|lon hon|>|toi thieu)\D{0,20}\d{2,4}\s*(cm|mm)?/)) {
113
+ m = n.match(/(?:rong|ngang|chieu rong|kich thuoc)\D{0,16}(\d{2,4})\s*(cm|mm)?/);
114
+ if (m) {
115
+ let v = parseInt(m[1]);
116
+ const unit = m[2] || '';
117
+ if (unit === 'cm' || v < 200) v *= 10;
118
+ if (v >= 300 && v <= 1400) cs.push({ type: 'width', value: v, label: 'Rộng ' + v + 'mm' });
119
+ }
120
+ }
121
+
122
+ // Max dimension
123
+ m = n.match(/(?:duoi|nho hon|<|toi da|khong qua)\s*(\d{2,4})\s*(cm|mm)?/) || n.match(/(?:kich thuoc|rong|ngang|dai|chieu rong)\D{0,18}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,4})\s*(cm|mm)?/);
124
+ if (m) {
125
+ let v = parseInt(m[1]);
126
+ const unit = m[2] || '';
127
+ if (unit === 'cm' || v < 200) v *= 10;
128
+ if (v >= 300 && v <= 2000) {
129
+ let role = 'max';
130
+ if (n.match(/rong|rộng|ngang|width/)) role = 'width';
131
+ else if (n.match(/sau|sâu|depth/)) role = 'depth';
132
+ else if (n.match(/cao|height/)) role = 'height';
133
+ else if (n.match(/dai|dài|length/)) role = 'width';
134
+ else if (n.match(/gia vi|ke gia vi|chai lo|dao thot|thung rac|gia bat|bat dia|xoong noi|tu kho|phu kien|ray/)) role = 'width';
135
+ const roleLabel = n.match(/dài|dai|rộng|rong|ngang|width/) ? 'chiều dài/ngang' : (n.match(/sau|sâu|depth/) ? 'chiều sâu' : (n.match(/cao|height/) ? 'chiều cao' : 'kích thước'));
136
+ cs.push({ type: 'maxdim', op: '<', value: v, role, label: roleLabel + ' dưới ' + v + 'mm' });
137
+ }
138
+ }
139
+
140
+ // Min dimension
141
+ m = n.match(/(?:tren|lon hon|>|toi thieu)\s*(\d{2,4})\s*(cm|mm)?/);
142
+ if (m) {
143
+ let v = parseInt(m[1]);
144
+ const unit = m[2] || '';
145
+ if (unit === 'cm' || v < 200) v *= 10;
146
+ if (v >= 300 && v <= 2000) cs.push({ type: 'maxdim', op: '>', value: v, label: 'Kích thước trên ' + v + 'mm' });
147
+ }
148
+
149
+ // Power (W)
150
+ m = n.match(/(?:duoi|nho hon|<|toi da|khong qua)\s*(\d{2,5})\s*w\b/) || n.match(/(?:cong suat|dien|dong co|motor)\D{0,22}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,5})\s*w\b/);
151
+ if (m) {
152
+ const v = parseInt(m[1]);
153
+ if (v > 0 && v <= 10000) cs.push({ type: 'powerw', op: '<', value: v, label: 'Công suất dưới ' + v + 'W' });
154
+ }
155
+
156
+ // Features
157
+ const featureMap = [
158
+ [/bldc|inverter/i, 'Động cơ BLDC/Inverter', ['bldc', 'inverter']],
159
+ [/cam ung|cảm ứng/i, 'Điều khiển cảm ứng', ['cam ung', 'cảm ứng']],
160
+ [/cu chi|khong cham|không chạm/i, 'Điều khiển cử chỉ/không chạm', ['cu chi', 'khong cham', 'không chạm']],
161
+ [/than hoat tinh|than hoạt tính/i, 'Than hoạt tính', ['than hoat tinh', 'than hoạt tính']],
162
+ [/say khi nong|sấy khí nóng|hot air/i, 'Sấy khí nóng', ['say khi nong', 'sấy khí nóng', 'hot air']],
163
+ [/uv|khang khuan|kháng khuẩn/i, 'UV/kháng khuẩn', ['uv', 'khang khuan', 'kháng khuẩn']],
164
+ ];
165
+ for (const [pat, label, terms] of featureMap) {
166
+ if (pat.test(n)) cs.push({ type: 'feature', label, terms });
167
+ }
168
+
169
+ return cs;
170
+ }
171
+
172
+ // ── Constraint matching ─────────────────────────────────────────────────────
173
+
174
+ function nlFullText(p) {
175
+ const specs = p.specs || {};
176
+ let sp = '';
177
+ if (specs && typeof specs === 'object') {
178
+ sp = Object.entries(specs).map(([k, v]) => k + ' ' + v).join(' ');
179
+ }
180
+ return _norm([p.name, p.cat, p.brand, p.summary || p.sum || '', p.desc || '', ...(p.feats || []), sp].join(' '));
181
+ }
182
+
183
+ function nlSpecPairs(p) {
184
+ const specs = p.specs || [];
185
+ const out = [];
186
+ if (specs && typeof specs === 'object') {
187
+ for (const [k, v] of Object.entries(specs)) {
188
+ out.push([_norm(k), _norm(v), k + ': ' + v]);
189
+ }
190
+ }
191
+ return out;
192
+ }
193
+
194
+ function zExtractDims(p) {
195
+ const dims = [];
196
+ const specs = p.specs || {};
197
+
198
+ function add(v, role) {
199
+ try { v = parseInt(v); } catch (e) { return; }
200
+ if (v < 200) v *= 10;
201
+ if (v >= 250 && v <= 2500 && ![201, 202, 304, 316, 430, 220, 240, 50, 60].includes(v)) {
202
+ dims.push({ v, role });
203
+ }
204
+ }
205
+
206
+ function parse(s, role) {
207
+ s = String(s || '').toLowerCase();
208
+ const m = s.match(/(\d{2,4})\s*(?:x|×|\*)\s*(\d{2,4})(?:\s*(?:x|×|\*)\s*(\d{2,4}))?/i);
209
+ if (m) {
210
+ add(m[1], 'width');
211
+ add(m[2], 'depth');
212
+ if (m[3]) add(m[3], 'height');
213
+ return;
214
+ }
215
+ let re2 = s.match(/(?:rộng|rong|ngang|width|w)\D{0,12}(\d{2,4})/i);
216
+ if (re2) add(re2[1], 'width');
217
+ re2 = s.match(/(?:dài|dai|length|l)\D{0,12}(\d{2,4})/i);
218
+ if (re2) add(re2[1], 'length');
219
+ re2 = s.match(/(?:sâu|sau|depth|d)\D{0,12}(\d{2,4})/i);
220
+ if (re2) add(re2[1], 'depth');
221
+ re2 = s.match(/(?:cao|height|h)\D{0,12}(\d{2,4})/i);
222
+ if (re2) add(re2[1], 'height');
223
+ }
224
+
225
+ if (specs && typeof specs === 'object') {
226
+ for (const [k, v] of Object.entries(specs)) {
227
+ const kn = _norm(k);
228
+ if (/kich thuoc|rong|ngang|dai|sau|cao|size|dimension/.test(kn)) parse(v, 'dim');
229
+ }
230
+ }
231
+ parse((p.name || '') + ' ' + (p.summary || p.sum || ''), 'dim');
232
+
233
+ const seen = new Set();
234
+ return dims.filter(d => {
235
+ const key = d.v + '_' + d.role;
236
+ if (seen.has(key)) return false;
237
+ seen.add(key);
238
+ return true;
239
+ });
240
+ }
241
+
242
+ function nlMatchesConstraints(p, constraints) {
243
+ if (!constraints || !constraints.length) return { ok: true, score: 0, labels: [] };
244
+ const full = nlFullText(p);
245
+ const pairs = nlSpecPairs(p);
246
+ let score = 0;
247
+ const labels = [];
248
+
249
+ for (const c of constraints) {
250
+ let ok = false;
251
+ if (c.type === 'diameter') {
252
+ const target = String(c.value);
253
+ for (const [k, v] of pairs) {
254
+ if (('ong'.includes(k) || 'thoat'.includes(k) || 'duong'.includes(k) || 'ong'.includes(v) || 'thoat'.includes(v)) && v.includes(target)) {
255
+ ok = true; break;
256
+ }
257
+ }
258
+ if (!ok) {
259
+ const re = new RegExp('(?:ø|phi|ong thoat|duong ong)\\D{0,12}' + target + '|' + target + '\\D{0,12}(?:ø|phi|ong thoat|duong ong)');
260
+ ok = re.test(full);
261
+ }
262
+ if (!ok) return { ok: false, score: 0, labels: [] };
263
+ score += 80; labels.push(c.label);
264
+ } else if (c.type === 'suction') {
265
+ const nums = [];
266
+ const re = /(?:cong suat hut|suc hut|hut)\D{0,18}(\d{3,4})|(?:(\d{3,4})\s*m3)/g;
267
+ let mm;
268
+ while ((mm = re.exec(full)) !== null) { if (mm[1]) nums.push(parseInt(mm[1])); if (mm[2]) nums.push(parseInt(mm[2])); }
269
+ ok = nums.some(v => c.op === '<=' ? v <= c.value : v >= c.value);
270
+ if (!ok) return { ok: false, score: 0, labels: [] };
271
+ score += 45; labels.push(c.label);
272
+ } else if (c.type === 'noise') {
273
+ const nums = [];
274
+ const re = /(?:do on)\D{0,14}(\d{2})|(\d{2})\s*db/g;
275
+ let mm;
276
+ while ((mm = re.exec(full)) !== null) { if (mm[1]) nums.push(parseInt(mm[1])); if (mm[2]) nums.push(parseInt(mm[2])); }
277
+ ok = nums.some(v => c.op === '>=' ? v >= c.value : v <= c.value);
278
+ if (!ok) return { ok: false, score: 0, labels: [] };
279
+ score += 35; labels.push(c.label);
280
+ } else if (c.type === 'width') {
281
+ const nums = [];
282
+ const re = /(?:chieu rong san pham|rong|ngang)\D{0,18}(\d{2,4})/g;
283
+ let mm;
284
+ while ((mm = re.exec(full)) !== null) { if (mm[1]) nums.push(parseInt(mm[1])); }
285
+ ok = nums.some(v => {
286
+ const vv = v < 200 ? v * 10 : v;
287
+ return Math.abs(vv - c.value) <= 30;
288
+ });
289
+ if (!ok) return { ok: false, score: 0, labels: [] };
290
+ score += 30; labels.push(c.label);
291
+ } else if (c.type === 'maxdim') {
292
+ const dimObjs = zExtractDims(p);
293
+ const role = c.role || 'max';
294
+ const vals = dimObjs.filter(d => role === 'max' || d.role === role || d.role === 'dim').map(d => d.v);
295
+ const chosen = vals.length ? (role === 'max' ? Math.max(...vals) : Math.min(...vals)) : null;
296
+ ok = vals.length > 0 && chosen !== null && (c.op === '<' ? chosen < c.value : chosen > c.value);
297
+ if (!ok) return { ok: false, score: 0, labels: [] };
298
+ score += 55; labels.push(c.label);
299
+ } else if (c.type === 'powerw') {
300
+ const nums = [];
301
+ const re = /(?:cong suat|dien nang|motor|dong co)?\D{0,16}(\d{2,5})\s*w\b/gi;
302
+ let mm;
303
+ while ((mm = re.exec(full)) !== null) { const v = parseInt(mm[1]); if (v > 0) nums.push(v); }
304
+ ok = nums.length ? nums.some(v => v < c.value) : true;
305
+ if (!ok) return { ok: false, score: 0, labels: [] };
306
+ score += 25; labels.push(c.label);
307
+ } else if (c.type === 'feature') {
308
+ ok = (c.terms || []).some(t => _norm(t).includes(full) || full.includes(_norm(t)));
309
+ if (!ok) return { ok: false, score: 0, labels: [] };
310
+ score += 22; labels.push(c.label);
311
+ }
312
+ }
313
+ return { ok: true, score, labels };
314
+ }
315
+
316
+ // ── Subtype validation ──────────────────────────────────────────────────────
317
+
318
+ function zProductSubtypeOk(p, text) {
319
+ const q = _norm(text || '');
320
+ const full = _norm((p.name || '') + ' ' + (p.cat || '') + ' ' + (p.summary || p.sum || ''));
321
+ const rules = [
322
+ [['gia vi', 'ke gia vi', 'chai lo'], ['gia vi', 'ke gia vi', 'chai lo'], ['tay rua', 'thung rac', 'dao thot', 'gia bat', 'xoong noi']],
323
+ [['tay rua', 'ke tay rua'], ['tay rua'], ['gia vi', 'chai lo']],
324
+ [['dao thot'], ['dao thot'], ['gia vi', 'tay rua']],
325
+ [['thung rac'], ['thung rac'], ['gia vi', 'tay rua']],
326
+ [['gia bat', 'bat dia'], ['gia bat', 'bat dia'], ['gia vi', 'tay rua']],
327
+ [['xoong noi'], ['xoong noi'], ['gia vi', 'tay rua']],
328
+ ];
329
+ for (const [ask, must, reject] of rules) {
330
+ if (ask.some(a => q.includes(a))) {
331
+ return must.some(m => full.includes(m)) && !reject.some(r => full.includes(r));
332
+ }
333
+ }
334
+ return true;
335
+ }
336
+
337
+ function nlValidCategoryProduct(p, cat) {
338
+ const name = _norm(p.name || '');
339
+ const catn = _norm(p.cat || '');
340
+ const full = name + ' ' + catn;
341
+ const reject = /tay rua|dung dich|ve sinh|nuoc rua|chai|lo nuoc|bo ve sinh/.test(full);
342
+
343
+ if (cat === 'bep tu') {
344
+ if (reject || /chao|noi |vi |khay|mat kinh|dao cao|khan/.test(full)) return false;
345
+ if (/bep gas|bep ga|gas 2|gas am/.test(name)) return false;
346
+ return /bep (tu|dien tu|cam ung)|induction/.test(name);
347
+ }
348
+ if (cat === 'may hut mui') {
349
+ if (reject || /than hoat tinh (thay|bo)|ong thoat (roi)|phu kien/.test(full)) return false;
350
+ return /may hut|hut mui|hut khoi|khu mui/.test(full);
351
+ }
352
+ if (cat === 'chau rua') {
353
+ if (reject || /bo xa|ro loc/.test(full)) return false;
354
+ return /chau rua|bon rua|sink/.test(full);
355
+ }
356
+ if (cat === 'voi rua') {
357
+ if (reject || /day cap|loi tron/.test(full)) return false;
358
+ return /voi rua|faucet/.test(full);
359
+ }
360
+ return true;
361
+ }
362
+
363
+ // ── Category search with scoring ────────────────────────────────────────────
364
+
365
+ function nlProductKey(p) {
366
+ return _normC(p._mod || p.sku || (p.name || '').split('|')[0]);
367
+ }
368
+
369
+ function nlSearchCategory(text, cat, products, limit, constraints, priceMax) {
370
+ limit = limit || 12;
371
+ priceMax = priceMax || 0;
372
+ const kws = NL_CATEGORY_MAP[cat] || [];
373
+ const brand = detectBrand(text);
374
+ const out = [];
375
+
376
+ for (const p of products) {
377
+ const name = _norm(p.name || '');
378
+ const catn = _norm(p.cat || '');
379
+ const pr = p.priceNum || 0;
380
+ const brandn = _norm(p.brand || '');
381
+
382
+ if (!kws.some(kw => name.includes(kw) || catn.includes(kw))) continue;
383
+ if (!zProductSubtypeOk(p, text)) continue;
384
+ if (!nlValidCategoryProduct(p, cat)) continue;
385
+ if (brand && !brandn.includes(brand) && !name.includes(brand)) continue;
386
+ if (priceMax && pr && pr > priceMax) continue;
387
+
388
+ const result = nlMatchesConstraints(p, constraints || []);
389
+ if (!result.ok) continue;
390
+
391
+ let score = 30 + result.score;
392
+ if (pr) score += Math.max(0, 18 - Math.floor(pr / 2000000));
393
+
394
+ if (cat === 'bep tu') {
395
+ if (/de ban|bep tu don|mini/.test(name) && !/de ban|mini|don/.test(_norm(text))) score -= 45;
396
+ if (/am|2 vung|doi|hai vung/.test(name)) score += 28;
397
+ if (/3 vung|4 vung/.test(name)) score += 12;
398
+ }
399
+ if (cat === 'may hut mui') {
400
+ if (/am tu|gan tu|ap tuong/.test(name)) score += 15;
401
+ if (name.includes('ecokitchen')) score += 4;
402
+ }
403
+ if (brandn.includes('malloca')) score += 12;
404
+ else if (brandn.includes('grob')) score += 10;
405
+ else if (brandn.includes('eurogold')) score += 7;
406
+
407
+ out.push({ p, score, price: pr, labels: result.labels, cat });
408
+ }
409
+
410
+ out.sort((a, b) => b.score - a.score || (a.price || 1e12) - (b.price || 1e12));
411
+
412
+ const seen = new Set();
413
+ const ded = [];
414
+ for (const r of out) {
415
+ const k = nlProductKey(r.p);
416
+ if (seen.has(k)) continue;
417
+ seen.add(k);
418
+ ded.push(r);
419
+ }
420
+ return ded.slice(0, limit);
421
+ }
422
+
423
+ // ── Combo builder ──────────────────────────────────────────────────────────
424
+
425
+ function nlBuildCombos(groups, budget, limit) {
426
+ if (!groups || groups.length < 2 || !budget) return [];
427
+ limit = limit || 5;
428
+ const combos = [];
429
+
430
+ function rec(pos, items, total, score) {
431
+ if (pos === groups.length) {
432
+ if (total <= budget) {
433
+ const util = Math.min(1, total / budget);
434
+ const rank = score + util * 120 - (util < 0.45 ? 35 : 0);
435
+ combos.push({ items: items.slice(), total, rank });
436
+ }
437
+ return;
438
+ }
439
+ for (const r of (groups[pos].items || []).slice(0, 10)) {
440
+ const pr = r.price || 0;
441
+ if (!pr || total + pr > budget) continue;
442
+ items.push(r);
443
+ rec(pos + 1, items, total + pr, score + (r.score || 0));
444
+ items.pop();
445
+ }
446
+ }
447
+
448
+ rec(0, [], 0, 0);
449
+ combos.sort((a, b) => b.rank - a.rank);
450
+ return combos.slice(0, limit);
451
+ }
452
+
453
+ // ── AI Fallback ─────────────────────────────────────────────────────────────
454
+
455
+ const AI_URL = 'https://router.huggingface.co/v1/chat/completions';
456
+ const AI_MODEL = 'Qwen/Qwen2.5-72B-Instruct';
457
+
458
+ async function callAI(query, product, aiToken) {
459
+ if (!aiToken) return null;
460
+ const ctx = [
461
+ '[' + [product.name, product.sku, product.brand, product.price || 'LH'].join('|') + ']',
462
+ ...Object.entries(product.specs || {}).filter(([k, v]) => k && v).slice(0, 12).map(([k, v]) => '-' + k + ':' + v),
463
+ ...(product.feats || []).filter(f => f).slice(0, 10).map(f => '-' + f),
464
+ product.summary || product.sum || '',
465
+ ].filter(Boolean).join('\n');
466
+
467
+ const systemPrompt = 'Tư vấn V.AI STUDIO. Xưng em. Ưu tiên Grob trước khi phù hợp, sau đó Eurogold.\n' + ctx + '\nCHỈ dùng data. KHÔNG bịa. Max 200.';
468
+
469
+ try {
470
+ const resp = await fetch(AI_URL, {
471
+ method: 'POST',
472
+ headers: {
473
+ 'Authorization': 'Bearer ' + aiToken,
474
+ 'Content-Type': 'application/json',
475
+ },
476
+ body: JSON.stringify({
477
+ model: AI_MODEL,
478
+ messages: [
479
+ { role: 'system', content: systemPrompt },
480
+ { role: 'user', content: query },
481
+ ],
482
+ max_tokens: 250,
483
+ temperature: 0.3,
484
+ }),
485
+ });
486
+ if (!resp.ok) return null;
487
+ const data = await resp.json();
488
+ let text = (data.choices?.[0]?.message?.content || '').trim();
489
+ text = text.replace(/ thinking.*<\/think>|\*\*|#{1,3}\s*/g, '').trim();
490
+ return text;
491
+ } catch (e) {
492
+ return null;
493
+ }
494
+ }
495
+
496
+ // ── Main AI Search ─────────────────────────────────────────────────────────
497
+
498
+ async function aiSearch(query, products, aiToken) {
499
+ if (!query || query.trim().length < 2) {
500
+ return { results: [], categories: [], constraints: [], budget: 0, combos: [], aiAnswer: '' };
501
+ }
502
+
503
+ const categories = nlDetectCategories(query);
504
+ const budget = nlParseBudget(query);
505
+ const constraints = nlDetectSpecConstraints(query);
506
+
507
+ let results = [];
508
+ if (categories.length > 0) {
509
+ for (const cat of categories) {
510
+ const catResults = nlSearchCategory(query, cat, products, 12, constraints, budget);
511
+ results.push(...catResults);
512
+ }
513
+ const seen = new Set();
514
+ results = results.filter(r => {
515
+ const k = nlProductKey(r.p);
516
+ if (seen.has(k)) return false;
517
+ seen.add(k);
518
+ return true;
519
+ });
520
+ results.sort((a, b) => b.score - a.score);
521
+ }
522
+
523
+ // Fallback: full-text search
524
+ if (results.length === 0) {
525
+ const qNorm = _norm(query);
526
+ const words = qNorm.split(/\s+/).filter(w => w.length >= 3);
527
+ if (words.length > 0) {
528
+ const fallback = products.filter(p => {
529
+ const idx = (p._idx || '').toLowerCase();
530
+ return words.every(w => idx.includes(w));
531
+ });
532
+ results = fallback.slice(0, 12).map(p => ({ p, score: 10, price: p.priceNum || 0, labels: [], cat: '' }));
533
+ }
534
+ }
535
+
536
+ // Combos
537
+ let combos = [];
538
+ if (budget > 0 && categories.length >= 2) {
539
+ const groups = categories.map(cat => ({
540
+ cat,
541
+ items: nlSearchCategory(query, cat, products, 5, constraints, budget),
542
+ })).filter(g => g.items.length > 0);
543
+ combos = nlBuildCombos(groups, budget, 3);
544
+ }
545
+
546
+ // AI fallback
547
+ let aiAnswer = '';
548
+ if (results.length < 3 && aiToken) {
549
+ aiAnswer = await callAI(query, { name: query, sku: '', brand: '', price: '', specs: {}, feats: [], summary: '' }, aiToken) || '';
550
+ }
551
+
552
+ // Add stable index for each result so showDetail() works reliably
553
+ const finalResults = results.slice(0, 24).map(function(r) {
554
+ var idx = window.D ? window.D.indexOf(r.p) : -1;
555
+ return Object.assign({}, r, { idx: idx });
556
+ });
557
+
558
+ return {
559
+ results: finalResults,
560
+ categories: categories,
561
+ constraints: constraints,
562
+ budget: budget,
563
+ combos: combos,
564
+ aiAnswer: aiAnswer,
565
+ };
566
+ }
567
+
568
+ // ── Render AI Search Results ────────────────────────────────────────────────
569
+
570
+ function formatPrice(n) {
571
+ if (!n) return 'Liên hệ';
572
+ return Number(n).toLocaleString('vi-VN').replace(/,/g, '.') + 'đ';
573
+ }
574
+
575
+ function renderAISearchResults(aiResult, query) {
576
+ const container = document.getElementById('grid');
577
+ const toolbarRc = document.getElementById('rc');
578
+ const sc = document.getElementById('sc');
579
+
580
+ if (!aiResult.results.length && !aiResult.aiAnswer) {
581
+ container.innerHTML = '<div class="empty" style="grid-column:1/-1"><i class="fas fa-robot"></i><h3>AI không tìm thấy kết quả</h3><p>Thử từ khóa khác hoặc mô tả chi tiết hơn</p></div>';
582
+ toolbarRc.innerHTML = '<strong>0</strong> kết quả AI';
583
+ sc.textContent = '';
584
+ return;
585
+ }
586
+
587
+ // Badges
588
+ let badges = '';
589
+ if (aiResult.categories.length > 0) {
590
+ badges += '<span style="display:inline-block;background:rgba(0,63,98,.08);color:var(--p);padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px"><i class="fas fa-layer-group" style="margin-right:4px"></i>' + aiResult.categories.map(c => c.replace(/_/g, ' ')).join(', ') + '</span>';
591
+ }
592
+ if (aiResult.constraints.length > 0) {
593
+ aiResult.constraints.forEach(c => {
594
+ badges += '<span style="display:inline-block;background:rgba(219,152,21,.15);color:#8a6d0e;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px"><i class="fas fa-sliders" style="margin-right:4px"></i>' + c.label + '</span>';
595
+ });
596
+ }
597
+ if (aiResult.budget > 0) {
598
+ badges += '<span style="display:inline-block;background:rgba(46,125,50,.1);color:#2e7d32;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px"><i class="fas fa-wallet" style="margin-right:4px"></i>Tối đa ' + (aiResult.budget / 1000000).toFixed(0) + ' triệu</span>';
599
+ }
600
+
601
+ // AI answer
602
+ let aiHtml = '';
603
+ if (aiResult.aiAnswer) {
604
+ aiHtml = '<div style="background:linear-gradient(135deg,rgba(0,63,98,.04),rgba(219,152,21,.06));border-radius:16px;padding:20px;margin-bottom:20px;border:2px solid var(--gl)"><div style="display:flex;align-items:center;gap:8px;margin-bottom:10px"><div style="width:32px;height:32px;border-radius:50%;background:var(--p);display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-robot"></i></div><span style="font-weight:700;color:var(--p);font-size:.88rem">V.AI STUDIO trả lời</span></div><div style="font-size:.88rem;color:var(--d);line-height:1.7">' + aiResult.aiAnswer + '</div></div>';
605
+ }
606
+
607
+ // Combos
608
+ let comboHtml = '';
609
+ if (aiResult.combos.length > 0) {
610
+ comboHtml = '<div style="background:linear-gradient(135deg,rgba(219,152,21,.06),rgba(46,125,50,.04));border-radius:16px;padding:20px;margin-bottom:20px;border:2px solid rgba(219,152,21,.2)"><div style="display:flex;align-items:center;gap:8px;margin-bottom:14px"><div style="width:32px;height:32px;border-radius:50%;background:var(--a);display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-gift"></i></div><span style="font-weight:700;color:var(--d);font-size:.88rem">Gợi ý combo trong ngân sách</span></div>';
611
+ aiResult.combos.forEach((combo, i) => {
612
+ const totalStr = combo.total > 0 ? formatPrice(combo.total) : 'Liên hệ';
613
+ comboHtml += '<div style="background:#fff;border-radius:12px;padding:14px;margin-bottom:10px;border:1px solid var(--gl)"><div style="font-weight:600;font-size:.82rem;color:var(--p);margin-bottom:8px">Combo ' + (i + 1) + ' — Tổng: ' + totalStr + '</div>';
614
+ combo.items.forEach(item => {
615
+ const p = item.p;
616
+ comboHtml += '<div style="display:flex;align-items:center;gap:8px;padding:6px 0;font-size:.78rem;color:var(--g)"><i class="fas fa-check-circle" style="color:#2e7d32;font-size:.65rem"></i>' + p.name + ' <span style="color:var(--p);font-weight:600;margin-left:auto">' + (p.price || 'LH') + '</span></div>';
617
+ });
618
+ comboHtml += '</div>';
619
+ });
620
+ comboHtml += '</div>';
621
+ }
622
+
623
+ // Products
624
+ const items = aiResult.results;
625
+ toolbarRc.innerHTML = '<strong>' + items.length + '</strong> kết quả AI';
626
+ sc.textContent = items.length + ' kết quả';
627
+
628
+ let html = '<div style="margin-bottom:16px">' + badges + '</div>' + aiHtml + comboHtml;
629
+ html += items.map((r, i) => {
630
+ const p = r.p;
631
+ const idx = (r.idx !== undefined && r.idx >= 0) ? r.idx : D.indexOf(p);
632
+ const brandColor = p.brand === 'Eurogold' ? '#c8102e' : p.brand === 'Grob' ? '#2e7d32' : 'var(--p)';
633
+ const labels = (r.labels || []).map(l => '<span style="display:inline-block;background:rgba(0,63,98,.06);padding:2px 6px;border-radius:4px;font-size:.6rem;color:var(--p);margin-right:3px">' + l + '</span>').join('');
634
+ return '<div class="pc fade" onclick="showDetail(' + idx + ')" style="transition-delay:' + Math.min(i * 25, 400) + 'ms"><div class="pi"><img src="' + p.image + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'"><span class="pi-badge"><i class="fas ' + p.catIcon + '"></i> ' + p.cat + '</span><span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 8px;border-radius:5px;font-size:.58rem;font-weight:700">' + p.brand + '</span></div><div class="pb"><div style="font-size:.65rem;font-weight:700;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:4px">' + (p.model || p.brand) + '</div><div class="pn">' + p.name + '</div>' + (labels ? '<div style="margin-top:6px">' + labels + '</div>' : '') + '<div class="pf"><span class="pp">' + (p.price || 'Liên hệ') + '</span><i class="fas fa-chevron-right"></i></div></div></div>';
635
+ }).join('');
636
+
637
+ container.innerHTML = html;
638
+ container.className = 'pg';
639
+ document.getElementById('pag').innerHTML = '';
640
+
641
+ requestAnimationFrame(() => document.querySelectorAll('.fade').forEach(e => e.classList.add('vis')));
642
+ }
643
+
644
+ // ── Export ──────────────────────────────────────────────────────────────────
645
+ window.aiSearch = aiSearch;
646
+ window.renderAISearchResults = renderAISearchResults;
647
+ window.formatPrice = formatPrice;
cart-fix.js ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Cart Fix v5 - V.AI STUDIO
3
+ * ==========================
4
+ * FIX 1: SP thêm sai - dùng slug/sku key, không idx (D[] thay đổi khi filter)
5
+ * - Quan trọng: OVERRIDE window.addToCart NGAY (ko đợi DOMContentLoaded)
6
+ * vì ai-search-fix.js định nghĩa addToCart ngay lập tức với vai_cart
7
+ * FIX 2: Ẩn nút Lưu + Đơn hàng trong modal giỏ hàng
8
+ */
9
+
10
+ // OVERRIDE NGAY - không đợi DOMContentLoaded
11
+ (function() {
12
+ // --- Hàm phụ trợ (phiên bản độc lập, k phụ thuộc DOM) ---
13
+ var CART_KEY = 'malloca_cart';
14
+ function _ls(key, def) { try { return JSON.parse(localStorage.getItem(key) || def); } catch(e) { return JSON.parse(def); } }
15
+ function _lss(key, val) { localStorage.setItem(key, JSON.stringify(val)); }
16
+
17
+ // Đồng bộ vai_cart -> malloca_cart
18
+ (function() {
19
+ var mc = _ls(CART_KEY, '[]');
20
+ var vc = _ls('vai_cart', '[]');
21
+ if (vc.length) {
22
+ vc.forEach(function(v) {
23
+ var ex = mc.find(function(m) { return m.sku && v.sku && m.sku === v.sku; });
24
+ if (ex) { ex.qty = (ex.qty||1) + (v.quantity||v.qty||1) - 1; }
25
+ else {
26
+ mc.push({
27
+ slug: v.slug||'', sku: v.sku||'',
28
+ name: v.name||'SP', price: typeof v.price==='number'?v.price.toLocaleString('vi-VN')+'đ':(v.price||'LH'),
29
+ priceNum: v.priceNum||v.price||0, image: v.image||'', qty: v.quantity||v.qty||1
30
+ });
31
+ }
32
+ });
33
+ _lss(CART_KEY, mc);
34
+ localStorage.removeItem('vai_cart');
35
+ }
36
+ window.cart = mc;
37
+ })();
38
+
39
+ // addToCart - override NGAY LẬP TỨC
40
+ window.addToCart = function(product) {
41
+ var cart = _ls(CART_KEY, '[]');
42
+
43
+ if (typeof product === 'number') {
44
+ var p = (typeof D !== 'undefined' && D) ? D[product] : null;
45
+ if (!p) { if (typeof showToast === 'function') showToast('Không tìm thấy SP'); return; }
46
+ var ex = cart.find(function(c) {
47
+ return (c.slug && p.slug && c.slug === p.slug) || (c.sku && p.sku && c.sku === p.sku);
48
+ });
49
+ if (ex) { ex.qty = (ex.qty||1) + 1; }
50
+ else {
51
+ cart.push({
52
+ slug: p.slug||'', sku: p.sku||'', name: p.name||'SP',
53
+ price: p.price||'LH', priceNum: p.priceNum||0, image: p.image||'', qty: 1
54
+ });
55
+ }
56
+ }
57
+ else if (typeof product === 'object' && product !== null) {
58
+ var pr = product;
59
+ var ei = cart.findIndex(function(c) { return c.sku && pr.sku && c.sku === pr.sku; });
60
+ if (ei >= 0) { cart[ei].qty = (cart[ei].qty||1) + 1; }
61
+ else {
62
+ cart.push({
63
+ slug: pr.slug||'', sku: pr.sku||'', name: pr.name||'SP',
64
+ price: typeof pr.price==='number'?pr.price.toLocaleString('vi-VN')+'đ':(pr.price||'LH'),
65
+ priceNum: pr.priceNum||pr.price||0, image: pr.image||'',
66
+ qty: pr.quantity||pr.qty||1
67
+ });
68
+ }
69
+ } else return;
70
+
71
+ _lss(CART_KEY, cart);
72
+ window.cart = cart;
73
+
74
+ // Update badge
75
+ var num = document.getElementById('cartNum');
76
+ if (num) {
77
+ var t = cart.reduce(function(s,c){return s+(c.qty||1);},0);
78
+ num.textContent = t; num.style.display = t>0?'flex':'none';
79
+ }
80
+
81
+ var nm = (typeof product==='number' && D && D[product]) ? D[product].name : (product.name||'');
82
+ if (typeof showToast === 'function') showToast('✅ Đã thêm "'+(nm||'').substring(0,30)+'" vào giỏ!');
83
+ };
84
+
85
+ // === CÁC HÀM CÒN LẠI (có thể đợi DOM) ===
86
+ function ready(fn) {
87
+ if (document.readyState !== 'loading') fn();
88
+ else document.addEventListener('DOMContentLoaded', fn);
89
+ }
90
+
91
+ ready(function() {
92
+ function loadCart() { try { return JSON.parse(localStorage.getItem(CART_KEY)||'[]'); } catch(e){return [];} }
93
+ function saveCart(cart) { localStorage.setItem(CART_KEY, JSON.stringify(cart)); window.cart = cart; }
94
+
95
+ function badgeUpdate() {
96
+ var cart = loadCart();
97
+ var num = document.getElementById('cartNum');
98
+ if (!num) return;
99
+ var t = cart.reduce(function(s,c){return s+(c.qty||1);},0);
100
+ num.textContent = t; num.style.display = t>0?'flex':'none';
101
+ }
102
+
103
+ window.changeQty = function(index, delta) {
104
+ var cart = loadCart();
105
+ if (index<0 || index>=cart.length) return;
106
+ cart[index].qty = (cart[index].qty||1) + delta;
107
+ if (cart[index].qty<=0) cart.splice(index,1);
108
+ saveCart(cart); renderCart(); badgeUpdate();
109
+ };
110
+
111
+ window.removeFromCart = function(index) {
112
+ var cart = loadCart();
113
+ if (index<0 || index>=cart.length) return;
114
+ cart.splice(index,1);
115
+ saveCart(cart); renderCart(); badgeUpdate();
116
+ };
117
+
118
+ window.openCart = function() {
119
+ var overlay = document.getElementById('cartOverlay');
120
+ var drawer = document.getElementById('cartDrawer');
121
+ if (!overlay||!drawer) return;
122
+ overlay.classList.add('open'); overlay.style.display = 'block';
123
+ drawer.classList.add('open'); drawer.style.visibility = 'visible'; drawer.style.transform = 'translateX(0)';
124
+ document.body.style.overflow = 'hidden';
125
+ renderCart();
126
+ };
127
+
128
+ window.closeCart = function() {
129
+ var overlay = document.getElementById('cartOverlay');
130
+ var drawer = document.getElementById('cartDrawer');
131
+ if (!overlay||!drawer) return;
132
+ overlay.classList.remove('open'); overlay.style.display = 'none';
133
+ drawer.classList.remove('open'); drawer.style.visibility = 'hidden';
134
+ document.body.style.overflow = '';
135
+ };
136
+
137
+ window.renderCart = function() {
138
+ var cart = loadCart();
139
+ var itemsEl = document.getElementById('cartItems');
140
+ var footerEl = document.getElementById('cartFooter');
141
+ var totalEl = document.getElementById('cartTotal');
142
+ if (!itemsEl) return;
143
+ if (!cart.length) {
144
+ itemsEl.innerHTML = '<div class="cart-empty"><i class="fas fa-shopping-bag"></i><h3>Giỏ hàng trống</h3><p style="font-size:.82rem">Thêm sản phẩm vào giỏ để đặt hàng</p></div>';
145
+ if (footerEl) footerEl.style.display = 'none'; return;
146
+ }
147
+ if (footerEl) footerEl.style.display = 'block';
148
+ itemsEl.innerHTML = cart.map(function(c,i) {
149
+ return '<div class="cart-item">'
150
+ + '<img src="'+(c.image||'')+'" alt="'+(c.name||'')+'" onerror="this.style.display=\'none\'">'
151
+ + '<div class="cart-item-info">'
152
+ + '<div class="cart-item-name">'+(c.name||'')+'</div>'
153
+ + '<div class="cart-item-price">'+(typeof c.priceNum==='number'?c.priceNum.toLocaleString('vi-VN')+'đ':(c.price||'LH'))+'</div>'
154
+ + '<div class="cart-item-qty">'
155
+ + '<button class="qty-btn" onclick="changeQty('+i+',-1)">−</button>'
156
+ + '<span class="qty-val">'+(c.qty||1)+'</span>'
157
+ + '<button class="qty-btn" onclick="changeQty('+i+',1)">+</button>'
158
+ + '</div></div>'
159
+ + '<button class="cart-item-del" onclick="removeFromCart('+i+')"><i class="fas fa-trash"></i></button>'
160
+ + '</div>';
161
+ }).join('');
162
+ var total = cart.reduce(function(s,c){return s+(c.priceNum||0)*(c.qty||1);},0);
163
+ if (totalEl) totalEl.textContent = total.toLocaleString('vi-VN')+'đ';
164
+ };
165
+
166
+ // ẨN NÚT LƯU + ĐƠN HÀNG
167
+ function hideFooterBtns() {
168
+ var footer = document.getElementById('cartFooter');
169
+ if (!footer) return;
170
+ footer.querySelectorAll('button').forEach(function(btn) {
171
+ var oc = (btn.getAttribute('onclick')||'').toLowerCase();
172
+ if (oc.indexOf('saveorder')!==-1 || oc.indexOf('openorderpage')!==-1) btn.style.display = 'none';
173
+ });
174
+ }
175
+
176
+ badgeUpdate();
177
+ hideFooterBtns();
178
+
179
+ var ftr = document.getElementById('cartFooter');
180
+ if (ftr) {
181
+ var obs = new MutationObserver(function(){ hideFooterBtns(); });
182
+ obs.observe(ftr, {childList:true,subtree:true});
183
+ }
184
+
185
+ var overlay = document.getElementById('cartOverlay');
186
+ if (overlay) overlay.onclick = function(e) { if (e.target===overlay) window.closeCart(); };
187
+
188
+ console.log('[Cart Fix v5] addToCart override NGAY, slug/sku key, an Luu+Don hang');
189
+ });
190
+ })();
catalogue_ocr.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
catalogues.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
comprehensive-fix.js ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Comprehensive Fix for Brand Discounts, Excel Formulas & Notes
3
+ * ========================================================================
4
+ * Fixes:
5
+ * 1. Brand-level discount parsing: "Malloca Ck% 35%", "eurogold ck% 50%" → itemDiscounts
6
+ * 2. Excel discount column (Giá CK) uses FORMULA (G*0.65) not hardcoded value
7
+ * 3. Notes from AI prompt are filtered out from Excel/modal (no "Ghi chú:" row)
8
+ * 4. Fees (giao hàng, lắp đặt, cọc, bốc xếp) appear as separate lines in Excel
9
+ * 5. Applies to both _doExportExcel and _doExportDeliveryExcel
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+ if (window.__COMPREHENSIVE_FIX__) return;
15
+ window.__COMPREHENSIVE_FIX__ = true;
16
+
17
+ // Wait for VAIR_QR to be available
18
+ function waitForVAIQR() {
19
+ return new Promise(resolve => {
20
+ if (window.VAI_QR) {
21
+ resolve(window.VAI_QR);
22
+ return;
23
+ }
24
+ let attempts = 0;
25
+ const check = setInterval(() => {
26
+ attempts++;
27
+ if (window.VAI_QR) {
28
+ clearInterval(check);
29
+ resolve(window.VAI_QR);
30
+ } else if (attempts > 100) { // 10 seconds
31
+ clearInterval(check);
32
+ console.warn('[Comprehensive Fix] VAI_QR not found after 10s');
33
+ resolve(null);
34
+ }
35
+ }, 100);
36
+ });
37
+ }
38
+
39
+ waitForVAIQR().then(VAI_QR => {
40
+ if (!VAI_QR) return;
41
+
42
+ // =====================================================================
43
+ // 1. PATCH parsePrompt TO HANDLE BRAND-LEVEL DISCOUNTS
44
+ // =====================================================================
45
+ if (VAI_QR.parsePrompt && !VAI_QR.parsePrompt._patchedBrandDiscount) {
46
+ const origParsePrompt = VAI_QR.parsePrompt;
47
+
48
+ VAI_QR.parsePrompt = function(text) {
49
+ // First, extract brand-level discounts BEFORE the original parser runs
50
+ // Pattern: "BrandName Ck% XX%" or "BrandName ck% XX" or "BrandName chiết khấu XX%"
51
+ const brandDiscounts = {};
52
+ const brandDiscountRegex = /\b([a-zA-Z][a-zA-Z0-9\s\-]{1,30}?)\s+(?:ck|chiết\s*khấu|chiet\s*khau|giảm|giam|discount)\s*([\d.,]+)\s*%/gi;
53
+ let match;
54
+ while ((match = brandDiscountRegex.exec(text)) !== null) {
55
+ const brand = match[1].trim().toLowerCase();
56
+ const pct = parseFloat(match[2].replace(/\./g, '').replace(',', '.'));
57
+ if (pct > 0 && pct <= 100) {
58
+ brandDiscounts[brand] = pct;
59
+ }
60
+ }
61
+
62
+ // Remove brand discount lines from text so they don't get parsed as fees
63
+ let cleanText = text;
64
+ if (Object.keys(brandDiscounts).length > 0) {
65
+ cleanText = text.replace(brandDiscountRegex, '');
66
+ }
67
+
68
+ // Call original parser with cleaned text
69
+ const result = origParsePrompt.call(this, cleanText);
70
+
71
+ // Merge brand discounts into itemDiscounts
72
+ // Store them in a special property for later matching
73
+ result._brandDiscounts = brandDiscounts;
74
+
75
+ // Also merge into itemDiscounts for backward compatibility
76
+ if (Object.keys(brandDiscounts).length > 0) {
77
+ result.itemDiscounts = { ...result.itemDiscounts, ...brandDiscounts };
78
+ }
79
+
80
+ return result;
81
+ };
82
+ VAI_QR.parsePrompt._patchedBrandDiscount = true;
83
+ console.log('[Comprehensive Fix] Patched VAI_QR.parsePrompt for brand-level discounts');
84
+ }
85
+
86
+ // =====================================================================
87
+ // 2. PATCH matchItemDiscount TO HANDLE BRAND MATCHING
88
+ // =====================================================================
89
+ if (VAI_QR.matchItemDiscount && !VAI_QR.matchItemDiscount._patchedBrandMatch) {
90
+ const origMatchItemDiscount = VAI_QR.matchItemDiscount;
91
+
92
+ VAI_QR.matchItemDiscount = function(it, itemDiscounts) {
93
+ // First try original matching (SKU/model/code matching)
94
+ const originalResult = origMatchItemDiscount.call(this, it, itemDiscounts);
95
+ if (originalResult > 0) return originalResult;
96
+
97
+ // If no match, try brand matching from _brandDiscounts
98
+ // Check if itemDiscounts has brand keys (lowercase, no special chars)
99
+ const brandKeys = Object.keys(itemDiscounts || {}).filter(k =>
100
+ !k.match(/[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d/) // Not a product code pattern
101
+ );
102
+
103
+ if (brandKeys.length === 0) return 0;
104
+
105
+ // Get item's brand from product data
106
+ let itemBrand = '';
107
+ if (it.brand) {
108
+ itemBrand = it.brand.toLowerCase();
109
+ } else if (window.D) {
110
+ // Try to find product in D array
111
+ const idx = it.idx != null ? it.idx : (it.productIdx != null ? it.productIdx : null);
112
+ let product = null;
113
+ if (idx != null && window.D[idx]) {
114
+ product = window.D[idx];
115
+ } else if (it.model || it.sku || it.ma || it.name) {
116
+ const ck = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
117
+ product = window.D.find(x => {
118
+ const xck = (x.model || x.sku || x.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
119
+ return xck.includes(ck) || ck.includes(xck);
120
+ });
121
+ }
122
+ if (product && product.brand) {
123
+ itemBrand = product.brand.toLowerCase();
124
+ }
125
+ }
126
+
127
+ // Match against brand discounts
128
+ const cleanItemBrand = itemBrand.replace(/[^a-z0-9]/g, '');
129
+ for (const bk of brandKeys) {
130
+ const cleanBk = bk.replace(/[^a-z0-9]/g, '');
131
+ if (cleanBk && cleanItemBrand && (cleanItemBrand.includes(cleanBk) || cleanBk.includes(cleanItemBrand))) {
132
+ return itemDiscounts[bk];
133
+ }
134
+ }
135
+
136
+ return 0;
137
+ };
138
+ VAI_QR.matchItemDiscount._patchedBrandMatch = true;
139
+ console.log('[Comprehensive Fix] Patched VAI_QR.matchItemDiscount for brand matching');
140
+ }
141
+
142
+ // =====================================================================
143
+ // 3. PATCH applyPromptDiscounts TO PROPAGATE BRAND DISCOUNTS
144
+ // =====================================================================
145
+ if (VAI_QR.applyPromptDiscounts && !VAI_QR.applyPromptDiscounts._patchedBrand) {
146
+ const origApplyPromptDiscounts = VAI_QR.applyPromptDiscounts;
147
+
148
+ VAI_QR.applyPromptDiscounts = function(qd, parsed) {
149
+ // Call original
150
+ const result = origApplyPromptDiscounts.call(this, qd, parsed);
151
+
152
+ // If we have brand discounts but no item-level discounts were applied,
153
+ // apply them to matching items
154
+ if (parsed && parsed._brandDiscounts && Object.keys(parsed._brandDiscounts).length > 0) {
155
+ if (result && result.items) {
156
+ result.items.forEach(it => {
157
+ // Check if this item already has a discount
158
+ if (!it.discountPercent || it.discountPercent === 0) {
159
+ // Try to match brand
160
+ let itemBrand = '';
161
+ if (it.brand) {
162
+ itemBrand = it.brand.toLowerCase();
163
+ } else if (window.D) {
164
+ const idx = it.idx != null ? it.idx : (it.productIdx != null ? it.productIdx : null);
165
+ let product = null;
166
+ if (idx != null && window.D[idx]) {
167
+ product = window.D[idx];
168
+ } else if (it.model || it.sku || it.ma || it.name) {
169
+ const ck = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
170
+ product = window.D.find(x => {
171
+ const xck = (x.model || x.sku || x.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
172
+ return xck.includes(ck) || ck.includes(xck);
173
+ });
174
+ }
175
+ if (product && product.brand) {
176
+ itemBrand = product.brand.toLowerCase();
177
+ }
178
+ }
179
+
180
+ const cleanItemBrand = itemBrand.replace(/[^a-z0-9]/g, '');
181
+ for (const [brand, pct] of Object.entries(parsed._brandDiscounts)) {
182
+ const cleanBrand = brand.replace(/[^a-z0-9]/g, '');
183
+ if (cleanBrand && cleanItemBrand && (cleanItemBrand.includes(cleanBrand) || cleanBrand.includes(cleanItemBrand))) {
184
+ it.discPrice = Math.round((it.price || 0) * (1 - pct / 100));
185
+ it.discountPercent = pct;
186
+ it.total = (it.discPrice || it.price || 0) * (it.qty || 1);
187
+ it._promptDiscount = true;
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ });
193
+
194
+ // Recalculate grandTotal
195
+ let productTotal = 0;
196
+ result.items.forEach(it => {
197
+ productTotal += Number(it.total || 0);
198
+ });
199
+ result.grandTotal = productTotal;
200
+ }
201
+ }
202
+
203
+ return result;
204
+ };
205
+ VAI_QR.applyPromptDiscounts._patchedBrand = true;
206
+ console.log('[Comprehensive Fix] Patched VAI_QR.applyPromptDiscounts for brand discounts');
207
+ }
208
+
209
+ // =====================================================================
210
+ // 4. PATCH _doExportExcel FOR FORMULA-BASED DISCOUNT & NO NOTES ROW
211
+ // =====================================================================
212
+ if (VAI_QR.exportExcel && !VAI_QR.exportExcel._patchedComprehensive) {
213
+ const orig = VAI_QR.exportExcel;
214
+
215
+ VAI_QR.exportExcel = async function(d, qd, code, qr) {
216
+ // Ensure items have correct discPrice from prompt discounts
217
+ if (qd && qd.items) {
218
+ qd.items.forEach((it, i) => {
219
+ // If item has discountPercent from prompt, ensure discPrice reflects it
220
+ if (it.discountPercent && it.discountPercent > 0 && it.price) {
221
+ it.discPrice = Math.round(it.price * (1 - it.discountPercent / 100));
222
+ it.total = it.discPrice * (it.qty || 1);
223
+ }
224
+ // Mark that this item has prompt-based discount (for formula)
225
+ it._promptDiscount = (it.discountPercent && it.discountPercent > 0) ||
226
+ (d.itemDiscounts && matchItemDiscountForPrompt(it, d.itemDiscounts)) ||
227
+ (d._brandDiscounts && matchBrandDiscountForPrompt(it, d._brandDiscounts));
228
+ });
229
+ }
230
+
231
+ // SUPPRESS NOTES ROW - set d.notes to empty array
232
+ // Fees (giao hàng, lắp đặt, cọc, bốc xếp) are in d.fees and SHOULD appear
233
+ const originalNotes = d.notes;
234
+ d.notes = [];
235
+
236
+ try {
237
+ return await orig.call(this, d, qd, code, qr);
238
+ } finally {
239
+ // Restore
240
+ d.notes = originalNotes;
241
+ }
242
+ };
243
+ VAI_QR.exportExcel._patchedComprehensive = true;
244
+ console.log('[Comprehensive Fix] Patched VAI_QR.exportExcel for formula discount & no notes');
245
+ }
246
+
247
+ // Helper: Check if item has prompt-based item discount (product code matching)
248
+ function matchItemDiscountForPrompt(it, itemDiscounts) {
249
+ if (!itemDiscounts || !it) return false;
250
+ const keys = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
251
+ for (const code in itemDiscounts) {
252
+ const ck = code.toLowerCase().replace(/[^a-z0-9]/g, '');
253
+ if (ck && keys && (keys.includes(ck) || ck.includes(keys)) &&
254
+ code.match(/[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d/)) { // product code pattern
255
+ return true;
256
+ }
257
+ }
258
+ return false;
259
+ }
260
+
261
+ // Helper: Check if item has prompt-based brand discount
262
+ function matchBrandDiscountForPrompt(it, brandDiscounts) {
263
+ if (!brandDiscounts || !it) return false;
264
+ let itemBrand = '';
265
+ if (it.brand) {
266
+ itemBrand = it.brand.toLowerCase();
267
+ } else if (window.D) {
268
+ const idx = it.idx != null ? it.idx : (it.productIdx != null ? it.productIdx : null);
269
+ let product = null;
270
+ if (idx != null && window.D[idx]) {
271
+ product = window.D[idx];
272
+ } else if (it.model || it.sku || it.ma || it.name) {
273
+ const ck = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
274
+ product = window.D.find(x => {
275
+ const xck = (x.model || x.sku || x.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
276
+ return xck.includes(ck) || ck.includes(xck);
277
+ });
278
+ }
279
+ if (product && product.brand) {
280
+ itemBrand = product.brand.toLowerCase();
281
+ }
282
+ }
283
+ const cleanItemBrand = itemBrand.replace(/[^a-z0-9]/g, '');
284
+ for (const brand in brandDiscounts) {
285
+ const cleanBrand = brand.replace(/[^a-z0-9]/g, '');
286
+ if (cleanBrand && cleanItemBrand && (cleanItemBrand.includes(cleanBrand) || cleanBrand.includes(cleanItemBrand))) {
287
+ return true;
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+
293
+ // =====================================================================
294
+ // 5. PATCH _doExportDeliveryExcel FOR NO NOTES ROW
295
+ // =====================================================================
296
+ if (VAI_QR.exportDeliveryExcel && !VAI_QR.exportDeliveryExcel._patchedComprehensive) {
297
+ const orig2 = VAI_QR.exportDeliveryExcel;
298
+
299
+ VAI_QR.exportDeliveryExcel = async function(qd, code) {
300
+ // Suppress notes row
301
+ const originalNotes = qd.notes;
302
+ qd.notes = [];
303
+
304
+ try {
305
+ return await orig2.call(this, qd, code);
306
+ } finally {
307
+ qd.notes = originalNotes;
308
+ }
309
+ };
310
+ VAI_QR.exportDeliveryExcel._patchedComprehensive = true;
311
+ console.log('[Comprehensive Fix] Patched VAI_QR.exportDeliveryExcel for no notes');
312
+ }
313
+
314
+ // =====================================================================
315
+ // 6. PATCH getData TO MARK PROMPT DISCOUNTS
316
+ // =====================================================================
317
+ if (VAI_QR.getData && !VAI_QR.getData._patchedComprehensive) {
318
+ const origGetData = VAI_QR.getData;
319
+
320
+ VAI_QR.getData = function() {
321
+ const result = origGetData.apply(this, arguments);
322
+
323
+ // Mark items with prompt discount (global, item-level, or brand-level)
324
+ if (result.qd && result.qd.items && (result.itemDiscounts || result.discountPercent || result._brandDiscounts)) {
325
+ result.qd.items.forEach(it => {
326
+ if (it.discountPercent && it.discountPercent > 0) {
327
+ it._promptDiscount = true;
328
+ }
329
+ });
330
+ }
331
+
332
+ // The notes are already in result.notes (from parsed.notes)
333
+ // Excel patch will suppress them by setting d.notes = []
334
+ // Fees in result.fees will still appear (giao hàng, lắp đặt, cọc, bốc xếp)
335
+
336
+ return result;
337
+ };
338
+ VAI_QR.getData._patchedComprehensive = true;
339
+ console.log('[Comprehensive Fix] Patched VAI_QR.getData for prompt discount marking');
340
+ }
341
+
342
+ // =====================================================================
343
+ // 7. PATCH buildQuoteHTML (PDF/Image export) TO SUPPRESS NOTES
344
+ // =====================================================================
345
+ if (VAI_QR.buildQuoteHTML && !VAI_QR.buildQuoteHTML._patchedComprehensive) {
346
+ const origBuildQuoteHTML = VAI_QR.buildQuoteHTML;
347
+
348
+ VAI_QR.buildQuoteHTML = function(d, code, qrUrl) {
349
+ // Suppress notes in HTML export too
350
+ const originalNotes = d.notes;
351
+ d.notes = [];
352
+
353
+ try {
354
+ return origBuildQuoteHTML.call(this, d, code, qrUrl);
355
+ } finally {
356
+ d.notes = originalNotes;
357
+ }
358
+ };
359
+ VAI_QR.buildQuoteHTML._patchedComprehensive = true;
360
+ console.log('[Comprehensive Fix] Patched VAI_QR.buildQuoteHTML for no notes');
361
+ }
362
+
363
+ // =====================================================================
364
+ // 8. PATCH buildDeliveryHTML (PDF/Image export) TO SUPPRESS NOTES
365
+ // =====================================================================
366
+ if (VAI_QR.buildDeliveryHTML && !VAI_QR.buildDeliveryHTML._patchedComprehensive) {
367
+ const origBuildDeliveryHTML = VAI_QR.buildDeliveryHTML;
368
+
369
+ VAI_QR.buildDeliveryHTML = function(d, code, qrUrl) {
370
+ // Suppress notes in delivery HTML export too
371
+ const originalNotes = d.notes;
372
+ d.notes = [];
373
+
374
+ try {
375
+ return origBuildDeliveryHTML.call(this, d, code, qrUrl);
376
+ } finally {
377
+ d.notes = originalNotes;
378
+ }
379
+ };
380
+ VAI_QR.buildDeliveryHTML._patchedComprehensive = true;
381
+ console.log('[Comprehensive Fix] Patched VAI_QR.buildDeliveryHTML for no notes');
382
+ }
383
+
384
+ console.log('[Comprehensive Fix] All 8 patches applied successfully!');
385
+ });
386
+ })();
discount-formula-fix.js ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Discount Formula & Notes Filter Fix
3
+ * ================================================
4
+ * Fixes:
5
+ * 1. Discount column (Giá CK) in Excel uses FORMULA (G*0.65) not hardcoded value
6
+ * 2. Notes from AI prompt (excluding fees: giao hàng, lắp đặt, cọc) are filtered out from Excel/modal
7
+ * 3. Applies to both _doExportExcel and _doExportDeliveryExcel
8
+ */
9
+
10
+ (function() {
11
+ 'use strict';
12
+ if (window.__DISCOUNT_FORMULA_FIX__) return;
13
+ window.__DISCOUNT_FORMULA_FIX__ = true;
14
+
15
+ // ===== PATCH _doExportExcel FOR FORMULA-BASED DISCOUNT =====
16
+ if (window._doExportExcel && !window._doExportExcel._patchedDiscountFormula) {
17
+ const orig = window._doExportExcel;
18
+ window._doExportExcel = async function(d, qd, code, qr) {
19
+ // Mark items that have discount from prompt (not manual)
20
+ // d.itemDiscounts contains item-specific discounts from prompt
21
+ // d.discountPercent contains global discount from prompt
22
+
23
+ const hasPromptDiscount = (d.discountPercent && d.discountPercent > 0) ||
24
+ (d.itemDiscounts && Object.keys(d.itemDiscounts).length > 0);
25
+
26
+ // Filter notes: only keep non-fee notes (notes that are NOT giao hàng, lắp đặt, cọc)
27
+ // parsed.fees already contains the fees, parsed.notes contains the rest
28
+ // But in getData(), d.notes includes parsed.notes which are already filtered
29
+ // The issue is d.fees includes giao hàng, lắp đặt, cọc - these SHOULD appear
30
+ // But we need to make sure notes from prompt don't appear as separate lines
31
+
32
+ // Actually looking at the code: d.fees are rendered as separate rows
33
+ // d.notes are rendered as a single "Ghi chú:" row at the end
34
+ // The user wants: NO extra lines for notes, only fees (giao hàng, lắp đặt, cọc) as lines
35
+
36
+ // Patch: Set d.notes to empty to prevent "Ghi chú:" row
37
+ // But keep d.fees for the fee lines
38
+ const originalNotes = d.notes;
39
+ const originalFees = d.fees;
40
+
41
+ // Filter fees: only keep actual fees (giao hàng, lắp đặt, bốc xếp, etc.)
42
+ // The parsePrompt already classifies them correctly
43
+
44
+ // For items: ensure discPrice is calculated correctly from formula
45
+ if (qd && qd.items) {
46
+ qd.items.forEach((it, i) => {
47
+ // If item has discountPercent from prompt, ensure discPrice reflects it
48
+ if (it.discountPercent && it.discountPercent > 0 && it.price) {
49
+ it.discPrice = Math.round(it.price * (1 - it.discountPercent / 100));
50
+ it.total = it.discPrice * (it.qty || 1);
51
+ }
52
+ // Mark that this item has prompt-based discount (for formula)
53
+ it._promptDiscount = (it.discountPercent && it.discountPercent > 0) ||
54
+ (d.itemDiscounts && matchItemDiscountForPrompt(it, d.itemDiscounts));
55
+ });
56
+ }
57
+
58
+ try {
59
+ return await orig.call(this, d, qd, code, qr);
60
+ } finally {
61
+ // Restore
62
+ d.notes = originalNotes;
63
+ d.fees = originalFees;
64
+ }
65
+ };
66
+ window._doExportExcel._patchedDiscountFormula = true;
67
+ console.log('[Discount Formula Fix] Patched _doExportExcel');
68
+ }
69
+
70
+ // Helper to check if item has prompt-based item discount
71
+ function matchItemDiscountForPrompt(it, itemDiscounts) {
72
+ if (!itemDiscounts || !it) return false;
73
+ const keys = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
74
+ for (const code in itemDiscounts) {
75
+ const ck = code.toLowerCase().replace(/[^a-z0-9]/g, '');
76
+ if (ck && keys && (keys.includes(ck) || ck.includes(keys))) return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ // ===== PATCH _doExportDeliveryExcel =====
82
+ if (window._doExportDeliveryExcel && !window._doExportDeliveryExcel._patchedDiscountFormula) {
83
+ const orig2 = window._doExportDeliveryExcel;
84
+ window._doExportDeliveryExcel = async function(qd, code) {
85
+ // Same logic
86
+ const originalNotes = qd.notes;
87
+ qd.notes = []; // Suppress notes row
88
+ try {
89
+ return await orig2.call(this, qd, code);
90
+ } finally {
91
+ qd.notes = originalNotes;
92
+ }
93
+ };
94
+ window._doExportDeliveryExcel._patchedDiscountFormula = true;
95
+ console.log('[Discount Formula Fix] Patched _doExportDeliveryExcel');
96
+ }
97
+
98
+ // ===== FIX MODAL QUOTE TABLE: Make discount input show formula hint =====
99
+ // The modal uses manual input for discPrice. We can't easily change to formula.
100
+ // But we can add a visual indicator when discount comes from prompt.
101
+
102
+ // Override updateQuoteTotal to handle formula display
103
+ if (typeof window.updateQuoteTotal === 'function' && !window.updateQuoteTotal._patchedFormula) {
104
+ const origUpdate = window.updateQuoteTotal;
105
+ window.updateQuoteTotal = function() {
106
+ // Call original
107
+ origUpdate.apply(this, arguments);
108
+
109
+ // Add formula hints for items with prompt discount
110
+ if (window.D && window.cart) {
111
+ window.cart.forEach((c, i) => {
112
+ const product = window.D[c.idx] || {};
113
+ // Check if this item has prompt discount
114
+ const discInput = document.querySelector(`.qt-disc[data-idx="${i}"]`);
115
+ if (discInput && c._promptDiscount) {
116
+ // Show formula hint in placeholder or tooltip
117
+ discInput.title = `Công thức: Đơn giá × (1 - CK%)`;
118
+ }
119
+ });
120
+ }
121
+ };
122
+ window.updateQuoteTotal._patchedFormula = true;
123
+ }
124
+
125
+ // ===== PATCH getData to mark prompt discounts =====
126
+ // This ensures the Excel export knows which discounts came from prompt
127
+ if (window.getData && !window.getData._patchedPromptDiscount) {
128
+ const origGetData = window.getData;
129
+ window.getData = function() {
130
+ const result = origGetData.apply(this, arguments);
131
+ // Mark items with prompt discount
132
+ if (result.qd && result.qd.items && (result.itemDiscounts || result.discountPercent)) {
133
+ result.qd.items.forEach(it => {
134
+ if (it.discountPercent && it.discountPercent > 0) {
135
+ it._promptDiscount = true;
136
+ }
137
+ });
138
+ }
139
+ // Filter notes: only keep non-fee notes?
140
+ // Actually the user says notes should NOT appear in Excel at all
141
+ // But fees (giao hàng, lắp đặt, cọc) SHOULD appear
142
+ // The parsePrompt already separates them into fees and notes
143
+ // So we just need to ensure notes don't create extra lines
144
+ if (result.notes && result.notes.length > 0) {
145
+ // Keep notes for potential display but Excel patch will suppress
146
+ result._promptNotes = result.notes;
147
+ }
148
+ return result;
149
+ };
150
+ window.getData._patchedPromptDiscount = true;
151
+ console.log('[Discount Formula Fix] Patched getData');
152
+ }
153
+
154
+ console.log('[Discount Formula Fix] Loaded: Formula-based discount in Excel, filtered notes');
155
+ })();
grob-img-fix.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Grob Image Fix
3
+ * ==============================
4
+ * Fixes Grob product images: imgs[0] (grob_products_img/) are 404.
5
+ * Swaps the primary image (p.i) to imgs[1] and removes imgs[0].
6
+ * Applied to all Grob brand products with >= 2 gallery images.
7
+ */
8
+ (function() {
9
+ 'use strict';
10
+ if (window.__GROB_IMG_FIX__) return;
11
+ window.__GROB_IMG_FIX__ = true;
12
+
13
+ function fixGrobImages() {
14
+ if (typeof window.D === 'undefined' || !Array.isArray(window.D) || !window.D.length) return;
15
+
16
+ var fixed = 0;
17
+ var skipped = 0;
18
+
19
+ for (var i = 0; i < window.D.length; i++) {
20
+ var p = window.D[i];
21
+ if (!p) continue;
22
+
23
+ // Only process Grob brand products
24
+ if (p.brand !== 'Grob') continue;
25
+
26
+ var imgs = p.images || [];
27
+ if (imgs.length >= 2) {
28
+ // imgs[0] is broken (grob_products_img/ -> 404)
29
+ // imgs[1] works (malloca-website/grob_img/)
30
+
31
+ // Set primary image to imgs[1]
32
+ p.image = imgs[1];
33
+
34
+ // Remove imgs[0], keep rest as gallery
35
+ p.images = imgs.slice(1);
36
+
37
+ fixed++;
38
+ } else {
39
+ skipped++;
40
+ }
41
+ }
42
+
43
+ if (fixed > 0) {
44
+ console.log('[Grob Img Fix] Fixed ' + fixed + ' product(s) in D (swapped imgs[0]->broken with imgs[1]->ok). Skipped ' + skipped + ' (only 1 image).');
45
+
46
+ // Trigger UI refresh if available
47
+ if (typeof window.refreshUI === 'function') {
48
+ window.refreshUI();
49
+ }
50
+ }
51
+ }
52
+
53
+ // Patch after D is populated
54
+ function patchD() {
55
+ fixGrobImages();
56
+ }
57
+
58
+ // Try patching at various intervals as data loads
59
+ function onReady() {
60
+ setTimeout(patchD, 200);
61
+ setTimeout(patchD, 500);
62
+ setTimeout(patchD, 1000);
63
+ setTimeout(patchD, 2000);
64
+ setTimeout(patchD, 3000);
65
+ setTimeout(patchD, 5000);
66
+ setTimeout(patchD, 8000);
67
+ }
68
+
69
+ if (document.readyState === 'loading') {
70
+ document.addEventListener('DOMContentLoaded', onReady);
71
+ } else {
72
+ onReady();
73
+ }
74
+
75
+ console.log('[Grob Img Fix] Loaded: ready to fix 471 Grob product images (imgs[0] -> imgs[1])');
76
+ })();
grob-price-fix.js ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Grob Price Fix
3
+ * ==============================
4
+ * Fixes incorrect prices from the catalogue-based PDF price list.
5
+ * Fixed as of July 2026 per Grob Vietnam catalogue.
6
+ */
7
+ (function() {
8
+ 'use strict';
9
+ if (window.__GROB_PRICE_FIX__) return;
10
+ window.__GROB_PRICE_FIX__ = true;
11
+
12
+ const PRICE_FIXES = {
13
+ // 0203 | Grob | Bản lề inox - cánh/phủ bì 203mm: 0đ -> 67.000đ
14
+ '0203': { p: '67.000đ', pn: 67000 },
15
+ // 0280 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 280mm: 885.000đ -> 965.000đ
16
+ '0280': { p: '965.000đ', pn: 965000 },
17
+ // 0290 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 290mm: 965.000đ -> 1.018.000đ
18
+ '0290': { p: '1.018.000đ', pn: 1018000 },
19
+ };
20
+
21
+ function patchProduct(prod) {
22
+ if (!prod) return;
23
+ var name = prod.n || prod.name || '';
24
+ for (var code in PRICE_FIXES) {
25
+ if (name.indexOf(code) === 0 && name.indexOf('Grob') !== -1) {
26
+ var fix = PRICE_FIXES[code];
27
+ if (prod.p) prod.p = fix.p;
28
+ if (prod.pn !== undefined) prod.pn = fix.pn;
29
+ if (prod.price) prod.price = fix.p;
30
+ if (prod.priceNum !== undefined) prod.priceNum = fix.pn;
31
+ return true;
32
+ }
33
+ }
34
+ return false;
35
+ }
36
+
37
+ // Patch after D is populated
38
+ function patchD() {
39
+ if (typeof window.D !== 'undefined' && Array.isArray(window.D)) {
40
+ var patched = 0;
41
+ window.D.forEach(function(p) {
42
+ if (patchProduct(p)) patched++;
43
+ });
44
+ if (patched > 0) {
45
+ console.log('[Grob Price Fix] Patched ' + patched + ' product(s) in D');
46
+ if (typeof window.refreshUI === 'function') window.refreshUI();
47
+ }
48
+ }
49
+ }
50
+
51
+ // Try patching at various intervals as data loads
52
+ function onReady() {
53
+ setTimeout(patchD, 200);
54
+ setTimeout(patchD, 500);
55
+ setTimeout(patchD, 1000);
56
+ setTimeout(patchD, 2000);
57
+ setTimeout(patchD, 3000);
58
+ setTimeout(patchD, 5000);
59
+ }
60
+
61
+ if (document.readyState === 'loading') {
62
+ document.addEventListener('DOMContentLoaded', onReady);
63
+ } else {
64
+ onReady();
65
+ }
66
+
67
+ console.log('[Grob Price Fix] Loaded: 02.03=67k, 02.80=965k, 02.90=1,018k');
68
+ })();
hafele-eurogold-img-fix.js ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Hafele & Eurogold Image Fix for Excel Export v4
3
+ * =============================================================
4
+ * Fixes product images in Excel/PDF export for:
5
+ * - Hafele (cdn-img-v2.mybota.vn): No CORS headers, fetch(mode:'cors') fails
6
+ * - Eurogold (www.eurogold.vn): 415 Unsupported Media Type
7
+ *
8
+ * Solution: Use images.weserv.nl / wsrv.nl as CORS proxy
9
+ * Both work with browser fetch when proper Accept header is sent
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+ if (window.__HAEFEURO_IMG_FIX_V4__) return;
15
+ window.__HAEFEURO_IMG_FIX_V4__ = true;
16
+
17
+ // ===== PROBLEMATIC DOMAINS =====
18
+ const PROXY_DOMAINS = [
19
+ 'cdn-img-v2.mybota.vn', // Hafele
20
+ 'www.eurogold.vn', // Eurogold
21
+ ];
22
+
23
+ function needsProxy(url) {
24
+ try {
25
+ const hostname = new URL(url).hostname;
26
+ return PROXY_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d));
27
+ } catch (e) { return false; }
28
+ }
29
+
30
+ // ===== PROXY URL =====
31
+ // Use wsrv.nl (faster) or images.weserv.nl
32
+ function getProxyUrl(url) {
33
+ const encoded = encodeURIComponent(url);
34
+ // wsrv.nl format: https://wsrv.nl/?url=ENCODED_URL&output=auto
35
+ return 'https://wsrv.nl/?url=' + encoded + '&output=auto&fit=cover&w=300&h=300';
36
+ }
37
+
38
+ // ===== HEADERS FOR PROXY FETCH =====
39
+ // Note: User-Agent is set automatically by browser, cannot be overridden in fetch()
40
+ // The key is Accept header for images
41
+ const PROXY_HEADERS = {
42
+ 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
43
+ };
44
+
45
+ // ===== PATCHED FETCH FOR EXCEL EXPORT =====
46
+ window._vaiFetchImageForExcel = async function(url) {
47
+ if (!url || !url.startsWith('http')) return null;
48
+
49
+ // Try direct fetch first (works for most domains)
50
+ try {
51
+ const resp = await fetch(url, { mode: 'cors', credentials: 'omit' });
52
+ if (resp.ok) {
53
+ const ct = resp.headers.get('Content-Type') || '';
54
+ if (ct.startsWith('image/')) return await resp.arrayBuffer();
55
+ }
56
+ } catch (e) { /* ignore, try proxy */ }
57
+
58
+ // If domain needs proxy, use proxy with proper headers
59
+ if (needsProxy(url)) {
60
+ const proxyUrl = getProxyUrl(url);
61
+ try {
62
+ const resp = await fetch(proxyUrl, {
63
+ mode: 'cors',
64
+ credentials: 'omit',
65
+ headers: PROXY_HEADERS
66
+ });
67
+ if (resp.ok) {
68
+ const ct = resp.headers.get('Content-Type') || '';
69
+ if (ct.startsWith('image/')) {
70
+ console.log('[Hafele/Eurogold Fix v4] Proxy success: ' + url);
71
+ return await resp.arrayBuffer();
72
+ }
73
+ }
74
+ } catch (e) {
75
+ console.warn('[Hafele/Eurogold Fix v4] Proxy failed for: ' + url, e);
76
+ }
77
+ }
78
+
79
+ console.warn('[Hafele/Eurogold Fix v4] All methods failed for: ' + url);
80
+ return null;
81
+ };
82
+
83
+ // ===== CANVAS-BASED CACHE (fallback for images already in DOM) =====
84
+ window._vaiImageCache = window._vaiImageCache || new Map();
85
+
86
+ function preloadAndCacheImage(url) {
87
+ if (!url || !url.startsWith('http') || window._vaiImageCache.has(url)) return;
88
+
89
+ return new Promise(resolve => {
90
+ const img = new Image();
91
+ img.crossOrigin = 'anonymous';
92
+ img.onload = () => {
93
+ try {
94
+ const canvas = document.createElement('canvas');
95
+ canvas.width = img.naturalWidth || img.width;
96
+ canvas.height = img.naturalHeight || img.height;
97
+ const ctx = canvas.getContext('2d');
98
+ ctx.drawImage(img, 0, 0);
99
+ const dataUrl = canvas.toDataURL('image/png');
100
+ window._vaiImageCache.set(url, dataUrl);
101
+ console.log('[Hafele/Eurogold Fix v4] Cached via canvas: ' + url);
102
+ resolve(dataUrl);
103
+ } catch (e) {
104
+ window._vaiImageCache.set(url, 'URL:' + url); // mark for proxy
105
+ resolve(null);
106
+ }
107
+ };
108
+ img.onerror = () => { window._vaiImageCache.set(url, 'URL:' + url); resolve(null); };
109
+ img.src = url;
110
+ });
111
+ }
112
+
113
+ // Preload all Hafele/Eurogold images from D
114
+ function preloadProductImages() {
115
+ if (!window.D || !Array.isArray(window.D) || !window.D.length) return;
116
+ const urls = new Set();
117
+ window.D.forEach(p => {
118
+ if (!p) return;
119
+ const brand = (p.brand || '').toLowerCase();
120
+ if (brand === 'hafele' || brand === 'eurogold') {
121
+ const img = p.image || p.i || (p.imgs && p.imgs[0]) || '';
122
+ if (img && img.startsWith('http')) urls.add(img);
123
+ }
124
+ });
125
+ [...urls].forEach((url, i) => setTimeout(() => preloadAndCacheImage(url), i * 50));
126
+ }
127
+
128
+ // ===== PATCH EXCEL EXPORT FUNCTIONS =====
129
+ function patchExcelExports() {
130
+ // Patch _doExportExcel
131
+ if (window._doExportExcel && !window._doExportExcel._patchedV4) {
132
+ const orig = window._doExportExcel;
133
+ window._doExportExcel = async function(d, qd, code, qr) {
134
+ const origFetch = window.fetch;
135
+ window.fetch = function(url, opts) {
136
+ if (typeof url === 'string' && url.startsWith('http') && needsProxy(url)) {
137
+ return window._vaiFetchImageForExcel(url).then(buf => {
138
+ if (buf) {
139
+ return new Response(buf, {
140
+ headers: { 'Content-Type': 'image/png' },
141
+ status: 200,
142
+ ok: true
143
+ });
144
+ }
145
+ return origFetch.apply(this, arguments);
146
+ });
147
+ }
148
+ return origFetch.apply(this, arguments);
149
+ };
150
+ try {
151
+ return await orig.call(this, d, qd, code, qr);
152
+ } finally {
153
+ window.fetch = origFetch;
154
+ }
155
+ };
156
+ window._doExportExcel._patchedV4 = true;
157
+ console.log('[Hafele/Eurogold Fix v4] Patched _doExportExcel');
158
+ }
159
+
160
+ // Patch _doExportDeliveryExcel
161
+ if (window._doExportDeliveryExcel && !window._doExportDeliveryExcel._patchedV4) {
162
+ const orig2 = window._doExportDeliveryExcel;
163
+ window._doExportDeliveryExcel = async function(qd, code) {
164
+ const origFetch = window.fetch;
165
+ window.fetch = function(url, opts) {
166
+ if (typeof url === 'string' && url.startsWith('http') && needsProxy(url)) {
167
+ return window._vaiFetchImageForExcel(url).then(buf => {
168
+ if (buf) return new Response(buf, { headers: { 'Content-Type': 'image/png' }, status: 200, ok: true });
169
+ return origFetch.apply(this, arguments);
170
+ });
171
+ }
172
+ return origFetch.apply(this, arguments);
173
+ };
174
+ try {
175
+ return await orig2.call(this, qd, code);
176
+ } finally {
177
+ window.fetch = origFetch;
178
+ }
179
+ };
180
+ window._doExportDeliveryExcel._patchedV4 = true;
181
+ console.log('[Hafele/Eurogold Fix v4] Patched _doExportDeliveryExcel');
182
+ }
183
+ }
184
+
185
+ // ===== INIT =====
186
+ function init() {
187
+ preloadProductImages();
188
+ patchExcelExports();
189
+
190
+ // Re-run as D loads
191
+ let retries = 0;
192
+ const interval = setInterval(() => {
193
+ preloadProductImages();
194
+ patchExcelExports();
195
+ retries++;
196
+ if (retries >= 10) clearInterval(interval);
197
+ }, 500);
198
+ }
199
+
200
+ if (document.readyState === 'loading') {
201
+ document.addEventListener('DOMContentLoaded', init);
202
+ } else {
203
+ init();
204
+ }
205
+
206
+ console.log('[Hafele/Eurogold Img Fix v4] Loaded: wsrv.nl proxy for mybota.vn & eurogold.vn');
207
+ })();
img-fix.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ // Image fallback
2
+ window._imgFallback = function(img) {
3
+ img.style.display = 'none';
4
+ };
order-store.js ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Order Store v19.2
3
+ *
4
+ * CHANGES from v19.1:
5
+ * - FIX: AbortController 8s timeout cho syncFromServer tránh treo UI khi bot ngủ
6
+ * - FIX: Skip BẢO HÀNH X NĂM/giao thứ X notes khỏi fees/phụ phí
7
+ *
8
+ * CHANGES from v18:
9
+ * - ADD PRODUCT TO ORDER: Nút "➕ Thêm SP" trong modal sửa đơn
10
+ * → Hiện thanh tìm kiếm SP → Chọn SP → Tự động thêm vào đơn + lưu
11
+ *
12
+ * FIX v19.1: Loại bỏ các dòng CK/giá trị số (vd "35đ", "50đ", "ck10")
13
+ * không bị đẩy vào fees/phụ phí trong modal báo giá và Excel
14
+ */
15
+ (function(){
16
+ 'use strict';
17
+ var STORE_KEY='vai_orders';
18
+ var QUEUE_KEY='vai_sync_queue';
19
+ var KETOAN_API='https://bep40-vaistudio-ketoan-bot.hf.space';
20
+ var KH_LIST=[];var KH_LOADED=false;
21
+
22
+ // ===== SYNC FROM SERVER (NEW in v17) =====
23
+
24
+ function resolveOrderItemImage(it){
25
+ var img=it.image||it.img||'';if(img)return img;
26
+ try{var data=(typeof D!=='undefined'&&D)||window._vaiGetD&&window._vaiGetD()||[];var code=(it.ma||it.model||'').toString().toLowerCase().replace(/[^a-z0-9]/g,'');var name=(it.ten||it.name||'').toString().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z0-9]/g,'');for(var i=0;i<data.length;i++){var p=data[i]||{};var pc=((p.sku||p.mod||p.model||'')+' '+(p.slug||'')).toLowerCase().replace(/[^a-z0-9]/g,'');var pn=(p.name||p.n||'').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z0-9]/g,'');if((code&&pc.indexOf(code)!==-1)||(name&&pn.indexOf(name.slice(0,20))!==-1))return p.image||p.img||p.i||'';}}
27
+ catch(e){}
28
+ return '';
29
+ }
30
+ function syncFromServer(){
31
+ var ac=new AbortController();setTimeout(function(){ac.abort();},8000);
32
+ fetch(KETOAN_API+'/orders',{method:'GET',signal:ac.signal}).then(function(r){return r.json();}).then(function(d){
33
+ var serverOrders=d.data||[];
34
+ if(!serverOrders.length)return;
35
+ var localOrders=getOrders();
36
+ var localCodes={};
37
+ localOrders.forEach(function(o){if(o.code)localCodes[o.code]=true;});
38
+ var added=0;
39
+ serverOrders.forEach(function(so){
40
+ var code=so.ma_don||so.code||'';
41
+ if(!code)return;
42
+ var existingIdx=localOrders.findIndex(function(o){return o.code===code;});
43
+ if(existingIdx>=0 && !so.overwrite && !so.savedAt)return;
44
+ // Convert server format to local format
45
+ var order={
46
+ code:code,
47
+ customer:so.khach||so.customer||'',
48
+ phone:so.dien_thoai||so.phone||'',
49
+ email:so.email||'',
50
+ addr:so.dia_chi||so.addr||'',
51
+ date:so.date||so.ngay||'',
52
+ items:(so.items||[]).map(function(it){return{name:it.ten||it.name||'',model:it.ma||it.model||'',brand:it.brand||'',image:resolveOrderItemImage(it),specs:orderItemInfo(it),info:orderItemInfo(it),dim_info:orderItemInfo(it),note:it.note||'',qty:it.sl||it.qty||1,price:it.listPrice||it.originalPrice||it.price||it.gia_goc||it.gia_niem_yet||it.gia_ban||0,discPrice:it.discPrice||it.gia_ban||it.price||0,total:(it.gia_ban||it.price||0)*(it.sl||it.qty||1);}),
53
+ fees:so.fees||[],
54
+ deposit:so.deposit||0,
55
+ discountPercent:so.discountPercent||0,
56
+ grandTotal:so.grandTotal||0,
57
+ remaining:so.remaining||0,
58
+ status:so.status||(so.confirmedAt?'confirmed':'pending'),
59
+ savedAt:so.savedAt||so.confirmedAt||'',
60
+ confirmedAt:so.confirmedAt||'',
61
+ ma_kh:so.ma_kh||''
62
+ };
63
+ if(existingIdx>=0)localOrders[existingIdx]=order;else localOrders.unshift(order);
64
+ localCodes[code]=true;
65
+ added++;
66
+ });
67
+ if(added>0){
68
+ saveOrders(localOrders);
69
+ console.log('[OS v19.2] ☁️ Synced '+added+' orders from server. Total local: '+localOrders.length);
70
+ }
71
+ }).catch(function(e){
72
+ if(e.name==='AbortError'){console.log('[OS v19.2] Server sync timeout (8s) — bot sleeping, skip');}
73
+ else{console.log('[OS v19.2] Server sync skip (bot may be sleeping)');}
74
+ });
75
+ }
76
+ // Sync from server 3s after page load (give bot time to wake up)
77
+ setTimeout(syncFromServer,3000);
78
+ // Also sync every 60s
79
+ setInterval(syncFromServer,60000);
80
+
81
+ // ===== RETRY QUEUE =====
82
+ function getQueue(){try{return JSON.parse(localStorage.getItem(QUEUE_KEY)||'[]');}catch(e){return[];}}
83
+ function saveQueue(q){localStorage.setItem(QUEUE_KEY,JSON.stringify(q));}
84
+ function addToQueue(endpoint,payload){var q=getQueue();q.push({endpoint:endpoint,payload:payload,attempts:0,addedAt:Date.now()});saveQueue(q);}
85
+ function processQueue(){
86
+ var q=getQueue();if(!q.length)return;
87
+ var item=q[0];
88
+ fetch(KETOAN_API+item.endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(item.payload)})
89
+ .then(function(r){if(r.ok){q.shift();saveQueue(q);console.log('[OS v19.2] Queue sync OK: '+item.endpoint+' ('+q.length+' remaining)');if(q.length)setTimeout(processQueue,2000);}else throw new Error('HTTP '+r.status);})
90
+ .catch(function(){item.attempts=(item.attempts||0)+1;if(item.attempts>50){q.shift();}saveQueue(q);});
91
+ }
92
+ setInterval(processQueue,30000);
93
+ setTimeout(processQueue,5000);
94
+
95
+ function syncOrderToBot(order){
96
+ var payload={ma_don:order.code,khach:order.customer||'',dien_thoai:order.phone||'',dia_chi:order.addr||'',items:(order.items||[]).map(function(it){return{ma:it.model||'',model:it.model||'',ten:it.name||'',name:it.name||'',brand:it.brand||'',image:resolveOrderItemImage(it),img:it.image||it.img||'',specs:orderItemInfo(it),info:orderItemInfo(it),dim_info:orderItemInfo(it),sl:it.qty||1,qty:it.qty||1,gia_ban:it.discPrice||it.price||0,price:it.listPrice||it.originalPrice||it.price||0,discPrice:it.discPrice||it.price||0};}),fees:order.fees||[],grandTotal:order.grandTotal||0,deposit:order.deposit||0,remaining:order.remaining||0,discountPercent:order.discountPercent||0,itemDiscounts:order.itemDiscounts||{},email:order.email||'',date:order.date||'',status:order.status||'pending',savedAt:order.savedAt||new Date().toISOString()};
97
+ if(order.confirmedAt)payload.confirmedAt=order.confirmedAt;
98
+ fetch(KETOAN_API+'/order-saved',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
99
+ .then(function(r){if(!r.ok)throw new Error();return r.json();})
100
+ .catch(function(){addToQueue('/order-saved',payload);});
101
+ }
102
+
103
+ function sendToKetoanBot(order){
104
+ var payload={ma_don:order.code,khach:order.customer||'',dien_thoai:order.phone||'',dia_chi:order.addr||'',items:(order.items||[]).map(function(it){return{ma:it.model||'',model:it.model||'',ten:it.name||'',name:it.name||'',brand:it.brand||'',image:resolveOrderItemImage(it),img:it.image||it.img||'',specs:orderItemInfo(it),info:orderItemInfo(it),dim_info:orderItemInfo(it),sl:it.qty||1,qty:it.qty||1,gia_ban:it.discPrice||it.price||0,price:it.listPrice||it.originalPrice||it.price||0,discPrice:it.discPrice||it.price||0};}),fees:order.fees||[],grandTotal:order.grandTotal||0,deposit:order.deposit||0,remaining:order.remaining||0,discountPercent:order.discountPercent||0,itemDiscounts:order.itemDiscounts||{},confirmedAt:order.confirmedAt||new Date().toISOString(),email:order.email||'',date:order.date||''};
105
+ fetch(KETOAN_API+'/order-confirmed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
106
+ .then(function(r){if(!r.ok)throw new Error();return r.json();})
107
+ .then(function(d){if(d.ok&&typeof showToast==='function')showToast('📨 Đã gửi kế toán!');})
108
+ .catch(function(){addToQueue('/order-confirmed',payload);if(typeof showToast==='function')showToast('⏳ Bot đang khởi động, sẽ tự gửi lại...');});
109
+ }
110
+
111
+ function loadKhachHang(){if(KH_LOADED)return Promise.resolve(KH_LIST);return fetch(KETOAN_API+'/khachhang',{method:'GET'}).then(function(r){return r.json();}).then(function(d){KH_LIST=d.data||[];KH_LOADED=true;return KH_LIST;}).catch(function(){KH_LOADED=true;return KH_LIST;});}
112
+ setTimeout(loadKhachHang,1000);
113
+ function findKH(query){if(!query||query.length<1)return[];var q=query.toLowerCase();return KH_LIST.filter(function(kh){return(kh.ma_kh||'').toLowerCase().includes(q)||(kh.ten||'').toLowerCase().includes(q)||(kh.sdt||'').includes(q);});}
114
+ function getKHCKRate(kh,brand){if(!kh||!brand)return null;var b=brand.toLowerCase().replace(/\s/g,'');var codes=kh.ck_codes||[];for(var i=0;i<codes.length;i++){var c=codes[i].toLowerCase().replace(/\s/g,'');if(c.includes(b)){var m=c.match(/ck(\d+)/);if(m)return parseInt(m[1]);}}return null;}
115
+
116
+ function getOrders(){try{return JSON.parse(localStorage.getItem(STORE_KEY)||'[]');}catch(e){return[];}}
117
+ function saveOrders(orders){localStorage.setItem(STORE_KEY,JSON.stringify(orders));}
118
+ function addOrder(order){var orders=getOrders();var idx=orders.findIndex(function(o){return o.code===order.code;});if(idx>=0)orders[idx]=order;else orders.unshift(order);saveOrders(orders);return orders;}
119
+ function deleteOrder(code){saveOrders(getOrders().filter(function(o){return o.code!==code;}));}
120
+ function searchOrders(q,statusFilter){var all=getOrders();if(statusFilter&&statusFilter!=='all')all=all.filter(function(o){return(o.status||'pending')===statusFilter;});if(!q||!q.trim())return all;var s=q.toLowerCase().trim();return all.filter(function(o){return(o.code||'').toLowerCase().includes(s)||(o.customer||'').toLowerCase().includes(s)||(o.phone||'').includes(s);});}
121
+ function fmt(n){if(!n||isNaN(n))return'0đ';return Number(n).toLocaleString('vi-VN')+'đ';}
122
+
123
+ function formatInfoVal(v){
124
+ if(v==null||v==='')return '';
125
+ if(typeof v==='string'){
126
+ var t=v.trim();
127
+ if(!t||t==='{}'||t==='[]')return '';
128
+ if((t[0]==='{'&&t[t.length-1]==='}')||(t[0]==='['&&t[t.length-1]===']')){try{return formatInfoVal(JSON.parse(t));}catch(e){return t;}}
129
+ return t;
130
+ }
131
+ if(Array.isArray(v))return v.map(formatInfoVal).filter(Boolean).join('; ');
132
+ if(typeof v==='object'){
133
+ var out=[];Object.keys(v).forEach(function(k){var val=v[k];if(val==null||val===''||(typeof val==='object'&&!Array.isArray(val)&&!Object.keys(val).length))return;out.push(k+': '+formatInfoVal(val));});
134
+ return out.join('; ');
135
+ }
136
+ return String(v);
137
+ }
138
+ function orderItemInfo(it){it=it||{};return formatInfoVal(it.info||it.thong_tin||it.dim_info||it.dimension_info||it.specs||it.summary||it.desc||'');}
139
+
140
+ function compactKey(s){return (s||'').toString().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d').toLowerCase().replace(/[^a-z0-9]/g,'');}
141
+ function itemKeys(it){var arr=[];['model','sku','code','ma','name'].forEach(function(k){if(it&&it[k])arr.push(compactKey(it[k]));});if(it&&it.name){String(it.name).split('|').forEach(function(x){arr.push(compactKey(x));});}return arr.filter(function(x){return x&&x.length>=3;});}
142
+ function matchItemDiscount(it,itemDiscounts){itemDiscounts=itemDiscounts||{};var keys=itemKeys(it);for(var i=0;i<keys.length;i++){if(itemDiscounts[keys[i]])return itemDiscounts[keys[i]];}for(var code in itemDiscounts){for(var j=0;j<keys.length;j++){var k=keys[j];if(code.length>=3&&k.length>=3&&(k.indexOf(code)!==-1||code.indexOf(k)!==-1))return itemDiscounts[code];}}return 0;}
143
+ function parseItemDiscountLine(line,map){var lo=(line||'').toString().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d').toLowerCase();var re1=/\b([a-z]{1,8}[a-z0-9._\-\/]{1,30}\d[a-z0-9._\-\/]*)\b\s*(?:[:=\-]|\s+)?\s*(?:ck|chiet\s*khau|giam|discount)\s*([\d.,]+)\s*%?/gi;var re2=/(?:ck|chiet\s*khau|giam|discount)\s*\b([a-z]{1,8}[a-z0-9._\-\/]{1,30}\d[a-z0-9._\-\/]*)\b\s*([\d.,]+)\s*%?/gi;var m;while((m=re1.exec(lo))!==null){var v=parseFloat(m[2].replace(/\./g,'').replace(',','.'));if(v>0&&v<=100)map[compactKey(m[1])]=v;}while((m=re2.exec(lo))!==null){var v2=parseFloat(m[2].replace(/\./g,'').replace(',','.'));if(v2>0&&v<=100)map[compactKey(m[1])]=v2;}}
144
+ function esc(s){return(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
145
+ function parsePrompt(text){
146
+ var fees=[],deposit=0,discountPercent=0,note='',itemDiscounts={};
147
+ if(!text)return{fees:fees,deposit:deposit,discountPercent:discountPercent,note:note,itemDiscounts:itemDiscounts};
148
+ text.split(/[,;\n]+/).forEach(function(line){
149
+ line=line.trim();if(!line)return;
150
+ parseItemDiscountLine(line,itemDiscounts);
151
+ if(Object.keys(itemDiscounts).length&&line.match(/\b[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d[A-Za-z0-9._\-\/]*\b/i)&&line.match(/\b(ck|chiết khấu|chiet khau|giảm|giam|discount)\b/i))return;
152
+ var lo=line.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d');
153
+ // Ghi chú
154
+ if(lo.match(/^(ghi chu|note|luu y)/)){note=line.replace(/^(ghi chú|ghi chu|note|lưu ý|luu y)[:\s]*/i,'').trim();return;}
155
+ // CK tổng (ví dụ "CK 10", "chiết khấu 15")
156
+ var ck=lo.match(/^(ck|chiet khau|giam|discount)\s*([\d.,]+)\s*(%|k|tr)?/);
157
+ if(ck){var v=parseFloat(ck[2].replace(/\./g,'').replace(',','.'));if((!ck[3]||ck[3]===''||ck[3]==='%')&&v>0&&v<=100)discountPercent=v;return;}
158
+ // Cọc
159
+ if(lo.match(/coc|dat coc|deposit/)){var dm=lo.match(/([\d.,]+)\s*(k|tr|trieu|%)?/);if(dm){var a=parseFloat(dm[1].replace(/\./g,'').replace(',','.'));var u2=(dm[2]||'').toLowerCase();if(u2==='%'){deposit=a;}else{if(u2==='k')a*=1000;if(u2==='tr'||u2==='trieu')a*=1000000;deposit=a;}}return;}
160
+ // Phụ phí: giao hàng, lắp đặt, vận chuyển, phí khác
161
+ var feeMatch=lo.match(/^(giao hang|giao|lap dat|lap|van chuyen|ship|phi giao|phi lap|phi khac|phu phi|phi)\s*([\d.,]+)\s*(k|tr|trieu|m|nghin)?/);
162
+ if(feeMatch){
163
+ var amt=parseFloat(feeMatch[2].replace(/\./g,'').replace(',','.'));
164
+ var u=(feeMatch[3]||'').toLowerCase();
165
+ if(u==='k'||u==='nghin')amt*=1000;else if(u==='tr'||u==='trieu'||u==='m')amt*=1000000;else if(amt>0&&amt<500)amt*=1000;
166
+ var labelMap={'giao hang':'Phí giao hàng','giao':'Phí giao hàng','lap dat':'Phí lắp đặt','lap':'Phí lắp đặt','van chuyen':'Phí vận chuyển','ship':'Phí vận chuyển','phi giao':'Phí giao hàng','phi lap':'Phí lắp đặt','phi khac':'Phí khác','phu phi':'Phụ phí','phi':'Phụ phí'};
167
+ fees.push({label:labelMap[feeMatch[1]]||'Phụ phí',amount:amt});
168
+ return;
169
+ }
170
+ // ===== FIX v19.1: Skip bare price/discount notations =====
171
+ // Dòng chỉ toàn số: "35", "50"
172
+ if(lo.match(/^[\d.,]+$/)) return;
173
+ // Dòng giá trị + đơn vị ngắn (vd "35đ", "50đ", "35k", "35000"): số + (k|đ|vnd) và không có text mô tả thực sự
174
+ var unitMatch=lo.match(/^([\d.,]+)\s*(k|nghin|tr|trieu|m|đ|vnd)?\s*(đ|vnd)?$/i);
175
+ if(unitMatch){
176
+ var remainder=line.replace(/[\d.,\sknghintrieumđvndKHTRML]/gi,'').trim();
177
+ if(remainder.length<2) return;
178
+ }
179
+ // Dòng bắt đầu bằng brand name + số (vd "Malloca Ck% 35đ", "eurogold ck% 50đ")
180
+ // Kiểm tra nếu là dòng có brand + ck/discount notation
181
+ var brandCk=lo.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*(ck|chiet.khau|giam|discount|chiết khấu)/i);
182
+ if(brandCk) return;
183
+ // Dòng chỉ có brand + số đơn thuần (vd "malloca 35đ", "eurogold 50đ")
184
+ var brandNum=lo.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*[\d.,]/i);
185
+ if(brandNum){
186
+ var textAfterBrand=line.replace(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)/i,'').trim();
187
+ // If rest is just a number/unit, skip
188
+ if(textAfterBrand.match(/^[\d.,\s%(k|tr|trieu|m|đ|vnd)]+$/i)) return;
189
+ }
190
+ // ===== FIX v19.2: Skip note patterns (BẢO HÀNH X NĂM, giao thứ X) =====
191
+ if(lo.match(/bao.hanh\s*\d+\s*(nam|thang)/)||lo.match(/giao\s+(thu|ngay)\s*\d+/)||lo.match(/\b(nam|thang)\s*(bao.hanh)/))return;
192
+ // Fallback: line with number
193
+ var m=lo.match(/([\d.,]+)\s*(k|tr|trieu|m|nghin)?/);
194
+ if(m){var amt2=parseFloat(m[1].replace(/\./g,'').replace(',','.'));var u3=(m[2]||'').toLowerCase();if(u3==='k'||u3==='nghin')amt2*=1000;if(u3==='tr'||u3==='trieu'||u3==='m')amt2*=1000000;if(amt2>0){var lb=line.replace(m[0],'').trim();if(!lb||lb.length<2)lb='Phụ phí';fees.push({label:lb,amount:amt2});}}
195
+ });
196
+ return{fees:fees,deposit:deposit,discountPercent:discountPercent,note:note,itemDiscounts:itemDiscounts};
197
+ }
198
+
199
+ function applyDiscount(order){var pct=order.discountPercent||0;var itemDiscounts=order.itemDiscounts||{};(order.items||[]).forEach(function(it){var base=it.price||0;var ip=matchItemDiscount(it,itemDiscounts);if(ip>0)it.discPrice=Math.round(base*(1-ip/100));else if(pct>0)it.discPrice=Math.round(base*(1-pct/100));if(!it.discPrice)it.discPrice=base;it.total=(it.discPrice||base)*(it.qty||1);});}
200
+ function recalcOrder(order){applyDiscount(order);var pt=0;(order.items||[]).forEach(function(it){pt+=it.total||0;});var sc=0;(order.fees||[]).forEach(function(f){sc+=(f.amount||0);});order.grandTotal=pt+sc;order.remaining=order.grandTotal-(order.deposit||0);if(order.remaining<0)order.remaining=0;}
201
+
202
+ function saveCurrentOrder(){if(!window.VAI_QR||!window.VAI_QR.getData)return;var d=window.VAI_QR.getData();var qd=d.qd;var code=window.VAI_QR.getEffectiveOrderCode();var order={code:code,customer:qd.customer.name||'',phone:qd.customer.phone||'',email:qd.customer.email||'',addr:qd.customer.addr||'',date:qd.customer.date||new Date().toLocaleDateString('vi-VN'),items:qd.items||[],fees:d.fees||[],deposit:d.deposit||0,discountPercent:d.discountPercent||0,itemDiscounts:d.itemDiscounts||{},notes:d.notes||[],grandTotal:d.grandTotal||0,remaining:d.remaining||0,promptText:window.VAI_QR.getPromptText?window.VAI_QR.getPromptText():'',status:'pending',savedAt:new Date().toISOString()};recalcOrder(order);addOrder(order);syncOrderToBot(order);if(typeof showToast==='function')showToast('✅ Đã lưu đơn '+code);}
203
+ function confirmOrder(order){order.status='confirmed';order.confirmedAt=new Date().toISOString();addOrder(order);sendToKetoanBot(order);}
204
+ function unconfirmOrder(order){order.status='pending';delete order.confirmedAt;addOrder(order);fetch(KETOAN_API+'/order-unconfirmed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ma_don:order.code})}).catch(function(){});}
205
+
206
+ function orderToVAIData(order){recalcOrder(order);return{qd:{customer:{name:order.customer||'',phone:order.phone||'',email:order.email||'',addr:order.addr||'',date:order.date||''},items:(order.items||[]).map(function(it,i){return{stt:i+1,image:it.image||'',name:it.name||'',model:it.model||'',specs:orderItemInfo(it),qty:it.qty||1,price:it.listPrice||it.originalPrice||it.price||0,discPrice:it.discPrice||it.price||0,total:it.total||0,note:it.note||''};}),grandTotal:order.grandTotal||0},fees:order.fees||[],notes:[],discount:null,discountPercent:order.discountPercent||0,itemDiscounts:order.itemDiscounts||{},deposit:order.deposit||0,productTotal:order.grandTotal-(order.fees||[]).reduce(function(s,f){return s+(f.amount||0);},0),grandTotal:order.grandTotal||0,remaining:order.remaining||0};}
207
+ function _orderBW(opt){try{return !!(opt&&opt.bw)||!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)||!!(document.getElementById('vai-bw-export')&&document.getElementById('vai-bw-export').checked);}catch(e){return!!(opt&&opt.bw);}}
208
+ async function exportPDF(order,opt){if(window.VAI_QR&&window.VAI_QR.exportPDF){var d=orderToVAIData(order);await window.VAI_QR.exportPDF(d,order.code,_orderBW(opt));}}
209
+ async function exportExcel(order,opt){if(typeof ExcelJS==='undefined')return;if(window.VAI_QR&&window.VAI_QR.exportExcel){var d=orderToVAIData(order),qd=d.qd,qrUrl=window.VAI_QR.getQRUrl(order.deposit>0?order.remaining:order.grandTotal,order.code);await window.VAI_QR.exportExcel(d,qd,order.code,qrUrl,_orderBW(opt));}}
210
+ async function exportDeliveryPDF(order,opt){if(window.VAI_QR&&window.VAI_QR.exportDeliveryPDF){var qd=orderToVAIData(order).qd;await window.VAI_QR.exportDeliveryPDF(qd,order.code,_orderBW(opt));}}
211
+ async function exportDeliveryExcel(order,opt){if(typeof ExcelJS==='undefined')return;if(window.VAI_QR&&window.VAI_QR.exportDeliveryExcel){var qd=orderToVAIData(order).qd;await window.VAI_QR.exportDeliveryExcel(qd,order.code,_orderBW(opt));}}
212
+
213
+ function openSearchModal(){var old=document.getElementById('vai-order-search-modal');if(old)old.remove();var orders=getOrders();var pC=orders.filter(function(o){return(o.status||'pending')==='pending';}).length;var cC=orders.filter(function(o){return o.status==='confirmed';}).length;var cf='all';var ov=document.createElement('div');ov.id='vai-order-search-modal';ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;display:flex;align-items:flex-start;justify-content:center;padding:40px 16px;overflow-y:auto';ov.onclick=function(e){if(e.target===ov)ov.remove();};ov.innerHTML='<div style="background:#fff;border-radius:16px;max-width:750px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column"><div style="padding:14px 18px;background:#003f62;display:flex;align-items:center"><div style="font-size:16px;font-weight:800;color:#fff;flex:1">📋 Đơn hàng ('+orders.length+')</div><button id="vai-os-refresh" style="background:rgba(255,255,255,.2);border:none;color:#fff;padding:4px 10px;border-radius:6px;cursor:pointer;font-size:11px;margin-right:8px">🔄</button><button id="vai-os-close" style="background:none;border:none;color:#fff;font-size:20px;cursor:pointer">✕</button></div><div style="padding:10px 18px;border-bottom:1px solid #e2e8f0"><div style="display:flex;gap:6px;margin-bottom:8px"><button class="os-filter" data-f="all" style="padding:5px 12px;border-radius:20px;border:2px solid #003f62;background:#003f62;color:#fff;font-size:11px;font-weight:700;cursor:pointer">Tất cả ('+orders.length+')</button><button class="os-filter" data-f="pending" style="padding:5px 12px;border-radius:20px;border:2px solid #f59e0b;background:#fffbeb;color:#92400e;font-size:11px;font-weight:700;cursor:pointer">⏳ ('+pC+')</button><button class="os-filter" data-f="confirmed" style="padding:5px 12px;border-radius:20px;border:2px solid #16a34a;background:#f0fdf4;color:#166534;font-size:11px;font-weight:700;cursor:pointer">✅ ('+cC+')</button></div><input id="vai-os-q" placeholder="Tìm mã đơn, tên KH, SĐT..." style="width:100%;padding:9px;border:2px solid #e2e8f0;border-radius:8px;font-size:12px;box-sizing:border-box"></div><div id="vai-os-r" style="flex:1;overflow-y:auto;padding:10px 18px"></div></div>';document.body.appendChild(ov);var re=document.getElementById('vai-os-r');function rl(ls){if(!ls.length){re.innerHTML='<div style="text-align:center;padding:30px;color:#94a3b8">Trống</div>';return;}re.innerHTML=ls.map(function(o,i){var isC=o.status==='confirmed';return'<div class="oi" data-i="'+i+'" style="padding:10px;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:6px;cursor:pointer;border-left:3px solid '+(isC?'#16a34a':'#f59e0b')+'"><div style="display:flex;justify-content:space-between"><div><b style="color:#003f62">'+o.code+'</b> '+(isC?'✅':'⏳')+'</div><button class="od" data-c="'+esc(o.code)+'" style="background:#fee2e2;border:none;color:#dc2626;padding:2px 6px;border-radius:4px;cursor:pointer;font-size:10px">🗑</button></div><div style="font-size:11px;color:#64748b;margin-top:3px">'+esc(o.customer)+' • '+fmt(o.grandTotal)+'</div></div>';}).join('');re.onclick=function(e){var d=e.target.closest('.od');if(d){e.stopPropagation();if(confirm('Xóa?')){deleteOrder(d.dataset.c);df();}return;}var it=e.target.closest('.oi');if(it){ov.remove();openOrderDetail(ls[parseInt(it.dataset.i)]);}};}function df(){rl(searchOrders(document.getElementById('vai-os-q').value,cf));}rl(orders);document.getElementById('vai-os-close').onclick=function(){ov.remove();};document.getElementById('vai-os-q').oninput=df;
214
+ // Refresh button — force sync from server
215
+ document.getElementById('vai-os-refresh').onclick=function(){
216
+ this.textContent='⏳';var self=this;
217
+ syncFromServer();
218
+ setTimeout(function(){self.textContent='🔄';ov.remove();openSearchModal();},2000);
219
+ };
220
+ ov.querySelectorAll('.os-filter').forEach(function(b){b.onclick=function(e){e.stopPropagation();cf=this.dataset.f;ov.querySelectorAll('.os-filter').forEach(function(x){x.style.background='#fff';x.style.color='#003f62';});this.style.background='#003f62';this.style.color='#fff';df();};});}
221
+
222
+ function openOrderDetail(order){var old=document.getElementById('vai-order-detail-modal');if(old)old.remove();order=JSON.parse(JSON.stringify(order));var ov=document.createElement('div');ov.id='vai-order-detail-modal';ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:16px;overflow-y:auto';ov.onclick=function(e){if(e.target===ov)ov.remove();};document.body.appendChild(ov);var modal=document.createElement('div');modal.style.cssText='background:#fff;border-radius:16px;max-width:750px;width:100%;max-height:92vh;overflow-y:auto';ov.appendChild(modal);
223
+ var _searchOpen=false;
224
+ function addProductToOrder(product){
225
+ var pn=product.priceNum||product.pn||0;
226
+ var item={name:product.name||product.n||'',model:product.sku||product.model||product.mod||'',brand:product.brand||'',image:product.image||product.img||product.i||'',specs:product.specs||'',info:product.info||product.thong_tin||'',dim_info:product.dim_info||product.dimension_info||'',qty:1,price:pn,discPrice:pn,total:pn,note:''};
227
+ order.items.push(item);
228
+ recalcOrder(order);
229
+ order.savedAt=new Date().toISOString();
230
+ addOrder(order);
231
+ syncOrderToBot(order);
232
+ if(typeof showToast==='function')showToast('✅ Đã thêm "'+((product.name||product.n||'').substring(0,25))+'"');
233
+ _searchOpen=false;
234
+ render();
235
+ }
236
+ function nrm(s){return(s||'').toString().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d').toLowerCase();}
237
+ function searchProducts(q){
238
+ if(!q||q.length<1)return[];
239
+ var db=[];if(typeof D!=='undefined'&&Array.isArray(D))db=D;if(typeof VAI_URL_ADDED_PRODUCTS!=='undefined'&&Array.isArray(VAI_URL_ADDED_PRODUCTS))db=db.concat(VAI_URL_ADDED_PRODUCTS);
240
+ var nq=nrm(q);var terms=nq.split(/\s+/).filter(function(t){return t.length>1;});
241
+ var scored=[];
242
+ for(var i=0;i<db.length;i++){
243
+ var p=db[i];if(!p)continue;
244
+ var name=nrm(p.name||p.n||'');var sku=nrm(p.sku||p.model||p.mod||'');var brand=nrm(p.brand||'');
245
+ var score=0;
246
+ for(var j=0;j<terms.length;j++){
247
+ var t=terms[j];
248
+ if(name.indexOf(t)!==-1)score+=3;
249
+ if(sku.indexOf(t)!==-1)score+=5;
250
+ if(brand.indexOf(t)!==-1)score+=2;
251
+ if(name===t||sku===t)score+=10;
252
+ }
253
+ if(score>0)scored.push({p:p,s:score});
254
+ }
255
+ scored.sort(function(a,b){return b.s-a.s;});
256
+ return scored.slice(0,20).map(function(x){return x.p;});
257
+ }
258
+ function toggleSearchPanel(){
259
+ _searchOpen=!_searchOpen;
260
+ render();
261
+ if(_searchOpen){
262
+ setTimeout(function(){
263
+ var inp=document.getElementById('od-sp-search');
264
+ if(inp)inp.focus();
265
+ },50);
266
+ }
267
+ }
268
+ function renderSearchPanel(){
269
+ if(!_searchOpen)return '';
270
+ return '<div id="od-sp-panel" style="margin-top:8px;padding:10px;background:#f8fafc;border-radius:8px;border:2px solid #003f62">'
271
+ +'<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">'
272
+ +'<span style="font-size:12px;font-weight:700;color:#003f62">🔍 Thêm sản phẩm</span>'
273
+ +'<button id="od-sp-close" style="margin-left:auto;background:none;border:none;color:#64748b;font-size:16px;cursor:pointer;padding:0 4px">✕</button></div>'
274
+ +'<input id="od-sp-search" placeholder="Tìm tên SP, mã SP, thương hiệu..." style="width:100%;padding:8px 10px;border:2px solid #003f62;border-radius:8px;font-size:12px;box-sizing:border-box;outline:none" onfocus="this.style.borderColor=\'#005a8c\'" onblur="this.style.borderColor=\'#003f62\'">'
275
+ +'<div id="od-sp-results" style="max-height:260px;overflow-y:auto;margin-top:6px"></div></div>';
276
+ }
277
+ function renderSearchResults(){
278
+ if(!_searchOpen)return;
279
+ var q=document.getElementById('od-sp-search');
280
+ if(!q)return;
281
+ var results=searchProducts(q.value);
282
+ var el=document.getElementById('od-sp-results');
283
+ if(!el)return;
284
+ if(!q.value.trim()){el.innerHTML='<div style="text-align:center;padding:16px;color:#94a3b8;font-size:11px">Nhập từ khóa để tìm sản phẩm</div>';return;}
285
+ if(!results.length){el.innerHTML='<div style="text-align:center;padding:16px;color:#94a3b8;font-size:11px">Không tìm thấy sản phẩm</div>';return;}
286
+ el.innerHTML=results.map(function(p){
287
+ var nm=p.name||p.n||'';
288
+ var sku=p.sku||p.model||p.mod||'';
289
+ var br=p.brand||'';
290
+ var img=p.image||p.img||p.i||'';
291
+ var price=p.priceNum||p.pn||0;
292
+ return '<div class="od-sp-item" style="display:flex;align-items:center;gap:8px;padding:6px;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:4px;cursor:pointer;background:#fff" onmouseover="this.style.background=\'#eff6ff\';this.style.borderColor=\'#93c5fd\'" onmouseout="this.style.background=\'#fff\';this.style.borderColor=\'#e2e8f0\'">'
293
+ +'<div style="width:44px;height:44px;flex-shrink:0;background:#f1f5f9;border-radius:4px;display:flex;align-items:center;justify-content:center;overflow:hidden">'+(img?'<img src="'+img+'" style="max-width:40px;max-height:40px;object-fit:contain" referrerpolicy="no-referrer" onerror="this.style.display=\'none\'">':'<span style="font-size:16px;color:#94a3b8">📦</span>')+'</div>'
294
+ +'<div style="flex:1;min-width:0"><div style="font-size:11px;font-weight:700;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(nm)+'</div><div style="font-size:9px;color:#64748b">'+esc(sku)+(br?' • '+esc(br):'')+'</div></div>'
295
+ +'<div style="font-size:11px;font-weight:700;color:#003f62;white-space:nowrap">'+fmt(price)+'</div>'
296
+ +'<button class="od-sp-add" data-name="'+esc(nm)+'" style="padding:4px 8px;background:#003f62;color:#fff;border:none;border-radius:5px;font-size:10px;font-weight:700;cursor:pointer;white-space:nowrap">+ Thêm</button></div>';
297
+ }).join('');
298
+ // Bind click events
299
+ el.querySelectorAll('.od-sp-item').forEach(function(item){
300
+ item.onclick=function(e){
301
+ if(e.target.closest('.od-sp-add'))return;
302
+ var nm=this.querySelector('.od-sp-add').dataset.name;
303
+ var idx=results.findIndex(function(p){return(p.name||p.n||'')===nm;});
304
+ if(idx>=0)addProductToOrder(results[idx]);
305
+ };
306
+ });
307
+ el.querySelectorAll('.od-sp-add').forEach(function(btn){
308
+ btn.onclick=function(e){
309
+ e.stopPropagation();
310
+ var nm=this.dataset.name;
311
+ var idx=results.findIndex(function(p){return(p.name||p.n||'')===nm;});
312
+ if(idx>=0)addProductToOrder(results[idx]);
313
+ };
314
+ });
315
+ }
316
+ function render(){recalcOrder(order);var isC=order.status==='confirmed';
317
+ var h='<div style="padding:12px 18px;background:#003f62;border-radius:16px 16px 0 0;display:flex;justify-content:space-between;align-items:center"><div><b style="color:#fff">📄 '+esc(order.code)+'</b> '+(isC?'<span style="background:#dcfce7;color:#16a34a;padding:2px 8px;border-radius:6px;font-size:10px;margin-left:8px">✅ Đã chốt</span>':'<span style="background:#fef9c3;color:#92400e;padding:2px 8px;border-radius:6px;font-size:10px;margin-left:8px">⏳</span>')+'</div><button id="xc" style="background:none;border:none;color:#fff;font-size:20px;cursor:pointer">✕</button></div><div style="padding:14px 18px">'
318
+ +'<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px;margin-bottom:10px;padding:10px;background:#f8fafc;border-radius:8px"><div><b>KH:</b> <input id="od-name" value="'+esc(order.customer||'')+'" style="border:1px solid #ddd;padding:3px 6px;border-radius:4px;width:55%;font-size:11px"></div><div><b>SĐT:</b> <input id="od-phone" value="'+esc(order.phone||'')+'" style="border:1px solid #ddd;padding:3px 6px;border-radius:4px;width:50%;font-size:11px"></div><div style="grid-column:1/-1"><b>ĐC:</b> <input id="od-addr" value="'+esc(order.addr||'')+'" style="border:1px solid #ddd;padding:3px 6px;border-radius:4px;width:85%;font-size:11px"></div></div>'
319
+ +'<div style="margin-bottom:10px;padding:8px 10px;background:#eff6ff;border-radius:8px;border:1px solid #bfdbfe;position:relative"><div style="font-size:10px;font-weight:700;color:#1e40af;margin-bottom:4px">👤 Mã KH</div><input id="od-makh" value="'+esc(order.ma_kh||'')+'" placeholder="Mã KH hoặc tên..." style="width:100%;padding:6px 10px;border:1px solid #93c5fd;border-radius:6px;font-size:11px;box-sizing:border-box"><div id="od-makh-suggest" style="position:absolute;left:10px;right:10px;top:58px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;max-height:100px;overflow-y:auto;display:none;z-index:10;box-shadow:0 4px 12px rgba(0,0,0,.1)"></div><div id="od-makh-info" style="font-size:9px;margin-top:3px;color:#64748b">'+(order.ma_kh?'✅ '+order.ma_kh:'')+'</div></div>'
320
+ +'<table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:8px"><thead><tr style="background:#003f62;color:#fff"><th style="padding:5px">#</th><th style="padding:5px">Ảnh</th><th style="padding:5px;text-align:left">SP</th><th style="padding:5px;text-align:left">Thông tin</th><th style="padding:5px">SL</th><th style="padding:5px;text-align:right">Giá niêm yết</th><th style="padding:5px;text-align:right">Giá CK</th><th style="padding:5px;text-align:right">TT</th><th></th></tr></thead><tbody>';
321
+ (order.items||[]).forEach(function(it,i){var ck=it.discPrice&&it.price&&it.discPrice<it.price;h+='<tr style="border-bottom:1px solid #f1f5f9"><td style="padding:3px;text-align:center">'+(i+1)+'</td><td style="padding:3px">'+(it.image?'<img src="'+it.image+'" style="width:40px;height:40px;object-fit:contain;border-radius:4px" referrerpolicy="no-referrer" onerror="this.style.display=\'none\'">':'')+'</td><td style="padding:3px"><b style="font-size:10px">'+esc((it.name||'').substring(0,28))+'</b><br><span style="font-size:9px;color:#64748b">'+esc(it.model||'')+'</span></td><td style="padding:3px;font-size:9px;color:#64748b;max-width:150px;white-space:normal">'+esc(orderItemInfo(it)).substring(0,260)+'</td><td style="padding:3px;text-align:center">'+(it.qty||1)+'</td><td style="padding:3px;text-align:right">'+fmt(it.price)+'</td><td style="padding:3px;text-align:right;'+(ck?'color:#dc3545;font-weight:700':'')+'">'+fmt(it.discPrice||it.price)+'</td><td style="padding:3px;text-align:right;font-weight:700">'+fmt(it.total)+'</td><td style="padding:3px"><button class="xd" data-i="'+i+'" style="background:#fee2e2;border:none;color:#dc2626;padding:1px 4px;border-radius:3px;cursor:pointer;font-size:9px">✕</button></td></tr>';});
322
+ h+='</tbody></table>';
323
+ if(order.fees&&order.fees.length){h+='<div style="padding:4px 10px;background:#fffbeb;border-radius:6px;margin-bottom:6px;font-size:10px">';order.fees.forEach(function(f){h+='<div style="display:flex;justify-content:space-between"><span style="color:#92400e">'+esc(f.label)+'</span><b>'+fmt(f.amount)+'</b></div>';});h+='</div>';}
324
+ h+='<div style="padding:8px 12px;background:#003f62;border-radius:6px;display:flex;justify-content:space-between;margin-bottom:6px"><span style="color:#fff;font-weight:800">TỔNG</span><span style="color:#f0b840;font-weight:900;font-size:15px">'+fmt(order.grandTotal)+'</span></div>';
325
+ if(order.deposit>0)h+='<div style="padding:4px 10px;background:#f0fdf4;border-radius:6px;font-size:10px;margin-bottom:6px"><span>Cọc: <b>'+fmt(order.deposit)+'</b></span> | <span>Còn: <b style="color:#dc2626">'+fmt(order.remaining)+'</b></span></div>';
326
+ h+='<div style="margin:8px 0;padding:8px;background:#f0f9ff;border-radius:8px;border:1px solid #bae6fd"><input id="od-prompt" value="'+esc(order.promptText||'')+'" placeholder="mh70btc ck 30%, CK 10, giao 200k, lắp 300k, cọc 5tr" style="width:100%;padding:6px;border:1px solid #e2e8f0;border-radius:6px;font-size:11px;box-sizing:border-box"><button id="od-apply" style="margin-top:4px;padding:4px 10px;background:#0369a1;color:#fff;border:none;border-radius:5px;font-weight:700;cursor:pointer;font-size:10px">⚡ Áp dụng</button></div>'
327
+ +'<label style="display:inline-flex;align-items:center;gap:5px;margin-top:8px;padding:6px 9px;background:#fff;color:#111;border:1px solid #111;border-radius:8px;font-weight:700;font-size:10px"><input id="od-bw" type="checkbox"> Trắng đen</label>'
328
+ +'<div style="display:flex;gap:5px;margin-top:8px;flex-wrap:wrap"><button id="od-add-sp" style="padding:7px 12px;background:#0891b2;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px">➕ Thêm SP</button><button id="od-save" style="padding:7px 12px;background:#7c3aed;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px">💾 Lưu</button><button id="od-pdf" style="padding:7px 10px;background:#db9815;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px">PDF</button><button id="od-xl" style="padding:7px 10px;background:#b45309;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px">Excel</button><button id="od-gh" style="padding:7px 10px;background:#0d9488;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px">GH PDF</button><button id="od-gh-xl" style="padding:7px 10px;background:#059669;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px">GH Excel</button>';
329
+ if(!isC)h+='<button id="od-confirm" style="padding:7px 14px;background:#16a34a;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:11px;margin-left:auto">✅ CHỐT</button>';
330
+ else h+='<button id="od-unconfirm" style="padding:7px 10px;background:#fef2f2;color:#dc2626;border:2px solid #fca5a5;border-radius:8px;font-weight:700;cursor:pointer;font-size:10px;margin-left:auto">↩️ Bỏ chốt</button>';
331
+ h+='</div>';
332
+ h+=renderSearchPanel();
333
+ h+='</div>';modal.innerHTML=h;
334
+ document.getElementById('xc').onclick=function(){ov.remove();};
335
+ modal.querySelectorAll('.xd').forEach(function(b){b.onclick=function(e){e.stopPropagation();order.items.splice(parseInt(this.dataset.i),1);render();};});
336
+ // Add SP button
337
+ var addSpBtn=document.getElementById('od-add-sp');
338
+ if(addSpBtn){addSpBtn.onclick=toggleSearchPanel;}
339
+ // Close search panel
340
+ var closeSpBtn=document.getElementById('od-sp-close');
341
+ if(closeSpBtn){closeSpBtn.onclick=function(){_searchOpen=false;render();};}
342
+ // Search input
343
+ var searchInput=document.getElementById('od-sp-search');
344
+ if(searchInput){searchInput.oninput=function(){renderSearchResults();};searchInput.onkeydown=function(e){if(e.key==='Escape'){_searchOpen=false;render();}};}
345
+ // Render search results if panel is open
346
+ if(_searchOpen){renderSearchResults();}
347
+ function cc(){order.customer=document.getElementById('od-name').value;order.phone=document.getElementById('od-phone').value;order.addr=document.getElementById('od-addr').value;}
348
+ var mI=document.getElementById('od-makh'),sB=document.getElementById('od-makh-suggest'),mInfo=document.getElementById('od-makh-info');
349
+ if(mI){mI.oninput=function(){var q=this.value.trim();if(q.length<1){sB.style.display='none';mInfo.textContent='';order.ma_kh='';return;}var ms=findKH(q);if(!ms.length){sB.style.display='none';mInfo.textContent='⚠️ Không tìm';order.ma_kh='';return;}var ex=ms.find(function(k){return k.ma_kh.toLowerCase()===q.toLowerCase();});if(ex){applyKH(ex);sB.style.display='none';return;}sB.style.display='block';sB.innerHTML=ms.slice(0,4).map(function(k){return'<div class="kh-opt" data-ma="'+esc(k.ma_kh)+'" style="padding:5px 8px;cursor:pointer;border-bottom:1px solid #f1f5f9;font-size:10px"><b>'+esc(k.ma_kh)+'</b> '+esc(k.ten)+'</div>';}).join('');sB.querySelectorAll('.kh-opt').forEach(function(o){o.onmousedown=function(e){e.preventDefault();var k=KH_LIST.find(function(x){return x.ma_kh===this.dataset.ma;}.bind(this));if(k){mI.value=k.ma_kh;applyKH(k);sB.style.display='none';}};});};mI.onblur=function(){setTimeout(function(){sB.style.display='none';},200);};}
350
+ function applyKH(kh){order.ma_kh=kh.ma_kh;var ap=[];(order.items||[]).forEach(function(it){var br=it.brand||(it.name||'').match(/(malloca|eurogold|grob|garis)/i);if(br){var bn=typeof br==='string'?br:br[1];var rate=getKHCKRate(kh,bn);if(rate!==null){it.discPrice=Math.round((it.price||0)*(1-rate/100));it.total=it.discPrice*(it.qty||1);ap.push(bn+' '+rate+'%');}}});order.discountPercent=0;recalcOrder(order);mInfo.innerHTML='✅ '+esc(kh.ma_kh)+' — '+esc(kh.ten)+(ap.length?' | '+ap.join(', '):'');render();}
351
+ document.getElementById('od-apply').onclick=function(){cc();var p=document.getElementById('od-prompt').value;order.promptText=p;var ps=parsePrompt(p);order.fees=ps.fees.length?ps.fees:(p.trim()?order.fees:[]);order.deposit=ps.deposit||0;if(ps.discountPercent)order.discountPercent=ps.discountPercent;order.itemDiscounts=ps.itemDiscounts||{};order.notes=ps.notes||[];recalcOrder(order);render();};
352
+ document.getElementById('od-save').onclick=function(){cc();var p=document.getElementById('od-prompt').value;if(p&&p!==order.promptText){order.promptText=p;var ps=parsePrompt(p);order.fees=ps.fees.length?ps.fees:[];order.deposit=ps.deposit||0;if(ps.discountPercent)order.discountPercent=ps.discountPercent;order.itemDiscounts=ps.itemDiscounts||{};order.notes=ps.notes||[];recalcOrder(order);}else{recalcOrder(order);}order.savedAt=new Date().toISOString();addOrder(order);syncOrderToBot(order);alert('✅ Lưu!');};
353
+ document.getElementById('od-pdf').onclick=function(){cc();recalcOrder(order);exportPDF(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
354
+ document.getElementById('od-xl').onclick=function(){cc();recalcOrder(order);exportExcel(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
355
+ document.getElementById('od-gh').onclick=function(){cc();recalcOrder(order);exportDeliveryPDF(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
356
+ document.getElementById('od-gh-xl').onclick=function(){cc();recalcOrder(order);exportDeliveryExcel(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
357
+ var cB=document.getElementById('od-confirm');if(cB)cB.onclick=function(){cc();recalcOrder(order);if(!confirm('Chốt đơn '+order.code+'?'))return;confirmOrder(order);if(typeof showToast==='function')showToast('✅ Đã chốt '+order.code);render();};
358
+ var uB=document.getElementById('od-unconfirm');if(uB)uB.onclick=function(){if(!confirm('Bỏ chốt?'))return;unconfirmOrder(order);render();};
359
+ }render();
360
+ }
361
+
362
+ function injectMaKHToQuote(){var modal=document.querySelector('.quote-overlay.open,.quote-modal.open,.quote-overlay[style*="block"],.quote-modal[style*="block"],[class*="quote"][class*="open"],[class*="quote"][style*="block"]');if(!modal)return;if(modal.querySelector('#vai-makh-section'))return;var pI=null;modal.querySelectorAll('input,textarea').forEach(function(el){var ph=(el.placeholder||'').toLowerCase();if(ph.includes('yêu cầu')||ph.includes('chiết khấu')||ph.includes('chiet khau'))pI=el;});if(!pI)return;
363
+ var sec=document.createElement('div');sec.id='vai-makh-section';sec.style.cssText='margin:8px 16px;padding:8px 10px;background:#eff6ff;border-radius:8px;border:1px solid #bfdbfe;position:relative';
364
+ sec.innerHTML='<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px"><span style="font-size:10px;font-weight:700;color:#1e40af">👤 Mã KH</span><span id="vai-makh-badge" style="font-size:9px;color:#64748b"></span></div><input id="vai-makh-input" placeholder="Mã KH hoặc tên..." style="width:100%;padding:6px 10px;border:1px solid #93c5fd;border-radius:6px;font-size:11px;box-sizing:border-box"><div id="vai-makh-dropdown" style="position:absolute;left:10px;right:10px;top:54px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;max-height:100px;overflow-y:auto;display:none;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,.1)"></div>';
365
+ pI.parentElement.insertBefore(sec,pI);
366
+ var input=document.getElementById('vai-makh-input'),dd=document.getElementById('vai-makh-dropdown'),badge=document.getElementById('vai-makh-badge');
367
+ input.oninput=function(){var q=this.value.trim();if(q.length<1){dd.style.display='none';badge.textContent='';return;}loadKhachHang().then(function(){var ms=findKH(q);if(!ms.length){dd.style.display='none';badge.textContent='⚠️';badge.style.color='#dc2626';return;}var ex=ms.find(function(k){return k.ma_kh.toLowerCase()===q.toLowerCase();});if(ex){dd.style.display='none';badge.innerHTML='✅ '+esc(ex.ten);badge.style.color='#16a34a';applyKHToQuoteModal(ex);return;}dd.style.display='block';dd.innerHTML=ms.slice(0,4).map(function(k){return'<div class="vai-kh-opt" data-ma="'+esc(k.ma_kh)+'" style="padding:5px 8px;cursor:pointer;border-bottom:1px solid #f1f5f9;font-size:10px"><b>'+esc(k.ma_kh)+'</b> '+esc(k.ten)+'</div>';}).join('');dd.querySelectorAll('.vai-kh-opt').forEach(function(o){o.onmousedown=function(e){e.preventDefault();var k=KH_LIST.find(function(x){return x.ma_kh===this.dataset.ma;}.bind(this));if(k){input.value=k.ma_kh;dd.style.display='none';badge.innerHTML='✅ '+esc(k.ten);badge.style.color='#16a34a';applyKHToQuoteModal(k);}};});});};
368
+ input.onblur=function(){setTimeout(function(){dd.style.display='none';},200);};
369
+ }
370
+ function applyKHToQuoteModal(kh){var codes=kh.ck_codes||[];if(!codes.length)return;var pI=null;document.querySelectorAll('input,textarea').forEach(function(el){var ph=(el.placeholder||'').toLowerCase();if(ph.includes('yêu cầu')||ph.includes('chiết khấu')||ph.includes('chiet khau'))pI=el;});if(!pI)return;var ckVal=null;for(var i=0;i<codes.length;i++){var m=codes[i].match(/ck(\d+)/i);if(m){ckVal=parseInt(m[1]);break;}}if(!ckVal)return;var cur=pI.value||'';cur=cur.replace(/^(ck|chiết khấu|chiet khau|giảm|giam|discount)\s*[\d.,]+\s*%?\s*[,;]?\s*/gim,'').trim();pI.value=('CK '+ckVal+(cur?', '+cur:'')).trim();pI.dispatchEvent(new Event('input',{bubbles:true}));}
371
+
372
+ var _mO=new MutationObserver(function(){setTimeout(injectMaKHToQuote,500);});_mO.observe(document.body,{childList:true,subtree:true,attributes:true,attributeFilter:['class','style']});setInterval(injectMaKHToQuote,3000);
373
+
374
+ window.VAI_ORDERS={save:saveCurrentOrder,search:searchOrders,openSearch:openSearchModal,openDetail:openOrderDetail,getAll:getOrders,getByCode:function(c){return getOrders().find(function(o){return o.code===c;});},add:addOrder,delete:deleteOrder,confirm:confirmOrder,unconfirm:unconfirmOrder,toVAIData:orderToVAIData,loadKH:loadKhachHang,findKH:findKH,processQueue:processQueue,syncFromServer:syncFromServer};
375
+ var qLen=getQueue().length;
376
+ console.log('[OS v19.2] '+getOrders().length+' orders'+(qLen?' | ⏳ '+qLen+' pending sync':'')+' | ☁️ Server sync enabled | ➕ Add product to order enabled | 🛠️ Fixed CK/price lines in fees | ⏱️ AbortController 8s | 🚫 Skip BH/giao thứ X notes');
377
+ })();
product_seo_min.json ADDED
The diff for this file is too large to render. See raw diff
 
products_with_slugs.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:990b946fdc9525584fc7f3022281bd3e1f44ce86b949ade6e5278dfd91df8302
3
+ size 32971353
qr-payment.js ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* V.AI STUDIO — QR Payment / Quote Data v1037.5
2
+ * FIX v1037.5: Khôi phục phụ phí - mọi text có số đều thành Phụ phí, ko đẩy vào notes
3
+ * FIX v1037.4: Skip note patterns (BẢO HÀNH X NĂM, giao thứ X) khỏi fees/phụ phí
4
+ * FIX v1037.3: Thêm CORS proxy fallback wsrv.nl cho ảnh trong Excel
5
+ * FIX v1037.2: parsePrompt() extract brand discount, formula =G*0.65
6
+ * FIX v1037.1: Loại bỏ dòng CK/giá trị số (35đ, 50đ) khỏi fees
7
+ * Restored from 9db23f9 (v24) — full Excel/PDF export
8
+ */
9
+ (function(){
10
+ 'use strict';
11
+ var BANK_ID='vib',BANK_ACCOUNT='918258385',BANK_NAME='TRAN QUOC VUONG',BANK_NAME_VN='Trần Quốc Vương';
12
+ function stripDiacritics(s){return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'D');}
13
+ function generateOrderCode(name){var now=new Date(),dd=String(now.getDate()).padStart(2,'0'),mm=String(now.getMonth()+1).padStart(2,'0'),yy=String(now.getFullYear()).slice(-2),ini='';if(name&&name.trim()){name.trim().split(/\s+/).forEach(function(w){if(w){var ch=stripDiacritics(w.charAt(0)).toUpperCase();if(/[A-Z]/.test(ch))ini+=ch;}});}return 'VAS'+(ini||'X')+dd+mm+yy;}
14
+ function getCustomerName(){var sels=['#quoteName','#quoteCustomerName','#checkoutName','.quote-field input:first-of-type'];for(var i=0;i<sels.length;i++){var el=document.querySelector(sels[i]);if(el&&el.value&&el.value.trim().length>1)return el.value.trim();}return '';}
15
+ function getEffectiveOrderCode(){var n=getCustomerName();if(n)return generateOrderCode(n);var h=window.location.hash;if(h&&h.length>1){var c=h.substring(1);if(/^VAS[A-Z0-9]{3,}$/.test(c))return c;}return generateOrderCode('');}
16
+ function getQRUrl(a,c){return 'https://img.vietqr.io/image/'+BANK_ID+'-'+BANK_ACCOUNT+'-compact2.png?amount='+(Math.round(a)||0)+'&addInfo='+encodeURIComponent(c||'VAISTUDIO')+'&accountName='+encodeURIComponent(BANK_NAME);}
17
+ function fmt(n){if(!n||isNaN(n))return '0đ';return Number(n).toLocaleString('vi-VN')+'đ';}
18
+ function vaiIsDimensionKey(k,v){
19
+ var txt=stripDiacritics(String(k||'')+' '+String(v||'')).toLowerCase();
20
+ return /kich thuoc|dimension|size|rong|ngang|dai|sau|cao|height|width|depth|cut.?out|cat da|lo da|lap noi|lap am|am ban|lot long|phu bi|khoang tu|mat canh|quy cach|r\s*[x=]|w\s*[x=]|d\s*[x=]|h\s*[x=]/.test(txt);
21
+ }
22
+ function vaiDimensionSummary(specs,limit){
23
+ limit=limit||900;var parts=[],seen={};
24
+ function add(k,v){if(!k||v==null||v==='')return;var t=String(k)+': '+String(v);var key=compactKey(t);if(seen[key])return;seen[key]=1;parts.push(t);}
25
+ if(specs&&typeof specs==='object'){
26
+ var entries=Object.entries(specs);entries.filter(function(e){return vaiIsDimensionKey(e[0],e[1]);}).forEach(function(e){add(e[0],e[1]);});
27
+ entries.filter(function(e){return !vaiIsDimensionKey(e[0],e[1]);}).slice(0,8).forEach(function(e){add(e[0],e[1]);});
28
+ }else if(specs){String(specs).split(/[;\n]+/).filter(Boolean).sort(function(a,b){return (vaiIsDimensionKey(b,'')?1:0)-(vaiIsDimensionKey(a,'')?1:0);}).forEach(function(x){var key=compactKey(x);if(!seen[key]){seen[key]=1;parts.push(x.trim());}});}
29
+ return parts.join('; ').slice(0,limit);
30
+ }
31
+ function vaiProductInfoForQuote(product){
32
+ if(!product)return '';
33
+ return vaiDimensionSummary(product.specs||{},900)||String(product.summary||product.desc||'').slice(0,900);
34
+ }
35
+
36
+ function compactKey(s){return stripDiacritics(String(s||'')).toLowerCase().replace(/[^a-z0-9]/g,'');}
37
+ function itemKeys(it){var arr=[];['model','sku','code','ma','name'].forEach(function(k){if(it&&it[k])arr.push(compactKey(it[k]));});if(it&&it.name){String(it.name).split('|').forEach(function(x){arr.push(compactKey(x));});}return arr.filter(function(x){return x&&x.length>=3;});}
38
+ function matchItemDiscount(it,itemDiscounts){
39
+ itemDiscounts=itemDiscounts||{};var keys=itemKeys(it);
40
+ for(var i=0;i<keys.length;i++){var k=keys[i];if(itemDiscounts[k])return itemDiscounts[k];}
41
+ for(var code in itemDiscounts){for(var j=0;j<keys.length;j++){var kk=keys[j];if(code.length>=3&&kk.length>=3&&(kk.indexOf(code)!==-1||code.indexOf(kk)!==-1))return itemDiscounts[code];}}
42
+ var brandKeys=Object.keys(itemDiscounts).filter(function(k){return !k.match(/\d[A-Za-z0-9._\/-]*$/);});
43
+ if(brandKeys.length>0){
44
+ var itemBrand='';var idx=(it.idx!=null?it.idx:(it.productIdx!=null?it.productIdx:null));
45
+ if(it.brand){itemBrand=String(it.brand).toLowerCase().replace(/[^a-z0-9]/g,'');}
46
+ else if(idx!=null&&window.D&&window.D[idx]&&window.D[idx].brand){itemBrand=String(window.D[idx].brand).toLowerCase().replace(/[^a-z0-9]/g,'');}
47
+ else if(window.D){var ck=compactKey(it.model||it.sku||it.ma||it.name||'');for(var di=0;di<window.D.length;di++){var dck=compactKey(window.D[di].model||window.D[di].sku||window.D[di].name||'');if(dck&&ck&&(dck.indexOf(ck)>=0||ck.indexOf(dck)>=0)&&window.D[di].brand){itemBrand=String(window.D[di].brand).toLowerCase().replace(/[^a-z0-9]/g,'');break;}}}
48
+ if(itemBrand){for(var bi=0;bi<brandKeys.length;bi++){var cleanBk=brandKeys[bi].replace(/[^a-z0-9]/g,'');if(cleanBk&&itemBrand&&(itemBrand.indexOf(cleanBk)>=0||cleanBk.indexOf(itemBrand)>=0))return itemDiscounts[brandKeys[bi]];}}
49
+ }
50
+ return 0;
51
+ }
52
+ function parseItemDiscountLine(line,map){
53
+ var lo=stripDiacritics(String(line||'')).toLowerCase();
54
+ var re1=/\b([a-z]{1,8}[a-z0-9._\-\/]{1,30}\d[a-z0-9._\-\/]*)\b\s*(?:[:=\-]|\s+)?\s*(?:ck|chiet\s*khau|giam|discount)\s*([\d.,]+)\s*%?/gi;
55
+ var re2=/(?:ck|chiet\s*khau|giam|discount)\s*\b([a-z]{1,8}[a-z0-9._\-\/]{1,30}\d[a-z0-9._\-\/]*)\b\s*([\d.,]+)\s*%?/gi;
56
+ var m;
57
+ while((m=re1.exec(lo))!==null){var v=parseFloat(m[2].replace(/\./g,'').replace(',','.'));if(v>0&&v<=100)map[compactKey(m[1])]=v;}
58
+ while((m=re2.exec(lo))!==null){var v2=parseFloat(m[2].replace(/\./g,'').replace(',','.'));if(v2>0&&v<=100)map[compactKey(m[1])]=v2;}
59
+ }
60
+ function applyPromptDiscounts(qd,parsed){
61
+ if(!qd||!qd.items)return qd;var productTotal=0;var itemDiscounts=(parsed&&parsed.itemDiscounts)||{};var globalPct=Number((parsed&&parsed.discountPercent)||0);
62
+ qd.items.forEach(function(it){var base=Number(it.price||0),qty=Number(it.qty||1);var pct=matchItemDiscount(it,itemDiscounts);if(pct>0){it.discPrice=Math.round(base*(1-pct/100));it.discountPercent=pct;}else if(globalPct>0&&globalPct<=100){it.discPrice=Math.round(base*(1-globalPct/100));it.discountPercent=globalPct;}else if(!it.discPrice){it.discPrice=base;}it.total=Number(it.discPrice||base)*qty;productTotal+=Number(it.total||0);});
63
+ qd.grandTotal=productTotal;return qd;
64
+ }
65
+
66
+ function parsePrompt(text){
67
+ var fees=[],notes=[],discount=null,deposit=0,discountPercent=0,itemDiscounts={};
68
+ if(!text)return{fees:fees,notes:notes,discount:discount,deposit:deposit,discountPercent:discountPercent,itemDiscounts:itemDiscounts};
69
+ text.split(/[,;\n]+/).forEach(function(line){
70
+ line=line.trim();if(!line)return;
71
+ parseItemDiscountLine(line,itemDiscounts);
72
+ if(Object.keys(itemDiscounts).length&&line.match(/\b[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d[A-Za-z0-9._\-\/]*\b/i)&&line.match(/\b(ck|chiết khấu|chiet khau|giảm|giam|discount)\b/i))return;
73
+ var brandCkMatch=line.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*(?:ck|chiet|chiet khau|giam|discount|chiết khấu)\s*([\d.,]+)/i);
74
+ if(brandCkMatch){var brandName=brandCkMatch[1].trim().toLowerCase().replace(/[^a-z0-9]/g,'');var pct=parseFloat(brandCkMatch[2].replace(/\./g,'').replace(',','.'));if(pct>0&&pct<=100)itemDiscounts[brandName]=pct;notes.push(line);return;}
75
+ if(line.match(/^(ck|chiết khấu|chiet khau|giảm|giam|discount)/i)){
76
+ discount=line;
77
+ var ckm=line.match(/([\d.,]+)\s*(%)?/);
78
+ if(ckm){var v=parseFloat(ckm[1].replace(/\./g,'').replace(',','.'));if(v>0&&v<=100)discountPercent=v;}
79
+ return;
80
+ }
81
+ if(line.match(/cọc|coc|deposit/i)){
82
+ var dm=line.match(/([\d.,]+)\s*(k|K|nghìn|nghin|triệu|tr|đ|d|vnd)?/);
83
+ if(dm){var da=parseFloat(dm[1].replace(/\./g,'').replace(',','.'));var du=(dm[2]||'').toLowerCase();if(du==='k'||du==='nghìn'||du==='nghin')da*=1000;if(du==='triệu'||du==='tr')da*=1000000;deposit=da;}
84
+ return;
85
+ }
86
+ if(line.match(/^[\d.,]+$/)){notes.push(line);return;}
87
+ var priceUnit=line.match(/^[\d.,]+\s*(k|nghìn|nghin|tr|triệu|m|%|đ|d|vnd)?$/i);
88
+ if(priceUnit&&line.replace(/[\d.,\s%\knhìnghintrriệuđdvnđvndKHTRML]/gi,'').trim().length<2){notes.push(line);return;}
89
+ var brandNum=line.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*[\d.,]/i);
90
+ if(brandNum){var textAfterBrand=line.replace(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)/i,'').trim();if(textAfterBrand.match(/^[\d.,\s%(k|nghìn|nghin|tr|triệu|m|đ|d|vnd)]+$/i)){notes.push(line);return;}}
91
+ // === FIX v1037.5: Skip note patterns nhưng KHÔNG chặn fee detection ===
92
+ var lo_check=stripDiacritics(line).toLowerCase();
93
+ var isNotePattern=false;
94
+ // Bảo hành X năm/tháng là ghi chú, ko phải phụ phí
95
+ if(lo_check.match(/bao hanh\s*\d+\s*(nam|thang)/)){notes.push(line);isNotePattern=true;}
96
+ // Giao thứ X / giao ngày X là lịch giao hàng, ko phải phí
97
+ if(!isNotePattern&&lo_check.match(/giao\s+(thu|ngay)\s*\d+/)){notes.push(line);isNotePattern=true;}
98
+ // Các ghi chú bảo hành khác
99
+ if(!isNotePattern&&(lo_check.match(/\b(nam|thang)\s*(bao hanh|bh)/)||lo_check.match(/bao hanh\s+chinh\s*hang/))){notes.push(line);isNotePattern=true;}
100
+ // Nếu đã là note pattern thì skip không xử lý fee nữa
101
+ if(isNotePattern)return;
102
+ // === End note skip ===
103
+ var m=line.match(/([\d.,]+)\s*(k|K|nghìn|nghin|triệu|tr|đ|d|vnd)?/);
104
+ if(m){var amt=parseFloat(m[1].replace(/\./g,'').replace(',','.'));var u=(m[2]||'').toLowerCase();if(u==='k'||u==='nghìn'||u==='nghin')amt*=1000;if(u==='triệu'||u==='tr')amt*=1000000;if(amt<=0){notes.push(line);return;}var lb=line.replace(m[0],'').trim();if(lb.match(/^(phi\s*giao|giao\s+hàng|ship)/i))lb='Phí giao hàng';else if(lb.match(/^(phi\s*lap|lap\s*dat|install)/i))lb='Phí lắp đặt';else if(lb.match(/^(phi\s*boc|bốc|boc|xế.p|xep|vác)/i))lb='Phí bốc xếp';else if(lb.match(/^(coc|dat.coc|deposit)/i))lb='Tiền cọc';else if(!lb||lb.length<2)lb='Phụ phí';else lb='Phụ phí';fees.push({label:lb,amount:amt});}
105
+ else{notes.push(line);}
106
+ });
107
+ return{fees:fees,notes:notes,discount:discount,deposit:deposit,discountPercent:discountPercent,itemDiscounts:itemDiscounts};
108
+ }
109
+ var _lastPromptText='';
110
+ function getPromptText(){
111
+ var direct=document.getElementById('qcAiPrompt');
112
+ if(direct){var dv=(direct.value||'').trim();if(dv&&dv.toUpperCase().replace(/[\s.]/g,'')!=='VAISTUDIO'){_lastPromptText=dv;return dv;}}
113
+ var inputs=document.querySelectorAll('input,textarea');
114
+ for(var j=0;j<inputs.length;j++){var ph=(inputs[j].placeholder||'').toLowerCase();if(ph.includes('yêu cầu')||ph.includes('chiết khấu')||ph.includes('yeu cau')||ph.includes('chiet khau')||ph.includes('ck')||ph.includes('giao hàng')){var val=(inputs[j].value||'').trim();if(!val||val.toUpperCase().replace(/[\s.]/g,'')==='VAISTUDIO')continue;_lastPromptText=val;return val;}}
115
+ return _lastPromptText;
116
+ }
117
+ function getData(){
118
+ var qd=window._origGetQuoteData?window._origGetQuoteData():getQuoteData();
119
+ var parsed=parsePrompt(getPromptText());
120
+ qd=applyPromptDiscounts(qd,parsed);
121
+ if(qd&&qd.items){qd.items.forEach(function(it){if(!it.specs||String(it.specs).length<40){var idx=(it.idx!=null?it.idx:(it.productIdx!=null?it.productIdx:null));var p=(idx!=null&&window.D)?window.D[idx]:null;if(!p&&window.D){var ck=compactKey(it.model||it.sku||it.ma||it.name||'');p=(window.D||[]).find(function(x){return compactKey(x.model||x.sku||x.name||'').indexOf(ck)>=0||ck.indexOf(compactKey(x.model||x.sku||''))>=0;});}it.specs=vaiProductInfoForQuote(p)||it.specs||'';}});}
122
+ var surcharge=0;parsed.fees.forEach(function(f){surcharge+=f.amount;});
123
+ var grandTotal=(qd.grandTotal||0)+surcharge;
124
+ var remaining=grandTotal-parsed.deposit;
125
+ return{qd:qd,fees:parsed.fees,notes:parsed.notes,discount:parsed.discount,discountPercent:parsed.discountPercent,itemDiscounts:parsed.itemDiscounts||{},deposit:parsed.deposit,productTotal:qd.grandTotal||0,grandTotal:grandTotal,remaining:remaining>0?remaining:0};
126
+ }
127
+
128
+ // === WEB MODAL INJECTION ===
129
+ function injectToModal(){
130
+ var modal=document.querySelector('.quote-overlay.open,.quote-modal.open,.quote-overlay[style*="block"],.quote-modal[style*="block"],[class*="quote"][class*="open"],[class*="quote"][style*="block"]');
131
+ if(!modal){var btns=document.querySelectorAll('button,a');for(var b=0;b<btns.length;b++){if((btns[b].textContent||'').match(/xuất|export|chia sẻ|share/i)){modal=btns[b].closest('[class*="overlay"],[class*="modal"],[class*="popup"]');if(modal)break;}}}
132
+ if(!modal)return;
133
+ var old=modal.querySelector('.vai-extra-section');if(old)old.remove();
134
+ var parsed=parsePrompt(getPromptText());
135
+ if(!parsed.fees.length&&!parsed.deposit)return;
136
+ var surcharge=0;parsed.fees.forEach(function(f){surcharge+=f.amount;});
137
+ var qd=window._origGetQuoteData?window._origGetQuoteData():(typeof getQuoteData==='function'?getQuoteData():{grandTotal:0});
138
+ var grandTotal=(qd.grandTotal||0)+surcharge;
139
+ var remaining=grandTotal-parsed.deposit;
140
+ var sec=document.createElement('div');sec.className='vai-extra-section';
141
+ sec.style.cssText='margin:10px 0;padding:0 16px;font-size:13px';
142
+ var html='';
143
+ if(parsed.fees.length){
144
+ html+='<div style="padding:8px 12px;background:#fffbeb;border-radius:6px;border:1px solid #fde68a;margin-bottom:8px"><div style="font-weight:700;color:#92400e;font-size:12px;margin-bottom:4px">PHỤ PHÍ</div>';
145
+ parsed.fees.forEach(function(f){html+='<div style="display:flex;justify-content:space-between;padding:2px 0"><span>'+f.label+'</span><span style="font-weight:700">'+fmt(f.amount)+'</span></div>';});
146
+ html+='</div>';
147
+ }
148
+ html+='<div style="padding:8px 12px;background:#003f62;border-radius:6px;display:flex;justify-content:space-between;align-items:center"><span style="color:#fff;font-weight:800">TỔNG CỘNG</span><span style="color:#f0b840;font-weight:900;font-size:16px">'+fmt(grandTotal)+'</span></div>';
149
+ if(parsed.deposit>0){
150
+ html+='<div style="margin-top:6px;padding:6px 12px;background:#f0fdf4;border-radius:6px;border:1px solid #86efac">';
151
+ html+='<div style="display:flex;justify-content:space-between"><span>Đã cọc</span><span style="font-weight:700;color:#166534">'+fmt(parsed.deposit)+'</span></div>';
152
+ html+='<div style="display:flex;justify-content:space-between;margin-top:2px"><span style="font-weight:700">Còn lại</span><span style="font-weight:900;color:#dc2626;font-size:15px">'+fmt(remaining>0?remaining:0)+'</span></div>';
153
+ html+='</div>';
154
+ }
155
+ sec.innerHTML=html;
156
+ var actions=modal.querySelector('.quote-actions,[class*="actions"],[class*="button"]');
157
+ if(actions&&actions.parentNode){actions.parentNode.insertBefore(sec,actions);}else{modal.appendChild(sec);}
158
+ }
159
+ var _obs=new MutationObserver(function(){setTimeout(injectToModal,300);});
160
+ _obs.observe(document.body,{attributes:true,childList:true,subtree:true});
161
+ setInterval(injectToModal,1500);
162
+ document.addEventListener('input',function(e){var ph=(e.target.placeholder||'').toLowerCase();var id=e.target.id||'';if(ph.includes('yêu cầu')||ph.includes('chiết khấu')||ph.includes('yeu cau')||ph.includes('chiet khau')||ph.includes('ck')||ph.includes('giao hàng')||id==='qcAiPrompt'){var val=(e.target.value||'').trim();if(val&&val.toUpperCase().replace(/[\s.]/g,'')!=='VAISTUDIO')_lastPromptText=val;setTimeout(injectToModal,200);}},true);
163
+
164
+ // === HTML BÁO GIÁ (PDF/Image) ===
165
+ function buildQuoteHTML(d,code,qrUrl){
166
+ var qd=d.qd;
167
+ var rows=qd.items.map(function(it){return '<tr style="border-bottom:1px solid #e2e8f0"><td style="padding:6px 4px;text-align:center;font-size:11px">'+(it.stt||'')+'</td><td style="padding:6px 4px"><img src="'+(it.image||'')+'" style="width:50px;height:50px;object-fit:contain;border-radius:4px" crossorigin="anonymous" onerror="this.style.display=\'none\'"></td><td style="padding:6px 4px;font-size:11px;font-weight:600">'+(it.name||'')+'</td><td style="padding:6px 4px;text-align:center;font-size:10px">'+(it.model||'')+'</td><td style="padding:6px 4px;font-size:9px;color:#64748b">'+(it.specs||'')+'</td><td style="padding:6px 4px;text-align:center">'+(it.qty||'')+'</td><td style="padding:6px 4px;text-align:right;font-size:11px">'+(it.price?fmt(it.price):'')+'</td><td style="padding:6px 4px;text-align:right;font-size:11px;'+(it.discPrice&&it.discPrice<it.price?'color:#dc3545;font-weight:700':'')+'">'+(it.discPrice?fmt(it.discPrice):'')+'</td><td style="padding:6px 4px;text-align:right;font-size:11px;font-weight:700">'+(it.total?fmt(it.total):'')+'</td><td style="padding:6px 4px;font-size:9px">'+(it.note||'')+'</td></tr>';}).join('');
168
+ var feeRows='';if(d.fees&&d.fees.length){d.fees.forEach(function(f){feeRows+='<tr style="background:#fffbeb"><td colspan="8" style="padding:5px 10px;font-size:11px;color:#92400e;border-bottom:1px solid #fde68a">⊕ '+f.label+'</td><td style="padding:5px 4px;text-align:right;font-size:11px;font-weight:700;color:#92400e;border-bottom:1px solid #fde68a">'+fmt(f.amount)+'</td><td style="border-bottom:1px solid #fde68a"></td></tr>';});}
169
+ var depositHtml='';if(d.deposit>0){depositHtml='<div style="margin-top:8px;padding:8px 14px;background:#f0fdf4;border-radius:6px;border:1px solid #86efac;font-size:12px"><div style="display:flex;justify-content:space-between"><span>Đã cọc</span><span style="font-weight:700;color:#166534">'+fmt(d.deposit)+'</span></div><div style="display:flex;justify-content:space-between;margin-top:3px"><span style="font-weight:800">Còn lại</span><span style="font-weight:900;color:#dc2626;font-size:14px">'+fmt(d.remaining)+'</span></div></div>';}
170
+ var notesHtml='';if(d.notes&&d.notes.length){notesHtml='<div style="margin-top:8px;padding:6px 12px;font-size:10px;color:#166534;background:#f0fdf4;border-radius:6px"><b>📝 Ghi chú:</b> '+d.notes.join('; ')+'</div>';}
171
+ var totalQty=(qd.items||[]).reduce(function(s,it){return s+Number(it.qty||1);},0);
172
+ return '<div style="display:flex;align-items:center;gap:10px;margin-bottom:4px"><img src="https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_200.png" style="width:40px;height:40px;object-fit:contain" crossorigin="anonymous"><div style="font-size:9px;color:#64748b;font-style:italic">❝ Niềm tin khách hàng là tài sản của chúng tôi ❞</div></div><div style="text-align:center;margin-bottom:12px"><div style="font-size:20px;font-weight:900;color:#db9815">BẢNG BÁO GIÁ</div></div><div style="display:flex;justify-content:space-between;margin-bottom:12px;font-size:11px"><div><b>Khách hàng:</b> '+(qd.customer.name||'')+'<br><b>SĐT:</b> '+(qd.customer.phone||'')+'<br><b>Email:</b> '+(qd.customer.email||'')+'<br><b>Địa chỉ:</b> '+(qd.customer.addr||'')+'</div><div style="text-align:right"><b>Mã đơn:</b> '+code+'<br><b>Ngày:</b> '+(qd.customer.date||'')+'</div></div><table style="width:100%;border-collapse:collapse"><thead><tr style="background:#003f62;color:#fff"><th style="padding:7px 3px;font-size:9px">STT</th><th style="padding:7px 3px;font-size:9px">Hình</th><th style="padding:7px 3px;font-size:9px;text-align:left">Tên SP</th><th style="padding:7px 3px;font-size:9px">Mã</th><th style="padding:7px 3px;font-size:9px">Thông tin</th><th style="padding:7px 3px;font-size:9px">SL</th><th style="padding:7px 3px;font-size:9px">Đơn giá</th><th style="padding:7px 3px;font-size:9px">Giá CK</th><th style="padding:7px 3px;font-size:9px">Thành tiền</th><th style="padding:7px 3px;font-size:9px">Ghi chú</th></tr></thead><tbody>'+rows+feeRows+'</tbody></table><div style="margin-top:0;padding:10px 14px;background:#003f62;display:grid;grid-template-columns:1fr 90px 180px;gap:10px;align-items:center"><span style="color:#fff;font-size:13px;font-weight:800">TỔNG CỘNG</span><span style="color:#fff;font-size:13px;font-weight:900;text-align:center">SL: '+totalQty+'</span><span style="color:#f0b840;font-size:17px;font-weight:900;text-align:right">'+fmt(d.grandTotal)+'</span></div>'+depositHtml+notesHtml+'<div style="display:flex;align-items:flex-start;gap:14px;margin-top:14px;padding:12px;background:#f8fafc;border-radius:8px"><img src="'+qrUrl+'" style="width:115px;height:115px;border-radius:6px;border:2px solid #e2e8f0" crossorigin="anonymous"><div style="font-size:10px;line-height:1.7"><div style="font-weight:700;color:#003f62;margin-bottom:3px">💳 CHUYỂN KHOẢN</div>🏦 VIB | 👤 '+BANK_NAME_VN+'<br>🔢 STK: <b>'+BANK_ACCOUNT+'</b><br>💰 <b>'+fmt(d.deposit>0?d.remaining:d.grandTotal)+'</b> | 📝 <b>'+code+'</b></div></div>';
173
+ }
174
+
175
+ function buildDeliveryHTML(qd,code){
176
+ var rows=(qd.items||[]).map(function(it,i){return '<tr style="border-bottom:1px solid #e2e8f0"><td style="padding:8px 4px;text-align:center;font-size:12px">'+(it.stt||(i+1))+'</td><td style="padding:8px 4px;font-weight:600;font-size:12px">'+(it.name||'')+'</td><td style="padding:8px 4px;text-align:center;font-size:11px">'+(it.model||'')+'</td><td style="padding:8px 4px;text-align:center;font-weight:700;font-size:13px">'+(it.qty||1)+'</td><td style="padding:8px 4px;font-size:11px">'+(it.note||'')+'</td></tr>';}).join('');
177
+ return '<div style="text-align:center;margin-bottom:14px"><div style="font-size:20px;font-weight:900;color:#003f62">PHIẾU GIAO HÀNG</div></div><div style="margin-bottom:12px;font-size:11px;line-height:1.6"><b>Khách hàng:</b> '+(qd.customer?qd.customer.name:'')+'&nbsp;|&nbsp;<b>SĐT:</b> '+(qd.customer?qd.customer.phone:'')+'<br><b>Địa chỉ:</b> '+(qd.customer?qd.customer.addr||'':'')+'<br><b>Mã đơn:</b> '+code+'&nbsp;|&nbsp;<b>Ngày:</b> '+(qd.customer?qd.customer.date||'':'')+'</div><table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#003f62;color:#fff"><th style="padding:8px 4px;width:35px">STT</th><th style="padding:8px 4px;text-align:left">Tên sản phẩm</th><th style="padding:8px 4px;width:90px">Mã SP</th><th style="padding:8px 4px;width:40px">SL</th><th style="padding:8px 4px;text-align:left;width:120px">Ghi chú</th></tr></thead><tbody>'+rows+'</tbody></table><div style="margin-top:60px;display:flex;justify-content:space-around;font-size:12px"><div style="text-align:center"><b>Bên giao</b><br><span style="font-size:9px;color:#64748b">(Ký, họ tên)</span><div style="margin-top:45px">_______________</div></div><div style="text-align:center"><b>Bên nhận</b><br><span style="font-size:9px;color:#64748b">(Ký, họ tên)</span><div style="margin-top:45px">_______________</div></div></div>';
178
+ }
179
+
180
+ async function toCanvas(html,w){
181
+ var div=document.createElement('div');
182
+ div.style.cssText='position:absolute;left:-10000px;top:0;width:'+(w||1000)+'px;background:#fff;padding:28px;font-family:Inter,Arial,DejaVu Sans,sans-serif;z-index:-1;box-sizing:border-box';
183
+ div.innerHTML=html;document.body.appendChild(div);
184
+ try{if(document.fonts&&document.fonts.ready)await document.fonts.ready;}catch(e){}
185
+ var imgs=Array.from(div.querySelectorAll('img'));
186
+ await Promise.all(imgs.map(function(img){return new Promise(function(res){if(img.complete)return res();img.onload=img.onerror=function(){res();};setTimeout(res,2500);});}));
187
+ await new Promise(function(r){setTimeout(r,500);});
188
+ var h=Math.max(div.scrollHeight,div.offsetHeight);
189
+ var cv=await html2canvas(div,{scale:2,useCORS:true,allowTaint:false,backgroundColor:'#fff',width:w||1000,height:h,windowWidth:w||1000,windowHeight:h,scrollX:0,scrollY:0});
190
+ div.remove();return cv;
191
+ }
192
+
193
+ function doOverride(){
194
+ if(typeof getQuoteData!=='function'||typeof html2canvas!=='function')return false;
195
+ window._origGetQuoteData=getQuoteData;
196
+ window.shareQuoteImage=async function(){var d=getData(),code=getEffectiveOrderCode(),qr=getQRUrl(d.deposit>0?d.remaining:d.grandTotal,code);try{var cv=await toCanvas(buildQuoteHTML(d,code,qr),1000);var blob=await new Promise(function(r){cv.toBlob(r,'image/png');});var f=new File([blob],code+'.png',{type:'image/png'});if(navigator.share&&navigator.canShare&&navigator.canShare({files:[f]})){await navigator.share({title:code,files:[f]});}else{var url=URL.createObjectURL(blob);var p=document.createElement('div');p.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:999;display:flex;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)';p.innerHTML='<div style="background:#fff;border-radius:16px;max-width:90vw;max-height:85vh;overflow:auto;padding:16px"><img src="'+url+'" style="max-width:100%;border-radius:8px"><div style="display:flex;gap:8px;margin-top:12px;justify-content:center"><a href="'+url+'" download="'+code+'.png" style="padding:10px 20px;background:#003f62;color:#fff;border-radius:8px;font-weight:700;text-decoration:none">⬇ Tải ảnh</a><button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:10px 20px;background:#e2e8f0;border:none;border-radius:8px;font-weight:700;cursor:pointer">Đóng</button></div></div>';document.body.appendChild(p);}}catch(e){console.error(e);}};
197
+ window.exportPDF=async function(){var d=getData(),code=getEffectiveOrderCode(),qr=getQRUrl(d.deposit>0?d.remaining:d.grandTotal,code);try{await _doExportPDF(d,code);}catch(e){console.error(e);alert('Không xuất được PDF: '+(e&&e.message?e.message:e));}};
198
+ window.exportExcel=async function(){if(typeof ExcelJS==='undefined')return;var d=getData(),qd=d.qd,code=getEffectiveOrderCode(),qr=getQRUrl(d.deposit>0?d.remaining:d.grandTotal,code);await _doExportExcel(d,qd,code,qr);};
199
+ window.exportDeliveryExcel=async function(){if(typeof ExcelJS==='undefined')return;var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryExcel(qd,code);};
200
+ window.exportDeliveryPDF=async function(){var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryPDF(qd,code);};
201
+ window.getQuoteData=function(){var qd=window._origGetQuoteData.apply(this,arguments);if(qd.customer)qd.customer.orderCode=getEffectiveOrderCode();return qd;};
202
+ console.log('✅ QR Payment v1037.5 — Restored surcharge detection, skip note patterns');return true;
203
+ }
204
+
205
+ async function _doExportPDF(d,code){
206
+ var qr=getQRUrl(d.deposit>0?d.remaining:d.grandTotal,code);
207
+ var cv=await toCanvas(buildQuoteHTML(d,code,qr),1000);
208
+ var img= cv.toDataURL('image/jpeg',0.92);
209
+ var pageW=595,pageH=842;
210
+ var imgW=pageW;var imgH=cv.height*imgW/cv.width;
211
+ var pdf=new jspdf.jsPDF({orientation:'p',unit:'pt',format:'a4'});
212
+ if(imgH<=pageH){pdf.addImage(img,'JPEG',0,0,imgW,imgH);}else{
213
+ var y=0;var remaining=imgH;var page=0;
214
+ while(remaining>0){if(page>0)pdf.addPage();pdf.addImage(img,'JPEG',0,-y,imgW,imgH);y+=pageH;remaining-=pageH;page++;}
215
+ }
216
+ pdf.save(code+'.pdf');
217
+ }
218
+ async function _doExportDeliveryPDF(qd,code){var cv=await toCanvas(buildDeliveryHTML(qd,code),700);var img=cv.toDataURL('image/png');var pdf=new jspdf.jsPDF({orientation:'p',unit:'pt',format:'a4'});var margin=25,usableW=595-margin*2;var ratio=cv.height/cv.width;var imgH=usableW*ratio;if(imgH>842-margin*2){imgH=842-margin*2;usableW=imgH/ratio;}pdf.addImage(img,'PNG',margin,margin,usableW,imgH);pdf.save('GH-'+code+'.pdf');}
219
+
220
+ // FIX v1037.2: _doExportExcel - Giá CK luôn là formula, ko hằng số
221
+ // FIX v1037.3: Thêm CORS proxy fallback wsrv.nl cho ảnh (Eurogold, Hafele...)
222
+ async function _doExportExcel(d,qd,code,qr){
223
+ var MONEY_FMT='#,##0"đ"';
224
+ var NUM_FMT='#,##0';
225
+ var WHITE={argb:'FFFFFFFF'};var WB={top:{style:'thin',color:WHITE},bottom:{style:'thin',color:WHITE},left:{style:'thin',color:WHITE},right:{style:'thin',color:WHITE}};
226
+ var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Báo giá');
227
+ ws.views=[{showGridLines:false}];ws.columns=[{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];
228
+ try{var lr=await fetch('https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_200.png',{mode:'cors'});if(lr.ok){var lb=await lr.arrayBuffer();ws.addImage(wb.addImage({buffer:lb,extension:'png'}),{tl:{col:0,row:0},ext:{width:40,height:40}});}}catch(e){}
229
+ ws.mergeCells('B1:J1');ws.getCell('B1').value='❝ Niềm tin khách hàng là tài sản của chúng tôi ❞';ws.getCell('B1').font={italic:true,size:9,color:{argb:'FF64748B'}};ws.getCell('B1').alignment={vertical:'middle'};ws.getCell('B1').border=WB;
230
+ ws.getRow(1).height=28;ws.getRow(2).height=5;
231
+ ws.mergeCells('A3:J3');ws.getCell('A3').value='BẢNG BÁO GIÁ';ws.getCell('A3').font={bold:true,size:18,color:{argb:'FFDB9815'}};ws.getCell('A3').alignment={horizontal:'center',vertical:'middle'};ws.getCell('A3').border=WB;ws.getRow(3).height=30;ws.getRow(4).height=5;
232
+ ws.getCell('A5').value='Khách hàng:';ws.getCell('A5').font={bold:true,size:9};ws.getCell('A5').border=WB;
233
+ ws.mergeCells('B5:E5');ws.getCell('B5').value=qd.customer.name||'';ws.getCell('B5').font={bold:true,size:10};ws.getCell('B5').border=WB;
234
+ ws.getCell('H5').value='Mã đơn:';ws.getCell('H5').font={size:9};ws.getCell('H5').border=WB;
235
+ ws.mergeCells('I5:J5');ws.getCell('I5').value=code;ws.getCell('I5').font={bold:true,size:10,color:{argb:'FF003F62'}};ws.getCell('I5').border=WB;
236
+ ws.getCell('A6').value='SĐT:';ws.getCell('A6').font={size:9};ws.getCell('A6').border=WB;
237
+ ws.getCell('B6').value=qd.customer.phone||'';ws.getCell('B6').border=WB;
238
+ ws.getCell('H6').value='Ngày:';ws.getCell('H6').font={size:9};ws.getCell('H6').border=WB;
239
+ ws.getCell('I6').value=qd.customer.date||'';ws.getCell('I6').border=WB;
240
+ ws.getCell('A7').value='Email:';ws.getCell('A7').font={size:9};ws.getCell('A7').border=WB;
241
+ ws.getCell('B7').value=qd.customer.email||'';ws.getCell('B7').border=WB;
242
+ ws.getCell('A8').value='Địa chỉ:';ws.getCell('A8').font={size:9};ws.getCell('A8').border=WB;
243
+ ws.mergeCells('B8:J8');ws.getCell('B8').value=qd.customer.addr||'';ws.getCell('B8').border=WB;
244
+ ws.getRow(9).height=5;
245
+ var HB={top:{style:'thin',color:{argb:'FF003F62'}},bottom:{style:'thin',color:{argb:'FF003F62'}},left:{style:'thin',color:{argb:'FF003F62'}},right:{style:'thin',color:{argb:'FF003F62'}}};
246
+ var headerRow=ws.getRow(10);
247
+ ['STT','Hình','Tên sản phẩm','Mã SP','Thông tin','SL','Đơn giá','Giá CK','Thành tiền','Ghi chú'].forEach(function(h,i){var c=headerRow.getCell(i+1);c.value=h;c.font={bold:true,color:{argb:'FFFFFFFF'},size:9};c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};c.alignment={horizontal:'center',vertical:'middle',wrapText:true};c.border=HB;});headerRow.height=20;
248
+ var cr=11;
249
+ var firstItemRow=cr;
250
+ var ckPct=Number(d.discountPercent||0);
251
+ for(var i=0;i<qd.items.length;i++){
252
+ var it=qd.items[i],row=ws.getRow(cr);row.height=50;
253
+ var bgColor=i%2===0?'FFF8FAFC':'FFFFFFFF';
254
+ var RB={top:{style:'thin',color:{argb:bgColor}},bottom:{style:'thin',color:{argb:bgColor}},left:{style:'thin',color:{argb:bgColor}},right:{style:'thin',color:{argb:bgColor}}};
255
+ row.getCell(1).value=it.stt||i+1;row.getCell(1).alignment={horizontal:'center',vertical:'middle'};
256
+ if(it.image&&it.image.startsWith('http')){
257
+ try{
258
+ var ir=await fetch(it.image,{mode:'cors'});
259
+ if(!ir.ok)throw new Error('HTTP '+ir.status);
260
+ var ib=await ir.arrayBuffer();
261
+ var ext=it.image.match(/\.(png|jpe?g)/i);ext=ext?ext[1].startsWith('png')?'png':'jpeg':'jpeg';
262
+ ws.addImage(wb.addImage({buffer:ib,extension:ext}),{tl:{col:1,row:cr-1},ext:{width:46,height:46}});
263
+ }catch(e1){
264
+ try{
265
+ var proxyUrl='https://wsrv.nl/?url='+encodeURIComponent(it.image)+'&w=200&output='+(it.image.match(/\.png/i)?'png':'jpg');
266
+ var ir2=await fetch(proxyUrl);
267
+ if(ir2.ok){
268
+ var ib2=await ir2.arrayBuffer();
269
+ var ext2=it.image.match(/\.(png|jpe?g)/i);ext2=ext2?ext2[1].startsWith('png')?'png':'jpeg':'jpeg';
270
+ ws.addImage(wb.addImage({buffer:ib2,extension:ext2}),{tl:{col:1,row:cr-1},ext:{width:46,height:46}});
271
+ }
272
+ }catch(e2){}
273
+ }
274
+ }
275
+ row.getCell(3).value=it.name||'';row.getCell(3).font={bold:true,size:9};row.getCell(3).alignment={wrapText:true,vertical:'middle'};
276
+ row.getCell(4).value=it.model||'';row.getCell(4).alignment={horizontal:'center',vertical:'middle'};
277
+ row.getCell(5).value=it.specs||'';row.getCell(5).font={size:8,color:{argb:'FF64748B'}};row.getCell(5).alignment={wrapText:true,vertical:'middle'};
278
+ row.getCell(6).value=Number(it.qty||1);row.getCell(6).numFmt=NUM_FMT;row.getCell(6).alignment={horizontal:'center',vertical:'middle'};
279
+ row.getCell(7).value=Number(it.price||0);row.getCell(7).numFmt=MONEY_FMT;row.getCell(7).alignment={horizontal:'right',vertical:'middle'};
280
+ var itemPct=matchItemDiscount(it,d.itemDiscounts||{});
281
+ var discountFactor=1.0;
282
+ if(itemPct>0&&itemPct<=100){discountFactor=1-itemPct/100;}
283
+ else if(ckPct>0&&ckPct<=100){discountFactor=1-ckPct/100;}
284
+ row.getCell(8).value={formula:'G'+cr+'*'+discountFactor.toFixed(4),result:Number(it.discPrice||it.price||0)};
285
+ row.getCell(8).numFmt=MONEY_FMT;row.getCell(8).alignment={horizontal:'right',vertical:'middle'};
286
+ if((it.discPrice||0)&&(it.price||0)&&it.discPrice<it.price)row.getCell(8).font={bold:true,color:{argb:'FFDC3545'}};
287
+ row.getCell(9).value={formula:'F'+cr+'*H'+cr,result:Number(it.total||0)};row.getCell(9).numFmt=MONEY_FMT;row.getCell(9).font={bold:true};row.getCell(9).alignment={horizontal:'right',vertical:'middle'};
288
+ row.getCell(10).value=it.note||'';row.getCell(10).font={size:8};row.getCell(10).alignment={wrapText:true,vertical:'middle'};
289
+ for(var ci=1;ci<=10;ci++){row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:bgColor}};row.getCell(ci).border=RB;}
290
+ cr++;
291
+ }
292
+ var lastItemRow=cr-1;
293
+ var totalQtyFormula=lastItemRow>=firstItemRow?'SUM(F'+firstItemRow+':F'+lastItemRow+')':'0';
294
+ var totalQtyValue=(qd.items||[]).reduce(function(s,it){return s+Number(it.qty||1);},0);
295
+ var sumParts=[];if(lastItemRow>=firstItemRow)sumParts.push('SUM(I'+firstItemRow+':I'+lastItemRow+')');
296
+ if(d.fees&&d.fees.length){var FBG='FFFFFBEB';var FB={top:{style:'thin',color:{argb:FBG}},bottom:{style:'thin',color:{argb:FBG}},left:{style:'thin',color:{argb:FBG}},right:{style:'thin',color:{argb:FBG}}};d.fees.forEach(function(f){var row=ws.getRow(cr);row.height=20;ws.mergeCells(cr,1,cr,8);row.getCell(1).value=' ⊕ '+f.label;row.getCell(1).font={size:9,color:{argb:'FF92400E'}};row.getCell(1).alignment={vertical:'middle'};row.getCell(9).value=Number(f.amount||0);row.getCell(9).numFmt=MONEY_FMT;row.getCell(9).font={bold:true,size:9,color:{argb:'FF92400E'}};row.getCell(9).alignment={horizontal:'right',vertical:'middle'};for(var ci=1;ci<=10;ci++){row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:FBG}};row.getCell(ci).border=FB;}sumParts.push('I'+cr);cr++;});}
297
+ var totalRowNum=cr;
298
+ var TBG='FF003F62';var TB={top:{style:'thin',color:{argb:TBG}},bottom:{style:'thin',color:{argb:TBG}},left:{style:'thin',color:{argb:TBG}},right:{style:'thin',color:{argb:TBG}}};
299
+ ws.mergeCells(cr,1,cr,5);
300
+ var tRow=ws.getRow(cr);tRow.height=28;
301
+ tRow.getCell(1).value='TỔNG CỘNG';tRow.getCell(1).font={bold:true,size:13,color:{argb:'FFFFFFFF'}};tRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};tRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(1).border=TB;
302
+ tRow.getCell(6).value={formula:totalQtyFormula,result:totalQtyValue};tRow.getCell(6).numFmt=NUM_FMT;tRow.getCell(6).font={bold:true,size:12,color:{argb:'FFFFFFFF'}};tRow.getCell(6).alignment={horizontal:'center',vertical:'middle'};tRow.getCell(6).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(6).border=TB;
303
+ tRow.getCell(7).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(7).border=TB;tRow.getCell(8).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(8).border=TB;
304
+ tRow.getCell(9).value={formula:sumParts.length?sumParts.join('+'):'0',result:Number(d.grandTotal||0)};tRow.getCell(9).numFmt=MONEY_FMT;tRow.getCell(9).font={bold:true,size:14,color:{argb:'FFF0B840'}};tRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};tRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(9).border=TB;tRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(10).border=TB;cr++;
305
+ if(d.deposit>0){var DBG='FFF0FDF4';var DB={top:{style:'thin',color:{argb:DBG}},bottom:{style:'thin',color:{argb:DBG}},left:{style:'thin',color:{argb:DBG}},right:{style:'thin',color:{argb:DBG}}};ws.mergeCells(cr,1,cr,8);var dRow=ws.getRow(cr);dRow.height=24;dRow.getCell(1).value='Đã cọc';dRow.getCell(1).font={bold:true,size:11,color:{argb:'FF166534'}};dRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};dRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(1).border=DB;dRow.getCell(9).value=Number(d.deposit||0);dRow.getCell(9).numFmt=MONEY_FMT;dRow.getCell(9).font={bold:true,size:11,color:{argb:'FF166534'}};dRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};dRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(9).border=DB;dRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(10).border=DB;var depositRowNum=cr;cr++;var RBG='FFFEF2F2';var RBD={top:{style:'thin',color:{argb:RBG}},bottom:{style:'thin',color:{argb:RBG}},left:{style:'thin',color:{argb:RBG}},right:{style:'thin',color:{argb:RBG}}};ws.mergeCells(cr,1,cr,8);var rRow=ws.getRow(cr);rRow.height=26;rRow.getCell(1).value='CÒN LẠI';rRow.getCell(1).font={bold:true,size:12,color:{argb:'FFDC2626'}};rRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};rRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(1).border=RBD;rRow.getCell(9).value={formula:'MAX(I'+totalRowNum+'-I'+depositRowNum+',0)',result:Number(d.remaining||0)};rRow.getCell(9).numFmt=MONEY_FMT;rRow.getCell(9).font={bold:true,size:13,color:{argb:'FFDC2626'}};rRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};rRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(9).border=RBD;rRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(10).border=RBD;cr++;}
306
+ if(d.notes&&d.notes.length){var NBG='FFF0FDF4';var NB2={top:{style:'thin',color:{argb:NBG}},bottom:{style:'thin',color:{argb:NBG}},left:{style:'thin',color:{argb:NBG}},right:{style:'thin',color:{argb:NBG}}};ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='📝 Ghi chú: '+d.notes.join('; ');ws.getCell('A'+cr).font={italic:true,size:9,color:{argb:'FF166534'}};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:NBG}};ws.getCell('A'+cr).border=NB2;cr++;}
307
+ cr++;var QBG='FFF8FAFC';var QB={top:{style:'thin',color:{argb:QBG}},bottom:{style:'thin',color:{argb:QBG}},left:{style:'thin',color:{argb:QBG}},right:{style:'thin',color:{argb:QBG}}};
308
+ ws.mergeCells(cr,1,cr,5);ws.getCell('A'+cr).value='💳 CHUYỂN KHOẢN';ws.getCell('A'+cr).font={bold:true,size:11,color:{argb:'FF003F62'}};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:QBG}};ws.getCell('A'+cr).border=QB;cr++;
309
+ ws.mergeCells(cr,1,cr,5);ws.getCell('A'+cr).value='🏦 VIB | 👤 '+BANK_NAME_VN;ws.getCell('A'+cr).font={size:10};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:QBG}};ws.getCell('A'+cr).border=QB;cr++;
310
+ ws.mergeCells(cr,1,cr,5);ws.getCell('A'+cr).value='🔢 STK: '+BANK_ACCOUNT;ws.getCell('A'+cr).font={bold:true,size:10};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:QBG}};ws.getCell('A'+cr).border=QB;cr++;
311
+ ws.mergeCells(cr,1,cr,5);ws.getCell('A'+cr).value='💰 '+fmt(d.deposit>0?d.remaining:d.grandTotal)+' | 📝 '+code;ws.getCell('A'+cr).font={bold:true,size:10};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:QBG}};ws.getCell('A'+cr).border=QB;cr++;
312
+ try{var qrR=await fetch(qr,{mode:'cors'});if(qrR.ok){var qb=await qrR.arrayBuffer();ws.addImage(wb.addImage({buffer:qb,extension:'png'}),{tl:{col:6,row:cr-5},ext:{width:115,height:115}});}}catch(e){}
313
+ ws.eachRow(function(row){row.eachCell(function(cell){if(!cell.border)cell.border=WB;});});
314
+ wb.calcProperties.fullCalcOnLoad = true;
315
+ var buf=await wb.xlsx.writeBuffer();var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=code+'.xlsx';a.click();setTimeout(function(){URL.revokeObjectURL(url);},60000);
316
+ }
317
+
318
+ async function _doExportDeliveryExcel(qd,code){
319
+ var WHITE={argb:'FFFFFFFF'};var WB={top:{style:'thin',color:WHITE},bottom:{style:'thin',color:WHITE},left:{style:'thin',color:WHITE},right:{style:'thin',color:WHITE}};
320
+ var HB={top:{style:'thin',color:{argb:'FF003F62'}},bottom:{style:'thin',color:{argb:'FF003F62'}},left:{style:'thin',color:{argb:'FF003F62'}},right:{style:'thin',color:{argb:'FF003F62'}}};
321
+ var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Giao hàng');
322
+ ws.views=[{showGridLines:false}];ws.columns=[{width:5},{width:34},{width:14},{width:8},{width:20}];
323
+ ws.mergeCells('A1:E1');ws.getCell('A1').value='PHIẾU GIAO HÀNG';ws.getCell('A1').font={bold:true,size:15,color:{argb:'FF003F62'}};ws.getCell('A1').alignment={horizontal:'center'};ws.getCell('A1').border=WB;ws.getRow(1).height=24;
324
+ ws.getCell('A2').value='KH: '+(qd.customer?qd.customer.name:'');ws.getCell('A2').font={bold:true,size:10};ws.getCell('A2').border=WB;
325
+ ws.getCell('D2').value='Mã: '+code;ws.getCell('D2').font={bold:true,size:9,color:{argb:'FF003F62'}};ws.getCell('D2').border=WB;
326
+ ws.getCell('A3').value='SĐT: '+(qd.customer?qd.customer.phone:'');ws.getCell('A3').border=WB;
327
+ ws.getCell('D3').value='Ngày: '+(qd.customer?qd.customer.date:'');ws.getCell('D3').border=WB;
328
+ ws.getCell('A4').value='Địa chỉ: '+(qd.customer?qd.customer.addr||'':'');ws.getCell('A4').border=WB;
329
+ ws.getRow(5).height=4;
330
+ var hr=ws.getRow(6);['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){var c=hr.getCell(i+1);c.value=h;c.font={bold:true,color:{argb:'FFFFFFFF'},size:10};c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};c.alignment={horizontal:'center',vertical:'middle'};c.border=HB;});hr.height=20;
331
+ var cr=7;var items=qd.items||[];
332
+ for(var i=0;i<items.length;i++){var it=items[i],row=ws.getRow(cr);row.height=20;var bgColor=i%2===0?'FFF8FAFC':'FFFFFFFF';var RB={top:{style:'thin',color:{argb:bgColor}},bottom:{style:'thin',color:{argb:bgColor}},left:{style:'thin',color:{argb:bgColor}},right:{style:'thin',color:{argb:bgColor}}};row.getCell(1).value=it.stt||i+1;row.getCell(1).alignment={horizontal:'center',vertical:'middle'};row.getCell(2).value=it.name||'';row.getCell(2).font={size:10};row.getCell(3).value=it.model||'';row.getCell(3).alignment={horizontal:'center',vertical:'middle'};row.getCell(4).value=it.qty||1;row.getCell(4).alignment={horizontal:'center',vertical:'middle'};row.getCell(4).font={bold:true};row.getCell(5).value=it.note||'';for(var ci=1;ci<=5;ci++){row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:bgColor}};row.getCell(ci).border=RB;}cr++;}
333
+ var totalQtyRow=cr;var TQBG='FFEFF6FF';var TQB={top:{style:'thin',color:{argb:TQBG}},bottom:{style:'thin',color:{argb:TQBG}},left:{style:'thin',color:{argb:TQBG}},right:{style:'thin',color:{argb:TQBG}}};ws.mergeCells(totalQtyRow,1,totalQtyRow,3);ws.getCell('A'+totalQtyRow).value='TỔNG CỘNG SỐ LƯỢNG';ws.getCell('A'+totalQtyRow).font={bold:true,size:10,color:{argb:'FF1E40AF'}};ws.getCell('A'+totalQtyRow).fill={type:'pattern',pattern:'solid',fgColor:{argb:TQBG}};ws.getCell('A'+totalQtyRow).border=TQB;ws.getCell('D'+totalQtyRow).value={formula:items.length?'SUM(D7:D'+(cr-1)+')':'0',result:items.reduce(function(s,it){return s+Number(it.qty||1);},0)};ws.getCell('D'+totalQtyRow).numFmt='#,##0';ws.getCell('D'+totalQtyRow).font={bold:true,size:10,color:{argb:'FF1E40AF'}};ws.getCell('D'+totalQtyRow).alignment={horizontal:'center'};ws.getCell('D'+totalQtyRow).fill={type:'pattern',pattern:'solid',fgColor:{argb:TQBG}};ws.getCell('D'+totalQtyRow).border=TQB;ws.getCell('E'+totalQtyRow).fill={type:'pattern',pattern:'solid',fgColor:{argb:TQBG}};ws.getCell('E'+totalQtyRow).border=TQB;cr++;
334
+ cr+=2;ws.getCell('A'+cr).value='Bên giao: ________________';ws.getCell('A'+cr).border=WB;ws.getCell('D'+cr).value='Bên nhận: ________________';ws.getCell('D'+cr).border=WB;
335
+ cr++;ws.getCell('A'+cr).value='(Ký, họ tên)';ws.getCell('A'+cr).font={italic:true,size:8,color:{argb:'FF64748B'}};ws.getCell('A'+cr).border=WB;ws.getCell('D'+cr).value='(Ký, họ tên)';ws.getCell('D'+cr).font={italic:true,size:8,color:{argb:'FF64748B'}};ws.getCell('D'+cr).border=WB;
336
+ ws.eachRow(function(row){row.eachCell(function(cell){if(!cell.border)cell.border=WB;});});
337
+ var buf=await wb.xlsx.writeBuffer();var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download='GH-'+code+'.xlsx';a.click();setTimeout(function(){URL.revokeObjectURL(url);},60000);
338
+ }
339
+
340
+ var att=0;var t=setInterval(function(){if(doOverride()||att>120)clearInterval(t);att++;},500);
341
+
342
+ document.addEventListener('click',function(e){
343
+ var btn=e.target.closest&&e.target.closest('button,a');
344
+ if(!btn)return;
345
+ var txt=(btn.textContent||'').toLowerCase();
346
+ if(!window._origGetQuoteData)return;
347
+ if(txt.includes('excel')&&txt.includes('xuất')){e.preventDefault();e.stopPropagation();window.exportExcel();return false;}
348
+ if(txt.includes('pdf')&&txt.includes('xuất')){e.preventDefault();e.stopPropagation();window.exportPDF();return false;}
349
+ if(txt.includes('chia sẻ')&&txt.includes('ảnh')){e.preventDefault();e.stopPropagation();window.shareQuoteImage();return false;}
350
+ },true);
351
+
352
+ window.VAI_QR={getEffectiveOrderCode:getEffectiveOrderCode,getQRUrl:getQRUrl,generateOrderCode:generateOrderCode,parsePrompt:parsePrompt,getPromptText:getPromptText,injectToModal:injectToModal,getData:getData,buildQuoteHTML:buildQuoteHTML,buildDeliveryHTML:buildDeliveryHTML,toCanvas:toCanvas,exportPDF:_doExportPDF,exportExcel:_doExportExcel,exportDeliveryPDF:_doExportDeliveryPDF,exportDeliveryExcel:_doExportDeliveryExcel,fmt:fmt,compactKey:compactKey,matchItemDiscount:matchItemDiscount,parseItemDiscountLine:parseItemDiscountLine,applyPromptDiscounts:applyPromptDiscounts};
353
+ })();
quote-ui.js ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Quote UI v7
3
+ *
4
+ * - Nút "💾 Lưu" LUÔN HIỆN (không cần mã truy cập) — lưu đơn cho mọi user
5
+ * - Nút "📋 Đơn hàng", "📦 GH Excel", "📦 GH PDF" ẨN cho đến khi nhập V.AISTUDIO
6
+ * - Khi Lưu → tự động lưu vào danh sách đơn hàng (VAI_ORDERS)
7
+ */
8
+ (function(){
9
+ 'use strict';
10
+
11
+ var ACCESS_CODE='V.AISTUDIO';
12
+
13
+ function isUnlocked(){
14
+ return document.body.classList.contains('vas-unlocked');
15
+ }
16
+
17
+ function unlock(){
18
+ document.body.classList.add('vas-unlocked');
19
+ console.log('[Quote UI v7] ✅ Unlocked');
20
+ // Auto-apply discount/fees/notes from prompt text
21
+ if(typeof window.applyAiDiscount === 'function'){
22
+ setTimeout(function(){ window.applyAiDiscount(); }, 300);
23
+ } else {
24
+ // Fallback: trigger input on qcAiPrompt
25
+ var inp = document.getElementById('qcAiPrompt');
26
+ if(inp) { inp.dispatchEvent(new Event('input', {bubbles: true})); }
27
+ }
28
+ setTimeout(addProtectedButtons, 200);
29
+ }
30
+
31
+ function lock(){
32
+ document.body.classList.remove('vas-unlocked');
33
+ removeProtectedButtons();
34
+ }
35
+
36
+ // === NÚT LƯU (LUÔN HIỆN) ===
37
+ function hasSaveButton(){
38
+ return !!document.querySelector('[data-vai-save-btn]');
39
+ }
40
+
41
+ function addSaveButton(){
42
+ if(hasSaveButton())return;
43
+ var actionsArea=document.querySelector('.quote-actions');
44
+ if(!actionsArea)return;
45
+
46
+ var btn=document.createElement('button');
47
+ btn.setAttribute('data-vai-save-btn','1');
48
+ btn.className='quote-btn'; // KHÔNG có checkout-done-btn → luôn hiện
49
+ btn.innerHTML='💾 Lưu';
50
+ btn.style.cssText='padding:12px 20px;border:none;border-radius:10px;font-weight:700;font-size:.82rem;cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .25s ease;white-space:nowrap;background:#7c3aed;color:#fff';
51
+ btn.onmouseenter=function(){this.style.background='#6d28d9';};
52
+ btn.onmouseleave=function(){this.style.background='#7c3aed';};
53
+ btn.onclick=function(){
54
+ if(window.VAI_ORDERS&&window.VAI_ORDERS.save){
55
+ window.VAI_ORDERS.save();
56
+ }else{
57
+ alert('⏳ Đang tải module đơn hàng...');
58
+ }
59
+ };
60
+ // Insert as first button in actions
61
+ actionsArea.insertBefore(btn, actionsArea.firstChild);
62
+ console.log('[Quote UI v7] 💾 Lưu button added (always visible)');
63
+ }
64
+
65
+ // === CÁC NÚT BẢO VỆ (ẨN CHO ĐẾN KHI UNLOCK) ===
66
+ function hasProtectedButtons(){
67
+ return !!document.querySelector('[data-vai-protected-btn]');
68
+ }
69
+
70
+ function removeProtectedButtons(){
71
+ document.querySelectorAll('[data-vai-protected-btn]').forEach(function(b){b.remove();});
72
+ }
73
+
74
+ function addProtectedButtons(){
75
+ if(!isUnlocked())return;
76
+ if(hasProtectedButtons())return;
77
+ var actionsArea=document.querySelector('.quote-actions');
78
+ if(!actionsArea)return;
79
+
80
+ var btnStyle='padding:12px 20px;border:none;border-radius:10px;font-weight:700;font-size:.82rem;cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .25s ease;white-space:nowrap';
81
+
82
+ var buttons=[
83
+ {html:'📋 Đơn hàng',bg:'#2563eb',hover:'#1d4ed8',fn:function(){if(window.VAI_ORDERS)window.VAI_ORDERS.openSearch();else alert('⏳ Đang tải...');}},
84
+ {html:'📦 GH Excel',bg:'#059669',hover:'#047857',fn:function(){if(typeof exportDeliveryExcel==='function')exportDeliveryExcel();}},
85
+ {html:'📦 GH PDF',bg:'#0d9488',hover:'#0f766e',fn:function(){if(typeof exportDeliveryPDF==='function')exportDeliveryPDF();}}
86
+ ];
87
+
88
+ buttons.forEach(function(cfg){
89
+ var btn=document.createElement('button');
90
+ btn.setAttribute('data-vai-protected-btn','1');
91
+ btn.className='quote-btn checkout-done-btn'; // ẨN cho đến khi vas-unlocked
92
+ btn.innerHTML=cfg.html;
93
+ btn.style.cssText=btnStyle+';background:'+cfg.bg+';color:#fff';
94
+ btn.onmouseenter=function(){this.style.background=cfg.hover;};
95
+ btn.onmouseleave=function(){this.style.background=cfg.bg;};
96
+ btn.onclick=cfg.fn;
97
+ actionsArea.appendChild(btn);
98
+ });
99
+ console.log('[Quote UI v7] Protected buttons injected (Đơn hàng, GH)');
100
+ }
101
+
102
+ function isQuoteOpen(){
103
+ var ov=document.querySelector('.quote-overlay');
104
+ return ov&&(ov.classList.contains('open')||ov.style.display==='flex'||ov.style.display==='block');
105
+ }
106
+
107
+ // === BIND mã truy cập input (#qcAccessCode) ===
108
+ function bindAccessCodeInput(){
109
+ var accessInput=document.getElementById('qcAccessCode');
110
+ if(!accessInput)return false;
111
+ if(accessInput.getAttribute('data-vai-access'))return true;
112
+ accessInput.setAttribute('data-vai-access','1');
113
+
114
+ function checkCode(){
115
+ var val=(accessInput.value||'').trim();
116
+ if(val===ACCESS_CODE||val.toUpperCase()===ACCESS_CODE){
117
+ if(!isUnlocked())unlock();
118
+ else if(typeof window.applyAiDiscount === 'function'){ setTimeout(function(){ window.applyAiDiscount(); }, 300); }
119
+ }else{
120
+ if(isUnlocked())lock();
121
+ }
122
+ }
123
+
124
+ accessInput.addEventListener('input',checkCode);
125
+ accessInput.addEventListener('change',checkCode);
126
+ accessInput.addEventListener('blur',checkCode);
127
+ accessInput.addEventListener('keypress',function(e){if(e.key==='Enter')checkCode();});
128
+ return true;
129
+ }
130
+
131
+ // === OBSERVER ===
132
+ var observer=new MutationObserver(function(){
133
+ bindAccessCodeInput();
134
+ if(isQuoteOpen()){
135
+ if(!hasSaveButton())setTimeout(addSaveButton,200);
136
+ if(isUnlocked()&&!hasProtectedButtons())setTimeout(addProtectedButtons,300);
137
+ }
138
+ if(!isQuoteOpen()){
139
+ if(hasSaveButton())document.querySelectorAll('[data-vai-save-btn]').forEach(function(b){b.remove();});
140
+ if(hasProtectedButtons())removeProtectedButtons();
141
+ }
142
+ if(!isUnlocked()&&hasProtectedButtons())removeProtectedButtons();
143
+ });
144
+ observer.observe(document.body,{childList:true,subtree:true,attributes:true,attributeFilter:['class','style']});
145
+
146
+ setInterval(function(){
147
+ bindAccessCodeInput();
148
+ if(isQuoteOpen()){
149
+ if(!hasSaveButton())addSaveButton();
150
+ if(isUnlocked()&&!hasProtectedButtons())addProtectedButtons();
151
+ }
152
+ if(!isQuoteOpen()){
153
+ if(hasSaveButton())document.querySelectorAll('[data-vai-save-btn]').forEach(function(b){b.remove();});
154
+ if(hasProtectedButtons())removeProtectedButtons();
155
+ }
156
+ if(!isUnlocked()&&hasProtectedButtons())removeProtectedButtons();
157
+ },2000);
158
+
159
+ setTimeout(bindAccessCodeInput,1000);
160
+ setTimeout(bindAccessCodeInput,3000);
161
+
162
+ console.log('[Quote UI v7] loaded — Lưu always visible, protected buttons need V.AISTUDIO');
163
+ })();
san-pham/02-03-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0203 | Grob | Bản lề inox - cánh/phủ bì 203mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0203 | Grob | Bản lề inox - cánh/phủ bì 203mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 67,000đ - Bản Lề Inox Khung Nhôm Cánh Kính Grob 02.03 (cánh/phủ bì 203mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-03-grob/index.html">
10
+ <meta property="product:price:amount" content="67000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0203 | Grob | Bản lề inox - cánh/phủ bì 203mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 67,000đ - Bản Lề Inox Khung Nhôm Cánh Kính Grob 02.03 (cánh/phủ bì 203mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-03-grob/index.html">
17
+ <script>location.replace('/?product=02-03-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/02-45-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0245 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 245mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0245 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 245mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 670,000đ - Khay chia thìa dĩa nhựa Grob 02.45 (cánh/phủ bì 245mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-45-grob/index.html">
10
+ <meta property="product:price:amount" content="670000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0245 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 245mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 670,000đ - Khay chia thìa dĩa nhựa Grob 02.45 (cánh/phủ bì 245mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-45-grob/index.html">
17
+ <script>location.replace('/?product=02-45-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/02-50-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0250 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 250mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0250 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 250mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 725,000đ - Khay chia thìa dĩa nhựa Grob 02.50 (cánh/phủ bì 250mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-50-grob/index.html">
10
+ <meta property="product:price:amount" content="725000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0250 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 250mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 725,000đ - Khay chia thìa dĩa nhựa Grob 02.50 (cánh/phủ bì 250mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-50-grob/index.html">
17
+ <script>location.replace('/?product=02-50-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/02-60-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0260 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 260mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0260 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 260mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 778,000đ - Khay chia thìa dĩa nhựa Grob 02.60 (cánh/phủ bì 260mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-60-grob/index.html">
10
+ <meta property="product:price:amount" content="778000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0260 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 260mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 778,000đ - Khay chia thìa dĩa nhựa Grob 02.60 (cánh/phủ bì 260mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-60-grob/index.html">
17
+ <script>location.replace('/?product=02-60-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/02-70-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0270 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 270mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0270 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 270mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 885,000đ - Khay chia thìa dĩa nhựa Grob 02.70 (cánh/phủ bì 270mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-70-grob/index.html">
10
+ <meta property="product:price:amount" content="885000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0270 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 270mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 885,000đ - Khay chia thìa dĩa nhựa Grob 02.70 (cánh/phủ bì 270mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-70-grob/index.html">
17
+ <script>location.replace('/?product=02-70-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/02-80-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0280 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 280mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0280 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 280mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 965,000đ - Khay chia thìa dĩa nhựa Grob 02.80 (cánh/phủ bì 280mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-80-grob/index.html">
10
+ <meta property="product:price:amount" content="965000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0280 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 280mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 965,000đ - Khay chia thìa dĩa nhựa Grob 02.80 (cánh/phủ bì 280mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-80-grob/index.html">
17
+ <script>location.replace('/?product=02-80-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/02-90-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>0290 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 290mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="0290 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 290mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 1,018,000đ - Khay chia thìa dĩa nhựa Grob 02.90 (cánh/phủ bì 290mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/02-90-grob/index.html">
10
+ <meta property="product:price:amount" content="1018000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="0290 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 290mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 1,018,000đ - Khay chia thìa dĩa nhựa Grob 02.90 (cánh/phủ bì 290mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p12.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/02-90-grob/index.html">
17
+ <script>location.replace('/?product=02-90-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/1044p-malloca-chau-rua-chen-1-ho-inox-304-kich-thuoc-44cm-lap-am-hoac-duong-2/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>1044P | Malloca | Chậu rửa chén 1 hố inox 304 kích thước 44cm – lắp âm hoặc dương | Malloca | V.AI STUDIO</title>
6
+ <meta property="og:title" content="1044P | Malloca | Chậu rửa chén 1 hố inox 304 kích thước 44cm – lắp âm hoặc dương | Malloca | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 3,672,000đ - Dòng chậu đơn chế tạo bán thủ công, thiết kế hiện đại. Chất liệu thép không rỉ 304, độ dày 1.2mm cực bền bỉ. Lớp chống...">
8
+ <meta property="og:image" content="https://bizweb.dktcdn.net/100/567/847/products/hinh-san-pham-chau-rua-chen-malloca-ms1044p-70286c3a-cc10-4264-b58c-2b0c152c6073.png?v=1767088266603">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/1044p-malloca-chau-rua-chen-1-ho-inox-304-kich-thuoc-44cm-lap-am-hoac-duong-2/index.html">
10
+ <meta property="product:price:amount" content="3672000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="1044P | Malloca | Chậu rửa chén 1 hố inox 304 kích thước 44cm – lắp âm hoặc dương | Malloca | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 3,672,000đ - Dòng chậu đơn chế tạo bán thủ công, thiết kế hiện đại. Chất liệu thép không rỉ 304, độ dày 1.2mm cực bền bỉ. Lớp chống...">
15
+ <meta name="twitter:image" content="https://bizweb.dktcdn.net/100/567/847/products/hinh-san-pham-chau-rua-chen-malloca-ms1044p-70286c3a-cc10-4264-b58c-2b0c152c6073.png?v=1767088266603">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/1044p-malloca-chau-rua-chen-1-ho-inox-304-kich-thuoc-44cm-lap-am-hoac-duong-2/index.html">
17
+ <script>location.replace('/?product=1044p-malloca-chau-rua-chen-1-ho-inox-304-kich-thuoc-44cm-lap-am-hoac-duong-2')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/28cc304-grob-ma-hang-28cc304/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>28CC304 | Grob | Mã Hàng: 28Cc304 - cánh/phủ bì 304mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="28CC304 | Grob | Mã Hàng: 28Cc304 - cánh/phủ bì 304mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 2,393,000đ - Mã Hàng: 28Cc304 (cánh/phủ bì 304mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p208.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/28cc304-grob-ma-hang-28cc304/index.html">
10
+ <meta property="product:price:amount" content="2393000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="28CC304 | Grob | Mã Hàng: 28Cc304 - cánh/phủ bì 304mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 2,393,000đ - Mã Hàng: 28Cc304 (cánh/phủ bì 304mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p208.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/28cc304-grob-ma-hang-28cc304/index.html">
17
+ <script>location.replace('/?product=28cc304-grob-ma-hang-28cc304')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/28cf304-grob-phu-kien-grob/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>28CF304 | Grob | 4.290.000 Vnđ - cánh/phủ bì 290mm | V.AI STUDIO</title>
6
+ <meta property="og:title" content="28CF304 | Grob | 4.290.000 Vnđ - cánh/phủ bì 290mm | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 2,503,000đ - 4.290.000 Vnđ (cánh/phủ bì 290mm)">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p207.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/28cf304-grob-phu-kien-grob/index.html">
10
+ <meta property="product:price:amount" content="2503000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="28CF304 | Grob | 4.290.000 Vnđ - cánh/phủ bì 290mm | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 2,503,000đ - 4.290.000 Vnđ (cánh/phủ bì 290mm)">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p207.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/28cf304-grob-phu-kien-grob/index.html">
17
+ <script>location.replace('/?product=28cf304-grob-phu-kien-grob')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/30l2304-grob-ma-hang-28cc304/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>30L2304 | Grob | Mã Hàng: 28Cc304 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="30L2304 | Grob | Mã Hàng: 28Cc304 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 1,907,000đ - Mã Hàng: 28Cc304">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p208.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/30l2304-grob-ma-hang-28cc304/index.html">
10
+ <meta property="product:price:amount" content="1907000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="30L2304 | Grob | Mã Hàng: 28Cc304 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 1,907,000đ - Mã Hàng: 28Cc304">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p208.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/30l2304-grob-ma-hang-28cc304/index.html">
17
+ <script>location.replace('/?product=30l2304-grob-ma-hang-28cc304')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/30lh304-grob-ma-hang-28cc304/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>30LH304 | Grob | Mã Hàng: 28Cc304 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="30LH304 | Grob | Mã Hàng: 28Cc304 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 3,600,000đ - Mã Hàng: 28Cc304">
8
+ <meta property="og:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p208.jpg">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/30lh304-grob-ma-hang-28cc304/index.html">
10
+ <meta property="product:price:amount" content="3600000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="30LH304 | Grob | Mã Hàng: 28Cc304 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 3,600,000đ - Mã Hàng: 28Cc304">
15
+ <meta name="twitter:image" content="https://huggingface.co/spaces/bep40/malloca-website/resolve/main/grob_img/grob_p208.jpg">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/30lh304-grob-ma-hang-28cc304/index.html">
17
+ <script>location.replace('/?product=30lh304-grob-ma-hang-28cc304')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/400-51-122-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-122-40051122/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>400.51.122 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50VFSR Hafele 400.51.122 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="400.51.122 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50VFSR Hafele 400.51.122 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 1,000,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2024/03/30/01/29/1711779996_anh-phu-kien-cua-lua-hafele-400-51-122.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/400-51-122-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-122-40051122/index.html">
10
+ <meta property="product:price:amount" content="1000000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="400.51.122 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50VFSR Hafele 400.51.122 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 1,000,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2024/03/30/01/29/1711779996_anh-phu-kien-cua-lua-hafele-400-51-122.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/400-51-122-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-122-40051122/index.html">
17
+ <script>location.replace('/?product=400-51-122-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-122-40051122')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/400-51-123-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-123-40051123/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>400.51.123 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50VFSR Hafele 400.51.123 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="400.51.123 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50VFSR Hafele 400.51.123 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 1,020,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2024/03/30/02/21/1711783084_anh-phu-kien-cua-lua-hafele-400-51-123.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/400-51-123-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-123-40051123/index.html">
10
+ <meta property="product:price:amount" content="1020000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="400.51.123 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50VFSR Hafele 400.51.123 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 1,020,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2024/03/30/02/21/1711783084_anh-phu-kien-cua-lua-hafele-400-51-123.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/400-51-123-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-123-40051123/index.html">
17
+ <script>location.replace('/?product=400-51-123-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50vfsr-hafele-400-51-123-40051123')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/401-30-000-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-000-40130000/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>401.30.000 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.000 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="401.30.000 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.000 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 438,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/09/07/25/1649489606_anh-phu-kien-cua-lua-hafele-401-30-000.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-000-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-000-40130000/index.html">
10
+ <meta property="product:price:amount" content="438000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="401.30.000 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.000 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 438,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/09/07/25/1649489606_anh-phu-kien-cua-lua-hafele-401-30-000.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-000-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-000-40130000/index.html">
17
+ <script>location.replace('/?product=401-30-000-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-000-40130000')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/401-30-001-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-001-40130001/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>401.30.001 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.001 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="401.30.001 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.001 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 460,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/10/01/48/1649555775_anh-phu-kien-cua-lua-hafele-401-30-001.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-001-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-001-40130001/index.html">
10
+ <meta property="product:price:amount" content="460000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="401.30.001 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.001 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 460,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/10/01/48/1649555775_anh-phu-kien-cua-lua-hafele-401-30-001.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-001-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-001-40130001/index.html">
17
+ <script>location.replace('/?product=401-30-001-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-001-40130001')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/401-30-002-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-002-40130002/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>401.30.002 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.002 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="401.30.002 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.002 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 355,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/13/06/45/1649832563_anh-phu-kien-cua-lua-hafele-401-30-002.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-002-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-002-40130002/index.html">
10
+ <meta property="product:price:amount" content="355000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="401.30.002 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.002 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 355,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/13/06/45/1649832563_anh-phu-kien-cua-lua-hafele-401-30-002.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-002-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-002-40130002/index.html">
17
+ <script>location.replace('/?product=401-30-002-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-002-40130002')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/401-30-003-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-003-40130003/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>401.30.003 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.003 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="401.30.003 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.003 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 645,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/13/09/51/1649843725_anh-phu-kien-cua-lua-hafele-401-30-003.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-003-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-003-40130003/index.html">
10
+ <meta property="product:price:amount" content="645000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="401.30.003 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.003 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 645,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/13/09/51/1649843725_anh-phu-kien-cua-lua-hafele-401-30-003.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-003-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-003-40130003/index.html">
17
+ <script>location.replace('/?product=401-30-003-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-003-40130003')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/401-30-004-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-004-40130004/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>401.30.004 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.004 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="401.30.004 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.004 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 380,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/15/10/25/1650018442_anh-phu-kien-cua-lua-hafele-401-30-004.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-004-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-004-40130004/index.html">
10
+ <meta property="product:price:amount" content="380000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="401.30.004 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.004 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 380,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/15/10/25/1650018442_anh-phu-kien-cua-lua-hafele-401-30-004.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-004-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-004-40130004/index.html">
17
+ <script>location.replace('/?product=401-30-004-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-004-40130004')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>
san-pham/401-30-005-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-005-40130005/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>401.30.005 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.005 | V.AI STUDIO</title>
6
+ <meta property="og:title" content="401.30.005 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.005 | V.AI STUDIO">
7
+ <meta property="og:description" content="Giá: 675,000đ - ">
8
+ <meta property="og:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/16/05/24/1650086752_anh-phu-kien-cua-lua-hafele-401-30-005.png">
9
+ <meta property="og:url" content="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-005-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-005-40130005/index.html">
10
+ <meta property="product:price:amount" content="675000">
11
+ <meta property="product:price:currency" content="VND">
12
+ <meta name="twitter:card" content="summary_large_image">
13
+ <meta name="twitter:title" content="401.30.005 | Hafele | Phụ Kiện Cửa Trượt Tủ Slido Classic 50 IF Hafele 401.30.005 | V.AI STUDIO">
14
+ <meta name="twitter:description" content="Giá: 675,000đ - ">
15
+ <meta name="twitter:image" content="https://cdn-img-v2.mybota.vn/uploadv2/web/12/12581/product/2022/04/16/05/24/1650086752_anh-phu-kien-cua-lua-hafele-401-30-005.png">
16
+ <link rel="canonical" href="https://bep40-v-aistudio.static.hf.space/san-pham/401-30-005-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-005-40130005/index.html">
17
+ <script>location.replace('/?product=401-30-005-hafele-phu-ki-n-c-a-tr-t-tu-slido-classic-50-if-hafele-401-30-005-40130005')</script>
18
+ </head>
19
+ <body style="font-family:sans-serif;padding:40px;text-align:center;color:#666">Đang chuyển hướng...</body>
20
+ </html>