Spaces:
Running
Running
Restore commit 30b624b1 - batch 1/171
Browse files- 404.html +13 -13
- ai-search-fix.js +331 -0
- ai-search.js +647 -0
- cart-fix.js +190 -0
- catalogue_ocr.json +1 -0
- catalogues.json +1 -0
- comprehensive-fix.js +386 -0
- discount-formula-fix.js +155 -0
- grob-img-fix.js +76 -0
- grob-price-fix.js +68 -0
- hafele-eurogold-img-fix.js +207 -0
- img-fix.js +4 -0
- order-store.js +377 -0
- product_seo_min.json +0 -0
- products_with_slugs.json +3 -0
- qr-payment.js +353 -0
- quote-ui.js +163 -0
- san-pham/02-03-grob/index.html +20 -0
- san-pham/02-45-grob/index.html +20 -0
- san-pham/02-50-grob/index.html +20 -0
- san-pham/02-60-grob/index.html +20 -0
- san-pham/02-70-grob/index.html +20 -0
- san-pham/02-80-grob/index.html +20 -0
- san-pham/02-90-grob/index.html +20 -0
- san-pham/1044p-malloca-chau-rua-chen-1-ho-inox-304-kich-thuoc-44cm-lap-am-hoac-duong-2/index.html +20 -0
- san-pham/28cc304-grob-ma-hang-28cc304/index.html +20 -0
- san-pham/28cf304-grob-phu-kien-grob/index.html +20 -0
- san-pham/30l2304-grob-ma-hang-28cc304/index.html +20 -0
- san-pham/30lh304-grob-ma-hang-28cc304/index.html +20 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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,'
|
| 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,'
|
| 101 |
-
+ (sku ? '<p class="meta">Mã: ' + sku.replace(/"/g,'
|
| 102 |
+ (priceStr ? '<p class="price">' + priceStr + '</p>' : '')
|
| 103 |
-
+ '<img src="' + image.replace(/"/g,'
|
| 104 |
-
+ '<p>' + desc.replace(/"/g,'
|
| 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,'"') + '</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();
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
| 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:'')+' | <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+' | <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>
|