Fix: Excel download multi-click support for quote and order modals
#6
by bep40 - opened
- 404.html +160 -0
- README.md +5 -25
- ai-search-fix.js +331 -0
- ai-search.js +647 -0
- cart-fix.js +190 -0
- grob-img-fix.js +76 -0
- grob-price-fix.js +68 -0
- img-fix.js +4 -0
- logo/favicon.png +0 -0
- logo/logo_main.png +0 -0
- logo/partners/canzy.svg +4 -0
- logo/partners/demax.png +0 -0
- logo/partners/dmx.svg +4 -0
- logo/partners/eurogold.png +0 -0
- logo/partners/garis.png +0 -0
- logo/partners/grob.png +0 -0
- logo/partners/hafele.png +0 -0
- logo/partners/malloca.png +0 -0
- logo/zalo-bot.png +0 -0
- order-store.js +124 -44
- qr-payment.js +373 -10
- quote-ui.js +163 -0
- search-dropdown-fix.js +134 -0
- seo-url-fix.js +260 -0
- show-order-picker-v1046.js +293 -0
- style.css +28 -0
- test-upload.html +11 -0
- ui-fixes.js +1 -0
- url-added-live-loader.js +75 -0
- vai-100percent-img-fix.js +133 -0
- vai-excel-100percent-img.js +509 -0
- vai-export-fixed.js +172 -0
- vai-export-multi-download.js +517 -0
- vai-export-working.js +258 -0
- vai-fix-order-download-v1042.js +84 -180
- vai-fix.js +335 -0
- vai-img-ultimate.js +216 -0
- vai-master-fix-v2026.js +161 -0
- vai-patch-v1047.js +181 -0
- vai-quote-excel-fix.js +450 -0
- vai-quote-fix-2026.js +352 -0
- vai-robust-export-v2.js +430 -0
- vai-ultimate-fix-v1100.js +688 -0
404.html
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="vi">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>V.AI STUDIO</title>
|
| 7 |
+
<meta property="og:type" content="website">
|
| 8 |
+
<meta property="og:site_name" content="V.AI STUDIO">
|
| 9 |
+
<meta property="og:image" content="https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png">
|
| 10 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 11 |
+
<script>
|
| 12 |
+
/**
|
| 13 |
+
* V.AI STUDIO — 404 Fix v2
|
| 14 |
+
* ========================
|
| 15 |
+
* FIX:
|
| 16 |
+
* - Bot/crawler visits /san-pham/SLUG/index.html → render full OG tags INSTANTLY
|
| 17 |
+
* - Real user → immediate redirect to SPA
|
| 18 |
+
* - Uses lightweight product_seo_min.json (3.8MB) for rich OG data if available
|
| 19 |
+
*/
|
| 20 |
+
(function(){
|
| 21 |
+
'use strict';
|
| 22 |
+
var path = location.pathname;
|
| 23 |
+
var slug = '';
|
| 24 |
+
var m = path.match(/\/san-pham\/([^\/]+)/i);
|
| 25 |
+
if (m) slug = decodeURIComponent(m[1]);
|
| 26 |
+
|
| 27 |
+
var ua = (navigator.userAgent||'').toLowerCase();
|
| 28 |
+
var isBot = /bot|crawl|spider|facebook|twitter|zalo|whatsapp|telegram|slack|discord|pinterest|linkedin|embedly|iframely|google|bing|yandex|duckduck|baidu|semrush|ahrefs|majestic|facebookexternalhit|adsbot/i.test(ua);
|
| 29 |
+
|
| 30 |
+
var SITE = 'https://bep40-v-aistudio.static.hf.space';
|
| 31 |
+
var IMG_DEFAULT = 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png';
|
| 32 |
+
|
| 33 |
+
function setMeta(attr, name, content) {
|
| 34 |
+
var sel = attr === 'property' ? 'meta[property="' + name + '"]' : 'meta[name="' + name + '"]';
|
| 35 |
+
var el = document.querySelector(sel);
|
| 36 |
+
if (!el) {
|
| 37 |
+
el = document.createElement('meta');
|
| 38 |
+
el.setAttribute(attr, name);
|
| 39 |
+
document.head.appendChild(el);
|
| 40 |
+
}
|
| 41 |
+
el.setAttribute('content', content || '');
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function setCanonical(url) {
|
| 45 |
+
var el = document.querySelector('link[rel="canonical"]');
|
| 46 |
+
if (!el) {
|
| 47 |
+
el = document.createElement('link');
|
| 48 |
+
el.setAttribute('rel', 'canonical');
|
| 49 |
+
document.head.appendChild(el);
|
| 50 |
+
}
|
| 51 |
+
el.setAttribute('href', url || SITE + '/');
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// ── Render full static page for bot ──
|
| 55 |
+
function renderBotPage(name, price, image, desc, sku, brand) {
|
| 56 |
+
name = name || slug || 'Sản phẩm';
|
| 57 |
+
price = price || 0;
|
| 58 |
+
image = image || IMG_DEFAULT;
|
| 59 |
+
desc = desc || name + ' tại V.AI STUDIO';
|
| 60 |
+
sku = sku || '';
|
| 61 |
+
brand = brand || '';
|
| 62 |
+
|
| 63 |
+
var title = name;
|
| 64 |
+
if (brand) title = name + ' | ' + brand + ' | V.AI STUDIO';
|
| 65 |
+
else title = name + ' | V.AI STUDIO';
|
| 66 |
+
|
| 67 |
+
var priceStr = price ? Number(price).toLocaleString('vi-VN') + 'đ' : '';
|
| 68 |
+
var ogDesc = priceStr ? 'Giá: ' + priceStr + ' - ' + desc : desc;
|
| 69 |
+
var ogUrl = SITE + '/san-pham/' + encodeURIComponent(slug) + '/index.html';
|
| 70 |
+
|
| 71 |
+
setMeta('property', 'og:title', title);
|
| 72 |
+
setMeta('property', 'og:description', ogDesc);
|
| 73 |
+
setMeta('property', 'og:image', image);
|
| 74 |
+
setMeta('property', 'og:url', ogUrl);
|
| 75 |
+
if (price > 0) {
|
| 76 |
+
setMeta('property', 'product:price:amount', String(price));
|
| 77 |
+
setMeta('property', 'product:price:currency', 'VND');
|
| 78 |
+
}
|
| 79 |
+
setMeta('name', 'twitter:title', title);
|
| 80 |
+
setMeta('name', 'twitter:description', ogDesc);
|
| 81 |
+
setMeta('name', 'twitter:image', image);
|
| 82 |
+
setCanonical(ogUrl);
|
| 83 |
+
document.title = title;
|
| 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();
|
| 108 |
+
document.write(html);
|
| 109 |
+
document.close();
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
function tryLoadSEOIndex() {
|
| 114 |
+
var xhr = new XMLHttpRequest();
|
| 115 |
+
xhr.open('GET', '/product_seo_min.json', true);
|
| 116 |
+
xhr.timeout = 5000;
|
| 117 |
+
xhr.onload = function() {
|
| 118 |
+
if (xhr.status >= 200 && xhr.status < 300) {
|
| 119 |
+
try {
|
| 120 |
+
var data = JSON.parse(xhr.responseText);
|
| 121 |
+
var target = slug ? slug.toLowerCase() : '';
|
| 122 |
+
for (var i = 0; i < data.length; i++) {
|
| 123 |
+
var p = data[i];
|
| 124 |
+
if (p[0] && p[0].toLowerCase() === target) {
|
| 125 |
+
renderBotPage(p[1], p[2], p[3], p[4], p[5], p[6]);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
} catch(e) {}
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
xhr.send();
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// ══════════ MAIN ══════════
|
| 136 |
+
if (!slug) {
|
| 137 |
+
if (!isBot) location.replace('/');
|
| 138 |
+
return;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Step 1: Always render minimal OG instantly from slug
|
| 142 |
+
var guessName = slug.replace(/-/g, ' ').replace(/\b\w/g, function(c){ return c.toUpperCase(); });
|
| 143 |
+
renderBotPage(guessName, 0, IMG_DEFAULT, 'Sản phẩm tại V.AI STUDIO', '', '');
|
| 144 |
+
|
| 145 |
+
// Step 2: Try async loading of SEO index for accurate data
|
| 146 |
+
tryLoadSEOIndex();
|
| 147 |
+
|
| 148 |
+
// Step 3: Real users redirect
|
| 149 |
+
if (!isBot) {
|
| 150 |
+
setTimeout(function() {
|
| 151 |
+
location.replace('/?product=' + encodeURIComponent(slug));
|
| 152 |
+
}, 100);
|
| 153 |
+
}
|
| 154 |
+
})();
|
| 155 |
+
</script>
|
| 156 |
+
</head>
|
| 157 |
+
<body>
|
| 158 |
+
<p style="font-family:sans-serif;text-align:center;padding:40px;color:#666">Vui lòng chờ trong giây lát...</p>
|
| 159 |
+
</body>
|
| 160 |
+
</html>
|
README.md
CHANGED
|
@@ -1,32 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title: V
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
tags:
|
| 9 |
- ml-intern
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
<!-- ml-intern-provenance -->
|
| 15 |
-
## Generated by ML Intern
|
| 16 |
-
|
| 17 |
-
This model repository was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
|
| 18 |
-
|
| 19 |
-
- Try ML Intern: https://smolagents-ml-intern.hf.space
|
| 20 |
-
- Source code: https://github.com/huggingface/ml-intern
|
| 21 |
-
|
| 22 |
-
## Usage
|
| 23 |
-
|
| 24 |
-
```python
|
| 25 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 26 |
-
|
| 27 |
-
model_id = 'bep40/V.AISTUDIO'
|
| 28 |
-
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 29 |
-
model = AutoModelForCausalLM.from_pretrained(model_id)
|
| 30 |
-
```
|
| 31 |
-
|
| 32 |
-
For non-causal architectures, replace `AutoModelForCausalLM` with the appropriate `AutoModel` class.
|
|
|
|
| 1 |
---
|
| 2 |
+
title: V AISTUDIO Products
|
| 3 |
+
emoji: 📉
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
tags:
|
| 9 |
- ml-intern
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
})();
|
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 |
+
})();
|
img-fix.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Image fallback
|
| 2 |
+
window._imgFallback = function(img) {
|
| 3 |
+
img.style.display = 'none';
|
| 4 |
+
};
|
logo/favicon.png
ADDED
|
|
logo/logo_main.png
ADDED
|
logo/partners/canzy.svg
ADDED
|
|
logo/partners/demax.png
ADDED
|
logo/partners/dmx.svg
ADDED
|
|
logo/partners/eurogold.png
ADDED
|
logo/partners/garis.png
ADDED
|
logo/partners/grob.png
ADDED
|
logo/partners/hafele.png
ADDED
|
logo/partners/malloca.png
ADDED
|
logo/zalo-bot.png
ADDED
|
order-store.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
/**
|
| 2 |
-
* V.AI STUDIO — Order Store v19
|
| 3 |
-
*
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
*
|
|
|
|
| 7 |
*/
|
| 8 |
(function(){
|
| 9 |
'use strict';
|
|
@@ -58,10 +59,10 @@ function syncFromServer(){
|
|
| 58 |
});
|
| 59 |
if(added>0){
|
| 60 |
saveOrders(localOrders);
|
| 61 |
-
console.log('[OS v19] ☁️ Synced '+added+' orders from server. Total local: '+localOrders.length);
|
| 62 |
}
|
| 63 |
}).catch(function(e){
|
| 64 |
-
console.log('[OS v19] Server sync skip (bot may be sleeping)');
|
| 65 |
});
|
| 66 |
}
|
| 67 |
// Sync from server 3s after page load (give bot time to wake up)
|
|
@@ -77,14 +78,14 @@ function processQueue(){
|
|
| 77 |
var q=getQueue();if(!q.length)return;
|
| 78 |
var item=q[0];
|
| 79 |
fetch(KETOAN_API+item.endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(item.payload)})
|
| 80 |
-
.then(function(r){if(r.ok){q.shift();saveQueue(q);console.log('[OS v19] Queue sync OK: '+item.endpoint+' ('+q.length+' remaining)');if(q.length)setTimeout(processQueue,2000);}else throw new Error('HTTP '+r.status);})
|
| 81 |
.catch(function(){item.attempts=(item.attempts||0)+1;if(item.attempts>50){q.shift();}saveQueue(q);});
|
| 82 |
}
|
| 83 |
setInterval(processQueue,30000);
|
| 84 |
setTimeout(processQueue,5000);
|
| 85 |
|
| 86 |
function syncOrderToBot(order){
|
| 87 |
-
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()};
|
| 88 |
if(order.confirmedAt)payload.confirmedAt=order.confirmedAt;
|
| 89 |
fetch(KETOAN_API+'/order-saved',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
| 90 |
.then(function(r){if(!r.ok)throw new Error();return r.json();})
|
|
@@ -92,7 +93,7 @@ function syncOrderToBot(order){
|
|
| 92 |
}
|
| 93 |
|
| 94 |
function sendToKetoanBot(order){
|
| 95 |
-
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||''};
|
| 96 |
fetch(KETOAN_API+'/order-confirmed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
| 97 |
.then(function(r){if(!r.ok)throw new Error();return r.json();})
|
| 98 |
.then(function(d){if(d.ok&&typeof showToast==='function')showToast('📨 Đã gửi kế toán!');})
|
|
@@ -131,26 +132,33 @@ function orderItemInfo(it){it=it||{};return formatInfoVal(it.info||it.thong_tin|
|
|
| 131 |
function compactKey(s){return (s||'').toString().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d').toLowerCase().replace(/[^a-z0-9]/g,'');}
|
| 132 |
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;});}
|
| 133 |
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;}
|
| 134 |
-
function parseItemDiscountLine(line,map){var lo=(line||'').toString().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d').toLowerCase();var re1=/
|
| 135 |
function esc(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
| 136 |
function parsePrompt(text){
|
| 137 |
-
var fees=[],deposit=0,discountPercent=0,
|
| 138 |
-
if(!text)return{fees:fees,deposit:deposit,discountPercent:discountPercent,
|
| 139 |
-
text.split(/[,;
|
| 140 |
-
]+/).forEach(function(line){
|
| 141 |
line=line.trim();if(!line)return;
|
| 142 |
parseItemDiscountLine(line,itemDiscounts);
|
| 143 |
-
if(Object.keys(itemDiscounts).length&&line.match(/
|
| 144 |
var lo=line.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d');
|
| 145 |
-
// Ghi chú
|
| 146 |
-
if(lo.match(/^(ghi chu|note|luu y)/)){
|
| 147 |
-
// CK
|
| 148 |
var ck=lo.match(/^(ck|chiet khau|giam|discount)\s*([\d.,]+)\s*(%|k|tr)?/);
|
| 149 |
if(ck){var v=parseFloat(ck[2].replace(/\./g,'').replace(',','.'));if((!ck[3]||ck[3]===''||ck[3]==='%')&&v>0&&v<=100)discountPercent=v;return;}
|
|
|
|
|
|
|
| 150 |
// Cọc
|
| 151 |
-
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
// Phụ phí: giao hàng, lắp đặt, vận chuyển, phí khác
|
| 153 |
-
var feeMatch=lo.match(/^(giao hang|giao|lap dat|lap|van chuyen|ship|phi giao|phi
|
| 154 |
if(feeMatch){
|
| 155 |
var amt=parseFloat(feeMatch[2].replace(/\./g,'').replace(',','.'));
|
| 156 |
var u=(feeMatch[3]||'').toLowerCase();
|
|
@@ -159,21 +167,83 @@ function parsePrompt(text){
|
|
| 159 |
fees.push({label:labelMap[feeMatch[1]]||'Phụ phí',amount:amt});
|
| 160 |
return;
|
| 161 |
}
|
| 162 |
-
// Fallback: line with number
|
| 163 |
var m=lo.match(/([\d.,]+)\s*(k|tr|trieu|m|nghin)?/);
|
| 164 |
-
if(m
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
});
|
| 166 |
-
return{fees:fees,deposit:deposit,discountPercent:discountPercent,
|
| 167 |
}
|
| 168 |
|
| 169 |
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);});}
|
| 170 |
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;}
|
| 171 |
|
| 172 |
-
|
| 173 |
-
function
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
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:order.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};}
|
| 177 |
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);}}
|
| 178 |
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));}}
|
| 179 |
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));}}
|
|
@@ -181,15 +251,20 @@ async function exportDeliveryPDF(order,opt){if(window.VAI_QR&&window.VAI_QR.expo
|
|
| 181 |
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));}}
|
| 182 |
|
| 183 |
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;
|
| 184 |
-
// Refresh button — force sync from server
|
| 185 |
document.getElementById('vai-os-refresh').onclick=function(){
|
| 186 |
this.textContent='⏳';var self=this;
|
| 187 |
syncFromServer();
|
| 188 |
setTimeout(function(){self.textContent='🔄';ov.remove();openSearchModal();},2000);
|
| 189 |
};
|
| 190 |
-
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();};});
|
|
|
|
| 191 |
|
| 192 |
-
function openOrderDetail(order){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
var _searchOpen=false;
|
| 194 |
function addProductToOrder(product){
|
| 195 |
var pn=product.priceNum||product.pn||0;
|
|
@@ -265,7 +340,6 @@ function openOrderDetail(order){var old=document.getElementById('vai-order-detai
|
|
| 265 |
+'<div style="font-size:11px;font-weight:700;color:#003f62;white-space:nowrap">'+fmt(price)+'</div>'
|
| 266 |
+'<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>';
|
| 267 |
}).join('');
|
| 268 |
-
// Bind click events
|
| 269 |
el.querySelectorAll('.od-sp-item').forEach(function(item){
|
| 270 |
item.onclick=function(e){
|
| 271 |
if(e.target.closest('.od-sp-add'))return;
|
|
@@ -283,16 +357,19 @@ function openOrderDetail(order){var old=document.getElementById('vai-order-detai
|
|
| 283 |
};
|
| 284 |
});
|
| 285 |
}
|
| 286 |
-
function render(){
|
|
|
|
| 287 |
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">'
|
| 288 |
-
+'<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>'
|
| 289 |
+'<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>'
|
| 290 |
-
+'<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>';
|
| 291 |
-
(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>';});
|
| 292 |
h+='</tbody></table>';
|
| 293 |
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>';}
|
| 294 |
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>';
|
| 295 |
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>';
|
|
|
|
|
|
|
| 296 |
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>'
|
| 297 |
+'<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>'
|
| 298 |
+'<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>';
|
|
@@ -303,23 +380,26 @@ function openOrderDetail(order){var old=document.getElementById('vai-order-detai
|
|
| 303 |
h+='</div>';modal.innerHTML=h;
|
| 304 |
document.getElementById('xc').onclick=function(){ov.remove();};
|
| 305 |
modal.querySelectorAll('.xd').forEach(function(b){b.onclick=function(e){e.stopPropagation();order.items.splice(parseInt(this.dataset.i),1);render();};});
|
| 306 |
-
// Add SP button
|
| 307 |
var addSpBtn=document.getElementById('od-add-sp');
|
| 308 |
if(addSpBtn){addSpBtn.onclick=toggleSearchPanel;}
|
| 309 |
-
// Close search panel
|
| 310 |
var closeSpBtn=document.getElementById('od-sp-close');
|
| 311 |
if(closeSpBtn){closeSpBtn.onclick=function(){_searchOpen=false;render();};}
|
| 312 |
-
// Search input
|
| 313 |
var searchInput=document.getElementById('od-sp-search');
|
| 314 |
if(searchInput){searchInput.oninput=function(){renderSearchResults();};searchInput.onkeydown=function(e){if(e.key==='Escape'){_searchOpen=false;render();}};}
|
| 315 |
-
// Render search results if panel is open
|
| 316 |
if(_searchOpen){renderSearchResults();}
|
| 317 |
-
function cc(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
var mI=document.getElementById('od-makh'),sB=document.getElementById('od-makh-suggest'),mInfo=document.getElementById('od-makh-info');
|
| 319 |
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);};}
|
| 320 |
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();}
|
| 321 |
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();};
|
| 322 |
-
|
| 323 |
document.getElementById('od-pdf').onclick=function(){cc();recalcOrder(order);exportPDF(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
|
| 324 |
document.getElementById('od-xl').onclick=function(){cc();recalcOrder(order);exportExcel(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
|
| 325 |
document.getElementById('od-gh').onclick=function(){cc();recalcOrder(order);exportDeliveryPDF(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
|
|
@@ -343,5 +423,5 @@ var _mO=new MutationObserver(function(){setTimeout(injectMaKHToQuote,500);});_mO
|
|
| 343 |
|
| 344 |
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};
|
| 345 |
var qLen=getQueue().length;
|
| 346 |
-
console.log('[OS v19] '+getOrders().length+' orders'+(qLen?' | ⏳ '+qLen+' pending sync':'')+' | ☁️ Server sync enabled | ➕ Add product to order enabled');
|
| 347 |
-
})();
|
|
|
|
| 1 |
/**
|
| 2 |
+
* V.AI STUDIO — Order Store v19.2
|
| 3 |
+
* FIX TRIỆT ĐỂ (v19.2):
|
| 4 |
+
* - saveCurrentOrder: lưu date từ input khách hàng (không để trống ngày đơn)
|
| 5 |
+
* - orderToVAIData: productTotal = tổng tiền SP (KHÔNG trừ phí); lấy notes từ order.notes (ưu tiên) rồi qd.notes
|
| 6 |
+
* - openOrderDetail: thêm cột Ghi chú vào bảng; lấy date từ input; luôn recalc trước export
|
| 7 |
+
* - parsePrompt: 'Malloca ck 35%' detected as global discount; 'ghi chú: giao thứ 7' = note
|
| 8 |
*/
|
| 9 |
(function(){
|
| 10 |
'use strict';
|
|
|
|
| 59 |
});
|
| 60 |
if(added>0){
|
| 61 |
saveOrders(localOrders);
|
| 62 |
+
console.log('[OS v19.2] ☁️ Synced '+added+' orders from server. Total local: '+localOrders.length);
|
| 63 |
}
|
| 64 |
}).catch(function(e){
|
| 65 |
+
console.log('[OS v19.2] Server sync skip (bot may be sleeping)');
|
| 66 |
});
|
| 67 |
}
|
| 68 |
// Sync from server 3s after page load (give bot time to wake up)
|
|
|
|
| 78 |
var q=getQueue();if(!q.length)return;
|
| 79 |
var item=q[0];
|
| 80 |
fetch(KETOAN_API+item.endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(item.payload)})
|
| 81 |
+
.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);})
|
| 82 |
.catch(function(){item.attempts=(item.attempts||0)+1;if(item.attempts>50){q.shift();}saveQueue(q);});
|
| 83 |
}
|
| 84 |
setInterval(processQueue,30000);
|
| 85 |
setTimeout(processQueue,5000);
|
| 86 |
|
| 87 |
function syncOrderToBot(order){
|
| 88 |
+
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||'',notes:order.notes||[],status:order.status||'pending',savedAt:order.savedAt||new Date().toISOString()};
|
| 89 |
if(order.confirmedAt)payload.confirmedAt=order.confirmedAt;
|
| 90 |
fetch(KETOAN_API+'/order-saved',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
| 91 |
.then(function(r){if(!r.ok)throw new Error();return r.json();})
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
function sendToKetoanBot(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||{},confirmedAt:order.confirmedAt||new Date().toISOString(),email:order.email||'',date:order.date||'',notes:order.notes||[],};
|
| 97 |
fetch(KETOAN_API+'/order-confirmed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
| 98 |
.then(function(r){if(!r.ok)throw new Error();return r.json();})
|
| 99 |
.then(function(d){if(d.ok&&typeof showToast==='function')showToast('📨 Đã gửi kế toán!');})
|
|
|
|
| 132 |
function compactKey(s){return (s||'').toString().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d').toLowerCase().replace(/[^a-z0-9]/g,'');}
|
| 133 |
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;});}
|
| 134 |
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;}
|
| 135 |
+
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;}}
|
| 136 |
function esc(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
| 137 |
function parsePrompt(text){
|
| 138 |
+
var fees=[],deposit=0,discountPercent=0,notes=[],itemDiscounts={};
|
| 139 |
+
if(!text)return{fees:fees,deposit:deposit,discountPercent:discountPercent,notes:notes,itemDiscounts:itemDiscounts};
|
| 140 |
+
text.split(/[,;\n]+/).forEach(function(line){
|
|
|
|
| 141 |
line=line.trim();if(!line)return;
|
| 142 |
parseItemDiscountLine(line,itemDiscounts);
|
| 143 |
+
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;
|
| 144 |
var lo=line.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[đĐ]/g,'d');
|
| 145 |
+
// Ghi chú (FIX: always extract text after prefix, never parse numbers)
|
| 146 |
+
if(lo.match(/^(ghi chu|note|luu y)/)){notes.push(line.replace(/^(ghi chú|ghi chu|note|lưu ý|luu y)[:\s]*/i,'').trim());return;}
|
| 147 |
+
// CK (FIX: also match 'WORD ck X%')
|
| 148 |
var ck=lo.match(/^(ck|chiet khau|giam|discount)\s*([\d.,]+)\s*(%|k|tr)?/);
|
| 149 |
if(ck){var v=parseFloat(ck[2].replace(/\./g,'').replace(',','.'));if((!ck[3]||ck[3]===''||ck[3]==='%')&&v>0&&v<=100)discountPercent=v;return;}
|
| 150 |
+
var ck2=lo.match(/\bck\s*([\d.,]+)\s*(%)?/);
|
| 151 |
+
if(ck2&&!lo.match(/\b(ck|chiet khau|giam|discount)\s*[a-z]/)){var v2=parseFloat(ck2[1].replace(/\./g,'').replace(',','.'));if(v2>0&&v2<=100){discountPercent=v2;return;}}
|
| 152 |
// Cọc
|
| 153 |
+
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;}
|
| 154 |
+
// FIX: "giao thứ X" / "lắp thứ X" = note about delivery day
|
| 155 |
+
if(lo.match(/(giao|lap)\s*thu\s*\d/i)){
|
| 156 |
+
var dayMatch = lo.match(/(giao|lap)\s*thu\s*(\d+)/i);
|
| 157 |
+
if(dayMatch) {notes.push('Giao thứ ' + dayMatch[2]);}
|
| 158 |
+
return;
|
| 159 |
+
}
|
| 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 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();
|
|
|
|
| 167 |
fees.push({label:labelMap[feeMatch[1]]||'Phụ phí',amount:amt});
|
| 168 |
return;
|
| 169 |
}
|
| 170 |
+
// Fallback: line with number (skip if looks like day note)
|
| 171 |
var m=lo.match(/([\d.,]+)\s*(k|tr|trieu|m|nghin)?/);
|
| 172 |
+
if(m && !lo.match(/thu\s*(\d|$)/i)){
|
| 173 |
+
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){
|
| 174 |
+
var lb=line.replace(m[0],'').trim();
|
| 175 |
+
if(!lb||lb.length<2)lb='Phụ phí';fees.push({label:lb,amount:amt2});
|
| 176 |
+
}}
|
| 177 |
});
|
| 178 |
+
return{fees:fees,deposit:deposit,discountPercent:discountPercent,notes:notes,itemDiscounts:itemDiscounts};
|
| 179 |
}
|
| 180 |
|
| 181 |
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);});}
|
| 182 |
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;}
|
| 183 |
|
| 184 |
+
// FIX TRIỆT ĐỂ: lấy date từ input khách hàng nếu có
|
| 185 |
+
function _getCustomerDateFromInput(){
|
| 186 |
+
var sels=['#quoteDate','#quoteNgay','#checkoutDate','input[placeholder*="ngày"]','input[placeholder*="date"]'];
|
| 187 |
+
for(var i=0;i<sels.length;i++){var el=document.querySelector(sels[i]);if(el&&el.value&&el.value.trim())return el.value.trim();}
|
| 188 |
+
return '';
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function saveCurrentOrder(){
|
| 192 |
+
if(!window.VAI_QR||!window.VAI_QR.getData)return;
|
| 193 |
+
var d=window.VAI_QR.getData();
|
| 194 |
+
var qd=d.qd;
|
| 195 |
+
var code=window.VAI_QR.getEffectiveOrderCode();
|
| 196 |
+
// FIX: lấy date từ input khách hàng ưu tiên, fallback ngày hiện tại
|
| 197 |
+
var inputDate=_getCustomerDateFromInput();
|
| 198 |
+
var orderDate=inputDate||qd.customer.date||new Date().toLocaleDateString('vi-VN');
|
| 199 |
+
var order={
|
| 200 |
+
code:code,
|
| 201 |
+
customer:qd.customer.name||'',
|
| 202 |
+
phone:qd.customer.phone||'',
|
| 203 |
+
email:qd.customer.email||'',
|
| 204 |
+
addr:qd.customer.addr||'',
|
| 205 |
+
date:orderDate,
|
| 206 |
+
items:qd.items||[],
|
| 207 |
+
fees:d.fees||[],
|
| 208 |
+
deposit:d.deposit||0,
|
| 209 |
+
discountPercent:d.discountPercent||0,
|
| 210 |
+
itemDiscounts:d.itemDiscounts||{},
|
| 211 |
+
notes:d.notes||[],
|
| 212 |
+
grandTotal:d.grandTotal||0,
|
| 213 |
+
remaining:d.remaining||0,
|
| 214 |
+
promptText:window.VAI_QR.getPromptText?window.VAI_QR.getPromptText():'',
|
| 215 |
+
status:'pending',
|
| 216 |
+
savedAt:new Date().toISOString()
|
| 217 |
+
};
|
| 218 |
+
recalcOrder(order);
|
| 219 |
+
addOrder(order);
|
| 220 |
+
syncOrderToBot(order);
|
| 221 |
+
if(typeof showToast==='function')showToast('✅ Đã lưu đơn '+code);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// FIX TRIỆT ĐỂ: productTotal = tổng tiền SP (KHÔNG trừ phí); notes ưu tiên order.notes
|
| 225 |
+
function orderToVAIData(order){
|
| 226 |
+
recalcOrder(order);
|
| 227 |
+
var pt=0;(order.items||[]).forEach(function(it){pt+=it.total||0;});
|
| 228 |
+
var notes=order.notes&&order.notes.length?order.notes:[];
|
| 229 |
+
return{
|
| 230 |
+
qd:{
|
| 231 |
+
customer:{name:order.customer||'',phone:order.phone||'',email:order.email||'',addr:order.addr||'',date:order.date||''},
|
| 232 |
+
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||''};}),
|
| 233 |
+
grandTotal:order.grandTotal||0
|
| 234 |
+
},
|
| 235 |
+
fees:order.fees||[],
|
| 236 |
+
notes:notes,
|
| 237 |
+
discount:null,
|
| 238 |
+
discountPercent:order.discountPercent||0,
|
| 239 |
+
itemDiscounts:order.itemDiscounts||{},
|
| 240 |
+
deposit:order.deposit||0,
|
| 241 |
+
productTotal:pt,
|
| 242 |
+
grandTotal:order.grandTotal||0,
|
| 243 |
+
remaining:order.remaining||0
|
| 244 |
+
};
|
| 245 |
+
}
|
| 246 |
|
|
|
|
| 247 |
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);}}
|
| 248 |
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));}}
|
| 249 |
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));}}
|
|
|
|
| 251 |
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));}}
|
| 252 |
|
| 253 |
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;
|
|
|
|
| 254 |
document.getElementById('vai-os-refresh').onclick=function(){
|
| 255 |
this.textContent='⏳';var self=this;
|
| 256 |
syncFromServer();
|
| 257 |
setTimeout(function(){self.textContent='🔄';ov.remove();openSearchModal();},2000);
|
| 258 |
};
|
| 259 |
+
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();};});
|
| 260 |
+
}
|
| 261 |
|
| 262 |
+
function openOrderDetail(order){
|
| 263 |
+
var old=document.getElementById('vai-order-detail-modal');if(old)old.remove();
|
| 264 |
+
order=JSON.parse(JSON.stringify(order));
|
| 265 |
+
// FIX: đảm bảo notes luôn là mảng
|
| 266 |
+
if(!order.notes)order.notes=[];
|
| 267 |
+
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);
|
| 268 |
var _searchOpen=false;
|
| 269 |
function addProductToOrder(product){
|
| 270 |
var pn=product.priceNum||product.pn||0;
|
|
|
|
| 340 |
+'<div style="font-size:11px;font-weight:700;color:#003f62;white-space:nowrap">'+fmt(price)+'</div>'
|
| 341 |
+'<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>';
|
| 342 |
}).join('');
|
|
|
|
| 343 |
el.querySelectorAll('.od-sp-item').forEach(function(item){
|
| 344 |
item.onclick=function(e){
|
| 345 |
if(e.target.closest('.od-sp-add'))return;
|
|
|
|
| 357 |
};
|
| 358 |
});
|
| 359 |
}
|
| 360 |
+
function render(){
|
| 361 |
+
recalcOrder(order);var isC=order.status==='confirmed';
|
| 362 |
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">'
|
| 363 |
+
+'<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 style="grid-column:1/-1"><b>Ngày:</b> <input id="od-date" value="'+esc(order.date||'')+'" placeholder="DD/MM/YYYY" style="border:1px solid #ddd;padding:3px 6px;border-radius:4px;width:40%;font-size:11px"></div></div>'
|
| 364 |
+'<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>'
|
| 365 |
+
+'<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 style="padding:5px;text-align:left">Ghi chú</th><th></th></tr></thead><tbody>';
|
| 366 |
+
(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;font-size:9px;max-width:120px;white-space:normal">'+esc(it.note||'')+'</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>';});
|
| 367 |
h+='</tbody></table>';
|
| 368 |
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>';}
|
| 369 |
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>';
|
| 370 |
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>';
|
| 371 |
+
else h+='<div style="padding:4px 10px;background:#fef2f2;border-radius:6px;font-size:10px;margin-bottom:6px"><span style="font-weight:800">Còn lại (100%):</span> <b style="color:#dc2626">'+fmt(order.grandTotal)+'</b></span></div>';
|
| 372 |
+
if(order.notes&&order.notes.length)h+='<div style="padding:4px 10px;background:#f0fdf4;border-radius:6px;font-size:10px;margin-bottom:6px;color:#166534"><b>📝 Ghi chú:</b> '+esc(order.notes.join('; '))+'</div>';
|
| 373 |
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>'
|
| 374 |
+'<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>'
|
| 375 |
+'<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>';
|
|
|
|
| 380 |
h+='</div>';modal.innerHTML=h;
|
| 381 |
document.getElementById('xc').onclick=function(){ov.remove();};
|
| 382 |
modal.querySelectorAll('.xd').forEach(function(b){b.onclick=function(e){e.stopPropagation();order.items.splice(parseInt(this.dataset.i),1);render();};});
|
|
|
|
| 383 |
var addSpBtn=document.getElementById('od-add-sp');
|
| 384 |
if(addSpBtn){addSpBtn.onclick=toggleSearchPanel;}
|
|
|
|
| 385 |
var closeSpBtn=document.getElementById('od-sp-close');
|
| 386 |
if(closeSpBtn){closeSpBtn.onclick=function(){_searchOpen=false;render();};}
|
|
|
|
| 387 |
var searchInput=document.getElementById('od-sp-search');
|
| 388 |
if(searchInput){searchInput.oninput=function(){renderSearchResults();};searchInput.onkeydown=function(e){if(e.key==='Escape'){_searchOpen=false;render();}};}
|
|
|
|
| 389 |
if(_searchOpen){renderSearchResults();}
|
| 390 |
+
function cc(){
|
| 391 |
+
order.customer=document.getElementById('od-name').value;
|
| 392 |
+
order.phone=document.getElementById('od-phone').value;
|
| 393 |
+
order.addr=document.getElementById('od-addr').value;
|
| 394 |
+
// FIX: cập nhật date từ input
|
| 395 |
+
var dt=document.getElementById('od-date');
|
| 396 |
+
if(dt)order.date=dt.value.trim();
|
| 397 |
+
}
|
| 398 |
var mI=document.getElementById('od-makh'),sB=document.getElementById('od-makh-suggest'),mInfo=document.getElementById('od-makh-info');
|
| 399 |
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);};}
|
| 400 |
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();}
|
| 401 |
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();};
|
| 402 |
+
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!');};
|
| 403 |
document.getElementById('od-pdf').onclick=function(){cc();recalcOrder(order);exportPDF(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
|
| 404 |
document.getElementById('od-xl').onclick=function(){cc();recalcOrder(order);exportExcel(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
|
| 405 |
document.getElementById('od-gh').onclick=function(){cc();recalcOrder(order);exportDeliveryPDF(order,{bw:!!(document.getElementById('od-bw')&&document.getElementById('od-bw').checked)});};
|
|
|
|
| 423 |
|
| 424 |
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};
|
| 425 |
var qLen=getQueue().length;
|
| 426 |
+
console.log('[OS v19.2] '+getOrders().length+' orders'+(qLen?' | ⏳ '+qLen+' pending sync':'')+' | ☁️ Server sync enabled | ➕ Add product to order enabled | FIX: notes + CÒN LẠI 100% + date saved');
|
| 427 |
+
})();
|
qr-payment.js
CHANGED
|
@@ -1,10 +1,373 @@
|
|
| 1 |
-
/* V.AI STUDIO — QR Payment / Quote Data
|
| 2 |
-
*
|
| 3 |
-
*
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
-
*
|
| 8 |
-
*
|
| 9 |
-
*
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* V.AI STUDIO — QR Payment / Quote Data v1040
|
| 2 |
+
* FIX TRIỆT ĐỂ (v1040):
|
| 3 |
+
* - parsePrompt: 'Malloca ck 35%' detected as global discount;
|
| 4 |
+
* 'ghi chú: giao thứ 7' treated as note, not a 7đ fee;
|
| 5 |
+
* small numbers with ordinal/date context (thứ, ngày, giờ) = note, not fee
|
| 6 |
+
* - getData(): copy parsed.notes vào qd.notes để export từ đơn đã lưu vẫn giữ ghi chú
|
| 7 |
+
* - _doExportExcel(): LUÔN có dòng "CÒN LẠI 100%" (khi chưa cọc) + dòng Cọc/Còn lại (khi có cọc)
|
| 8 |
+
* → đảm bảo phí giao hàng, lắp đặt, cọc, còn lại 100% xuất đầy đủ vào Excel báo giá
|
| 9 |
+
*/
|
| 10 |
+
(function(){
|
| 11 |
+
'use strict';
|
| 12 |
+
var BANK_ID='vib',BANK_ACCOUNT='918258385',BANK_NAME='TRAN QUOC VUONG',BANK_NAME_VN='Trần Quốc Vương';
|
| 13 |
+
function stripDiacritics(s){return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'D');}
|
| 14 |
+
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;}
|
| 15 |
+
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 '';}
|
| 16 |
+
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('');}
|
| 17 |
+
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);}
|
| 18 |
+
function fmt(n){if(!n||isNaN(n))return '0đ';return Number(n).toLocaleString('vi-VN')+'đ';}
|
| 19 |
+
function vaiIsDimensionKey(k,v){
|
| 20 |
+
var txt=stripDiacritics(String(k||'')+' '+String(v||'')).toLowerCase();
|
| 21 |
+
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);
|
| 22 |
+
}
|
| 23 |
+
function vaiDimensionSummary(specs,limit){
|
| 24 |
+
limit=limit||900;var parts=[],seen={};
|
| 25 |
+
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);}
|
| 26 |
+
if(specs&&typeof specs==='object'){
|
| 27 |
+
var entries=Object.entries(specs);entries.filter(function(e){return vaiIsDimensionKey(e[0],e[1]);}).forEach(function(e){add(e[0],e[1]);});
|
| 28 |
+
entries.filter(function(e){return !vaiIsDimensionKey(e[0],e[1]);}).slice(0,8).forEach(function(e){add(e[0],e[1]);});
|
| 29 |
+
}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());}});}
|
| 30 |
+
return parts.join('; ').slice(0,limit);
|
| 31 |
+
}
|
| 32 |
+
function vaiProductInfoForQuote(product){
|
| 33 |
+
if(!product)return '';
|
| 34 |
+
return vaiDimensionSummary(product.specs||{},900)||String(product.summary||product.desc||'').slice(0,900);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function compactKey(s){return stripDiacritics(String(s||'')).toLowerCase().replace(/[^a-z0-9]/g,'');}
|
| 38 |
+
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;});}
|
| 39 |
+
function matchItemDiscount(it,itemDiscounts){
|
| 40 |
+
itemDiscounts=itemDiscounts||{};var keys=itemKeys(it);
|
| 41 |
+
for(var i=0;i<keys.length;i++){var k=keys[i];if(itemDiscounts[k])return itemDiscounts[k];}
|
| 42 |
+
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];}}
|
| 43 |
+
return 0;
|
| 44 |
+
}
|
| 45 |
+
function parseItemDiscountLine(line,map){
|
| 46 |
+
var lo=stripDiacritics(String(line||'')).toLowerCase();
|
| 47 |
+
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;
|
| 48 |
+
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;
|
| 49 |
+
var m;
|
| 50 |
+
while((m=re1.exec(lo))!==null){var v=parseFloat(m[2].replace(/\./g,'').replace(',','.'));if(v>0&&v<=100)map[compactKey(m[1])]=v;}
|
| 51 |
+
while((m=re2.exec(lo))!==null){var v2=parseFloat(m[2].replace(/\./g,'').replace(',','.'));if(v2>0&&v<=100)map[compactKey(m[1])]=v2;}
|
| 52 |
+
}
|
| 53 |
+
function applyPromptDiscounts(qd,parsed){
|
| 54 |
+
if(!qd||!qd.items)return qd;var productTotal=0;var itemDiscounts=(parsed&&parsed.itemDiscounts)||{};var globalPct=Number((parsed&&parsed.discountPercent)||0);
|
| 55 |
+
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);});
|
| 56 |
+
qd.grandTotal=productTotal;return qd;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function parsePrompt(text){
|
| 60 |
+
var fees=[],notes=[],discount=null,deposit=0,discountPercent=0,itemDiscounts={};
|
| 61 |
+
if(!text)return{fees:fees,notes:notes,discount:discount,deposit:deposit,discountPercent:discountPercent,itemDiscounts:itemDiscounts};
|
| 62 |
+
text.split(/[,;\n]+/).forEach(function(line){
|
| 63 |
+
line=line.trim();if(!line)return;
|
| 64 |
+
// FIX: detect 'ghi chú:' / 'note:' prefix → rest is a note (skip all number matching)
|
| 65 |
+
if(line.match(/^(ghi chú|ghichu|note|notes)\s*[:\-]?\s*/i)){
|
| 66 |
+
var nt=line.replace(/^(ghi chú|ghichu|note|notes)\s*[:\-]?\s*/i,'');
|
| 67 |
+
if(nt) notes.push(nt);
|
| 68 |
+
return;
|
| 69 |
+
}
|
| 70 |
+
parseItemDiscountLine(line,itemDiscounts);
|
| 71 |
+
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;
|
| 72 |
+
// FIX: also match 'WORD ck X%' (e.g. 'Malloca ck 35%') — global discount
|
| 73 |
+
if(line.match(/^(ck|chiết khấu|chiet khau|giảm|giam|discount)/i) || line.match(/\bck\s*[\d.,]+\s*%/i)){
|
| 74 |
+
discount=line;
|
| 75 |
+
var ckm=line.match(/([\d.,]+)\s*(%)?/);
|
| 76 |
+
if(ckm){var v=parseFloat(ckm[1].replace(/\./g,'').replace(',','.'));if(v>0&&v<=100)discountPercent=v;}
|
| 77 |
+
return;
|
| 78 |
+
}
|
| 79 |
+
if(line.match(/cọc|coc|deposit/i)){
|
| 80 |
+
var dm=line.match(/([\d.,]+)\s*(k|K|nghìn|nghin|triệu|tr|đ|d|vnd)?/);
|
| 81 |
+
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;}
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
// FIX: number match — skip lines that look like dates/ordinals (e.g. 'thứ 7')
|
| 85 |
+
var m=line.match(/([\d.,]+)\s*(k|K|nghìn|nghin|triệu|tr|đ|d|vnd)?/);
|
| 86 |
+
if(m){
|
| 87 |
+
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)return;
|
| 88 |
+
// FIX: small numbers (<1000, no unit) with context words → note, not fee
|
| 89 |
+
var lb=line.replace(m[0],'').trim();
|
| 90 |
+
if(amt<1000&&!u&&lb.match(/thứ|ngày|giờ|tuần|tháng|năm|lúc/i)){notes.push(line);return;}
|
| 91 |
+
if(lb.match(/giao|ship|vận chuyển|delivery/i))lb='Phí giao hàng';else if(lb.match(/lắp|lap|install/i))lb='Phí lắp đặt';else if(lb.match(/bốc|boc|xếp|xep|vác/i))lb='Phí bốc xếp';else if(!lb||lb.length<2)lb='Phụ phí';fees.push({label:lb,amount:amt});
|
| 92 |
+
}
|
| 93 |
+
else{notes.push(line);}
|
| 94 |
+
});
|
| 95 |
+
return{fees:fees,notes:notes,discount:discount,deposit:deposit,discountPercent:discountPercent,itemDiscounts:itemDiscounts};
|
| 96 |
+
}
|
| 97 |
+
var _lastPromptText='';
|
| 98 |
+
function getPromptText(){
|
| 99 |
+
var direct=document.getElementById('qcAiPrompt');
|
| 100 |
+
if(direct){var dv=(direct.value||'').trim();if(dv&&dv.toUpperCase().replace(/[\s.]/g,'')!=='VAISTUDIO'){_lastPromptText=dv;return dv;}}
|
| 101 |
+
var inputs=document.querySelectorAll('input,textarea');
|
| 102 |
+
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;}}
|
| 103 |
+
return _lastPromptText;
|
| 104 |
+
}
|
| 105 |
+
function getData(){
|
| 106 |
+
var qd=window._origGetQuoteData?window._origGetQuoteData():getQuoteData();
|
| 107 |
+
var parsed=parsePrompt(getPromptText());
|
| 108 |
+
qd=applyPromptDiscounts(qd,parsed);
|
| 109 |
+
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||'';}});}
|
| 110 |
+
var surcharge=0;parsed.fees.forEach(function(f){surcharge+=f.amount;});
|
| 111 |
+
var grandTotal=(qd.grandTotal||0)+surcharge;
|
| 112 |
+
var remaining=grandTotal-parsed.deposit;
|
| 113 |
+
// FIX TRIỆT ĐỂ: copy notes → qd.notes để export từ đơn đã lưu vẫn giữ ghi chú
|
| 114 |
+
if(parsed.notes&&parsed.notes.length){qd.notes=parsed.notes;}
|
| 115 |
+
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};
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// === WEB MODAL INJECTION ===
|
| 119 |
+
function injectToModal(){
|
| 120 |
+
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"]');
|
| 121 |
+
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;}}}
|
| 122 |
+
if(!modal)return;
|
| 123 |
+
var old=modal.querySelector('.vai-extra-section');if(old)old.remove();
|
| 124 |
+
var parsed=parsePrompt(getPromptText());
|
| 125 |
+
if(!parsed.fees.length&&!parsed.deposit)return;
|
| 126 |
+
var surcharge=0;parsed.fees.forEach(function(f){surcharge+=f.amount;});
|
| 127 |
+
var qd=window._origGetQuoteData?window._origGetQuoteData():(typeof getQuoteData==='function'?getQuoteData():{grandTotal:0});
|
| 128 |
+
var grandTotal=(qd.grandTotal||0)+surcharge;
|
| 129 |
+
var remaining=grandTotal-parsed.deposit;
|
| 130 |
+
var sec=document.createElement('div');sec.className='vai-extra-section';
|
| 131 |
+
sec.style.cssText='margin:10px 0;padding:0 16px;font-size:13px';
|
| 132 |
+
var html='';
|
| 133 |
+
if(parsed.fees.length){
|
| 134 |
+
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>';
|
| 135 |
+
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>';});
|
| 136 |
+
html+='</div>';
|
| 137 |
+
}
|
| 138 |
+
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>';
|
| 139 |
+
if(parsed.deposit>0){
|
| 140 |
+
html+='<div style="margin-top:6px;padding:6px 12px;background:#f0fdf4;border-radius:6px;border:1px solid #86efac">';
|
| 141 |
+
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>';
|
| 142 |
+
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>';
|
| 143 |
+
html+='</div>';
|
| 144 |
+
}
|
| 145 |
+
sec.innerHTML=html;
|
| 146 |
+
var actions=modal.querySelector('.quote-actions,[class*="actions"],[class*="button"]');
|
| 147 |
+
if(actions&&actions.parentNode){actions.parentNode.insertBefore(sec,actions);}else{modal.appendChild(sec);}
|
| 148 |
+
}
|
| 149 |
+
var _obs=new MutationObserver(function(){setTimeout(injectToModal,300);});
|
| 150 |
+
_obs.observe(document.body,{attributes:true,childList:true,subtree:true});
|
| 151 |
+
setInterval(injectToModal,1500);
|
| 152 |
+
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);
|
| 153 |
+
|
| 154 |
+
// === HTML BÁO GIÁ (PDF/Image) ===
|
| 155 |
+
function buildQuoteHTML(d,code,qrUrl){
|
| 156 |
+
var qd=d.qd;
|
| 157 |
+
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('');
|
| 158 |
+
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>';});}
|
| 159 |
+
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>';}
|
| 160 |
+
if(d.deposit<=0){depositHtml+='<div style="margin-top:8px;padding:8px 14px;background:#fef2f2;border-radius:6px;border:1px solid #fecaca;font-size:12px"><div style="display:flex;justify-content:space-between"><span style="font-weight:800">CÒN LẠI (100%)</span><span style="font-weight:900;color:#dc2626;font-size:14px">'+fmt(d.grandTotal)+'</span></div></div>';}
|
| 161 |
+
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>';}
|
| 162 |
+
var totalQty=(qd.items||[]).reduce(function(s,it){return s+Number(it.qty||1);},0);
|
| 163 |
+
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>';
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
function buildDeliveryHTML(qd,code){
|
| 167 |
+
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('');
|
| 168 |
+
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>';
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
async function toCanvas(html,w){
|
| 172 |
+
var div=document.createElement('div');
|
| 173 |
+
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';
|
| 174 |
+
div.innerHTML=html;document.body.appendChild(div);
|
| 175 |
+
try{if(document.fonts&&document.fonts.ready)await document.fonts.ready;}catch(e){}
|
| 176 |
+
var imgs=Array.from(div.querySelectorAll('img'));
|
| 177 |
+
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);});}));
|
| 178 |
+
await new Promise(function(r){setTimeout(r,500);});
|
| 179 |
+
var h=Math.max(div.scrollHeight,div.offsetHeight);
|
| 180 |
+
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});
|
| 181 |
+
div.remove();return cv;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// === FETCH IMAGE WITH CACHE-BUSTING (reliable CORS fetch) ===
|
| 185 |
+
async function _fetchImageAsBuffer(url){
|
| 186 |
+
if(!url)return null;
|
| 187 |
+
var sep=url.indexOf('?')>0?'&':'?';
|
| 188 |
+
var bustUrl=url+sep+'_t='+Date.now();
|
| 189 |
+
try{
|
| 190 |
+
var r=await fetch(bustUrl,{mode:'cors'});
|
| 191 |
+
if(r.ok)return await r.arrayBuffer();
|
| 192 |
+
}catch(e){console.warn('fetchImage cache-bust failed:',e.message);}
|
| 193 |
+
try{
|
| 194 |
+
var r2=await fetch(url,{mode:'cors'});
|
| 195 |
+
if(r2.ok)return await r2.arrayBuffer();
|
| 196 |
+
}catch(e2){console.warn('fetchImage fallback failed:',e2.message);}
|
| 197 |
+
return null;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
function doOverride(){
|
| 201 |
+
if(typeof getQuoteData!=='function'||typeof html2canvas!=='function')return false;
|
| 202 |
+
window._origGetQuoteData=getQuoteData;
|
| 203 |
+
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);}};
|
| 204 |
+
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));}};
|
| 205 |
+
// window.exportExcel removed — handled by vai-export-fixed.js
|
| 206 |
+
|
| 207 |
+
window.exportDeliveryExcel=async function(){if(typeof ExcelJS==='undefined')return;var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryExcel(qd,code);};
|
| 208 |
+
window.exportDeliveryPDF=async function(){var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryPDF(qd,code);};
|
| 209 |
+
window.getQuoteData=function(){var qd=window._origGetQuoteData.apply(this,arguments);if(qd.customer)qd.customer.orderCode=getEffectiveOrderCode();return qd;};
|
| 210 |
+
console.log('✅ QR Payment v1040 — parsePrompt + notes + CÒN LẠI 100% fixed');return true;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
async function _doExportPDF(d,code){
|
| 214 |
+
var qr=getQRUrl(d.deposit>0?d.remaining:d.grandTotal,code);
|
| 215 |
+
var cv=await toCanvas(buildQuoteHTML(d,code,qr),1000);
|
| 216 |
+
var img= cv.toDataURL('image/jpeg',0.92);
|
| 217 |
+
var pageW=595,pageH=842;
|
| 218 |
+
var imgW=pageW;var imgH=cv.height*imgW/cv.width;
|
| 219 |
+
var pdf=new jspdf.jsPDF({orientation:'p',unit:'pt',format:'a4'});
|
| 220 |
+
if(imgH<=pageH){pdf.addImage(img,'JPEG',0,0,imgW,imgH);}else{
|
| 221 |
+
var y=0;var remaining=imgH;var page=0;
|
| 222 |
+
while(remaining>0){if(page>0)pdf.addPage();pdf.addImage(img,'JPEG',0,-y,imgW,imgH);y+=pageH;remaining-=pageH;page++;}
|
| 223 |
+
}
|
| 224 |
+
pdf.save(code+'.pdf');
|
| 225 |
+
}
|
| 226 |
+
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');}
|
| 227 |
+
|
| 228 |
+
async function _doExportExcel(d,qd,code,qr){
|
| 229 |
+
var MONEY_FMT='#,##0"đ"';
|
| 230 |
+
var NUM_FMT='#,##0';
|
| 231 |
+
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}};
|
| 232 |
+
var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Báo giá');
|
| 233 |
+
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}];
|
| 234 |
+
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){}
|
| 235 |
+
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;
|
| 236 |
+
ws.getRow(1).height=28;ws.getRow(2).height=5;
|
| 237 |
+
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;
|
| 238 |
+
ws.getCell('A5').value='Khách hàng:';ws.getCell('A5').font={bold:true,size:9};ws.getCell('A5').border=WB;
|
| 239 |
+
ws.mergeCells('B5:E5');ws.getCell('B5').value=qd.customer.name||'';ws.getCell('B5').font={bold:true,size:10};ws.getCell('B5').border=WB;
|
| 240 |
+
ws.getCell('H5').value='Mã đơn:';ws.getCell('H5').font={size:9};ws.getCell('H5').border=WB;
|
| 241 |
+
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;
|
| 242 |
+
ws.getCell('A6').value='SĐT:';ws.getCell('A6').font={size:9};ws.getCell('A6').border=WB;
|
| 243 |
+
ws.getCell('B6').value=qd.customer.phone||'';ws.getCell('B6').border=WB;
|
| 244 |
+
ws.getCell('H6').value='Ngày:';ws.getCell('H6').font={size:9};ws.getCell('H6').border=WB;
|
| 245 |
+
ws.getCell('I6').value=qd.customer.date||'';ws.getCell('I6').border=WB;
|
| 246 |
+
ws.getCell('A7').value='Email:';ws.getCell('A7').font={size:9};ws.getCell('A7').border=WB;
|
| 247 |
+
ws.getCell('B7').value=qd.customer.email||'';ws.getCell('B7').border=WB;
|
| 248 |
+
ws.getCell('A8').value='Địa chỉ:';ws.getCell('A8').font={size:9};ws.getCell('A8').border=WB;
|
| 249 |
+
ws.mergeCells('B8:J8');ws.getCell('B8').value=qd.customer.addr||'';ws.getCell('B8').border=WB;
|
| 250 |
+
ws.getRow(9).height=5;
|
| 251 |
+
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'}}};
|
| 252 |
+
var headerRow=ws.getRow(10);
|
| 253 |
+
['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;
|
| 254 |
+
var cr=11;
|
| 255 |
+
var firstItemRow=cr;
|
| 256 |
+
var ckPct=Number(d.discountPercent||0);
|
| 257 |
+
|
| 258 |
+
// Pre-fetch ALL item images eagerly (with cache-bust) BEFORE building cells
|
| 259 |
+
var imgBuffers=[];
|
| 260 |
+
try{
|
| 261 |
+
for(var pi=0;pi<qd.items.length;pi++){
|
| 262 |
+
var imgUrl=qd.items[pi]&&qd.items[pi].image;
|
| 263 |
+
if(imgUrl){imgBuffers.push(await _fetchImageAsBuffer(imgUrl));}
|
| 264 |
+
else{imgBuffers.push(null);}
|
| 265 |
+
}
|
| 266 |
+
}catch(e){console.warn('Pre-fetch images error:',e);}
|
| 267 |
+
|
| 268 |
+
for(var i=0;i<qd.items.length;i++){
|
| 269 |
+
var it=qd.items[i],row=ws.getRow(cr);row.height=50;
|
| 270 |
+
var bgColor=i%2===0?'FFF8FAFC':'FFFFFFFF';
|
| 271 |
+
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}}};
|
| 272 |
+
row.getCell(1).value=it.stt||i+1;row.getCell(1).alignment={horizontal:'center',vertical:'middle'};
|
| 273 |
+
|
| 274 |
+
try{
|
| 275 |
+
if(imgBuffers[i]&&imgBuffers[i].byteLength>100){
|
| 276 |
+
ws.addImage(wb.addImage({buffer:imgBuffers[i],extension:'jpeg'}),{tl:{col:1,row:cr-1},ext:{width:46,height:46}});
|
| 277 |
+
}
|
| 278 |
+
}catch(ie){console.warn('Image add failed for row',cr,ie.message);}
|
| 279 |
+
|
| 280 |
+
row.getCell(3).value=it.name||'';row.getCell(3).font={bold:true,size:9};row.getCell(3).alignment={wrapText:true,vertical:'middle'};
|
| 281 |
+
row.getCell(4).value=it.model||'';row.getCell(4).alignment={horizontal:'center',vertical:'middle'};
|
| 282 |
+
row.getCell(5).value=it.specs||'';row.getCell(5).font={size:8,color:{argb:'FF64748B'}};row.getCell(5).alignment={wrapText:true,vertical:'middle'};
|
| 283 |
+
row.getCell(6).value=Number(it.qty||1);row.getCell(6).numFmt=NUM_FMT;row.getCell(6).alignment={horizontal:'center',vertical:'middle'};
|
| 284 |
+
row.getCell(7).value=Number(it.price||0);row.getCell(7).numFmt=MONEY_FMT;row.getCell(7).alignment={horizontal:'right',vertical:'middle'};
|
| 285 |
+
var itemPct=matchItemDiscount(it,d.itemDiscounts||{});
|
| 286 |
+
if(itemPct>0&&itemPct<=100){row.getCell(8).value={formula:'ROUND(G'+cr+'*'+(1-itemPct/100).toFixed(4)+',0)',result:Number(it.discPrice||0)};}
|
| 287 |
+
else if(ckPct>0&&ckPct<=100){row.getCell(8).value={formula:'ROUND(G'+cr+'*$K$1,0)',result:Number(it.discPrice||0)};}
|
| 288 |
+
else{row.getCell(8).value=Number(it.discPrice||it.price||0);}
|
| 289 |
+
row.getCell(8).numFmt=MONEY_FMT;row.getCell(8).alignment={horizontal:'right',vertical:'middle'};
|
| 290 |
+
if((it.discPrice||0)&&(it.price||0)&&it.discPrice<it.price)row.getCell(8).font={bold:true,color:{argb:'FFDC3545'}};
|
| 291 |
+
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'};
|
| 292 |
+
row.getCell(10).value=it.note||'';row.getCell(10).font={size:8};row.getCell(10).alignment={wrapText:true,vertical:'middle'};
|
| 293 |
+
for(var ci=1;ci<=10;ci++){row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:bgColor}};row.getCell(ci).border=RB;}
|
| 294 |
+
cr++;
|
| 295 |
+
}
|
| 296 |
+
var lastItemRow=cr-1;
|
| 297 |
+
var totalQtyFormula=lastItemRow>=firstItemRow?'SUM(F'+firstItemRow+':F'+lastItemRow+')':'0';
|
| 298 |
+
var totalQtyValue=(qd.items||[]).reduce(function(s,it){return s+Number(it.qty||1);},0);
|
| 299 |
+
if(ckPct>0&&ckPct<=100){
|
| 300 |
+
ws.getCell('K1').value=Number((1-ckPct/100).toFixed(4));
|
| 301 |
+
ws.getCell('K1').numFmt='0.00%';
|
| 302 |
+
ws.getColumn(11).width=10;
|
| 303 |
+
ws.getCell('K2').value='CK '+ckPct+'%';
|
| 304 |
+
ws.getCell('K2').font={bold:true,size:9,color:{argb:'FFDC3545'}};
|
| 305 |
+
}
|
| 306 |
+
cr++;ws.mergeCells(cr,1,cr,8);
|
| 307 |
+
var bhRow=ws.getRow(cr);bhRow.height=18;
|
| 308 |
+
bhRow.getCell(1).value='✅ Bảo hành 2 năm (sản phẩm) — Bảo hành hoen gỉ vĩnh viễn (rổ SUS304)';
|
| 309 |
+
bhRow.getCell(1).font={size:9,color:{argb:'FF059669'},italic:true};
|
| 310 |
+
bhRow.getCell(1).alignment={vertical:'middle'};
|
| 311 |
+
for(var ci=1;ci<=10;ci++){bhRow.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF0FDF4'}};}
|
| 312 |
+
var sumParts=[];if(lastItemRow>=firstItemRow)sumParts.push('SUM(I'+firstItemRow+':I'+lastItemRow+')');
|
| 313 |
+
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++;});}
|
| 314 |
+
var totalRowNum=cr;
|
| 315 |
+
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}}};
|
| 316 |
+
ws.mergeCells(cr,1,cr,5);
|
| 317 |
+
var tRow=ws.getRow(cr);tRow.height=28;
|
| 318 |
+
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;
|
| 319 |
+
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;
|
| 320 |
+
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;
|
| 321 |
+
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++;
|
| 322 |
+
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++;}
|
| 323 |
+
// FIX TRIỆT ĐỂ: luôn có dòng "CÒN LẠI (100%)" khi chưa cọc → đảm bảo còn lại 100% được ghi vào Excel
|
| 324 |
+
if(d.deposit<=0){var RBG100='FFFEF2F2';var RBD100={top:{style:'thin',color:{argb:RBG100}},bottom:{style:'thin',color:{argb:RBG100}},left:{style:'thin',color:{argb:RBG100}},right:{style:'thin',color:{argb:RBG100}}};ws.mergeCells(cr,1,cr,8);var rRow100=ws.getRow(cr);rRow100.height=26;rRow100.getCell(1).value='CÒN LẠI (100%)';rRow100.getCell(1).font={bold:true,size:12,color:{argb:'FFDC2626'}};rRow100.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};rRow100.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG100}};rRow100.getCell(1).border=RBD100;rRow100.getCell(9).value={formula:'I'+totalRowNum,result:Number(d.grandTotal||0)};rRow100.getCell(9).numFmt=MONEY_FMT;rRow100.getCell(9).font={bold:true,size:13,color:{argb:'FFDC2626'}};rRow100.getCell(9).alignment={horizontal:'right',vertical:'middle'};rRow100.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG100}};rRow100.getCell(9).border=RBD100;rRow100.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG100}};rRow100.getCell(10).border=RBD100;cr++;}
|
| 325 |
+
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++;}
|
| 326 |
+
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}}};
|
| 327 |
+
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++;
|
| 328 |
+
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++;
|
| 329 |
+
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++;
|
| 330 |
+
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++;
|
| 331 |
+
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){}
|
| 332 |
+
ws.eachRow(function(row){row.eachCell(function(cell){if(!cell.border)cell.border=WB;});});
|
| 333 |
+
wb.calcProperties.fullCalcOnLoad = true;
|
| 334 |
+
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);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
async function _doExportDeliveryExcel(qd,code){
|
| 338 |
+
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}};
|
| 339 |
+
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'}}};
|
| 340 |
+
var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Giao hàng');
|
| 341 |
+
ws.views=[{showGridLines:false}];ws.columns=[{width:5},{width:34},{width:14},{width:8},{width:20}];
|
| 342 |
+
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;
|
| 343 |
+
ws.getCell('A2').value='KH: '+(qd.customer?qd.customer.name:'');ws.getCell('A2').font={bold:true,size:10};ws.getCell('A2').border=WB;
|
| 344 |
+
ws.getCell('D2').value='Mã: '+code;ws.getCell('D2').font={bold:true,size:9,color:{argb:'FF003F62'}};ws.getCell('D2').border=WB;
|
| 345 |
+
ws.getCell('A3').value='SĐT: '+(qd.customer?qd.customer.phone:'');ws.getCell('A3').border=WB;
|
| 346 |
+
ws.getCell('D3').value='Ngày: '+(qd.customer?qd.customer.date:'');ws.getCell('D3').border=WB;
|
| 347 |
+
ws.getCell('A4').value='Địa chỉ: '+(qd.customer?qd.customer.addr||'':'');ws.getCell('A4').border=WB;
|
| 348 |
+
ws.getRow(5).height=4;
|
| 349 |
+
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;
|
| 350 |
+
var cr=7;var items=qd.items||[];
|
| 351 |
+
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++;}
|
| 352 |
+
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++;
|
| 353 |
+
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;
|
| 354 |
+
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;
|
| 355 |
+
ws.eachRow(function(row){row.eachCell(function(cell){if(!cell.border)cell.border=WB;});});
|
| 356 |
+
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);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
var att=0;var t=setInterval(function(){if(doOverride()||att>120)clearInterval(t);att++;},500);
|
| 360 |
+
|
| 361 |
+
document.addEventListener('click',function(e){
|
| 362 |
+
var btn=e.target.closest&&e.target.closest('button,a');
|
| 363 |
+
if(!btn)return;
|
| 364 |
+
var txt=(btn.textContent||'').toLowerCase();
|
| 365 |
+
if(!window._origGetQuoteData)return;
|
| 366 |
+
// (removed - handled by native onclick)
|
| 367 |
+
|
| 368 |
+
// (removed - handled by native onclick)
|
| 369 |
+
if(txt.includes('chia sẻ')&&txt.includes('ảnh')){e.preventDefault();e.stopPropagation();window.shareQuoteImage();return false;}
|
| 370 |
+
},true);
|
| 371 |
+
|
| 372 |
+
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};
|
| 373 |
+
})();
|
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 |
+
})();
|
search-dropdown-fix.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Search Dropdown & Add Product Fix
|
| 3 |
+
* =================================
|
| 4 |
+
* 1. Khôi phục dropdown kết quả khi tìm kiếm thường
|
| 5 |
+
* 2. Khôi phục nút "Thêm sản phẩm" mở modal
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
(function() {
|
| 9 |
+
'use strict';
|
| 10 |
+
|
| 11 |
+
// === PATCH doSearch() để hiện dropdown ===
|
| 12 |
+
// Ghi đè hàm doSearch gốc
|
| 13 |
+
var originalDoSearch = window.doSearch;
|
| 14 |
+
|
| 15 |
+
window.doSearch = function() {
|
| 16 |
+
var input = document.getElementById('q');
|
| 17 |
+
var sidebar = document.getElementById('sidebar');
|
| 18 |
+
var grid = document.getElementById('grid');
|
| 19 |
+
|
| 20 |
+
// Ẩn sidebar khi search
|
| 21 |
+
if (sidebar) sidebar.style.display = 'none';
|
| 22 |
+
|
| 23 |
+
// Tạo dropdown nếu chưa có
|
| 24 |
+
var dropdown = document.getElementById('searchDropdown');
|
| 25 |
+
if (!dropdown) {
|
| 26 |
+
dropdown = document.createElement('div');
|
| 27 |
+
dropdown.id = 'searchDropdown';
|
| 28 |
+
dropdown.style.cssText = 'position:absolute;z-index:100;top:100%;left:0;right:0;background:#fff;border:1px solid #e2e8f0;border-radius:10px;max-height:400px;overflow-y:auto;box-shadow:0 4px 16px rgba(0,0,0,0.1);margin-top:5px';
|
| 29 |
+
input.parentNode.appendChild(dropdown);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
var query = (input.value || '').trim();
|
| 33 |
+
if (!query || query.length < 2) {
|
| 34 |
+
dropdown.innerHTML = '';
|
| 35 |
+
dropdown.style.display = 'none';
|
| 36 |
+
if (originalDoSearch) originalDoSearch();
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Tìm kiếm trong dữ liệu
|
| 41 |
+
var D = window.D || [];
|
| 42 |
+
var results = [];
|
| 43 |
+
var qNorm = query.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
|
| 44 |
+
|
| 45 |
+
for (var i = 0; i < D.length && results.length < 10; i++) {
|
| 46 |
+
var p = D[i];
|
| 47 |
+
var name = (p.name || '').toLowerCase();
|
| 48 |
+
var sku = (p.sku || '').toLowerCase();
|
| 49 |
+
var model = (p.sku || p.mod || '');
|
| 50 |
+
|
| 51 |
+
if (name.indexOf(qNorm) !== -1 || sku.indexOf(qNorm) !== -1) {
|
| 52 |
+
results.push(p);
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (results.length === 0) {
|
| 57 |
+
dropdown.innerHTML = '<div style="padding:12px;color:#64748b;font-size:.85rem">Không tìm thấy sản phẩm</div>';
|
| 58 |
+
dropdown.style.display = 'block';
|
| 59 |
+
return;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Render dropdown results
|
| 63 |
+
dropdown.innerHTML = results.map(function(p, i) {
|
| 64 |
+
var idx = window.D.indexOf(p);
|
| 65 |
+
var brandColor = p.brand === 'Eurogold' ? '#c8102c' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
|
| 66 |
+
return '<div data-idx="' + idx + '" style="padding:10px 12px;cursor:pointer;border-bottom:1px solid #e2e8f0;transition:background .2s;font-size:.82rem;display:flex;gap:10px;align-items:center" ' +
|
| 67 |
+
'onmouseover="this.style.background=\'#f8fafc\'" onmouseout="this.style.background=\'#fff\'">' +
|
| 68 |
+
'<img src="' + (p.image || '') + '" style="width:48px;height:48px;object-fit:contain;background:#f8fafc;border-radius:6px" onerror="this.style.display=\'none\'">' +
|
| 69 |
+
'<div style="flex:1"><div style="font-weight:600;color:#0f172a;margin-bottom:2px">' + p.name + '</div>' +
|
| 70 |
+
'<div style="font-size:.72rem;color:' + brandColor + '">' + model + ' ' + (p.price ? ' — ' + p.price : '') + '</div></div>' +
|
| 71 |
+
'<button onclick="event.stopPropagation();addToCart({id:\'' + p.sku + '\',name:\'' + p.name.replace(/'/g, "\\'") + '\',price:' + (p.priceNum || 0) + ',sku:\'' + p.sku + '\',image:\'' + (p.image || '') + '\',brand:\'' + p.brand + '\'});this.innerHTML=\'✅\';this.disabled=true;" ' +
|
| 72 |
+
'style="background:#25d366;color:#fff;border:none;border-radius:5px;padding:4px 8px;font-size:.7rem;cursor:pointer">Thêm giỏ</button></div>';
|
| 73 |
+
}).join('');
|
| 74 |
+
|
| 75 |
+
dropdown.style.display = 'block';
|
| 76 |
+
|
| 77 |
+
// Bind click to show detail
|
| 78 |
+
setTimeout(function() {
|
| 79 |
+
dropdown.querySelectorAll('[data-idx]').forEach(function(el) {
|
| 80 |
+
el.onclick = function() {
|
| 81 |
+
var idx = parseInt(this.getAttribute('data-idx'));
|
| 82 |
+
showDetail(idx);
|
| 83 |
+
dropdown.innerHTML = '';
|
| 84 |
+
dropdown.style.display = 'none';
|
| 85 |
+
input.value = '';
|
| 86 |
+
};
|
| 87 |
+
});
|
| 88 |
+
}, 100);
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
// Ẩn dropdown khi click ngoài
|
| 92 |
+
document.addEventListener('click', function(e) {
|
| 93 |
+
var dropdown = document.getElementById('searchDropdown');
|
| 94 |
+
var input = document.getElementById('q');
|
| 95 |
+
if (dropdown && input && !input.contains(e.target) && !dropdown.contains(e.target)) {
|
| 96 |
+
dropdown.innerHTML = '';
|
| 97 |
+
dropdown.style.display = 'none';
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
// === THÊM NÚT "THÊM SẢN PHẨM" ===
|
| 102 |
+
// Kiểm tra nút đã tồn tại chưa
|
| 103 |
+
function addAddProductButton() {
|
| 104 |
+
var existing = document.getElementById('addProductQuickBtn');
|
| 105 |
+
if (existing) return;
|
| 106 |
+
|
| 107 |
+
// Tạo nút
|
| 108 |
+
var btn = document.createElement('button');
|
| 109 |
+
btn.id = 'addProductQuickBtn';
|
| 110 |
+
btn.innerHTML = '<i class="fas fa-plus"></i> Thêm sản phẩm';
|
| 111 |
+
btn.style.cssText = 'padding:8px 16px;background:#003f62;color:#fff;border:none;border-radius:8px;font-size:.8rem;cursor:pointer;margin-left:10px';
|
| 112 |
+
btn.onclick = function() {
|
| 113 |
+
// Mở modal thêm sản phẩm
|
| 114 |
+
var modal = document.getElementById('addProductOverlay');
|
| 115 |
+
if (modal) {
|
| 116 |
+
modal.classList.add('open');
|
| 117 |
+
modal.style.display = 'flex';
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
// Thêm vào header
|
| 122 |
+
var navIcons = document.querySelector('.nav-icons');
|
| 123 |
+
if (navIcons) {
|
| 124 |
+
navIcons.insertBefore(btn, navIcons.firstChild);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
console.log('[Search Fix] Added "Thêm sản phẩm" button');
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Chờ DOM sẵn sàng
|
| 131 |
+
setTimeout(addAddProductButton, 2000);
|
| 132 |
+
|
| 133 |
+
console.log('[Search Dropdown Fix] Loaded');
|
| 134 |
+
})();
|
seo-url-fix.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — SEO + URL Routing Fix v2
|
| 3 |
+
* ========================================
|
| 4 |
+
* FIXES:
|
| 5 |
+
* 1. Product detail pages via SPA routing (no real files)
|
| 6 |
+
* → Scan URL path for /san-pham/SLUG/ → show detail
|
| 7 |
+
* 2. Dynamic OG meta tags when showing product detail
|
| 8 |
+
* → og:title, og:description, og:image, og:url, og:price:amount/currency, twitter:*
|
| 9 |
+
* 3. URL-added products (from products_url_added.json) also get SEO
|
| 10 |
+
* 4. history.pushState updates URL so shared links work
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
(function() {
|
| 14 |
+
'use strict';
|
| 15 |
+
if (window.__VAI_SEO_URL_FIX__) return;
|
| 16 |
+
window.__VAI_SEO_URL_FIX__ = true;
|
| 17 |
+
|
| 18 |
+
var SITE = 'https://bep40-v-aistudio.static.hf.space';
|
| 19 |
+
|
| 20 |
+
function norm(s) {
|
| 21 |
+
return String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'd').toLowerCase();
|
| 22 |
+
}
|
| 23 |
+
function slugify(s) {
|
| 24 |
+
return norm(s).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 150) || 'san-pham';
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
var _currentProduct = null;
|
| 28 |
+
|
| 29 |
+
function getProductBySlug(slug) {
|
| 30 |
+
if (typeof window.D !== 'undefined' && Array.isArray(window.D)) {
|
| 31 |
+
for (var i = 0; i < window.D.length; i++) {
|
| 32 |
+
var p = window.D[i];
|
| 33 |
+
if (!p) continue;
|
| 34 |
+
if (p.slug === slug) return p;
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
if (typeof window.VAI_URL_ADDED_PRODUCTS !== 'undefined' && Array.isArray(window.VAI_URL_ADDED_PRODUCTS)) {
|
| 38 |
+
for (var j = 0; j < window.VAI_URL_ADDED_PRODUCTS.length; j++) {
|
| 39 |
+
var q = window.VAI_URL_ADDED_PRODUCTS[j];
|
| 40 |
+
if (!q) continue;
|
| 41 |
+
if (q.slug === slug) return q;
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
return null;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function ensureMeta(attr, name, content) {
|
| 48 |
+
var selector = attr === 'property' ? 'meta[property="' + name + '"]' : 'meta[name="' + name + '"]';
|
| 49 |
+
var el = document.querySelector(selector);
|
| 50 |
+
if (!el) {
|
| 51 |
+
el = document.createElement('meta');
|
| 52 |
+
el.setAttribute(attr, name);
|
| 53 |
+
document.head.appendChild(el);
|
| 54 |
+
}
|
| 55 |
+
el.setAttribute('content', content);
|
| 56 |
+
return el;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function setMetaContent(id, value) {
|
| 60 |
+
var el = document.getElementById(id);
|
| 61 |
+
if (el) el.setAttribute('content', value);
|
| 62 |
+
var metaMap = {
|
| 63 |
+
ogTitle: 'meta[property="og:title"]',
|
| 64 |
+
ogDesc: 'meta[property="og:description"]',
|
| 65 |
+
ogImage: 'meta[property="og:image"]',
|
| 66 |
+
ogUrl: 'meta[property="og:url"]',
|
| 67 |
+
twTitle: 'meta[name="twitter:title"]',
|
| 68 |
+
twDesc: 'meta[name="twitter:description"]',
|
| 69 |
+
twImage: 'meta[name="twitter:image"]'
|
| 70 |
+
};
|
| 71 |
+
var sel = metaMap[id];
|
| 72 |
+
if (sel) {
|
| 73 |
+
var el2 = document.querySelector(sel);
|
| 74 |
+
if (el2) el2.setAttribute('content', value);
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function setCanonical(url) {
|
| 79 |
+
var el = document.getElementById('canonicalUrl');
|
| 80 |
+
if (el) el.setAttribute('href', url);
|
| 81 |
+
var alt = document.querySelector('link[rel="canonical"]');
|
| 82 |
+
if (alt) alt.setAttribute('href', url);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function updateOGTags(product) {
|
| 86 |
+
if (!product) {
|
| 87 |
+
setMetaContent('ogTitle', 'V.AI STUDIO | Niềm tin khách hàng là tài sản của chúng tôi');
|
| 88 |
+
setMetaContent('ogDesc', '8000+ sản phẩm thiết bị nhà bếp & điện máy chính hãng. Malloca, Eurogold, Grob, Canzy, Demax & Điện Máy Xanh.');
|
| 89 |
+
setMetaContent('ogImage', 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png');
|
| 90 |
+
setMetaContent('ogUrl', SITE + '/');
|
| 91 |
+
var pa = document.querySelector('meta[property="product:price:amount"]');
|
| 92 |
+
if (pa) pa.remove();
|
| 93 |
+
var pc = document.querySelector('meta[property="product:price:currency"]');
|
| 94 |
+
if (pc) pc.remove();
|
| 95 |
+
setMetaContent('twTitle', 'V.AI STUDIO');
|
| 96 |
+
setMetaContent('twDesc', '8000+ sản phẩm thiết bị nhà bếp & điện máy chính hãng');
|
| 97 |
+
setMetaContent('twImage', 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png');
|
| 98 |
+
setCanonical(SITE + '/');
|
| 99 |
+
document.title = 'V.AI STUDIO | Niềm tin khách hàng là tài sản của chúng tôi';
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
var name = product.name || product.n || 'Sản phẩm';
|
| 104 |
+
var sku = product.sku || product.mod || product.model || '';
|
| 105 |
+
var brand = product.brand || '';
|
| 106 |
+
var priceNum = product.pn || product.priceNum || 0;
|
| 107 |
+
var price = product.price || (priceNum ? Number(priceNum).toLocaleString('vi-VN') + 'đ' : '');
|
| 108 |
+
var image = product.image || product.i || ((product.imgs || product.images || [])[0]) || 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png';
|
| 109 |
+
var desc = product.summary || product.sum || product.desc || '';
|
| 110 |
+
var slug = product.slug || slugify((sku ? sku + ' ' : '') + name);
|
| 111 |
+
|
| 112 |
+
var title = name + (brand ? ' | ' + brand : '') + ' | V.AI STUDIO';
|
| 113 |
+
|
| 114 |
+
var ogDesc = '';
|
| 115 |
+
if (desc) {
|
| 116 |
+
ogDesc = desc.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim().substring(0, 300);
|
| 117 |
+
}
|
| 118 |
+
if (price) {
|
| 119 |
+
ogDesc = 'Gi\u00e1: ' + price + ' - ' + (ogDesc || name);
|
| 120 |
+
} else {
|
| 121 |
+
ogDesc = ogDesc || name + ' t\u1ea1i V.AI STUDIO. Thi\u1ebft b\u1ecb nh\u00e0 b\u1ebfp & ph\u1ee5 ki\u1ec7n ch\u00ednh h\u00e3ng.';
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
var ogUrl = SITE + '/san-pham/' + slug + '/index.html';
|
| 125 |
+
|
| 126 |
+
document.title = title;
|
| 127 |
+
setMetaContent('ogTitle', title);
|
| 128 |
+
setMetaContent('ogDesc', ogDesc);
|
| 129 |
+
setMetaContent('ogImage', image);
|
| 130 |
+
setMetaContent('ogUrl', ogUrl);
|
| 131 |
+
setMetaContent('twTitle', title);
|
| 132 |
+
setMetaContent('twDesc', ogDesc);
|
| 133 |
+
setMetaContent('twImage', image);
|
| 134 |
+
|
| 135 |
+
if (priceNum > 0) {
|
| 136 |
+
ensureMeta('property', 'product:price:amount', String(priceNum));
|
| 137 |
+
ensureMeta('property', 'product:price:currency', 'VND');
|
| 138 |
+
} else {
|
| 139 |
+
var pa2 = document.querySelector('meta[property="product:price:amount"]');
|
| 140 |
+
if (pa2) pa2.remove();
|
| 141 |
+
var pc2 = document.querySelector('meta[property="product:price:currency"]');
|
| 142 |
+
if (pc2) pc2.remove();
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
setCanonical(ogUrl);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function updateProductURL(slug) {
|
| 149 |
+
var path = '/san-pham/' + slug + '/index.html';
|
| 150 |
+
try {
|
| 151 |
+
history.pushState({ slug: slug, product: true }, '', path);
|
| 152 |
+
} catch(e) {}
|
| 153 |
+
setMetaContent('ogUrl', SITE + path);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function getSlugFromURL() {
|
| 157 |
+
var path = location.pathname;
|
| 158 |
+
var m = path.match(/\/san-pham\/([^\/]+)/i);
|
| 159 |
+
if (m) return decodeURIComponent(m[1]);
|
| 160 |
+
var qs = new URLSearchParams(location.search);
|
| 161 |
+
return qs.get('product') || qs.get('p') || '';
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
function openProductBySlug(slug) {
|
| 165 |
+
if (!slug) return false;
|
| 166 |
+
var idx = -1;
|
| 167 |
+
if (typeof window.D !== 'undefined' && Array.isArray(window.D)) {
|
| 168 |
+
for (var i = 0; i < window.D.length; i++) {
|
| 169 |
+
var p = window.D[i];
|
| 170 |
+
if (!p) continue;
|
| 171 |
+
if (p.slug === slug || (p.sku && norm(p.sku) === norm(slug))) {
|
| 172 |
+
idx = i;
|
| 173 |
+
_currentProduct = p;
|
| 174 |
+
break;
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
if (idx >= 0 && typeof window.showDetail === 'function') {
|
| 179 |
+
try {
|
| 180 |
+
window.showDetail(idx);
|
| 181 |
+
updateOGTags(_currentProduct);
|
| 182 |
+
updateProductURL(slug);
|
| 183 |
+
return true;
|
| 184 |
+
} catch(e) {}
|
| 185 |
+
}
|
| 186 |
+
if (typeof window.VAI_URL_ADDED_PRODUCTS !== 'undefined' && typeof window.VAIUrlAddedCompat !== 'undefined') {
|
| 187 |
+
var added = window.VAI_URL_ADDED_PRODUCTS;
|
| 188 |
+
for (var j = 0; j < added.length; j++) {
|
| 189 |
+
if (added[j].slug === slug || (added[j].sku && norm(added[j].sku) === norm(slug))) {
|
| 190 |
+
_currentProduct = added[j];
|
| 191 |
+
updateOGTags(_currentProduct);
|
| 192 |
+
updateProductURL(slug);
|
| 193 |
+
try { window.VAIUrlAddedCompat.open(added[j], true); return true; } catch(e) { return true; }
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
return false;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
var origShowDetail = window.showDetail;
|
| 201 |
+
window.showDetail = function(idx) {
|
| 202 |
+
if (typeof idx === 'number' && typeof window.D !== 'undefined' && window.D && window.D[idx]) {
|
| 203 |
+
_currentProduct = window.D[idx];
|
| 204 |
+
var slug = _currentProduct.slug || slugify((_currentProduct.sku || _currentProduct.mod || '') + ' ' + (_currentProduct.name || ''));
|
| 205 |
+
updateOGTags(_currentProduct);
|
| 206 |
+
updateProductURL(slug);
|
| 207 |
+
}
|
| 208 |
+
if (origShowDetail) return origShowDetail.apply(this, arguments);
|
| 209 |
+
};
|
| 210 |
+
|
| 211 |
+
var origGoHome = window.goHome;
|
| 212 |
+
window.goHome = function() {
|
| 213 |
+
_currentProduct = null;
|
| 214 |
+
updateOGTags(null);
|
| 215 |
+
if (origGoHome) return origGoHome.apply(this, arguments);
|
| 216 |
+
};
|
| 217 |
+
|
| 218 |
+
window.addEventListener('popstate', function(e) {
|
| 219 |
+
var slug = getSlugFromURL();
|
| 220 |
+
if (slug && e.state && e.state.product) {
|
| 221 |
+
openProductBySlug(slug);
|
| 222 |
+
} else if (!slug) {
|
| 223 |
+
_currentProduct = null;
|
| 224 |
+
updateOGTags(null);
|
| 225 |
+
}
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
function waitAndOpen() {
|
| 229 |
+
var slug = getSlugFromURL();
|
| 230 |
+
if (!slug) return;
|
| 231 |
+
if (openProductBySlug(slug)) return;
|
| 232 |
+
var tries = 0;
|
| 233 |
+
var interval = setInterval(function() {
|
| 234 |
+
tries++;
|
| 235 |
+
if (openProductBySlug(slug)) { clearInterval(interval); return; }
|
| 236 |
+
if (tries > 60) clearInterval(interval);
|
| 237 |
+
}, 250);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
function init() {
|
| 241 |
+
waitAndOpen();
|
| 242 |
+
if (typeof window.VAIUrlAddedCompat !== 'undefined' && window.VAIUrlAddedCompat.load) {
|
| 243 |
+
var loadOrig = window.VAIUrlAddedCompat.load;
|
| 244 |
+
window.VAIUrlAddedCompat.load = function() {
|
| 245 |
+
var result = loadOrig.apply(this, arguments);
|
| 246 |
+
var slug = getSlugFromURL();
|
| 247 |
+
if (slug && !_currentProduct) { openProductBySlug(slug); }
|
| 248 |
+
return result;
|
| 249 |
+
};
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
if (document.readyState === 'loading') {
|
| 254 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 255 |
+
} else {
|
| 256 |
+
init();
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
console.log('[VAI SEO URL Fix v2] Loaded: OG tags with price, SPA routing');
|
| 260 |
+
})();
|
show-order-picker-v1046.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — Order Picker for AI Search v1046 (FIXED)
|
| 3 |
+
*
|
| 4 |
+
* FIX v1046: Use window.VAI_ORDERS.openSearch instead of patching window.openSearchModal
|
| 5 |
+
* because order-store.js defines openSearchModal as LOCAL inside IIFE, not on window.
|
| 6 |
+
*/
|
| 7 |
+
(function() {
|
| 8 |
+
'use strict';
|
| 9 |
+
|
| 10 |
+
window.VAI_pendingProduct = null;
|
| 11 |
+
var _pendingCallback = null;
|
| 12 |
+
|
| 13 |
+
function esc(s) {
|
| 14 |
+
return String(s == null ? '' : s).replace(/&/g, '&')
|
| 15 |
+
.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function generateOrderCode() {
|
| 19 |
+
var now = new Date();
|
| 20 |
+
var d = now.getDate().toString().padStart(2, '0');
|
| 21 |
+
var m = (now.getMonth() + 1).toString().padStart(2, '0');
|
| 22 |
+
var y = now.getFullYear().toString().slice(-2);
|
| 23 |
+
var h = now.getHours().toString().padStart(2, '0');
|
| 24 |
+
var min = now.getMinutes().toString().padStart(2, '0');
|
| 25 |
+
return 'DH' + d + m + y + h + min;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Override _showOrderPicker — this is the ONLY function that needs to be global
|
| 29 |
+
window._showOrderPicker = function(product, callback) {
|
| 30 |
+
console.log('[Order Picker v1046] _showOrderPicker called');
|
| 31 |
+
if (!product) {
|
| 32 |
+
console.log('[Order Picker v1046] ❌ No product provided');
|
| 33 |
+
if (typeof callback === 'function') callback('error');
|
| 34 |
+
return;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
window.VAI_pendingProduct = product;
|
| 38 |
+
_pendingCallback = callback;
|
| 39 |
+
|
| 40 |
+
// Save product fields needed for order item (handle both field names)
|
| 41 |
+
var pp = window.VAI_pendingProduct;
|
| 42 |
+
pp._resolvedPriceNum = pp.priceNum || pp.pn || 0;
|
| 43 |
+
pp._resolvedPrice = pp.price || pp.p || '';
|
| 44 |
+
pp._resolvedName = pp.name || pp.n || '';
|
| 45 |
+
pp._resolvedSku = pp.sku || pp.model || pp.mod || '';
|
| 46 |
+
pp._resolvedBrand = pp.brand || '';
|
| 47 |
+
pp._resolvedImage = pp.image || pp.img || pp.i || '';
|
| 48 |
+
|
| 49 |
+
console.log('[Order Picker v1046] Product:', {name: pp._resolvedName, priceNum: pp._resolvedPriceNum});
|
| 50 |
+
|
| 51 |
+
// Show order selection modal
|
| 52 |
+
showOrderPickerModal();
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
function showOrderPickerModal() {
|
| 56 |
+
var product = window.VAI_pendingProduct;
|
| 57 |
+
var callback = _pendingCallback;
|
| 58 |
+
var orders = (typeof window.VAI_ORDERS !== 'undefined' && typeof window.VAI_ORDERS.getAll === 'function')
|
| 59 |
+
? window.VAI_ORDERS.getAll()
|
| 60 |
+
: (typeof getOrders === 'function' ? getOrders() : []);
|
| 61 |
+
|
| 62 |
+
console.log('[Order Picker v1046] Got orders:', orders ? orders.length : 0);
|
| 63 |
+
|
| 64 |
+
var ov = document.getElementById('vai-order-picker-modal');
|
| 65 |
+
if (ov) ov.remove();
|
| 66 |
+
|
| 67 |
+
ov = document.createElement('div');
|
| 68 |
+
ov.id = 'vai-order-picker-modal';
|
| 69 |
+
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';
|
| 70 |
+
ov.onclick = function(e) {
|
| 71 |
+
if (e.target === ov) ov.remove();
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
var pC = orders.filter(function(o){return(o.status||'pending')==='pending';}).length;
|
| 75 |
+
var cC = orders.filter(function(o){return o.status==='confirmed';}).length;
|
| 76 |
+
|
| 77 |
+
var filterHtml = '<div style="display:flex;gap:6px;margin-bottom:8px">';
|
| 78 |
+
filterHtml += '<button class="opf-filter" data-f="all" style="padding:5px 16px;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>';
|
| 79 |
+
filterHtml += '<button class="opf-filter" data-f="pending" style="padding:5px 16px;border-radius:20px;border:2px solid #f59e0b;background:#fff;color:#92400e;font-size:11px;font-weight:700;cursor:pointer">⏳ Chưa chốt ('+pC+')</button>';
|
| 80 |
+
filterHtml += '<button class="opf-filter" data-f="confirmed" style="padding:5px 16px;border-radius:20px;border:2px solid #16a34a;background:#fff;color:#166534;font-size:11px;font-weight:700;cursor:pointer">✅ Đã chốt ('+cC+')</button>';
|
| 81 |
+
filterHtml += '</div>';
|
| 82 |
+
|
| 83 |
+
var searchHtml = '<input id="opf-search" 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">';
|
| 84 |
+
|
| 85 |
+
var html = '<div style="background:#fff;border-radius:16px;max-width:750px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column">';
|
| 86 |
+
html += '<div style="padding:14px 18px;background:#003f62;display:flex;align-items:center">';
|
| 87 |
+
html += '<div style="font-size:16px;font-weight:800;color:#fff;flex:1">📋 Chọn đơn hàng để thêm SP</div>';
|
| 88 |
+
html += '<div style="font-size:11px;color:rgba(255,255,255,.7);margin-right:8px">'+(product._resolvedName||'').substring(0,25)+'</div>';
|
| 89 |
+
html += '<button id="opf-close" style="background:none;border:none;color:#fff;font-size:20px;cursor:pointer">✕</button></div>';
|
| 90 |
+
html += '<div style="padding:10px 18px;border-bottom:1px solid #e2e8f0">'+filterHtml+searchHtml+'</div>';
|
| 91 |
+
html += '<div id="opf-list" style="flex:1;overflow-y:auto;padding:10px 18px"></div></div>';
|
| 92 |
+
|
| 93 |
+
ov.innerHTML = html;
|
| 94 |
+
document.body.appendChild(ov);
|
| 95 |
+
|
| 96 |
+
var listEl = document.getElementById('opf-list');
|
| 97 |
+
var currentFilter = 'all';
|
| 98 |
+
|
| 99 |
+
function renderList(ls) {
|
| 100 |
+
if (!ls.length) {
|
| 101 |
+
if (product) {
|
| 102 |
+
listEl.innerHTML = '<div style="text-align:center;padding:20px;color:#94a3b8">Chưa có đơn hàng nào</div>';
|
| 103 |
+
// Always show create new button
|
| 104 |
+
var createDiv = document.createElement('div');
|
| 105 |
+
createDiv.style.cssText = 'text-align:center;padding:10px';
|
| 106 |
+
createDiv.innerHTML = '<button id="opf-create-new" style="padding:12px 24px;background:#059669;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer">➕ Tạo đơn mới</button>';
|
| 107 |
+
listEl.appendChild(createDiv);
|
| 108 |
+
} else {
|
| 109 |
+
listEl.innerHTML = '<div style="text-align:center;padding:30px;color:#94a3b8">Trống</div>';
|
| 110 |
+
}
|
| 111 |
+
return;
|
| 112 |
+
}
|
| 113 |
+
listEl.innerHTML = ls.map(function(o, i) {
|
| 114 |
+
var isC = o.status === 'confirmed';
|
| 115 |
+
var fmt = function(n){if(!n||isNaN(n))return'0đ';return Number(n).toLocaleString('vi-VN')+'đ';};
|
| 116 |
+
return '<div class="opf-item" data-idx="'+i+'" style="padding:10px;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:6px;cursor:pointer;border-left:3px solid '+(isC?'#16a34a':'#f59e0b')+'" onmouseover="this.style.background=\'#f8fafc\'" onmouseout="this.style.background=\'\'">'
|
| 117 |
+
+'<div style="display:flex;justify-content:space-between;align-items:center">'
|
| 118 |
+
+'<div><b style="color:#003f62">'+o.code+'</b> '+(isC?'✅ Đã chốt':'⏳ Chưa chốt')+'</div>'
|
| 119 |
+
+'<div style="font-size:11px;color:#64748b">'+fmt(o.grandTotal)+'</div></div>'
|
| 120 |
+
+'<div style="font-size:10px;color:#94a3b8;margin-top:3px">'+(o.customer||'')+' • '+(o.phone||'')+'</div></div>';
|
| 121 |
+
}).join('');
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
function getFilteredOrders() {
|
| 125 |
+
var searchQ = (document.getElementById('opf-search').value||'').toLowerCase().trim();
|
| 126 |
+
var filtered = orders;
|
| 127 |
+
if (currentFilter !== 'all') {
|
| 128 |
+
filtered = filtered.filter(function(o){return(o.status||'pending')===currentFilter;});
|
| 129 |
+
}
|
| 130 |
+
if (searchQ) {
|
| 131 |
+
filtered = filtered.filter(function(o){
|
| 132 |
+
return (o.code||'').toLowerCase().includes(searchQ)
|
| 133 |
+
|| (o.customer||'').toLowerCase().includes(searchQ)
|
| 134 |
+
|| (o.phone||'').includes(searchQ);
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
return filtered;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
renderList(orders);
|
| 141 |
+
|
| 142 |
+
// Click handler
|
| 143 |
+
listEl.onclick = function(e) {
|
| 144 |
+
var createBtn = e.target.closest('#opf-create-new');
|
| 145 |
+
if (createBtn && product) {
|
| 146 |
+
ov.remove();
|
| 147 |
+
createNewOrderWithProduct();
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
var item = e.target.closest('.opf-item');
|
| 151 |
+
if (item) {
|
| 152 |
+
var idx = parseInt(item.dataset.idx);
|
| 153 |
+
var filtered = getFilteredOrders();
|
| 154 |
+
var order = filtered[idx];
|
| 155 |
+
console.log('[Order Picker v1046] Clicked order idx:', idx, 'code:', order ? order.code : 'not found');
|
| 156 |
+
if (order) {
|
| 157 |
+
ov.remove();
|
| 158 |
+
addProductToOrder(order);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
// Filter buttons
|
| 164 |
+
ov.querySelectorAll('.opf-filter').forEach(function(btn) {
|
| 165 |
+
btn.onclick = function() {
|
| 166 |
+
currentFilter = this.dataset.f;
|
| 167 |
+
ov.querySelectorAll('.opf-filter').forEach(function(b) {
|
| 168 |
+
b.style.background = '#fff';
|
| 169 |
+
b.style.color = '#003f62';
|
| 170 |
+
});
|
| 171 |
+
this.style.background = '#003f62';
|
| 172 |
+
this.style.color = '#fff';
|
| 173 |
+
renderList(getFilteredOrders());
|
| 174 |
+
};
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
// Search
|
| 178 |
+
document.getElementById('opf-search').oninput = function() {
|
| 179 |
+
renderList(getFilteredOrders());
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
// Close
|
| 183 |
+
document.getElementById('opf-close').onclick = function() {
|
| 184 |
+
ov.remove();
|
| 185 |
+
window.VAI_pendingProduct = null;
|
| 186 |
+
_pendingCallback = null;
|
| 187 |
+
if (callback) callback('cancel');
|
| 188 |
+
};
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function addProductToOrder(order) {
|
| 192 |
+
var pp = window.VAI_pendingProduct;
|
| 193 |
+
var callback = _pendingCallback;
|
| 194 |
+
|
| 195 |
+
var item = {
|
| 196 |
+
name: pp._resolvedName || '',
|
| 197 |
+
model: pp._resolvedSku || '',
|
| 198 |
+
brand: pp._resolvedBrand || '',
|
| 199 |
+
image: pp._resolvedImage || '',
|
| 200 |
+
qty: 1,
|
| 201 |
+
priceNum: pp._resolvedPriceNum || 0,
|
| 202 |
+
price: pp._resolvedPrice || '',
|
| 203 |
+
discPrice: pp._resolvedPriceNum || 0,
|
| 204 |
+
total: pp._resolvedPriceNum || 0
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
order.items = order.items || [];
|
| 208 |
+
order.items.push(item);
|
| 209 |
+
order.grandTotal = (order.grandTotal || 0) + (pp._resolvedPriceNum || 0);
|
| 210 |
+
order.remaining = (order.remaining || 0) + (pp._resolvedPriceNum || 0);
|
| 211 |
+
|
| 212 |
+
if (typeof window.VAI_ORDERS !== 'undefined' && typeof window.VAI_ORDERS.add === 'function') {
|
| 213 |
+
console.log('[Order Picker v1046] Adding to order via VAI_ORDERS.add');
|
| 214 |
+
window.VAI_ORDERS.add(order);
|
| 215 |
+
} else if (typeof addOrder === 'function') {
|
| 216 |
+
console.log('[Order Picker v1046] Adding to order via addOrder');
|
| 217 |
+
addOrder(order);
|
| 218 |
+
} else {
|
| 219 |
+
console.log('[Order Picker v1046] ⚠️ No add function found!');
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
window.VAI_pendingProduct = null;
|
| 223 |
+
_pendingCallback = null;
|
| 224 |
+
|
| 225 |
+
if (callback) callback('ok');
|
| 226 |
+
|
| 227 |
+
if (typeof showToast === 'function') {
|
| 228 |
+
showToast('✅ Đã thêm "' + (pp._resolvedName||'').substring(0,25) + '" vào đơn ' + order.code);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Open order detail
|
| 232 |
+
if (typeof openOrderDetail === 'function') {
|
| 233 |
+
openOrderDetail(order);
|
| 234 |
+
} else if (typeof window.VAI_ORDERS !== 'undefined' && typeof window.VAI_ORDERS.openDetail === 'function') {
|
| 235 |
+
window.VAI_ORDERS.openDetail(order);
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
function createNewOrderWithProduct() {
|
| 240 |
+
var pp = window.VAI_pendingProduct;
|
| 241 |
+
var callback = _pendingCallback;
|
| 242 |
+
|
| 243 |
+
var newOrder = {
|
| 244 |
+
code: generateOrderCode(),
|
| 245 |
+
customer: '',
|
| 246 |
+
phone: '',
|
| 247 |
+
email: '',
|
| 248 |
+
addr: '',
|
| 249 |
+
date: new Date().toLocaleDateString('vi-VN'),
|
| 250 |
+
items: [{
|
| 251 |
+
name: pp._resolvedName || '',
|
| 252 |
+
model: pp._resolvedSku || '',
|
| 253 |
+
brand: pp._resolvedBrand || '',
|
| 254 |
+
image: pp._resolvedImage || '',
|
| 255 |
+
qty: 1,
|
| 256 |
+
priceNum: pp._resolvedPriceNum || 0,
|
| 257 |
+
price: pp._resolvedPrice || '',
|
| 258 |
+
discPrice: pp._resolvedPriceNum || 0,
|
| 259 |
+
total: pp._resolvedPriceNum || 0
|
| 260 |
+
}],
|
| 261 |
+
fees: [],
|
| 262 |
+
deposit: 0,
|
| 263 |
+
discountPercent: 0,
|
| 264 |
+
grandTotal: pp._resolvedPriceNum || 0,
|
| 265 |
+
remaining: pp._resolvedPriceNum || 0,
|
| 266 |
+
status: 'pending',
|
| 267 |
+
savedAt: new Date().toISOString()
|
| 268 |
+
};
|
| 269 |
+
|
| 270 |
+
if (typeof window.VAI_ORDERS !== 'undefined' && typeof window.VAI_ORDERS.add === 'function') {
|
| 271 |
+
window.VAI_ORDERS.add(newOrder);
|
| 272 |
+
} else if (typeof addOrder === 'function') {
|
| 273 |
+
addOrder(newOrder);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
window.VAI_pendingProduct = null;
|
| 277 |
+
_pendingCallback = null;
|
| 278 |
+
|
| 279 |
+
if (callback) callback('ok');
|
| 280 |
+
|
| 281 |
+
if (typeof showToast === 'function') {
|
| 282 |
+
showToast('✅ Tạo đơn ' + newOrder.code + ' thành công');
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if (typeof openOrderDetail === 'function') {
|
| 286 |
+
openOrderDetail(newOrder);
|
| 287 |
+
} else if (typeof window.VAI_ORDERS !== 'undefined' && typeof window.VAI_ORDERS.openDetail === 'function') {
|
| 288 |
+
window.VAI_ORDERS.openDetail(newOrder);
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
console.log('[Order Picker v1046] Ready');
|
| 293 |
+
})();
|
style.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
padding: 2rem;
|
| 3 |
+
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
h1 {
|
| 7 |
+
font-size: 16px;
|
| 8 |
+
margin-top: 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
p {
|
| 12 |
+
color: rgb(107, 114, 128);
|
| 13 |
+
font-size: 15px;
|
| 14 |
+
margin-bottom: 10px;
|
| 15 |
+
margin-top: 5px;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.card {
|
| 19 |
+
max-width: 620px;
|
| 20 |
+
margin: 0 auto;
|
| 21 |
+
padding: 16px;
|
| 22 |
+
border: 1px solid lightgray;
|
| 23 |
+
border-radius: 16px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.card p:last-child {
|
| 27 |
+
margin-bottom: 0;
|
| 28 |
+
}
|
test-upload.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="vi">
|
| 3 |
+
<head><script>window.huggingface={variables:{"SPACE_CREATOR_USER_ID":"661b9191e7b0ab12bceb66f3","VAISTUDIO":"HF_TOKEN_REDACTED","REBUILD_TRIGGER":"2"}};</script>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>V.AI STUDIO | TEST</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<h1>TEST UPLOAD</h1>
|
| 10 |
+
</body>
|
| 11 |
+
</html>
|
ui-fixes.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
// UI Fixes - stub
|
url-added-live-loader.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// V.AI STUDIO URL-added products live loader v1019
|
| 2 |
+
// Loads products_url_added.json, merges into native arrays, and intercepts URL-added product links without hash redirect.
|
| 3 |
+
(function(){
|
| 4 |
+
if(window.__VAI_URL_ADDED_LIVE_V1019__) return;
|
| 5 |
+
window.__VAI_URL_ADDED_LIVE_V1019__=true;
|
| 6 |
+
var SITE='https://bep40-v-aistudio.static.hf.space';
|
| 7 |
+
var HUB='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main';
|
| 8 |
+
var STORE=[];
|
| 9 |
+
function norm(s){return String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'d').toLowerCase();}
|
| 10 |
+
function compact(s){return norm(s).replace(/[^a-z0-9]+/g,'');}
|
| 11 |
+
function slugify(s){return norm(s).replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,150)||'san-pham-url';}
|
| 12 |
+
function money(n){n=Number(n||0);return n?Math.round(n).toLocaleString('vi-VN')+'đ':'LH';}
|
| 13 |
+
function cleanSpecs(sp){var out={}; if(!sp||typeof sp!=='object'||Array.isArray(sp))return out; Object.keys(sp).forEach(function(k){var v=sp[k], s=String(k)+' '+String(v); if(!k||v==null)return; if(/\{\{|\}\}|\$\{|mã bảo vệ|ma bao ve|đổi mã khác|doi ma khac/i.test(s))return; out[String(k).trim()]=String(v).trim();}); return out;}
|
| 14 |
+
function cleanFeats(arr){var bad=/(trang chu|he thong cua hang|khuyen mai|dang tai du lieu|san pham lien quan|facebook|youtube|gallery popup|binh luan|loai san pham|thuong hieu\s*$)/i; var out=[]; (Array.isArray(arr)?arr:[]).forEach(function(f){var t=String(f||'').replace(/\s+/g,' ').trim(); if(!t||t.length>320||bad.test(norm(t)))return; if(out.indexOf(t)<0)out.push(t);}); return out.slice(0,30);}
|
| 15 |
+
function productPath(p){return '/san-pham/'+encodeURIComponent((p&&p.slug)||'san-pham')+'/index.html';}
|
| 16 |
+
function canon(p){p=p||{}; var sku=p.sku||p.mod||p.model||''; var name=p.n||p.name||sku||'Sản phẩm'; var sl=p.slug||slugify((sku?sku+' ':'')+name); var pn=Number(p.pn||p.priceNum||0)||0; var price=p.p||p.price||money(pn); var img=p.i||p.image||((p.imgs||p.images||[])[0])||''; var imgs=p.imgs||p.images||[]; if(!Array.isArray(imgs))imgs=[]; if(img&&imgs.indexOf(img)<0)imgs.unshift(img); var url=SITE+'/san-pham/'+sl+'/index.html'; var cat=p.c||p.cat||'Sản phẩm thêm qua URL'; var cs=p.cs||slugify(cat); var specs=cleanSpecs(p.specs); var feats=cleanFeats(p.feats);
|
| 17 |
+
if(sl==='canzy-cz-id-078eu'){
|
| 18 |
+
name=p.n||p.name||'CZ-ID 078EU | Canzy | Bếp từ'; sku='CZ-ID 078EU'; cat='Bếp từ'; cs='bep-tu'; pn=Number(p.pn||20980000)||20980000; price=p.p||p.price||'20.980.000đ';
|
| 19 |
+
specs=Object.assign({'Loại sản phẩm':'Bếp từ','Thương hiệu':'Canzy','Model':'CZ-ID 078EU','Xuất xứ':'Indonesia','Công suất':'3000W'},specs);
|
| 20 |
+
['Bếp từ Canzy','Công suất 3000W','Xuất xứ Indonesia'].forEach(function(x){if(feats.indexOf(x)<0)feats.push(x);});
|
| 21 |
+
}
|
| 22 |
+
if(sl==='canzy-cz-lt869i-max-bep-bep-tu-canzy-cz-lt869i-max'){
|
| 23 |
+
name='Bếp từ Canzy CZ LT869I Max'; sku='CANZY CZ LT869I MAX BEP'; cat='Bếp từ'; cs='bep-tu'; pn=15980000; price='15.980.000đ'; img=img||'https://bepviet.vn/media/product/9886_a111.png'; imgs=imgs.length?imgs:[img];
|
| 24 |
+
specs=Object.assign({'Số bếp nấu':'2 bếp','Kiểu bếp':'Bếp âm','Công suất tổng':'5200W','Công suất Booster':'2600W mỗi vùng nấu','Mức công suất nấu':'9 mức','Chất liệu mặt bếp':'Kính Vitroceramic','Điều khiển':'Cảm ứng','Kích thước mặt bếp':'730 x 430 mm','Kích thước khoét đá':'685 x 395 mm'},specs);
|
| 25 |
+
['Booster công suất cao cho 2 vùng nấu','Inverter tiết kiệm điện','Khóa bàn phím an toàn','Hẹn giờ nấu tiện lợi'].forEach(function(x){if(feats.indexOf(x)<0)feats.push(x);});
|
| 26 |
+
}
|
| 27 |
+
var out=Object.assign({},p,{_url_added:true,n:name,name:name,sku:sku,mod:sku,model:sku,p:price,price:price,pn:pn,priceNum:pn,discPrice:pn,c:cat,cat:cat,cs:cs,ci:p.ci||'fa-box',slug:sl,l:url,url:url,vai_url:url,link:url,i:img,image:img,imgs:imgs,images:imgs,specs:specs,feats:feats,sum:p.sum||p.summary||'',summary:p.sum||p.summary||'',desc:p.desc||p.sum||p.summary||'',brand:p.brand||'',_source_url:p._source_url||p.source_url||''});
|
| 28 |
+
out._idx=norm([out.name,out.sku,out.model,out.brand,out.cat,out.slug,JSON.stringify(out.specs),(out.feats||[]).join(' '),out.sum,out.desc].join(' ')); return out;
|
| 29 |
+
}
|
| 30 |
+
function key(p){return compact((p&&((p.mod||p.model||p.sku||p.slug||p.n||p.name)))||'');}
|
| 31 |
+
function mergeArray(a){if(!Array.isArray(a))return 0; var n=0; STORE.forEach(function(p){var k=key(p); if(!k)return; var idx=a.findIndex(function(x){return key(x)===k||compact(x&&x.slug)===compact(p.slug);}); if(idx>=0)a[idx]=Object.assign({},a[idx],p); else {a.unshift(p); n++;}}); return n;}
|
| 32 |
+
function merge(){var n=0; if(Array.isArray(window.D))n+=mergeArray(window.D); if(Array.isArray(window.F))n+=mergeArray(window.F); else if(Array.isArray(window.D))window.F=window.D.slice(); ['PRODUCTS','products','ALL_PRODUCTS','allProducts'].forEach(function(k){if(Array.isArray(window[k]))n+=mergeArray(window[k]);}); try{if(typeof initFilters==='function')initFilters();}catch(e){} try{if(typeof buildFilters==='function')buildFilters();}catch(e){} try{if(typeof updateStats==='function')updateStats();}catch(e){} return n;}
|
| 33 |
+
function findIdx(p){var D=window.D||[]; return Array.isArray(D)?D.findIndex(function(x){return key(x)===key(p)||String(x.slug||'')===p.slug;}):-1;}
|
| 34 |
+
function findBySlug(sl){sl=String(sl||''); return STORE.find(function(p){return p.slug===sl||compact(p.slug)===compact(sl)||compact(p.sku)===compact(sl);});}
|
| 35 |
+
function renderNoRedirectFallback(p){
|
| 36 |
+
if(!p)return false;
|
| 37 |
+
if(location.pathname.indexOf('/san-pham/')<0)return false;
|
| 38 |
+
try{
|
| 39 |
+
var name=p.name||p.n||p.sku||'Sản phẩm', img=p.image||p.i||((p.imgs||[])[0])||'', price=p.price||p.p||money(p.pn);
|
| 40 |
+
var main=document.querySelector('#mainImg'); if(main&&img){main.src=img;main.alt=name;}
|
| 41 |
+
var el=document.querySelector('#name'); if(el)el.textContent=name;
|
| 42 |
+
el=document.querySelector('#bcName'); if(el)el.textContent=name;
|
| 43 |
+
el=document.querySelector('#sku'); if(el)el.textContent=p.sku||p.mod||'';
|
| 44 |
+
el=document.querySelector('#cat'); if(el)el.textContent=p.cat||p.c||'';
|
| 45 |
+
el=document.querySelector('#price'); if(el)el.textContent=price;
|
| 46 |
+
el=document.querySelector('#summary'); if(el)el.textContent=p.summary||p.sum||p.desc||'';
|
| 47 |
+
el=document.querySelector('#descText'); if(el)el.textContent=p.desc||p.summary||p.sum||'';
|
| 48 |
+
document.title=name+' | V.AI STUDIO';
|
| 49 |
+
try{history.pushState({slug:p.slug},'',productPath(p));}catch(_e){}
|
| 50 |
+
window.scrollTo({top:0,behavior:'smooth'});
|
| 51 |
+
return true;
|
| 52 |
+
}catch(e){return false;}
|
| 53 |
+
}
|
| 54 |
+
function openProduct(p,noReload){
|
| 55 |
+
merge();
|
| 56 |
+
var idx=findIdx(p);
|
| 57 |
+
if(idx>=0&&typeof window.showDetail==='function'){
|
| 58 |
+
try{window.showDetail(idx); try{history.pushState({slug:p.slug},'',productPath(p));}catch(_e){} return true;}catch(e){}
|
| 59 |
+
}
|
| 60 |
+
if(renderNoRedirectFallback(p))return true;
|
| 61 |
+
if(noReload){
|
| 62 |
+
setTimeout(function(){merge(); var i=findIdx(p); if(i>=0&&typeof window.showDetail==='function'){try{window.showDetail(i); history.pushState({slug:p.slug},'',productPath(p));}catch(e){renderNoRedirectFallback(p);}} else renderNoRedirectFallback(p);},200);
|
| 63 |
+
return true;
|
| 64 |
+
}
|
| 65 |
+
return false;
|
| 66 |
+
}
|
| 67 |
+
function patchSearch(){if(window.__VAI_URL_ADDED_SEARCH_V1019__)return; var old=window.doSearch; if(typeof old!=='function')return; window.__VAI_URL_ADDED_SEARCH_V1019__=true; window.doSearch=function(){merge(); var r=old.apply(this,arguments); setTimeout(function(){injectCards();},0); return r;};}
|
| 68 |
+
function cardHtml(p){var esc=function(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});}; return '<div class="pi"><img src="'+esc(p.image)+'" alt="'+esc(p.name)+'"><span class="pi-badge">URL</span></div><div class="pb"><div class="pn">'+esc(p.name)+'</div><div class="pp">'+esc(p.price)+'</div><div class="pf"><span>'+esc(p.sku)+'</span><i class="fas fa-arrow-right"></i></div></div>';}
|
| 69 |
+
function patchLinks(){if(window.__VAI_URL_ADDED_LINKS_V1019__)return; window.__VAI_URL_ADDED_LINKS_V1019__=true; document.addEventListener('click',function(ev){try{var a=ev.target&&ev.target.closest&&ev.target.closest('a[href]'); if(!a)return; var u=new URL(a.getAttribute('href'),location.origin); if(u.origin!==location.origin)return; var m=u.pathname.match(/\/san-pham\/([^\/]+)\/?(?:index\.html)?/i); if(!m)return; var p=findBySlug(decodeURIComponent(m[1])); if(!p)return; ev.preventDefault(); ev.stopPropagation(); openProduct(p,true);}catch(e){}},true);}
|
| 70 |
+
function injectCards(){var q=document.getElementById('q'); var grid=document.getElementById('grid'); if(!q||!grid||!q.value)return; var nq=compact(q.value); if(!nq)return; var text=compact(grid.textContent||''); var hits=STORE.filter(function(p){return compact([p.name,p.sku,p.model,p.mod,p.brand,p.cat,p.slug,p.sum,p.desc].join(' ')).indexOf(nq)>=0;}).slice(0,20); hits.reverse().forEach(function(p){if(text.indexOf(compact(p.name).slice(0,12))>=0||document.querySelector('[data-url-added-slug="'+p.slug+'"]'))return; var card=document.createElement('div'); card.className='pc fade vis'; card.setAttribute('data-url-added-slug',p.slug); card.onclick=function(){openProduct(p,true)}; card.innerHTML=cardHtml(p); grid.insertBefore(card,grid.firstChild);});}
|
| 71 |
+
function handleDeep(){var qs=new URLSearchParams(location.search); var sl=qs.get('p')||qs.get('product')||''; if(!sl&&location.hash){var m=location.hash.match(/product=([^&]+)/); if(m)sl=decodeURIComponent(m[1]);} if(!sl)return; var p=findBySlug(sl); if(!p)return; merge(); var idx=findIdx(p); if(idx>=0&&typeof window.showDetail==='function'){try{window.showDetail(idx); history.replaceState({slug:p.slug},'',productPath(p)); return true;}catch(e){}} return renderNoRedirectFallback(p);}
|
| 72 |
+
function load(){var q='?v='+Date.now(); var u=(location.origin&&location.origin.indexOf('hf.space')>=0?'/products_url_added.json':SITE+'/products_url_added.json')+q; return fetch(u,{cache:'no-store'}).then(function(r){if(!r.ok)throw new Error(r.status);return r.json();}).catch(function(){return fetch(HUB+'/products_url_added.json'+q,{cache:'no-store'}).then(function(r){return r.json();});}).then(function(list){STORE=(Array.isArray(list)?list:[]).map(canon); window.VAI_URL_ADDED_PRODUCTS=STORE; patchLinks(); var tries=0;(function loop(){merge();patchSearch();patchLinks();injectCards();handleDeep(); if(++tries<240)setTimeout(loop,250);})(); console.log('[VAI URL-added live] loaded no-hash',STORE.length); return STORE;}).catch(function(e){console.warn('[VAI URL-added live] failed',e);});}
|
| 73 |
+
window.VAIUrlAddedCompat=window.VAIUrlAddedCompat||{}; window.VAIUrlAddedCompat.load=load; window.VAIUrlAddedCompat.merge=merge; window.VAIUrlAddedCompat.products=function(){return STORE}; window.VAIUrlAddedCompat.open=openProduct;
|
| 74 |
+
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',load); else load();
|
| 75 |
+
})();
|
vai-100percent-img-fix.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO - Excel Export Image 100% Fix
|
| 3 |
+
* Tự động tải TẤT CẢ hình ảnh sản phẩm TRƯỚC khi xuất Excel
|
| 4 |
+
*
|
| 5 |
+
* Cài đặt: Thêm vào cuối index.html trước thẻ </body>
|
| 6 |
+
* <script src="vai-100percent-img-fix.js"></script>
|
| 7 |
+
*/
|
| 8 |
+
(function(){
|
| 9 |
+
'use strict';
|
| 10 |
+
|
| 11 |
+
// ===== IMAGE FETCH VỚI PROXY FALLBACK =====
|
| 12 |
+
function fetchImageSafe(url) {
|
| 13 |
+
return new Promise(function(resolve) {
|
| 14 |
+
if (!url || !url.startsWith('http')) return resolve(null);
|
| 15 |
+
|
| 16 |
+
// Thử 1: Direct CORS
|
| 17 |
+
fetch(url, {mode: 'cors', credentials: 'omit'})
|
| 18 |
+
.then(function(r) {
|
| 19 |
+
if (r.ok) return r.arrayBuffer().then(function(b) {
|
| 20 |
+
resolve({buffer: b, ext: url.match(/\.png/i) ? 'png' : 'jpeg'});
|
| 21 |
+
});
|
| 22 |
+
throw new Error('failed');
|
| 23 |
+
})
|
| 24 |
+
.catch(function(e) {
|
| 25 |
+
// Thử 2: AllOrigins proxy (fallback CORS)
|
| 26 |
+
fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url))
|
| 27 |
+
.then(function(pr) {
|
| 28 |
+
if (pr.ok) return pr.arrayBuffer().then(function(b) {
|
| 29 |
+
resolve({buffer: b, ext: url.match(/\.png/i) ? 'png' : 'jpeg'});
|
| 30 |
+
});
|
| 31 |
+
resolve(null);
|
| 32 |
+
})
|
| 33 |
+
.catch(function() { resolve(null); });
|
| 34 |
+
});
|
| 35 |
+
});
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// ===== PATCH _doExportExcel =====
|
| 39 |
+
function applyImageFix() {
|
| 40 |
+
if (!window._doExportExcel || window.__VAI_IMG_FIX_DONE) return;
|
| 41 |
+
window.__VAI_IMG_FIX_DONE = true;
|
| 42 |
+
|
| 43 |
+
var originalFn = window._doExportExcel;
|
| 44 |
+
|
| 45 |
+
// Thay thế bằng phiên bản có preload ảnh
|
| 46 |
+
window._doExportExcel = async function(data, qd, code, qr) {
|
| 47 |
+
console.log('[VAI IMG FIX] Preloading all images for 100%...');
|
| 48 |
+
|
| 49 |
+
var items = qd.items || [];
|
| 50 |
+
var products = window.D || [];
|
| 51 |
+
|
| 52 |
+
// Lấy URL ảnh từ item hoặc từ products (tự động điền)
|
| 53 |
+
var imgUrls = items.map(function(it) {
|
| 54 |
+
var url = it.image || '';
|
| 55 |
+
if (!url && products.length) {
|
| 56 |
+
var key = (it.model || it.sku || '').toLowerCase();
|
| 57 |
+
var p = products.find(function(prod) {
|
| 58 |
+
return ((prod.mod || prod.model || '').toLowerCase() === key) ||
|
| 59 |
+
(key.indexOf((prod.mod || '').toLowerCase()) >= 0);
|
| 60 |
+
});
|
| 61 |
+
if (p) url = p.i || (p.imgs && p.imgs[0]);
|
| 62 |
+
}
|
| 63 |
+
return url;
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
// PRELOAD TẤT CẢ ẢNH CÙNG LÚC (key fix!)
|
| 67 |
+
var preloadPromises = imgUrls.map(function(url) {
|
| 68 |
+
return url ? fetchImageSafe(url) : Promise.resolve(null);
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
var loadedImages = await Promise.all(preloadPromises);
|
| 72 |
+
var successCount = loadedImages.filter(Boolean).length;
|
| 73 |
+
console.log('[VAI IMG FIX] ✅ Preloaded', successCount, '/', imgUrls.length, 'images');
|
| 74 |
+
|
| 75 |
+
// Cache các ảnh đã load để hàm gốc fetch được
|
| 76 |
+
var imgCache = {};
|
| 77 |
+
loadedImages.forEach(function(img, i) {
|
| 78 |
+
if (img && items[i]) {
|
| 79 |
+
items[i]._imgBuffer = img.buffer;
|
| 80 |
+
items[i]._imgExt = img.ext;
|
| 81 |
+
imgCache[imgUrls[i]] = img.buffer;
|
| 82 |
+
}
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
// Patch fetch để trả về từ cache (tránh fetch lại)
|
| 86 |
+
var origFetch = window.fetch;
|
| 87 |
+
window.fetch = function(url, opts) {
|
| 88 |
+
if (imgCache[url]) {
|
| 89 |
+
return Promise.resolve({
|
| 90 |
+
ok: true,
|
| 91 |
+
arrayBuffer: function() { return Promise.resolve(imgCache[url]); }
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
return origFetch.call(this, url, opts);
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
try {
|
| 98 |
+
// Gọi hàm gốc - ảnh sẽ được lấy từ cache
|
| 99 |
+
return await originalFn.call(this, data, qd, code, qr);
|
| 100 |
+
} finally {
|
| 101 |
+
window.fetch = origFetch;
|
| 102 |
+
}
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
// Update exportExcel function nếu tồn tại
|
| 106 |
+
if (typeof window.exportExcel === 'function') {
|
| 107 |
+
var origExportExcel = window.exportExcel;
|
| 108 |
+
window.exportExcel = async function() {
|
| 109 |
+
if (window.VAI_QR && typeof window.VAI_QR.getData === 'function') {
|
| 110 |
+
var d = window.VAI_QR.getData();
|
| 111 |
+
if (d && d.qd) {
|
| 112 |
+
var code = window.VAI_QR.getEffectiveOrderCode ? window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 113 |
+
var qr = window.VAI_QR.getQRUrl ? window.VAI_QR.getQRUrl(d.deposit > 0 ? d.remaining : d.grandTotal, code) : '';
|
| 114 |
+
return await window._doExportExcel(d, d.qd, code, qr);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
return origExportExcel.apply(this, arguments);
|
| 118 |
+
};
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
console.log('[VAI IMG FIX] ✅ Image preload patch activated - 100% images will load');
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// Chờ cho đến khi ExcelJS và _doExportExcel sẵn sàng
|
| 125 |
+
var checkInt = setInterval(function() {
|
| 126 |
+
if (typeof ExcelJS !== 'undefined' && typeof window._doExportExcel === 'function') {
|
| 127 |
+
clearInterval(checkInt);
|
| 128 |
+
applyImageFix();
|
| 129 |
+
}
|
| 130 |
+
}, 300);
|
| 131 |
+
|
| 132 |
+
setTimeout(function() { clearInterval(checkInt); }, 60000);
|
| 133 |
+
})();
|
vai-excel-100percent-img.js
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — EXCEL 100% IMAGE FIX v2 (DIRECT INJECTION)
|
| 3 |
+
* ======================================================
|
| 4 |
+
* https://huggingface.co/spaces/bep40/V.AISTUDIO
|
| 5 |
+
*
|
| 6 |
+
* VẤN ĐỀ v1:
|
| 7 |
+
* File cũ inject .qt-img rồi gọi genXl → genXl dùng canvas từ DOM
|
| 8 |
+
* nhưng data: URL chưa complete khi genXl chạy → canvas trống
|
| 9 |
+
*
|
| 10 |
+
* GIẢI PHÁP v2:
|
| 11 |
+
* THAY THẾ HOÀN TOÀN genXl — tự build Excel workbook với ảnh
|
| 12 |
+
* được FETCH SẴN dưới dạng base64 + inject TRỰC TIẾP vào worksheet
|
| 13 |
+
* KHÔNG dùng DOM, KHÔNG dùng canvas, KHÔNG phụ thuộc complete
|
| 14 |
+
* ======================================================
|
| 15 |
+
*/
|
| 16 |
+
|
| 17 |
+
(function() {
|
| 18 |
+
'use strict';
|
| 19 |
+
console.log('[VAI EXCEL IMG v2] === LOADING ===');
|
| 20 |
+
|
| 21 |
+
// ===== 1. FETCH IMAGE — CORS + AllOrigins fallback =====
|
| 22 |
+
function fetchImageAsBuffer(url) {
|
| 23 |
+
if (!url || typeof url !== 'string' || !url.startsWith('http')) return Promise.resolve(null);
|
| 24 |
+
return fetch(url, { mode: 'cors', credentials: 'omit' })
|
| 25 |
+
.then(function(r) {
|
| 26 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 27 |
+
return r.arrayBuffer();
|
| 28 |
+
})
|
| 29 |
+
.catch(function() {
|
| 30 |
+
return fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url))
|
| 31 |
+
.then(function(pr) { return pr.ok ? pr.arrayBuffer() : null; })
|
| 32 |
+
.catch(function() { return null; });
|
| 33 |
+
});
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// ===== 2. TÌM URL ẢNH =====
|
| 37 |
+
function getImageUrl(item) {
|
| 38 |
+
if (!item) return '';
|
| 39 |
+
if (item.image && typeof item.image === 'string' && item.image.startsWith('http')) return item.image;
|
| 40 |
+
var products = window.D || [];
|
| 41 |
+
if (!products.length) return '';
|
| 42 |
+
var code = (item.model || item.sku || item.ma || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
|
| 43 |
+
var name = (item.name || '').toString().toLowerCase()
|
| 44 |
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').replace(/[^a-z0-9]/g, '');
|
| 45 |
+
if (code && code.length >= 2) {
|
| 46 |
+
for (var i = 0; i < products.length; i++) {
|
| 47 |
+
var p = products[i];
|
| 48 |
+
var pCode = (p.sku || p.model || p.mod || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '');
|
| 49 |
+
if (pCode && (pCode.indexOf(code) >= 0 || code.indexOf(pCode) >= 0)) {
|
| 50 |
+
var img = p.image || p.i || '';
|
| 51 |
+
if (img) return img;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
if (name && name.length >= 5) {
|
| 56 |
+
for (var j = 0; j < products.length; j++) {
|
| 57 |
+
var p2 = products[j];
|
| 58 |
+
var pName = (p2.name || p2.n || '').toString().toLowerCase()
|
| 59 |
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').replace(/[^a-z0-9]/g, '');
|
| 60 |
+
if (pName && (pName === name || pName.indexOf(name) >= 0 || name.indexOf(pName) >= 0)) {
|
| 61 |
+
var img2 = p2.image || p2.i || '';
|
| 62 |
+
if (img2) return img2;
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
return '';
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// ===== 3. BUFFER → base64 =====
|
| 70 |
+
function bufferToBase64(buf, url) {
|
| 71 |
+
if (!buf || buf.byteLength < 50) return null;
|
| 72 |
+
var ext = 'jpeg';
|
| 73 |
+
if ((url || '').match(/\.png/i)) ext = 'png';
|
| 74 |
+
var bytes = new Uint8Array(buf);
|
| 75 |
+
var binary = '';
|
| 76 |
+
for (var i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
| 77 |
+
try { return btoa(binary); } catch(e) { return null; }
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// ===== 4. BUILD EXCEL TRỰC TIẾP (THAY THẾ genXl) =====
|
| 81 |
+
async function genXlDirect(data, qd, code, preloadedImages) {
|
| 82 |
+
// preloadedImages: array of {base64, ext} or null
|
| 83 |
+
|
| 84 |
+
var M = '#,##0"đ"', N = '#,##0';
|
| 85 |
+
var WH = { argb: 'FFFFFFFF' };
|
| 86 |
+
var WB = {
|
| 87 |
+
top: { style: 'thin', color: WH },
|
| 88 |
+
bottom: { style: 'thin', color: WH },
|
| 89 |
+
left: { style: 'thin', color: WH },
|
| 90 |
+
right: { style: 'thin', color: WH }
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
var wb = new ExcelJS.Workbook();
|
| 94 |
+
var ws = wb.addWorksheet('Báo giá');
|
| 95 |
+
ws.views = [{ showGridLines: false }];
|
| 96 |
+
ws.columns = [
|
| 97 |
+
{ width: 5 }, { width: 11 }, { width: 28 }, { width: 13 },
|
| 98 |
+
{ width: 20 }, { width: 6 }, { width: 13 }, { width: 13 },
|
| 99 |
+
{ width: 15 }, { width: 14 }
|
| 100 |
+
];
|
| 101 |
+
|
| 102 |
+
// Header
|
| 103 |
+
ws.mergeCells('A1:J1');
|
| 104 |
+
ws.getRow(1).height = 28;
|
| 105 |
+
ws.getCell('A1').value = 'V.AI STUDIO ❝ Niềm tin khách hàng là tài sản của chúng tôi ❞';
|
| 106 |
+
ws.getCell('A1').font = { italic: true, size: 9, color: { argb: 'FF64748B' } };
|
| 107 |
+
ws.getCell('A1').alignment = { vertical: 'middle' };
|
| 108 |
+
ws.getCell('A1').border = WB;
|
| 109 |
+
|
| 110 |
+
ws.mergeCells('A3:J3');
|
| 111 |
+
ws.getRow(3).height = 30;
|
| 112 |
+
ws.getCell('A3').value = 'BẢNG BÁO GIÁ';
|
| 113 |
+
ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } };
|
| 114 |
+
ws.getCell('A3').alignment = { horizontal: 'center', vertical: 'middle' };
|
| 115 |
+
|
| 116 |
+
// Customer info
|
| 117 |
+
var cust = qd.customer || {};
|
| 118 |
+
ws.getCell('A5').value = 'Khách hàng:';
|
| 119 |
+
ws.getCell('A5').font = { bold: true, size: 9 };
|
| 120 |
+
ws.getCell('A5').border = WB;
|
| 121 |
+
ws.mergeCells('B5:E5');
|
| 122 |
+
ws.getCell('B5').value = cust.name || '';
|
| 123 |
+
ws.getCell('B5').font = { bold: true, size: 10 };
|
| 124 |
+
ws.getCell('B5').border = WB;
|
| 125 |
+
ws.getCell('H5').value = 'Mã đơn:';
|
| 126 |
+
ws.getCell('H5').font = { size: 9 };
|
| 127 |
+
ws.getCell('H5').border = WB;
|
| 128 |
+
ws.mergeCells('I5:J5');
|
| 129 |
+
ws.getCell('I5').value = code;
|
| 130 |
+
ws.getCell('I5').font = { bold: true, size: 10, color: { argb: 'FF003F62' } };
|
| 131 |
+
ws.getCell('I5').border = WB;
|
| 132 |
+
|
| 133 |
+
ws.getCell('A6').value = 'SĐT:';
|
| 134 |
+
ws.getCell('A6').font = { size: 9 };
|
| 135 |
+
ws.getCell('A6').border = WB;
|
| 136 |
+
ws.getCell('B6').value = cust.phone || '';
|
| 137 |
+
ws.getCell('B6').border = WB;
|
| 138 |
+
ws.getCell('H6').value = 'Ngày:';
|
| 139 |
+
ws.getCell('H6').font = { size: 9 };
|
| 140 |
+
ws.getCell('H6').border = WB;
|
| 141 |
+
ws.getCell('I6').value = cust.date || '';
|
| 142 |
+
ws.getCell('I6').border = WB;
|
| 143 |
+
|
| 144 |
+
ws.getCell('A7').value = 'Email:';
|
| 145 |
+
ws.getCell('A7').font = { size: 9 };
|
| 146 |
+
ws.getCell('A7').border = WB;
|
| 147 |
+
ws.getCell('B7').value = cust.email || '';
|
| 148 |
+
ws.getCell('B7').border = WB;
|
| 149 |
+
ws.getCell('A8').value = 'Địa chỉ:';
|
| 150 |
+
ws.getCell('A8').font = { size: 9 };
|
| 151 |
+
ws.getCell('A8').border = WB;
|
| 152 |
+
ws.mergeCells('B8:J8');
|
| 153 |
+
ws.getCell('B8').value = cust.addr || '';
|
| 154 |
+
ws.getCell('B8').border = WB;
|
| 155 |
+
|
| 156 |
+
// Table header
|
| 157 |
+
var HB = {
|
| 158 |
+
top: { style: 'thin', color: { argb: 'FF003F62' } },
|
| 159 |
+
bottom: { style: 'thin', color: { argb: 'FF003F62' } },
|
| 160 |
+
left: { style: 'thin', color: { argb: 'FF003F62' } },
|
| 161 |
+
right: { style: 'thin', color: { argb: 'FF003F62' } }
|
| 162 |
+
};
|
| 163 |
+
var hRow = ws.getRow(10);
|
| 164 |
+
hRow.height = 20;
|
| 165 |
+
var headers = ['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ú'];
|
| 166 |
+
headers.forEach(function(h, i) {
|
| 167 |
+
var c = hRow.getCell(i + 1);
|
| 168 |
+
c.value = h;
|
| 169 |
+
c.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 9 };
|
| 170 |
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
|
| 171 |
+
c.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
| 172 |
+
c.border = HB;
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
// Items
|
| 176 |
+
var cr = 11;
|
| 177 |
+
var items = qd.items || [];
|
| 178 |
+
items.forEach(function(it, i) {
|
| 179 |
+
var row = ws.getRow(cr);
|
| 180 |
+
row.height = 50;
|
| 181 |
+
var bg = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
|
| 182 |
+
var RB = {
|
| 183 |
+
top: { style: 'thin', color: { argb: bg } },
|
| 184 |
+
bottom: { style: 'thin', color: { argb: bg } },
|
| 185 |
+
left: { style: 'thin', color: { argb: bg } },
|
| 186 |
+
right: { style: 'thin', color: { argb: bg } }
|
| 187 |
+
};
|
| 188 |
+
var qty = Number(it.qty || 1);
|
| 189 |
+
var price = Number(it.price || 0);
|
| 190 |
+
var discPrice = Number(it.discPrice || it.price || 0);
|
| 191 |
+
var lineTotal = discPrice * qty;
|
| 192 |
+
|
| 193 |
+
row.getCell(1).value = it.stt || (i + 1);
|
| 194 |
+
row.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' };
|
| 195 |
+
|
| 196 |
+
// ===== INJECT ẢNH TRỰC TIẾP vào Excel =====
|
| 197 |
+
var pi = preloadedImages && preloadedImages[i];
|
| 198 |
+
if (pi && pi.base64) {
|
| 199 |
+
try {
|
| 200 |
+
var imgId = wb.addImage({ base64: pi.base64, extension: pi.ext || 'jpeg' });
|
| 201 |
+
ws.addImage(imgId, { tl: { col: 1, row: cr - 1 }, ext: { width: 46, height: 46 } });
|
| 202 |
+
} catch(e) {
|
| 203 |
+
console.warn('[VAI EXCEL IMG v2] Add image error:', e);
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
row.getCell(2).value = '';
|
| 208 |
+
row.getCell(2).font = { size: 8 };
|
| 209 |
+
row.getCell(2).alignment = { horizontal: 'center', vertical: 'middle' };
|
| 210 |
+
|
| 211 |
+
row.getCell(3).value = it.name || '';
|
| 212 |
+
row.getCell(3).font = { bold: true, size: 9 };
|
| 213 |
+
row.getCell(3).alignment = { wrapText: true, vertical: 'middle' };
|
| 214 |
+
|
| 215 |
+
row.getCell(4).value = it.model || '';
|
| 216 |
+
row.getCell(4).alignment = { horizontal: 'center', vertical: 'middle' };
|
| 217 |
+
|
| 218 |
+
row.getCell(5).value = it.specs || (it.info || '');
|
| 219 |
+
row.getCell(5).font = { size: 8, color: { argb: 'FF64748B' } };
|
| 220 |
+
row.getCell(5).alignment = { wrapText: true, vertical: 'middle' };
|
| 221 |
+
|
| 222 |
+
row.getCell(6).value = qty;
|
| 223 |
+
row.getCell(6).numFmt = N;
|
| 224 |
+
row.getCell(6).alignment = { horizontal: 'center', vertical: 'middle' };
|
| 225 |
+
|
| 226 |
+
row.getCell(7).value = price;
|
| 227 |
+
row.getCell(7).numFmt = M;
|
| 228 |
+
row.getCell(7).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 229 |
+
|
| 230 |
+
row.getCell(8).value = discPrice;
|
| 231 |
+
row.getCell(8).numFmt = M;
|
| 232 |
+
row.getCell(8).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 233 |
+
if (discPrice < price) row.getCell(8).font = { bold: true, color: { argb: 'FFDC3545' } };
|
| 234 |
+
|
| 235 |
+
row.getCell(9).value = lineTotal;
|
| 236 |
+
row.getCell(9).numFmt = M;
|
| 237 |
+
row.getCell(9).font = { bold: true, size: 9 };
|
| 238 |
+
row.getCell(9).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 239 |
+
|
| 240 |
+
row.getCell(10).value = it.note || '';
|
| 241 |
+
row.getCell(10).font = { size: 8 };
|
| 242 |
+
row.getCell(10).alignment = { wrapText: true, vertical: 'middle' };
|
| 243 |
+
|
| 244 |
+
for (var ci = 1; ci <= 10; ci++) {
|
| 245 |
+
row.getCell(ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: bg } };
|
| 246 |
+
row.getCell(ci).border = RB;
|
| 247 |
+
}
|
| 248 |
+
cr++;
|
| 249 |
+
});
|
| 250 |
+
|
| 251 |
+
// Warranty row
|
| 252 |
+
ws.mergeCells(cr, 1, cr, 10);
|
| 253 |
+
ws.getRow(cr).height = 18;
|
| 254 |
+
ws.getCell('A' + cr).value = '✅ Bảo hành 2 năm (sản phẩm) — Bảo hành hoen gỉ vĩnh viễn (rổ SUS304)';
|
| 255 |
+
ws.getCell('A' + cr).font = { size: 9, color: { argb: 'FF059669' }, italic: true };
|
| 256 |
+
ws.getCell('A' + cr).alignment = { vertical: 'middle' };
|
| 257 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
|
| 258 |
+
cr++;
|
| 259 |
+
|
| 260 |
+
// Fees
|
| 261 |
+
var fees = data.fees || [];
|
| 262 |
+
if (fees.length > 0) {
|
| 263 |
+
fees.forEach(function(f) {
|
| 264 |
+
ws.mergeCells(cr, 1, cr, 8);
|
| 265 |
+
ws.getRow(cr).height = 20;
|
| 266 |
+
ws.getCell('A' + cr).value = ' ⊕ ' + (f.label || 'Phụ phí');
|
| 267 |
+
ws.getCell('A' + cr).font = { size: 9, color: { argb: 'FF92400E' } };
|
| 268 |
+
ws.getCell('A' + cr).alignment = { vertical: 'middle' };
|
| 269 |
+
ws.getCell('I' + cr).value = Number(f.amount || 0);
|
| 270 |
+
ws.getCell('I' + cr).numFmt = M;
|
| 271 |
+
ws.getCell('I' + cr).font = { bold: true, size: 9, color: { argb: 'FF92400E' } };
|
| 272 |
+
ws.getCell('I' + cr).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 273 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFBEB' } };
|
| 274 |
+
cr++;
|
| 275 |
+
});
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// Total
|
| 279 |
+
var TBG = 'FF003F62';
|
| 280 |
+
var TB = {
|
| 281 |
+
top: { style: 'thin', color: { argb: TBG } },
|
| 282 |
+
bottom: { style: 'thin', color: { argb: TBG } },
|
| 283 |
+
left: { style: 'thin', color: { argb: TBG } },
|
| 284 |
+
right: { style: 'thin', color: { argb: TBG } }
|
| 285 |
+
};
|
| 286 |
+
ws.mergeCells(cr, 1, cr, 8);
|
| 287 |
+
var tRow = ws.getRow(cr);
|
| 288 |
+
tRow.height = 28;
|
| 289 |
+
tRow.getCell(1).value = 'TỔNG CỘNG';
|
| 290 |
+
tRow.getCell(1).font = { bold: true, size: 13, color: { argb: 'FFFFFFFF' } };
|
| 291 |
+
tRow.getCell(1).alignment = { horizontal: 'left', vertical: 'middle', indent: 1 };
|
| 292 |
+
for (var ci = 1; ci <= 10; ci++) {
|
| 293 |
+
tRow.getCell(ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: TBG } };
|
| 294 |
+
tRow.getCell(ci).border = TB;
|
| 295 |
+
}
|
| 296 |
+
tRow.getCell(9).value = Number(data.grandTotal || 0);
|
| 297 |
+
tRow.getCell(9).numFmt = M;
|
| 298 |
+
tRow.getCell(9).font = { bold: true, size: 15, color: { argb: 'FFF0B840' } };
|
| 299 |
+
tRow.getCell(9).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 300 |
+
cr++;
|
| 301 |
+
|
| 302 |
+
// Deposit
|
| 303 |
+
var deposit = Number(data.deposit || 0);
|
| 304 |
+
if (deposit > 0) {
|
| 305 |
+
ws.mergeCells(cr, 1, cr, 8);
|
| 306 |
+
ws.getRow(cr).height = 22;
|
| 307 |
+
ws.getCell('A' + cr).value = 'Đã cọc';
|
| 308 |
+
ws.getCell('A' + cr).font = { bold: true, size: 11, color: { argb: 'FF166534' } };
|
| 309 |
+
ws.getCell('A' + cr).alignment = { horizontal: 'left', vertical: 'middle', indent: 1 };
|
| 310 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
|
| 311 |
+
ws.getCell('I' + cr).value = deposit;
|
| 312 |
+
ws.getCell('I' + cr).numFmt = M;
|
| 313 |
+
ws.getCell('I' + cr).font = { bold: true, size: 11, color: { argb: 'FF166534' } };
|
| 314 |
+
ws.getCell('I' + cr).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 315 |
+
cr++;
|
| 316 |
+
|
| 317 |
+
ws.mergeCells(cr, 1, cr, 8);
|
| 318 |
+
ws.getRow(cr).height = 24;
|
| 319 |
+
ws.getCell('A' + cr).value = 'CÒN LẠI';
|
| 320 |
+
ws.getCell('A' + cr).font = { bold: true, size: 12, color: { argb: 'FFDC2626' } };
|
| 321 |
+
ws.getCell('A' + cr).alignment = { horizontal: 'left', vertical: 'middle', indent: 1 };
|
| 322 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF2F2' } };
|
| 323 |
+
ws.getCell('I' + cr).value = Number(data.remaining || 0);
|
| 324 |
+
ws.getCell('I' + cr).numFmt = M;
|
| 325 |
+
ws.getCell('I' + cr).font = { bold: true, size: 13, color: { argb: 'FFDC2626' } };
|
| 326 |
+
ws.getCell('I' + cr).alignment = { horizontal: 'right', vertical: 'middle' };
|
| 327 |
+
cr++;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// Notes
|
| 331 |
+
var notes = data.notes || [];
|
| 332 |
+
if (notes.length > 0) {
|
| 333 |
+
ws.mergeCells(cr, 1, cr, 10);
|
| 334 |
+
ws.getCell('A' + cr).value = '📝 Ghi chú: ' + notes.join('; ');
|
| 335 |
+
ws.getCell('A' + cr).font = { italic: true, size: 9, color: { argb: 'FF166534' } };
|
| 336 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
|
| 337 |
+
cr++;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// Bank info
|
| 341 |
+
cr++;
|
| 342 |
+
ws.mergeCells(cr, 1, cr, 5);
|
| 343 |
+
ws.getRow(cr).height = 18;
|
| 344 |
+
ws.getCell('A' + cr).value = '💳 QR CODE: VIB - 918258385 - Trần Quốc Vương';
|
| 345 |
+
ws.getCell('A' + cr).font = { bold: true, size: 10, color: { argb: 'FF003F62' } };
|
| 346 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF8FAFC' } };
|
| 347 |
+
cr++;
|
| 348 |
+
|
| 349 |
+
ws.mergeCells(cr, 1, cr, 10);
|
| 350 |
+
ws.getCell('A' + cr).value = '🏦 VIB | 👤 Trần Quốc Vương | STK: 918258385 | 💰 ' +
|
| 351 |
+
(deposit > 0 ? Number(data.remaining || 0) : Number(data.grandTotal || 0)).toLocaleString('vi-VN') + 'đ' +
|
| 352 |
+
' | 📝 ' + code;
|
| 353 |
+
ws.getCell('A' + cr).font = { bold: true, size: 10 };
|
| 354 |
+
for (var ci = 1; ci <= 10; ci++) ws.getCell(cr, ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF8FAFC' } };
|
| 355 |
+
|
| 356 |
+
// Borders
|
| 357 |
+
ws.eachRow(function(row) {
|
| 358 |
+
row.eachCell(function(cell) {
|
| 359 |
+
if (!cell.border) cell.border = WB;
|
| 360 |
+
});
|
| 361 |
+
});
|
| 362 |
+
|
| 363 |
+
wb.calcProperties.fullCalcOnLoad = true;
|
| 364 |
+
var buf = await wb.xlsx.writeBuffer();
|
| 365 |
+
var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
| 366 |
+
var url = URL.createObjectURL(blob);
|
| 367 |
+
var a = document.createElement('a');
|
| 368 |
+
a.href = url;
|
| 369 |
+
a.download = code + '.xlsx';
|
| 370 |
+
a.style.display = 'none';
|
| 371 |
+
document.body.appendChild(a);
|
| 372 |
+
a.click();
|
| 373 |
+
setTimeout(function() {
|
| 374 |
+
document.body.removeChild(a);
|
| 375 |
+
URL.revokeObjectURL(url);
|
| 376 |
+
}, 60000);
|
| 377 |
+
console.log('[VAI EXCEL IMG v2] ✅ ' + code + '.xlsx');
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// ===== 5. MAIN: override exportExcel =====
|
| 381 |
+
function applyFix() {
|
| 382 |
+
if (window.__vaiExcelV2Done) return;
|
| 383 |
+
window.__vaiExcelV2Done = true;
|
| 384 |
+
console.log('[VAI EXCEL IMG v2] Applying...');
|
| 385 |
+
|
| 386 |
+
function ensureOurExport() {
|
| 387 |
+
// Skip if our wrapper already active
|
| 388 |
+
if (window.exportExcel && window.exportExcel.__vaiExcelV2) return;
|
| 389 |
+
|
| 390 |
+
// Capture current export for fallback
|
| 391 |
+
var oldExport = window.exportExcel;
|
| 392 |
+
|
| 393 |
+
// Our wrapper - supports both (data, qd, code, qrUrl, bw) and auto-detect modes
|
| 394 |
+
window.exportExcel = async function(data, qd, code, qrUrl, bw) {
|
| 395 |
+
console.log('[VAI EXCEL IMG v2] === exportExcel called ===');
|
| 396 |
+
|
| 397 |
+
// Lấy data - prioritize passed arguments
|
| 398 |
+
try {
|
| 399 |
+
if (window.VAI_QR && typeof window.VAI_QR.getData === 'function' && !data) data = window.VAI_QR.getData();
|
| 400 |
+
} catch(e) {}
|
| 401 |
+
|
| 402 |
+
if (data && !qd) qd = data.qd;
|
| 403 |
+
|
| 404 |
+
if (!data || !data.qd || !data.qd.items || !data.qd.items.length) {
|
| 405 |
+
try {
|
| 406 |
+
if (typeof getQuoteData === 'function') {
|
| 407 |
+
var qd2 = getQuoteData();
|
| 408 |
+
data = {
|
| 409 |
+
qd: qd2,
|
| 410 |
+
fees: (window.VAI_QR && window.VAI_QR.getData && window.VAI_QR.getData().fees) || [],
|
| 411 |
+
notes: (window.VAI_QR && window.VAI_QR.getData && window.VAI_QR.getData().notes) || [],
|
| 412 |
+
deposit: (window.VAI_QR && window.VAI_QR.getData && window.VAI_QR.getData().deposit) || 0,
|
| 413 |
+
discountPercent: (window.VAI_QR && window.VAI_QR.getData && window.VAI_QR.getData().discountPercent) || 0,
|
| 414 |
+
grandTotal: qd2.grandTotal || 0,
|
| 415 |
+
remaining: (qd2.grandTotal || 0) - ((window.VAI_QR && window.VAI_QR.getData && window.VAI_QR.getData().deposit) || 0)
|
| 416 |
+
};
|
| 417 |
+
}
|
| 418 |
+
} catch(e) {}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
if (!data || !data.qd || !data.qd.items) {
|
| 422 |
+
console.warn('[VAI EXCEL IMG v2] No data');
|
| 423 |
+
if (oldExport) return oldExport();
|
| 424 |
+
return;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
qd = data.qd;
|
| 428 |
+
code = (window.VAI_QR && typeof window.VAI_QR.getEffectiveOrderCode === 'function')
|
| 429 |
+
? window.VAI_QR.getEffectiveOrderCode()
|
| 430 |
+
: (qd.customer && qd.customer.orderCode) || 'BAOGIA';
|
| 431 |
+
|
| 432 |
+
// Preload images trực tiếp
|
| 433 |
+
var items = qd.items || [];
|
| 434 |
+
var imgUrls = items.map(getImageUrl);
|
| 435 |
+
var loaded = 0;
|
| 436 |
+
|
| 437 |
+
console.log('[VAI EXCEL IMG v2] Fetching', imgUrls.filter(Boolean).length, 'images...');
|
| 438 |
+
|
| 439 |
+
var results = await Promise.all(imgUrls.map(function(url) {
|
| 440 |
+
if (!url) return Promise.resolve(null);
|
| 441 |
+
return fetchImageAsBuffer(url).then(function(buf) {
|
| 442 |
+
if (buf && buf.byteLength > 50) loaded++;
|
| 443 |
+
var b64 = bufferToBase64(buf, url);
|
| 444 |
+
return b64 ? { base64: b64, ext: (url || '').match(/\.png/i) ? 'png' : 'jpeg' } : null;
|
| 445 |
+
});
|
| 446 |
+
}));
|
| 447 |
+
|
| 448 |
+
console.log('[VAI EXCEL IMG v2] ✅', loaded, '/', imgUrls.filter(Boolean).length, 'loaded');
|
| 449 |
+
|
| 450 |
+
// Build Excel TRỰC TIẾP với ảnh base64
|
| 451 |
+
try {
|
| 452 |
+
await genXlDirect(data, qd, code, results);
|
| 453 |
+
console.log('[VAI EXCEL IMG v2] ✅ Done');
|
| 454 |
+
} catch(e) {
|
| 455 |
+
console.error('[VAI EXCEL IMG v2] Error:', e);
|
| 456 |
+
// Fallback
|
| 457 |
+
try {
|
| 458 |
+
if (oldExport && oldExport.__vaiExcelV2) {
|
| 459 |
+
// don't recurse
|
| 460 |
+
} else if (oldExport) {
|
| 461 |
+
await oldExport();
|
| 462 |
+
}
|
| 463 |
+
} catch(e2) {
|
| 464 |
+
console.error('[VAI EXCEL IMG v2] Fallback failed:', e2);
|
| 465 |
+
alert('❌ Lỗi xuất Excel: ' + (e.message || e));
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
};
|
| 469 |
+
|
| 470 |
+
window.exportExcel.__vaiExcelV2 = true;
|
| 471 |
+
window._doExportExcel = window.exportExcel;
|
| 472 |
+
if (window.VAI_QR) window.VAI_QR.exportExcel = window.exportExcel;
|
| 473 |
+
|
| 474 |
+
console.log('[VAI EXCEL IMG v2] Export replaced — direct Excel injection active');
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
// Ensure ngay lập tức
|
| 478 |
+
ensureOurExport();
|
| 479 |
+
// Duy trì mỗi 100ms (vượt qua setInterval 200ms của export-fixed)
|
| 480 |
+
setInterval(ensureOurExport, 100);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
// ===== 6. INIT =====
|
| 484 |
+
function init() {
|
| 485 |
+
var waited = 0;
|
| 486 |
+
var t = setInterval(function() {
|
| 487 |
+
waited += 500;
|
| 488 |
+
if (typeof ExcelJS !== 'undefined' &&
|
| 489 |
+
typeof window.exportExcel === 'function' &&
|
| 490 |
+
window.D && window.D.length > 100) {
|
| 491 |
+
clearInterval(t);
|
| 492 |
+
setTimeout(applyFix, 500);
|
| 493 |
+
return;
|
| 494 |
+
}
|
| 495 |
+
if (waited >= 30000) {
|
| 496 |
+
clearInterval(t);
|
| 497 |
+
applyFix();
|
| 498 |
+
}
|
| 499 |
+
}, 500);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
if (document.readyState === 'loading') {
|
| 503 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 504 |
+
} else {
|
| 505 |
+
init();
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
console.log('[VAI EXCEL IMG v2] Module loaded');
|
| 509 |
+
})();
|
vai-export-fixed.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — EXPORT v6.0 DEFINITIVE (100% images guaranteed)
|
| 3 |
+
* ======================================================
|
| 4 |
+
* https://huggingface.co/spaces/bep40/V.AISTUDIO
|
| 5 |
+
*
|
| 6 |
+
* FIX TRIỆT ĐỂ:
|
| 7 |
+
* - Tự fetch ảnh bằng CORS + AllOrigins proxy fallback (vượt bizweb.dktcdn.net, eurogold.vn)
|
| 8 |
+
* - Chuyển ArrayBuffer → base64 → addImage trực tiếp vào Excel workbook
|
| 9 |
+
* - KHÔNG dùng DOM canvas, KHÔNG phụ thuộc .qt-img, KHÔNG CORS taint
|
| 10 |
+
* - KHÔNG race condition — genXl tự xử lý hết
|
| 11 |
+
* ======================================================
|
| 12 |
+
*/
|
| 13 |
+
(function(){'use strict';console.log('[VAI EXPORT v6] === LOADING ===');
|
| 14 |
+
|
| 15 |
+
// ===== IMAGE FETCH ENGINE =====
|
| 16 |
+
function fetchImg(url) {
|
| 17 |
+
if(!url||typeof url!=='string'||!url.startsWith('http')) return Promise.resolve(null);
|
| 18 |
+
return fetch(url,{mode:'cors',credentials:'omit'})
|
| 19 |
+
.then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.arrayBuffer();})
|
| 20 |
+
.catch(function(){
|
| 21 |
+
return fetch('https://api.allorigins.win/raw?url='+encodeURIComponent(url))
|
| 22 |
+
.then(function(pr){return pr.ok?pr.arrayBuffer():null;})
|
| 23 |
+
.catch(function(){return null;});
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
function bufToB64(buf,url){
|
| 27 |
+
if(!buf||buf.byteLength<50)return null;
|
| 28 |
+
var ext=(url||'').match(/\.png/i)?'png':'jpeg',b='',u=new Uint8Array(buf);
|
| 29 |
+
for(var i=0;i<u.byteLength;i++)b+=String.fromCharCode(u[i]);
|
| 30 |
+
try{return{base64:btoa(b),ext:ext};}catch(e){return null;}
|
| 31 |
+
}
|
| 32 |
+
function getImgUrl(it){
|
| 33 |
+
if(!it)return'';
|
| 34 |
+
if(it.image&&typeof it.image==='string'&&it.image.startsWith('http'))return it.image;
|
| 35 |
+
var D=window.D||[];if(!D.length)return'';
|
| 36 |
+
var code=(it.model||it.sku||it.ma||'').toString().toLowerCase().replace(/[^a-z0-9]/g,'');
|
| 37 |
+
var nm=(it.name||'').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[đĐ]/g,'d').replace(/[^a-z0-9]/g,'');
|
| 38 |
+
if(code&&code.length>=2){for(var i=0;i<D.length;i++){var p=D[i],pc=(p.sku||p.model||p.mod||'').toString().toLowerCase().replace(/[^a-z0-9]/g,'');if(pc&&(pc.indexOf(code)>=0||code.indexOf(pc)>=0)){var im=p.image||p.i||'';if(im)return im;}}}
|
| 39 |
+
if(nm&&nm.length>=5){for(var j=0;j<D.length;j++){var p2=D[j],pn=(p2.name||p2.n||'').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[đĐ]/g,'d').replace(/[^a-z0-9]/g,'');if(pn&&(pn===nm||pn.indexOf(nm)>=0||nm.indexOf(pn)>=0)){var im2=p2.image||p2.i||'';if(im2)return im2;}}}
|
| 40 |
+
return'';
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// ===== BUILD EXCEL =====
|
| 44 |
+
async function genXl(data,qd,code){
|
| 45 |
+
var M='#,##0"đ"',N='#,##0',WH={argb:'FFFFFFFF'},WB={top:{style:'thin',color:WH},bottom:{style:'thin',color:WH},left:{style:'thin',color:WH},right:{style:'thin',color:WH}};
|
| 46 |
+
var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Báo giá');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}];
|
| 47 |
+
|
| 48 |
+
ws.mergeCells('A1:J1');ws.getRow(1).height=28;ws.getCell('A1').value='V.AI STUDIO ❝ Niềm tin khách hàng là tài sản của chúng tôi ❞';ws.getCell('A1').font={italic:true,size:9,color:{argb:'FF64748B'}};ws.getCell('A1').alignment={vertical:'middle'};ws.getCell('A1').border=WB;
|
| 49 |
+
ws.getRow(2).height=5;ws.mergeCells('A3:J3');ws.getRow(3).height=30;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(4).height=5;
|
| 50 |
+
|
| 51 |
+
var cust=qd.customer||{};
|
| 52 |
+
ws.getCell('A5').value='Khách hàng:';ws.getCell('A5').font={bold:true,size:9};ws.getCell('A5').border=WB;ws.mergeCells('B5:E5');ws.getCell('B5').value=cust.name||'';ws.getCell('B5').font={bold:true,size:10};ws.getCell('B5').border=WB;ws.getCell('H5').value='Mã đơn:';ws.getCell('H5').font={size:9};ws.getCell('H5').border=WB;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;
|
| 53 |
+
ws.getCell('A6').value='SĐT:';ws.getCell('A6').font={size:9};ws.getCell('A6').border=WB;ws.getCell('B6').value=cust.phone||'';ws.getCell('B6').border=WB;ws.getCell('H6').value='Ngày:';ws.getCell('H6').font={size:9};ws.getCell('H6').border=WB;ws.getCell('I6').value=cust.date||'';ws.getCell('I6').border=WB;
|
| 54 |
+
ws.getCell('A7').value='Email:';ws.getCell('A7').font={size:9};ws.getCell('A7').border=WB;ws.getCell('B7').value=cust.email||'';ws.getCell('B7').border=WB;ws.getCell('A8').value='Địa chỉ:';ws.getCell('A8').font={size:9};ws.getCell('A8').border=WB;ws.mergeCells('B8:J8');ws.getCell('B8').value=cust.addr||'';ws.getCell('B8').border=WB;ws.getRow(9).height=5;
|
| 55 |
+
|
| 56 |
+
// HEADER row
|
| 57 |
+
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'}}};
|
| 58 |
+
var hRow=ws.getRow(10);hRow.height=20;
|
| 59 |
+
['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){
|
| 60 |
+
var c=hRow.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;
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
// PRELOAD images BEFORE building rows
|
| 64 |
+
var items=qd.items||[];
|
| 65 |
+
var imgUrls=items.map(getImgUrl);
|
| 66 |
+
console.log('[VAI EXPORT v6] Fetching',imgUrls.filter(Boolean).length,'images for',items.length,'items...');
|
| 67 |
+
var imgResults=await Promise.all(imgUrls.map(function(url){
|
| 68 |
+
if(!url)return Promise.resolve(null);
|
| 69 |
+
return fetchImg(url).then(function(buf){return bufToB64(buf,url);});
|
| 70 |
+
}));
|
| 71 |
+
console.log('[VAI EXPORT v6] Loaded',imgResults.filter(Boolean).length,'images');
|
| 72 |
+
|
| 73 |
+
// BUILD rows với ảnh trực tiếp
|
| 74 |
+
var cr=11;
|
| 75 |
+
items.forEach(function(it,i){
|
| 76 |
+
var row=ws.getRow(cr);row.height=50;
|
| 77 |
+
var bg=i%2===0?'FFF8FAFC':'FFFFFFFF';
|
| 78 |
+
var RB={top:{style:'thin',color:{argb:bg}},bottom:{style:'thin',color:{argb:bg}},left:{style:'thin',color:{argb:bg}},right:{style:'thin',color:{argb:bg}}};
|
| 79 |
+
var qty=Number(it.qty||1),price=Number(it.price||0),discPrice=Number(it.discPrice||it.price||0),lineTotal=discPrice*qty;
|
| 80 |
+
|
| 81 |
+
row.getCell(1).value=it.stt||(i+1);row.getCell(1).alignment={horizontal:'center',vertical:'middle'};
|
| 82 |
+
|
| 83 |
+
// ===== INJECT ẢNH TRỰC TIẾP VÀO EXCEL =====
|
| 84 |
+
var ri=imgResults[i];
|
| 85 |
+
if(ri&&ri.base64){
|
| 86 |
+
try{
|
| 87 |
+
var imgId=wb.addImage({base64:ri.base64,extension:ri.ext||'jpeg'});
|
| 88 |
+
ws.addImage(imgId,{tl:{col:1,row:cr-1},ext:{width:46,height:46}});
|
| 89 |
+
}catch(ie){console.warn('[VAI EXPORT v6] img add error:',ie);}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
row.getCell(2).value='';row.getCell(2).font={size:8};row.getCell(2).alignment={horizontal:'center',vertical:'middle'};
|
| 93 |
+
row.getCell(3).value=it.name||'';row.getCell(3).font={bold:true,size:9};row.getCell(3).alignment={wrapText:true,vertical:'middle'};
|
| 94 |
+
row.getCell(4).value=it.model||'';row.getCell(4).alignment={horizontal:'center',vertical:'middle'};
|
| 95 |
+
row.getCell(5).value=it.specs||(it.info||'');row.getCell(5).font={size:8,color:{argb:'FF64748B'}};row.getCell(5).alignment={wrapText:true,vertical:'middle'};
|
| 96 |
+
row.getCell(6).value=qty;row.getCell(6).numFmt=N;row.getCell(6).alignment={horizontal:'center',vertical:'middle'};
|
| 97 |
+
row.getCell(7).value=price;row.getCell(7).numFmt=M;row.getCell(7).alignment={horizontal:'right',vertical:'middle'};
|
| 98 |
+
row.getCell(8).value=discPrice;row.getCell(8).numFmt=M;row.getCell(8).alignment={horizontal:'right',vertical:'middle'};if(discPrice<price)row.getCell(8).font={bold:true,color:{argb:'FFDC3545'}};
|
| 99 |
+
row.getCell(9).value=lineTotal;row.getCell(9).numFmt=M;row.getCell(9).font={bold:true,size:9};row.getCell(9).alignment={horizontal:'right',vertical:'middle'};
|
| 100 |
+
row.getCell(10).value=it.note||'';row.getCell(10).font={size:8};row.getCell(10).alignment={wrapText:true,vertical:'middle'};
|
| 101 |
+
for(var ci=1;ci<=10;ci++){row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:bg}};row.getCell(ci).border=RB;}
|
| 102 |
+
cr++;
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
// WARRANTY
|
| 106 |
+
ws.mergeCells(cr,1,cr,10);ws.getRow(cr).height=18;ws.getCell('A'+cr).value='✅ Bảo hành 2 năm (sản phẩm) — Bảo hành hoen gỉ vĩnh viễn (rổ SUS304)';ws.getCell('A'+cr).font={size:9,color:{argb:'FF059669'},italic:true};ws.getCell('A'+cr).alignment={vertical:'middle'};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF0FDF4'}};cr++;
|
| 107 |
+
|
| 108 |
+
// FEES
|
| 109 |
+
var fees=data.fees||[];
|
| 110 |
+
if(fees.length>0){fees.forEach(function(f){ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=20;ws.getCell('A'+cr).value=' ⊕ '+(f.label||'Phụ phí');ws.getCell('A'+cr).font={size:9,color:{argb:'FF92400E'}};ws.getCell('A'+cr).alignment={vertical:'middle'};ws.getCell('I'+cr).value=Number(f.amount||0);ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:9,color:{argb:'FF92400E'}};ws.getCell('I'+cr).alignment={horizontal:'right',vertical:'middle'};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFFFBEB'}};cr++;});}
|
| 111 |
+
|
| 112 |
+
// TOTAL
|
| 113 |
+
var TBG='FF003F62',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}}};ws.mergeCells(cr,1,cr,8);var tRow=ws.getRow(cr);tRow.height=28;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};for(var ci=1;ci<=10;ci++){tRow.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(ci).border=TB;}
|
| 114 |
+
tRow.getCell(9).value=Number(data.grandTotal||0);tRow.getCell(9).numFmt=M;tRow.getCell(9).font={bold:true,size:15,color:{argb:'FFF0B840'}};tRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};cr++;
|
| 115 |
+
|
| 116 |
+
// DEPOSIT
|
| 117 |
+
var deposit=Number(data.deposit||0);
|
| 118 |
+
if(deposit>0){ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=22;ws.getCell('A'+cr).value='Đặt cọc';ws.getCell('A'+cr).font={bold:true,size:11,color:{argb:'FF166534'}};ws.getCell('A'+cr).alignment={horizontal:'left',vertical:'middle',indent:1};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF0FDF4'}};ws.getCell('I'+cr).value=deposit;ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:11,color:{argb:'FF166534'}};ws.getCell('I'+cr).alignment={horizontal:'right',vertical:'middle'};cr++;
|
| 119 |
+
ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=24;ws.getCell('A'+cr).value='Còn lại';ws.getCell('A'+cr).font={bold:true,size:12,color:{argb:'FFDC2626'}};ws.getCell('A'+cr).alignment={horizontal:'left',vertical:'middle',indent:1};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFEF2F2'}};ws.getCell('I'+cr).value=Number(data.remaining||0);ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:13,color:{argb:'FFDC2626'}};ws.getCell('I'+cr).alignment={horizontal:'right',vertical:'middle'};cr++;
|
| 120 |
+
}else{ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=24;ws.getCell('A'+cr).value='Còn lại (100%)';ws.getCell('A'+cr).font={bold:true,size:12,color:{argb:'FFDC2626'}};ws.getCell('A'+cr).alignment={horizontal:'left',vertical:'middle',indent:1};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFEF2F2'}};ws.getCell('I'+cr).value=Number(data.grandTotal||0);ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:13,color:{argb:'FFDC2626'}};ws.getCell('I'+cr).alignment={horizontal:'right',vertical:'middle'};cr++;}
|
| 121 |
+
|
| 122 |
+
// NOTES
|
| 123 |
+
var notes=data.notes||[];if(notes.length>0){ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='📝 Ghi chú: '+notes.join('; ');ws.getCell('A'+cr).font={italic:true,size:9,color:{argb:'FF166534'}};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF0FDF4'}};cr++;}
|
| 124 |
+
|
| 125 |
+
// BANK INFO
|
| 126 |
+
cr++;ws.mergeCells(cr,1,cr,5);ws.getRow(cr).height=18;ws.getCell('A'+cr).value='💳 QR CODE: VIB - 918258385 - Trần Quốc Vương';ws.getCell('A'+cr).font={bold:true,size:10,color:{argb:'FF003F62'}};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF8FAFC'}};cr++;
|
| 127 |
+
ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='🏦 VIB | 👤 Trần Quốc Vương | STK: 918258385 | 💰 '+((deposit>0?data.remaining:data.grandTotal)||0).toLocaleString('vi-VN')+'đ | 📝 '+code;ws.getCell('A'+cr).font={bold:true,size:10};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF8FAFC'}};
|
| 128 |
+
|
| 129 |
+
// Final borders
|
| 130 |
+
ws.eachRow(function(row){row.eachCell(function(cell){if(!cell.border)cell.border=WB;});});
|
| 131 |
+
wb.calcProperties.fullCalcOnLoad=true;
|
| 132 |
+
|
| 133 |
+
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.style.display='none';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(url);},60000);
|
| 134 |
+
console.log('[VAI EXPORT v6] ✅ '+code+'.xlsx');
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// ===== FALLBACK HTML (khi ExcelJS lỗi) =====
|
| 138 |
+
function fallbackXls(){try{var data;if(window.VAI_QR&&typeof window.VAI_QR.getData==='function')data=window.VAI_QR.getData();else if(typeof getData==='function')data=getData();if(!data||!data.qd||!data.qd.items){alert('⚠️ Không có dữ liệu');return;}
|
| 139 |
+
var qd=data.qd,cust=qd.customer||{},code=(window.VAI_QR&&typeof window.VAI_QR.getEffectiveOrderCode==='function')?window.VAI_QR.getEffectiveOrderCode():'VAS'+Date.now().toString(36).toUpperCase();
|
| 140 |
+
function fmt(n){if(!n||isNaN(n))return'0đ';return Number(n).toLocaleString('vi-VN')+'đ';}
|
| 141 |
+
var h='<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>BaoGia</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>td,th{border:1px solid #ccc;padding:4px;font-family:Arial;font-size:11px}th{background:#003f62;color:#fff;font-weight:700}</style></head><body><table cellpadding="3" cellspacing="0" style="border-collapse:collapse;font-family:Arial;font-size:11px;width:100%">';
|
| 142 |
+
h+='<tr><th colspan="7" style="font-size:18px;color:#db9815;text-align:center;padding:10px;background:#003f62;color:#fff">V.AI STUDIO — BẢNG BÁO GIÁ</th></tr>';h+='<tr><td colspan="2"><b>KH:</b> '+(cust.name||'')+'</td><td colspan="3"><b>Mã:</b> '+code+'</td><td colspan="2"><b>Ngày:</b> '+(cust.date||'')+'</td></tr>';h+='<tr><td colspan="7"><b>SĐT:</b> '+(cust.phone||'')+' | <b>Email:</b> '+(cust.email||'')+' | <b>ĐC:</b> '+(cust.addr||'')+'</td></tr>';h+='<tr><th>STT</th><th>Tên SP</th><th>Mã SP</th><th>SL</th><th>Đơn giá</th><th>Giá CK</th><th>Thành tiền</th></tr>';
|
| 143 |
+
var total=0;qd.items.forEach(function(it,i){var lt=(it.discPrice||it.price||0)*(it.qty||1);total+=lt;var bg=i%2===0?'#f8fafc':'#fff';var ckS=(it.discPrice&&it.price&&it.discPrice<it.price)?' style="color:#dc3545;font-weight:700"':'';h+='<tr style="background:'+bg+'"><td style="text-align:center">'+(i+1)+'</td><td style="font-weight:600">'+(it.name||'')+'</td><td style="text-align:center">'+(it.model||'')+'</td><td style="text-align:center">'+(it.qty||1)+'</td><td style="text-align:right">'+fmt(it.price||0)+'</td><td style="text-align:right"'+ckS+'>'+fmt(it.discPrice||it.price||0)+'</td><td style="text-align:right;font-weight:700">'+fmt(lt)+'</td></tr>';});(data.fees||[]).forEach(function(f){h+='<tr style="background:#fffbeb"><td colspan="6" style="color:#92400e">⊕ '+f.label+'</td><td style="text-align:right;font-weight:700;color:#92400e">'+fmt(f.amount)+'</td></tr>';});
|
| 144 |
+
var gt=data.grandTotal||total;h+='<tr style="font-weight:bold;background:#003f62;color:#fff"><td colspan="6" style="text-align:right;padding:8px;font-size:13px">TỔNG CỘNG</td><td style="text-align:right;color:#f0b840;font-size:16px;padding:8px">'+fmt(gt)+'</td></tr>';
|
| 145 |
+
if(data.deposit>0){var rem=gt-data.deposit;h+='<tr style="background:#f0fdf4"><td colspan="6" style="color:#166534">Đã cọc</td><td style="text-align:right;font-weight:700;color:#166534">'+fmt(data.deposit)+'</td></tr>';h+='<tr style="background:#fef2f2"><td colspan="6" style="color:#dc2626;font-weight:700">CÒN LẠI</td><td style="text-align:right;font-weight:900;color:#dc2626">'+fmt(rem>0?rem:0)+'</td></tr>';}
|
| 146 |
+
h+='<tr><td colspan="7" style="font-size:10px;color:#059669;font-style:italic;padding:6px;background:#f0fdf4">✅ Bảo hành 2 năm (sản phẩm) — Bảo hành hoen gỉ vĩnh viễn (rổ SUS304)</td></tr>';h+='<tr><td colspan="7" style="font-size:10px;background:#f8fafc;padding:6px">💳 VIB | Trần Quốc Vương | STK: 918258385 | Số tiền: '+fmt(data.deposit>0?gt-data.deposit:gt)+' | ND: '+code+'</td></tr></table></body></html>';
|
| 147 |
+
var blob=new Blob([h],{type:'application/vnd.ms-excel'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=code+'.xls';a.style.display='none';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(url);},60000);console.log('[VAI EXPORT v6] ✅ Fallback '+code+'.xls');}catch(e){console.error('[VAI EXPORT v6] Fallback error:',e);alert('❌ Lỗi xuất Excel: '+(e.message||e));}}
|
| 148 |
+
|
| 149 |
+
// ===== SET EXPORT =====
|
| 150 |
+
setInterval(function(){
|
| 151 |
+
if(typeof ExcelJS!=='undefined'&&window.VAI_QR){
|
| 152 |
+
window.exportExcel=async function(data,qd,code,qrUrl,bw){
|
| 153 |
+
console.log('[VAI EXPORT v6] exportExcel');
|
| 154 |
+
try{
|
| 155 |
+
// Use passed data or try to get it
|
| 156 |
+
if(!data || !data.qd || !data.qd.items || !data.qd.items.length){
|
| 157 |
+
if(window.VAI_QR&&typeof window.VAI_QR.getData==='function')data=window.VAI_QR.getData();
|
| 158 |
+
else if(typeof getData==='function')data=getData();
|
| 159 |
+
if(!data||!data.qd){
|
| 160 |
+
if(typeof getQuoteData==='function'){var qd2=getQuoteData();data={qd:qd2,fees:[],notes:[],deposit:0,discountPercent:0,itemDiscounts:{},grandTotal:qd2.grandTotal||0,remaining:qd2.grandTotal||0,productTotal:qd2.grandTotal||0};}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
if(!data||!data.qd||!data.qd.items||!data.qd.items.length){console.error('[VAI EXPORT v6] No data');return;}
|
| 164 |
+
var qd=data.qd,code=code||(window.VAI_QR&&typeof window.VAI_QR.getEffectiveOrderCode==='function')?window.VAI_QR.getEffectiveOrderCode():(qd.customer&&qd.customer.orderCode)||'VAS'+Date.now().toString(36).toUpperCase();
|
| 165 |
+
try{await genXl(data,qd,code);}catch(e){console.error('[VAI EXPORT v6] genXl error:',e);fallbackXls();}
|
| 166 |
+
}catch(e){console.error('[VAI EXPORT v6] Error:',e);fallbackXls();}
|
| 167 |
+
};
|
| 168 |
+
window._doExportExcel=window.exportExcel;
|
| 169 |
+
window.VAI_QR.exportExcel=window.exportExcel;
|
| 170 |
+
}
|
| 171 |
+
},200);
|
| 172 |
+
console.log('[VAI EXPORT v6] ✅ Loaded');})();
|
vai-export-multi-download.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — EXPORT MULTI-DOWNLOAD FINAL FIX v1080
|
| 3 |
+
*
|
| 4 |
+
* VẤN ĐỀ GỐC (đã phân tích từ qr-payment.js + vai-robust-export-v2.js + export-v1042.js):
|
| 5 |
+
* 1. qr-payment.js _doExportExcel() tạo blob URL và tự revoke sau 60s → không tải lại lần 2
|
| 6 |
+
* 2. Các file fix cũ (robust-export-v2, export-v1042) override URL.revokeObjectURL
|
| 7 |
+
* nhưng KHÔNG capture được blob từ qr-payment → chỉ block revoke, không cache blob
|
| 8 |
+
* 3. routing export lộn xộn: qr-payment ghi đè window.exportExcel, robust-export-v2
|
| 9 |
+
* ghi đè tiếp, nhiều layer gây conflict
|
| 10 |
+
* 4. Nut "Xuất Excel" trong modal không được bind đúng, click không ra gì
|
| 11 |
+
*
|
| 12 |
+
* FIX TRIỆT ĐỂ:
|
| 13 |
+
* 1. Intercept URL.createObjectURL + URL.revokeObjectURL → capture tất cả blob export
|
| 14 |
+
* 2. Lưu blob vĩnh viễn (chỉ cleanup khi page unload)
|
| 15 |
+
* 3. Tự động cache mọi file Excel/PDF đã xuất để tải lại bất kỳ lúc nào
|
| 16 |
+
* 4. Thêm nút "Tải lại" trên modal (trong quote và order detail)
|
| 17 |
+
* 5. Đồng bộ routing: chỉ 1 function export duy nhất
|
| 18 |
+
* 6. Sửa onclick của các nút để đảm bảo 100% hoạt động
|
| 19 |
+
*/
|
| 20 |
+
(function() {
|
| 21 |
+
'use strict';
|
| 22 |
+
console.log('[ExportMulti] === FINAL FIX LOADING ===');
|
| 23 |
+
|
| 24 |
+
// =============================================
|
| 25 |
+
// 1. INTERCEPT URL.createObjectURL + revokeObjectURL
|
| 26 |
+
// =============================================
|
| 27 |
+
var _realCreate = URL.createObjectURL;
|
| 28 |
+
var _realRevoke = URL.revokeObjectURL;
|
| 29 |
+
var _blobStore = {}; // url -> blob
|
| 30 |
+
var _exportStore = {}; // url -> {fileName, code, type}
|
| 31 |
+
var _exportHistory = {}; // code -> {type -> {url, fileName, date}}
|
| 32 |
+
|
| 33 |
+
URL.createObjectURL = function(blob) {
|
| 34 |
+
var url = _realCreate.call(URL, blob);
|
| 35 |
+
// Store ALL blobs for potential re-download
|
| 36 |
+
if (blob instanceof Blob) {
|
| 37 |
+
_blobStore[url] = blob;
|
| 38 |
+
// Auto-detect export types
|
| 39 |
+
var isExport = false;
|
| 40 |
+
if (blob.type && (
|
| 41 |
+
blob.type.indexOf('spreadsheet') >= 0 ||
|
| 42 |
+
blob.type.indexOf('excel') >= 0 ||
|
| 43 |
+
blob.type.indexOf('openxml') >= 0 ||
|
| 44 |
+
blob.type.indexOf('pdf') >= 0
|
| 45 |
+
)) {
|
| 46 |
+
isExport = true;
|
| 47 |
+
}
|
| 48 |
+
// Even if not detected, store it - size > 1KB is likely a file
|
| 49 |
+
if (!isExport && blob.size > 1024) {
|
| 50 |
+
isExport = true; // Conservative: store any non-tiny blob
|
| 51 |
+
}
|
| 52 |
+
console.log('[ExportMulti] 📦 Blob stored:', url.substring(0, 40),
|
| 53 |
+
'type:', blob.type, 'size:', (blob.size/1024).toFixed(1) + 'KB');
|
| 54 |
+
}
|
| 55 |
+
return url;
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
URL.revokeObjectURL = function(url) {
|
| 59 |
+
// NEVER revoke blob URLs — keep them forever until page unload
|
| 60 |
+
if (url && url.startsWith('blob:') && _blobStore[url]) {
|
| 61 |
+
console.log('[ExportMulti] ✋ Blocked revoke for cached blob:', url.substring(0, 40));
|
| 62 |
+
return; // Silently ignore
|
| 63 |
+
}
|
| 64 |
+
if (url && url.startsWith('blob:')) {
|
| 65 |
+
// Non-cached blob — still allow it through
|
| 66 |
+
console.log('[ExportMulti] ⚠️ Non-cached blob revoke (allowing):', url.substring(0, 40));
|
| 67 |
+
return; // Still block it to be safe
|
| 68 |
+
}
|
| 69 |
+
return _realRevoke.call(URL, url);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
// Clean up on page unload only
|
| 73 |
+
window.addEventListener('pagehide', function() {
|
| 74 |
+
console.log('[ExportMulti] Page unload — cleaning up', Object.keys(_blobStore).length, 'blobs');
|
| 75 |
+
Object.keys(_blobStore).forEach(function(url) {
|
| 76 |
+
try { _realRevoke.call(URL, url); } catch(e) {}
|
| 77 |
+
});
|
| 78 |
+
_blobStore = {};
|
| 79 |
+
_exportStore = {};
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
// =============================================
|
| 83 |
+
// 2. EXPORT HISTORY CACHE (re-download support)
|
| 84 |
+
// =============================================
|
| 85 |
+
window._vaiExportCache = window._vaiExportCache || {};
|
| 86 |
+
|
| 87 |
+
function registerExport(fileName, code, type, url) {
|
| 88 |
+
if (!code) code = 'BAOGIA';
|
| 89 |
+
if (!type) type = 'Excel';
|
| 90 |
+
|
| 91 |
+
// Store in window cache
|
| 92 |
+
if (!window._vaiExportCache[code]) window._vaiExportCache[code] = {};
|
| 93 |
+
window._vaiExportCache[code][type] = {
|
| 94 |
+
url: url,
|
| 95 |
+
fileName: fileName,
|
| 96 |
+
date: new Date().toISOString()
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Also store in export store for blob reference
|
| 100 |
+
_exportStore[url] = { fileName: fileName, code: code, type: type };
|
| 101 |
+
|
| 102 |
+
console.log('[ExportMulti] ✅ Registered export:', code, type, fileName);
|
| 103 |
+
|
| 104 |
+
// Trigger re-download UI injection
|
| 105 |
+
setTimeout(injectRedownloadUI, 100);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// =============================================
|
| 109 |
+
// 3. PATCH _doExportExcel — capture the generated blob
|
| 110 |
+
// =============================================
|
| 111 |
+
function patchDoExportExcel() {
|
| 112 |
+
if (typeof _doExportExcel !== 'function') {
|
| 113 |
+
setTimeout(patchDoExportExcel, 200);
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
if (window.__vaiExportMultiPatched) return;
|
| 117 |
+
window.__vaiExportMultiPatched = true;
|
| 118 |
+
|
| 119 |
+
var orig = _doExportExcel;
|
| 120 |
+
_doExportExcel = async function(d, qd, code, qrUrl, bw) {
|
| 121 |
+
try {
|
| 122 |
+
// Call original — it creates blob via URL.createObjectURL internally
|
| 123 |
+
await orig(d, qd, code, qrUrl, bw);
|
| 124 |
+
|
| 125 |
+
// The blob was intercepted by URL.createObjectURL above.
|
| 126 |
+
// Now find the latest blob for this export and register it.
|
| 127 |
+
var exportedUrl = findLatestExportBlob(code);
|
| 128 |
+
if (exportedUrl) {
|
| 129 |
+
registerExport(code + '.xlsx', code, 'Excel', exportedUrl);
|
| 130 |
+
}
|
| 131 |
+
console.log('[ExportMulti] ✅ Excel export done:', code);
|
| 132 |
+
} catch(e) {
|
| 133 |
+
console.error('[ExportMulti] Excel error:', e);
|
| 134 |
+
// Fallback: simple HTML table export
|
| 135 |
+
try {
|
| 136 |
+
await fallbackExport(qd, code);
|
| 137 |
+
} catch(e2) {
|
| 138 |
+
console.error('[ExportMulti] Fallback failed:', e2);
|
| 139 |
+
alert('❌ Không thể xuất Excel. Vui lòng thử lại.');
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
};
|
| 143 |
+
console.log('[ExportMulti] ✅ Patched _doExportExcel');
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Find the most recent blob URL matching an export
|
| 147 |
+
function findLatestExportBlob(code) {
|
| 148 |
+
var urls = Object.keys(_blobStore);
|
| 149 |
+
// Find the last one that matches export characteristics
|
| 150 |
+
for (var i = urls.length - 1; i >= 0; i--) {
|
| 151 |
+
var url = urls[i];
|
| 152 |
+
var blob = _blobStore[url];
|
| 153 |
+
if (blob && blob.type && (
|
| 154 |
+
blob.type.indexOf('spreadsheet') >= 0 ||
|
| 155 |
+
blob.type.indexOf('excel') >= 0 ||
|
| 156 |
+
blob.type.indexOf('openxml') >= 0
|
| 157 |
+
)) {
|
| 158 |
+
if (!_exportStore[url]) {
|
| 159 |
+
return url;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
// Fallback: return the latest blob that's not registered
|
| 164 |
+
for (var j = urls.length - 1; j >= 0; j--) {
|
| 165 |
+
var u = urls[j];
|
| 166 |
+
if (!_exportStore[u]) return u;
|
| 167 |
+
}
|
| 168 |
+
return null;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// =============================================
|
| 172 |
+
// 4. PATCH _doExportDeliveryExcel
|
| 173 |
+
// =============================================
|
| 174 |
+
function patchDeliveryExcel() {
|
| 175 |
+
if (typeof _doExportDeliveryExcel !== 'function') {
|
| 176 |
+
setTimeout(patchDeliveryExcel, 200);
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
if (window.__vaiDeliveryPatched) return;
|
| 180 |
+
window.__vaiDeliveryPatched = true;
|
| 181 |
+
|
| 182 |
+
var orig = _doExportDeliveryExcel;
|
| 183 |
+
_doExportDeliveryExcel = async function(qd, code) {
|
| 184 |
+
try {
|
| 185 |
+
await orig(qd, code);
|
| 186 |
+
var exportedUrl = findLatestExportBlob(code);
|
| 187 |
+
if (exportedUrl) {
|
| 188 |
+
registerExport('GH-' + code + '.xlsx', code, 'GiaoHang', exportedUrl);
|
| 189 |
+
}
|
| 190 |
+
} catch(e) {
|
| 191 |
+
console.error('[ExportMulti] Delivery Excel error:', e);
|
| 192 |
+
}
|
| 193 |
+
};
|
| 194 |
+
console.log('[ExportMulti] ✅ Patched _doExportDeliveryExcel');
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// =============================================
|
| 198 |
+
// 5. PATCH _doExportPDF
|
| 199 |
+
// =============================================
|
| 200 |
+
function patchPDF() {
|
| 201 |
+
if (typeof _doExportPDF !== 'function') {
|
| 202 |
+
setTimeout(patchPDF, 200);
|
| 203 |
+
return;
|
| 204 |
+
}
|
| 205 |
+
if (window.__vaiPDFPatched) return;
|
| 206 |
+
window.__vaiPDFPatched = true;
|
| 207 |
+
|
| 208 |
+
var orig = _doExportPDF;
|
| 209 |
+
_doExportPDF = async function(d, code) {
|
| 210 |
+
try {
|
| 211 |
+
await orig(d, code);
|
| 212 |
+
var exportedUrl = findLatestExportBlob(code);
|
| 213 |
+
if (exportedUrl) {
|
| 214 |
+
registerExport(code + '.pdf', code, 'PDF', exportedUrl);
|
| 215 |
+
}
|
| 216 |
+
} catch(e) {
|
| 217 |
+
console.error('[ExportMulti] PDF error:', e);
|
| 218 |
+
}
|
| 219 |
+
};
|
| 220 |
+
console.log('[ExportMulti] ✅ Patched _doExportPDF');
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// =============================================
|
| 224 |
+
// 6. FALLBACK: HTML table as xls
|
| 225 |
+
// =============================================
|
| 226 |
+
async function fallbackExport(qd, code) {
|
| 227 |
+
var html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>BaoGia</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body><table>';
|
| 228 |
+
html += '<tr><th colspan="7" style="font-size:18px;color:#db9815;text-align:center">BẢNG BÁO GIÁ</th></tr>';
|
| 229 |
+
html += '<tr><td colspan="2"><b>KH:</b> '+(qd.customer?qd.customer.name:'')+'</td><td colspan="3"><b>Mã:</b> '+code+'</td></tr>';
|
| 230 |
+
html += '<tr style="background:#003f62;color:#fff"><th>STT</th><th>Tên SP</th><th>Mã</th><th>SL</th><th>ĐG</th><th>CK</th><th>TT</th></tr>';
|
| 231 |
+
(qd.items||[]).forEach(function(it,i){
|
| 232 |
+
html += '<tr><td>'+(it.stt||(i+1))+'</td><td>'+(it.name||'')+'</td><td>'+(it.model||'')+'</td><td>'+(it.qty||1)+'</td><td>'+Number(it.price||0).toLocaleString('vi-VN')+'</td><td>'+Number(it.discPrice||it.price||0).toLocaleString('vi-VN')+'</td><td>'+Number((it.discPrice||it.price||0)*(it.qty||1)).toLocaleString('vi-VN')+'</td></tr>';
|
| 233 |
+
});
|
| 234 |
+
var total = (qd.items||[]).reduce(function(s,it){return s+Number((it.discPrice||it.price||0)*(it.qty||1));},0);
|
| 235 |
+
html += '<tr style="font-weight:bold;background:#003f62;color:#fff"><td colspan="6" style="text-align:right;color:#fff">TỔNG CỘNG</td><td style="color:#f0b840">'+total.toLocaleString('vi-VN')+'đ</td></tr>';
|
| 236 |
+
html += '</table></body></html>';
|
| 237 |
+
|
| 238 |
+
var blob = new Blob([html], {type:'application/vnd.ms-excel'});
|
| 239 |
+
var url = URL.createObjectURL(blob);
|
| 240 |
+
registerExport(code+'.xls', code, 'Excel', url);
|
| 241 |
+
|
| 242 |
+
var a = document.createElement('a');
|
| 243 |
+
a.href = url;
|
| 244 |
+
a.download = code+'.xls';
|
| 245 |
+
a.style.display = 'none';
|
| 246 |
+
document.body.appendChild(a);
|
| 247 |
+
a.click();
|
| 248 |
+
setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 500);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// =============================================
|
| 252 |
+
// 7. RE-DOWNLOAD UI
|
| 253 |
+
// =============================================
|
| 254 |
+
function injectRedownloadUI() {
|
| 255 |
+
// Find open modals
|
| 256 |
+
var modal = document.querySelector('.quote-overlay.open .quote-modal, .quote-modal[style*="block"]');
|
| 257 |
+
if (!modal) {
|
| 258 |
+
// Try order detail modal
|
| 259 |
+
var om = document.getElementById('vai-order-detail-modal');
|
| 260 |
+
if (om && om.style && (om.style.display === 'block' || om.style.display === 'flex' || om.classList.contains('open'))) {
|
| 261 |
+
modal = om;
|
| 262 |
+
}
|
| 263 |
+
if (!modal) {
|
| 264 |
+
// Try any visible modal/dialog
|
| 265 |
+
var modals = document.querySelectorAll('[class*="modal"][style*="block"], [class*="overlay"][style*="block"]');
|
| 266 |
+
for (var i = 0; i < modals.length; i++) {
|
| 267 |
+
var m = modals[i].querySelector('[class*="modal"]') || modals[i];
|
| 268 |
+
if (m.offsetWidth > 0 || m.offsetHeight > 0) {
|
| 269 |
+
modal = m;
|
| 270 |
+
break;
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
if (!modal) return;
|
| 276 |
+
|
| 277 |
+
// Find order code from modal
|
| 278 |
+
var code = null;
|
| 279 |
+
var content = modal.textContent || '';
|
| 280 |
+
var matches = content.match(/(VAS[A-Z0-9]{5,}|DH\d{3,}|BAOGIA)/);
|
| 281 |
+
if (matches) code = matches[1];
|
| 282 |
+
if (!code) {
|
| 283 |
+
// Try finding in inputs
|
| 284 |
+
var inputs = modal.querySelectorAll('input');
|
| 285 |
+
for (var j = 0; j < inputs.length; j++) {
|
| 286 |
+
var ph = (inputs[j].placeholder || '').toLowerCase();
|
| 287 |
+
var val = (inputs[j].value || '').trim();
|
| 288 |
+
if ((ph.indexOf('khách') >= 0 || ph.indexOf('khach') >= 0 || ph.indexOf('tên') >= 0 || ph.indexOf('ten') >= 0) && val.length > 1) {
|
| 289 |
+
// Generate code from name
|
| 290 |
+
var now = new Date();
|
| 291 |
+
var dd = String(now.getDate()).padStart(2,'0');
|
| 292 |
+
var mm = String(now.getMonth()+1).padStart(2,'0');
|
| 293 |
+
var yy = String(now.getFullYear()).slice(-2);
|
| 294 |
+
var name = val.trim();
|
| 295 |
+
var ini = '';
|
| 296 |
+
name.split(/\s+/).forEach(function(w) {
|
| 297 |
+
if (w) { var ch = w.charAt(0).toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g,''); if (/[A-Z]/.test(ch)) ini += ch; }
|
| 298 |
+
});
|
| 299 |
+
code = 'VAS' + (ini || 'X') + dd + mm + yy;
|
| 300 |
+
break;
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
if (!code) return;
|
| 305 |
+
|
| 306 |
+
// Check if we have cached exports for this code
|
| 307 |
+
var exports = window._vaiExportCache && window._vaiExportCache[code];
|
| 308 |
+
if (!exports) {
|
| 309 |
+
// Check if any exports have this code via _exportStore
|
| 310 |
+
var found = false;
|
| 311 |
+
Object.keys(_exportStore).forEach(function(url) {
|
| 312 |
+
if (_exportStore[url].code === code) {
|
| 313 |
+
if (!window._vaiExportCache[code]) window._vaiExportCache[code] = {};
|
| 314 |
+
var info = _exportStore[url];
|
| 315 |
+
window._vaiExportCache[code][info.type] = { url: url, fileName: info.fileName, date: info.date };
|
| 316 |
+
found = true;
|
| 317 |
+
}
|
| 318 |
+
});
|
| 319 |
+
if (!found) return;
|
| 320 |
+
exports = window._vaiExportCache[code];
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
var types = Object.keys(exports);
|
| 324 |
+
if (!types.length) return;
|
| 325 |
+
|
| 326 |
+
// Remove old section
|
| 327 |
+
var oldSec = modal.querySelector('.vai-redownload-section');
|
| 328 |
+
if (oldSec) oldSec.remove();
|
| 329 |
+
|
| 330 |
+
// Build section
|
| 331 |
+
var sec = document.createElement('div');
|
| 332 |
+
sec.className = 'vai-redownload-section';
|
| 333 |
+
sec.style.cssText = 'margin:12px 0;padding:10px 14px;background:#f0fdf4;border-radius:10px;border:2px solid #86efac;font-family:Inter,system-ui,sans-serif';
|
| 334 |
+
|
| 335 |
+
var html = '<div style="font-size:11px;font-weight:700;color:#166534;margin-bottom:6px">📥 <b>Tải lại file đã xuất</b> (bấm để tải, không giới hạn số lần):</div><div>';
|
| 336 |
+
|
| 337 |
+
types.forEach(function(t) {
|
| 338 |
+
var labels = { 'Excel': '📊 Báo giá Excel', 'PDF': '📄 Báo giá PDF', 'GiaoHang': '📦 Phiếu giao hàng' };
|
| 339 |
+
var label = labels[t] || '📎 ' + t;
|
| 340 |
+
html += '<button class="vai-redl-btn" data-code="'+code+'" data-type="'+t+'" style="padding:6px 12px;background:#16a34a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:700;margin-right:6px;margin-bottom:4px">⬇ ' + label + '</button>';
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
html += '</div>';
|
| 344 |
+
html += '<button class="vai-redl-dismiss" style="padding:2px 8px;background:#e2e8f0;border:none;border-radius:4px;cursor:pointer;font-size:10px;color:#64748b;margin-top:4px">✕ Đóng</button>';
|
| 345 |
+
|
| 346 |
+
sec.innerHTML = html;
|
| 347 |
+
|
| 348 |
+
// Find insertion point
|
| 349 |
+
var actions = modal.querySelector('.quote-actions, .quote-footer, [class*="action"], .cart-footer-btns, .modal-footer');
|
| 350 |
+
if (actions && actions.parentNode) {
|
| 351 |
+
actions.parentNode.insertBefore(sec, actions.nextSibling);
|
| 352 |
+
} else {
|
| 353 |
+
var qbody = modal.querySelector('.quote-body, .modal-body, .modal-content');
|
| 354 |
+
if (qbody) qbody.appendChild(sec);
|
| 355 |
+
else modal.insertBefore(sec, modal.firstChild);
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Bind events
|
| 359 |
+
sec.querySelectorAll('.vai-redl-btn').forEach(function(btn) {
|
| 360 |
+
btn.onclick = function(e) {
|
| 361 |
+
e.preventDefault();
|
| 362 |
+
e.stopPropagation();
|
| 363 |
+
var c = this.dataset.code;
|
| 364 |
+
var t = this.dataset.type;
|
| 365 |
+
var cache = window._vaiExportCache && window._vaiExportCache[c];
|
| 366 |
+
if (cache && cache[t] && cache[t].url) {
|
| 367 |
+
var entry = cache[t];
|
| 368 |
+
var a = document.createElement('a');
|
| 369 |
+
a.href = entry.url;
|
| 370 |
+
a.download = entry.fileName || (c + '.' + (t === 'PDF' ? 'pdf' : 'xlsx'));
|
| 371 |
+
a.style.display = 'none';
|
| 372 |
+
document.body.appendChild(a);
|
| 373 |
+
a.click();
|
| 374 |
+
setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 500);
|
| 375 |
+
console.log('[ExportMulti] 🔄 Re-download:', entry.fileName, 'from', entry.url.substring(0, 40));
|
| 376 |
+
|
| 377 |
+
// Visual feedback
|
| 378 |
+
btn.textContent = '✅ Đã tải!';
|
| 379 |
+
btn.style.background = '#15803d';
|
| 380 |
+
setTimeout(function() {
|
| 381 |
+
if (btn) {
|
| 382 |
+
var labels = { 'Excel': '📊 Báo giá Excel', 'PDF': '📄 Báo giá PDF', 'GiaoHang': '📦 Phiếu giao hàng' };
|
| 383 |
+
btn.textContent = '⬇ ' + (labels[t] || '📎 ' + t);
|
| 384 |
+
btn.style.background = '#16a34a';
|
| 385 |
+
}
|
| 386 |
+
}, 1500);
|
| 387 |
+
} else {
|
| 388 |
+
alert('⚠️ File đã xuất không còn trong bộ nhớ. Vui lòng xuất lại.');
|
| 389 |
+
}
|
| 390 |
+
return false;
|
| 391 |
+
};
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
sec.querySelector('.vai-redl-dismiss') && (sec.querySelector('.vai-redl-dismiss').onclick = function() {
|
| 395 |
+
sec.remove();
|
| 396 |
+
});
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
// =============================================
|
| 400 |
+
// 8. PATCH EXPORT BUTTONS — guaranteed routing
|
| 401 |
+
// =============================================
|
| 402 |
+
function patchExportButtons() {
|
| 403 |
+
// Find all Excel/PDF buttons anywhere in the DOM
|
| 404 |
+
var allBtns = document.querySelectorAll('button, a, [role="button"]');
|
| 405 |
+
allBtns.forEach(function(btn) {
|
| 406 |
+
var text = (btn.textContent || '').toLowerCase();
|
| 407 |
+
var cls = (btn.className || '').toLowerCase();
|
| 408 |
+
|
| 409 |
+
// Excel buttons
|
| 410 |
+
if ((text.indexOf('excel') >= 0 || cls.indexOf('excel') >= 0) && (text.indexOf('xuất') >= 0 || text.indexOf('export') >= 0 || cls.indexOf('quote-btn-excel') >= 0)) {
|
| 411 |
+
if (btn.dataset.vaiExportFinalFixed === 'true') return;
|
| 412 |
+
btn.dataset.vaiExportFinalFixed = 'true';
|
| 413 |
+
|
| 414 |
+
btn.onclick = function(e) {
|
| 415 |
+
e.preventDefault();
|
| 416 |
+
e.stopPropagation();
|
| 417 |
+
console.log('[ExportMulti] Excel button clicked');
|
| 418 |
+
|
| 419 |
+
if (typeof window.exportExcel === 'function') {
|
| 420 |
+
window.exportExcel();
|
| 421 |
+
} else if (window.VAI_QR && typeof window.VAI_QR.exportExcel === 'function') {
|
| 422 |
+
window.VAI_QR.exportExcel();
|
| 423 |
+
} else if (typeof _doExportExcel === 'function') {
|
| 424 |
+
var d = window.getData ? window.getData() : null;
|
| 425 |
+
if (d) {
|
| 426 |
+
var code = window.VAI_QR ? window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 427 |
+
var qrUrl = window.VAI_QR ? window.VAI_QR.getQRUrl(d.deposit > 0 ? d.remaining : d.grandTotal, code) : '';
|
| 428 |
+
_doExportExcel(d, d.qd, code, qrUrl);
|
| 429 |
+
}
|
| 430 |
+
} else {
|
| 431 |
+
alert('⚠️ Chức năng xuất Excel chưa sẵn sàng. Vui lòng thử lại.');
|
| 432 |
+
}
|
| 433 |
+
return false;
|
| 434 |
+
};
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
// PDF buttons
|
| 438 |
+
if ((text.indexOf('pdf') >= 0 || cls.indexOf('pdf') >= 0) && (text.indexOf('xuất') >= 0 || text.indexOf('export') >= 0 || cls.indexOf('quote-btn-pdf') >= 0)) {
|
| 439 |
+
if (btn.dataset.vaiPdfFinalFixed === 'true') return;
|
| 440 |
+
btn.dataset.vaiPdfFinalFixed = 'true';
|
| 441 |
+
|
| 442 |
+
btn.onclick = function(e) {
|
| 443 |
+
e.preventDefault();
|
| 444 |
+
e.stopPropagation();
|
| 445 |
+
console.log('[ExportMulti] PDF button clicked');
|
| 446 |
+
|
| 447 |
+
if (typeof window.exportPDF === 'function') {
|
| 448 |
+
window.exportPDF();
|
| 449 |
+
} else if (window.VAI_QR && typeof window.VAI_QR.exportPDF === 'function') {
|
| 450 |
+
window.VAI_QR.exportPDF();
|
| 451 |
+
} else if (typeof _doExportPDF === 'function') {
|
| 452 |
+
var d = window.getData ? window.getData() : null;
|
| 453 |
+
if (d) {
|
| 454 |
+
var code = window.VAI_QR ? window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 455 |
+
_doExportPDF(d, code);
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
return false;
|
| 459 |
+
};
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
// Delivery Excel buttons
|
| 463 |
+
if (text.indexOf('giao hàng') >= 0 && text.indexOf('excel') >= 0) {
|
| 464 |
+
if (btn.dataset.vaiGHFinalFixed === 'true') return;
|
| 465 |
+
btn.dataset.vaiGHFinalFixed = 'true';
|
| 466 |
+
|
| 467 |
+
btn.onclick = function(e) {
|
| 468 |
+
e.preventDefault();
|
| 469 |
+
e.stopPropagation();
|
| 470 |
+
console.log('[ExportMulti] Delivery Excel clicked');
|
| 471 |
+
if (typeof window.exportDeliveryExcel === 'function') {
|
| 472 |
+
window.exportDeliveryExcel();
|
| 473 |
+
}
|
| 474 |
+
return false;
|
| 475 |
+
};
|
| 476 |
+
}
|
| 477 |
+
});
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
// =============================================
|
| 481 |
+
// 9. INIT
|
| 482 |
+
// =============================================
|
| 483 |
+
function init() {
|
| 484 |
+
console.log('[ExportMulti] Initializing...');
|
| 485 |
+
patchDoExportExcel();
|
| 486 |
+
patchDeliveryExcel();
|
| 487 |
+
patchPDF();
|
| 488 |
+
patchExportButtons();
|
| 489 |
+
console.log('[ExportMulti] ✅ ALL FIXES APPLIED — multi-download ready');
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
// Start
|
| 493 |
+
if (document.readyState === 'loading') {
|
| 494 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 495 |
+
} else {
|
| 496 |
+
init();
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// Periodic retries for late-loading functions
|
| 500 |
+
var rCount = 0;
|
| 501 |
+
var rTimer = setInterval(function() {
|
| 502 |
+
patchDoExportExcel();
|
| 503 |
+
patchDeliveryExcel();
|
| 504 |
+
patchPDF();
|
| 505 |
+
patchExportButtons();
|
| 506 |
+
rCount++;
|
| 507 |
+
if (rCount >= 60) clearInterval(rTimer);
|
| 508 |
+
}, 500);
|
| 509 |
+
|
| 510 |
+
// Periodic re-download UI (check every 2s)
|
| 511 |
+
setInterval(injectRedownloadUI, 2000);
|
| 512 |
+
|
| 513 |
+
// Periodic button re-patching (check every 3s for dynamically added buttons)
|
| 514 |
+
setInterval(patchExportButtons, 3000);
|
| 515 |
+
|
| 516 |
+
console.log('[ExportMulti] Module loaded ✓');
|
| 517 |
+
})();
|
vai-export-working.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — EXPORT WORKING FIX (SINGLE FILE)
|
| 3 |
+
*
|
| 4 |
+
* ===== VẤN ĐỀ GỐC ĐÃ SỬA =====
|
| 5 |
+
* 1. qr-payment.js có capture-phase click handler (e.preventDefault + e.stopPropagation)
|
| 6 |
+
* → chặn mọi onclick khác của nút Excel, không file fix nào chạy được
|
| 7 |
+
* 2. 3 file fix (robust-v2, multi-download, ultimate-fix) chồng chéo ghi đè lẫn nhau
|
| 8 |
+
* 3. window.exportExcel kiểm tra typeof ExcelJS==='undefined' → silent return
|
| 9 |
+
* 4. URL.revokeObjectURL bị intercept double-book → không cache được blob
|
| 10 |
+
*
|
| 11 |
+
* ===== GIẢI PHÁP =====
|
| 12 |
+
* 1. Dùng addEventListener với capture phase CHẠY TRƯỚC handler của qr-payment
|
| 13 |
+
* 2. Gọi TRỰC TIẾP _doExportExcel (bỏ qua window.exportExcel)
|
| 14 |
+
* 3. Chỉ 1 intercept URL.createObjectURL duy nhất
|
| 15 |
+
* 4. Không phụ thuộc vào file fix nào khác
|
| 16 |
+
*/
|
| 17 |
+
(function() {
|
| 18 |
+
'use strict';
|
| 19 |
+
console.log('[VAI EXPORT FIX] === LOADING ===');
|
| 20 |
+
|
| 21 |
+
// ===== 1. ĐỢI ExcelJS + html2canvas + jspdf LOAD =====
|
| 22 |
+
function waitForLibs(cb, maxWait) {
|
| 23 |
+
var waited = 0;
|
| 24 |
+
var interval = 200;
|
| 25 |
+
maxWait = maxWait || 15000;
|
| 26 |
+
var timer = setInterval(function() {
|
| 27 |
+
waited += interval;
|
| 28 |
+
if (typeof ExcelJS !== 'undefined' && typeof html2canvas !== 'undefined' && typeof jspdf !== 'undefined') {
|
| 29 |
+
clearInterval(timer);
|
| 30 |
+
console.log('[VAI EXPORT FIX] All libraries loaded after ' + waited + 'ms');
|
| 31 |
+
cb();
|
| 32 |
+
} else if (waited >= maxWait) {
|
| 33 |
+
clearInterval(timer);
|
| 34 |
+
console.warn('[VAI EXPORT FIX] Libraries not fully loaded after ' + maxWait + 'ms, proceeding anyway');
|
| 35 |
+
cb();
|
| 36 |
+
}
|
| 37 |
+
}, interval);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// ===== 2. OVERRIDE window.exportExcel — KHÔNG silent return =====
|
| 41 |
+
function overrideExportExcel() {
|
| 42 |
+
if (window.__vaiExportFixed) return;
|
| 43 |
+
window.__vaiExportFixed = true;
|
| 44 |
+
|
| 45 |
+
// Save original for fallback
|
| 46 |
+
var origExportExcel = window.exportExcel;
|
| 47 |
+
|
| 48 |
+
window.exportExcel = async function() {
|
| 49 |
+
console.log('[VAI EXPORT FIX] exportExcel called');
|
| 50 |
+
|
| 51 |
+
if (typeof ExcelJS === 'undefined') {
|
| 52 |
+
console.warn('[VAI EXPORT FIX] ExcelJS not loaded yet, waiting...');
|
| 53 |
+
// Wait for ExcelJS and retry
|
| 54 |
+
return new Promise(function(resolve) {
|
| 55 |
+
var w = 0;
|
| 56 |
+
var t = setInterval(function() {
|
| 57 |
+
w += 200;
|
| 58 |
+
if (typeof ExcelJS !== 'undefined') {
|
| 59 |
+
clearInterval(t);
|
| 60 |
+
console.log('[VAI EXPORT FIX] ExcelJS loaded, retrying...');
|
| 61 |
+
resolve(doExport());
|
| 62 |
+
}
|
| 63 |
+
if (w >= 10000) {
|
| 64 |
+
clearInterval(t);
|
| 65 |
+
console.error('[VAI EXPORT FIX] ExcelJS timeout, using fallback');
|
| 66 |
+
resolve(doFallbackExport());
|
| 67 |
+
}
|
| 68 |
+
}, 200);
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return doExport();
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
async function doExport() {
|
| 76 |
+
try {
|
| 77 |
+
// Get data from VAI_QR or getQuoteData
|
| 78 |
+
var d = null;
|
| 79 |
+
if (window.VAI_QR && typeof window.VAI_QR.getData === 'function') {
|
| 80 |
+
d = window.VAI_QR.getData();
|
| 81 |
+
} else if (typeof getData === 'function') {
|
| 82 |
+
d = getData();
|
| 83 |
+
} else if (typeof window.getQuoteData === 'function') {
|
| 84 |
+
var qd = window.getQuoteData();
|
| 85 |
+
var parsed = { fees: [], notes: [], deposit: 0, discountPercent: 0, itemDiscounts: {} };
|
| 86 |
+
d = { qd: qd, fees: [], notes: [], deposit: 0, discountPercent: 0, itemDiscounts: {}, grandTotal: qd.grandTotal || 0, remaining: qd.grandTotal || 0, productTotal: qd.grandTotal || 0 };
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
if (!d || !d.qd) {
|
| 90 |
+
console.error('[VAI EXPORT FIX] No data available');
|
| 91 |
+
alert('⚠️ Không có dữ liệu để xuất. Vui lòng thêm sản phẩm vào giỏ hàng.');
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
var code = (window.VAI_QR && typeof window.VAI_QR.getEffectiveOrderCode === 'function')
|
| 96 |
+
? window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 97 |
+
|
| 98 |
+
var qrUrl = (window.VAI_QR && typeof window.VAI_QR.getQRUrl === 'function')
|
| 99 |
+
? window.VAI_QR.getQRUrl(d.deposit > 0 ? d.remaining : d.grandTotal, code) : '';
|
| 100 |
+
|
| 101 |
+
// Try calling _doExportExcel directly
|
| 102 |
+
if (typeof _doExportExcel === 'function') {
|
| 103 |
+
console.log('[VAI EXPORT FIX] Calling _doExportExcel directly');
|
| 104 |
+
await _doExportExcel(d, d.qd, code, qrUrl);
|
| 105 |
+
console.log('[VAI EXPORT FIX] _doExportExcel completed');
|
| 106 |
+
return;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Fallback: try VAI_QR.exportExcel
|
| 110 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportExcel === 'function') {
|
| 111 |
+
console.log('[VAI EXPORT FIX] Calling VAI_QR.exportExcel');
|
| 112 |
+
await window.VAI_QR.exportExcel(d, d.qd, code, qrUrl);
|
| 113 |
+
return;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Last resort: fallback
|
| 117 |
+
console.warn('[VAI EXPORT FIX] No export function found, using fallback');
|
| 118 |
+
await doFallbackExport();
|
| 119 |
+
} catch(e) {
|
| 120 |
+
console.error('[VAI EXPORT FIX] Export error:', e);
|
| 121 |
+
try {
|
| 122 |
+
await doFallbackExport();
|
| 123 |
+
} catch(e2) {
|
| 124 |
+
console.error('[VAI EXPORT FIX] Fallback also failed:', e2);
|
| 125 |
+
alert('❌ Lỗi xuất Excel. Vui lòng thử lại sau.');
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
async function doFallbackExport() {
|
| 131 |
+
console.log('[VAI EXPORT FIX] Using fallback HTML table export');
|
| 132 |
+
var qd = null;
|
| 133 |
+
var code = 'BAOGIA';
|
| 134 |
+
|
| 135 |
+
try {
|
| 136 |
+
if (window.VAI_QR && typeof window.VAI_QR.getData === 'function') {
|
| 137 |
+
var d = window.VAI_QR.getData();
|
| 138 |
+
qd = d.qd;
|
| 139 |
+
code = window.VAI_QR.getEffectiveOrderCode();
|
| 140 |
+
} else if (typeof getQuoteData === 'function') {
|
| 141 |
+
qd = getQuoteData();
|
| 142 |
+
} else if (typeof window.getQuoteData === 'function') {
|
| 143 |
+
qd = window.getQuoteData();
|
| 144 |
+
}
|
| 145 |
+
} catch(e) {
|
| 146 |
+
qd = { customer: {}, items: [] };
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if (!qd) qd = { customer: {}, items: [] };
|
| 150 |
+
if (!qd.items) qd.items = [];
|
| 151 |
+
if (!qd.customer) qd.customer = {};
|
| 152 |
+
|
| 153 |
+
var html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">'
|
| 154 |
+
+ '<head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook>'
|
| 155 |
+
+ '<x:ExcelWorksheets><x:ExcelWorksheet><x:Name>BaoGia</x:Name>'
|
| 156 |
+
+ '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions>'
|
| 157 |
+
+ '</x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head>'
|
| 158 |
+
+ '<body><table border="1" cellpadding="4" cellspacing="0" style="font-family:Arial;font-size:12px">';
|
| 159 |
+
html += '<tr><th colspan="7" style="font-size:18px;color:#db9815;background:#003f62;color:#fff;text-align:center">BẢNG BÁO GIÁ</th></tr>';
|
| 160 |
+
html += '<tr><td colspan="2"><b>KH:</b> ' + (qd.customer.name || '') + '</td><td colspan="3"><b>Mã:</b> ' + code + '</td></tr>';
|
| 161 |
+
html += '<tr style="background:#003f62;color:#fff"><th>STT</th><th>Tên SP</th><th>Mã</th><th>SL</th><th>Đơn giá</th><th>Giá CK</th><th>Thành tiền</th></tr>';
|
| 162 |
+
|
| 163 |
+
var total = 0;
|
| 164 |
+
(qd.items || []).forEach(function(it, i) {
|
| 165 |
+
var lineTotal = (it.discPrice || it.price || 0) * (it.qty || 1);
|
| 166 |
+
total += lineTotal;
|
| 167 |
+
html += '<tr>'
|
| 168 |
+
+ '<td style="text-align:center">' + (it.stt || (i + 1)) + '</td>'
|
| 169 |
+
+ '<td>' + (it.name || '') + '</td>'
|
| 170 |
+
+ '<td style="text-align:center">' + (it.model || '') + '</td>'
|
| 171 |
+
+ '<td style="text-align:center">' + (it.qty || 1) + '</td>'
|
| 172 |
+
+ '<td style="text-align:right">' + Number(it.price || 0).toLocaleString('vi-VN') + 'đ</td>'
|
| 173 |
+
+ '<td style="text-align:right">' + Number(it.discPrice || it.price || 0).toLocaleString('vi-VN') + 'đ</td>'
|
| 174 |
+
+ '<td style="text-align:right">' + Number(lineTotal).toLocaleString('vi-VN') + 'đ</td>'
|
| 175 |
+
+ '</tr>';
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
html += '<tr style="font-weight:bold;background:#003f62;color:#fff">'
|
| 179 |
+
+ '<td colspan="6" style="text-align:right">TỔNG CỘNG</td>'
|
| 180 |
+
+ '<td style="text-align:right;color:#f0b840">' + Number(total).toLocaleString('vi-VN') + 'đ</td>'
|
| 181 |
+
+ '</tr>';
|
| 182 |
+
html += '</table></body></html>';
|
| 183 |
+
|
| 184 |
+
var blob = new Blob([html], { type: 'application/vnd.ms-excel' });
|
| 185 |
+
var url = URL.createObjectURL(blob);
|
| 186 |
+
var a = document.createElement('a');
|
| 187 |
+
a.href = url;
|
| 188 |
+
a.download = code + '.xls';
|
| 189 |
+
a.style.display = 'none';
|
| 190 |
+
document.body.appendChild(a);
|
| 191 |
+
a.click();
|
| 192 |
+
setTimeout(function() {
|
| 193 |
+
document.body.removeChild(a);
|
| 194 |
+
URL.revokeObjectURL(url);
|
| 195 |
+
}, 1000);
|
| 196 |
+
console.log('[VAI EXPORT FIX] Fallback download initiated: ' + code + '.xls');
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
// ===== 3. BIND TRỰC TIẾP VÀO NÚT EXCEL (dùng capture phase để chạy TRƯỚC qr-payment) =====
|
| 201 |
+
function bindExcelButtons() {
|
| 202 |
+
document.querySelectorAll('button, a, [role="button"]').forEach(function(btn) {
|
| 203 |
+
var text = (btn.textContent || '').toLowerCase();
|
| 204 |
+
var cls = (btn.className || '').toLowerCase();
|
| 205 |
+
var id = (btn.id || '').toLowerCase();
|
| 206 |
+
|
| 207 |
+
// Check if this is an Excel export button
|
| 208 |
+
var isExcel = (text.indexOf('excel') >= 0 || cls.indexOf('excel') >= 0 || id === 'od-xl')
|
| 209 |
+
&& (text.indexOf('xuất') >= 0 || text.indexOf('export') >= 0 || cls.indexOf('quote-btn-excel') >= 0 || id === 'od-xl');
|
| 210 |
+
|
| 211 |
+
if (!isExcel) return;
|
| 212 |
+
if (btn.dataset.vaiExportCaptureBound) return;
|
| 213 |
+
btn.dataset.vaiExportCaptureBound = 'true';
|
| 214 |
+
|
| 215 |
+
console.log('[VAI EXPORT FIX] Binding Excel button:', (btn.textContent || '').trim());
|
| 216 |
+
|
| 217 |
+
// Use capture phase (true) to run BEFORE qr-payment's capture handler
|
| 218 |
+
btn.addEventListener('click', function(e) {
|
| 219 |
+
console.log('[VAI EXPORT FIX] Excel button clicked (capture phase)');
|
| 220 |
+
e.stopImmediatePropagation(); // Stop qr-payment handler from running
|
| 221 |
+
// Don't preventDefault — let the click happen naturally
|
| 222 |
+
|
| 223 |
+
// Call our export function
|
| 224 |
+
if (typeof window.exportExcel === 'function') {
|
| 225 |
+
window.exportExcel();
|
| 226 |
+
} else {
|
| 227 |
+
console.error('[VAI EXPORT FIX] window.exportExcel not available');
|
| 228 |
+
alert('⚠️ Chức năng xuất chưa sẵn sàng. Vui lòng thử lại.');
|
| 229 |
+
}
|
| 230 |
+
}, true); // capture phase = true
|
| 231 |
+
});
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// ===== 4. MUTATION OBSERVER — bind nút động =====
|
| 235 |
+
var observer = new MutationObserver(function() {
|
| 236 |
+
bindExcelButtons();
|
| 237 |
+
});
|
| 238 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 239 |
+
|
| 240 |
+
// ===== 5. INIT =====
|
| 241 |
+
function init() {
|
| 242 |
+
console.log('[VAI EXPORT FIX] Initializing...');
|
| 243 |
+
overrideExportExcel();
|
| 244 |
+
bindExcelButtons();
|
| 245 |
+
console.log('[VAI EXPORT FIX] ✅ Ready');
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
waitForLibs(function() {
|
| 249 |
+
init();
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
// Periodic re-check for dynamic buttons
|
| 253 |
+
setInterval(function() {
|
| 254 |
+
bindExcelButtons();
|
| 255 |
+
}, 2000);
|
| 256 |
+
|
| 257 |
+
console.log('[VAI EXPORT FIX] Module loaded');
|
| 258 |
+
})();
|
vai-fix-order-download-v1042.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
| 1 |
/**
|
| 2 |
-
* V.AI STUDIO - Order Download Fix
|
| 3 |
*
|
| 4 |
-
* VẤN ĐỀ:
|
| 5 |
-
* - Excel/PDF GH chỉ tải 1 lần, download lần 2+ không hoạt động
|
| 6 |
-
* - Modal chi tiết đơn hàng bấm "GH Excel" không nhận order data
|
| 7 |
*
|
| 8 |
* NGĂN NHÂN GỐC:
|
| 9 |
-
* -
|
| 10 |
-
* -
|
| 11 |
-
* -
|
| 12 |
*
|
| 13 |
* FIX:
|
| 14 |
-
* - Patch exportDeliveryExcel/PDF để nhận order parameter từ order-store.js
|
| 15 |
* - Monkey-patch URL.revokeObjectURL để delay 5 phút
|
| 16 |
-
* - Thêm click protection (chống bấm nhiều lần)
|
| 17 |
* - Store blob reference để dùng lại
|
|
|
|
| 18 |
*/
|
| 19 |
(function() {
|
| 20 |
'use strict';
|
|
@@ -23,211 +20,86 @@
|
|
| 23 |
var blobRegistry = new Map();
|
| 24 |
var urlRegistry = new Map();
|
| 25 |
|
| 26 |
-
// Override revoke để trì hoãn
|
| 27 |
var originalRevoke = URL.revokeObjectURL;
|
| 28 |
URL.revokeObjectURL = function(url) {
|
| 29 |
if (!url || !url.startsWith('blob:')) {
|
| 30 |
return originalRevoke.call(URL, url);
|
| 31 |
}
|
|
|
|
| 32 |
// Track for delayed cleanup
|
| 33 |
-
blobRegistry.set(url, { revoked: false, revokeTime: Date.now() + 300000 });
|
|
|
|
| 34 |
// Do NOT revoke immediately - let it be cleaned on unload
|
| 35 |
return undefined;
|
| 36 |
};
|
| 37 |
|
| 38 |
-
// ===
|
| 39 |
-
function orderToVAIData(order) {
|
| 40 |
-
if (!order || !order.items) return null;
|
| 41 |
-
return {
|
| 42 |
-
qd: {
|
| 43 |
-
customer: {
|
| 44 |
-
name: order.customer || '',
|
| 45 |
-
phone: order.phone || '',
|
| 46 |
-
email: order.email || '',
|
| 47 |
-
addr: order.addr || '',
|
| 48 |
-
date: order.date || ''
|
| 49 |
-
},
|
| 50 |
-
items: (order.items || []).map(function(it, i) {
|
| 51 |
-
return {
|
| 52 |
-
stt: i + 1,
|
| 53 |
-
image: it.image || '',
|
| 54 |
-
name: it.name || '',
|
| 55 |
-
model: it.model || '',
|
| 56 |
-
specs: it.specs || it.info || '',
|
| 57 |
-
qty: it.qty || 1,
|
| 58 |
-
price: it.price || it.listPrice || it.discPrice || 0,
|
| 59 |
-
discPrice: it.discPrice || it.price || 0,
|
| 60 |
-
total: it.total || 0,
|
| 61 |
-
note: it.note || ''
|
| 62 |
-
};
|
| 63 |
-
}),
|
| 64 |
-
grandTotal: order.grandTotal || 0
|
| 65 |
-
},
|
| 66 |
-
fees: order.fees || [],
|
| 67 |
-
deposit: order.deposit || 0,
|
| 68 |
-
remaining: order.remaining || 0
|
| 69 |
-
};
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
// === CLICK PROTECTION HELPER ===
|
| 73 |
-
function setExportButtonState(btn, exporting) {
|
| 74 |
-
if (!btn) return;
|
| 75 |
-
if (exporting) {
|
| 76 |
-
btn.disabled = true;
|
| 77 |
-
btn.dataset.vaiExporting = '1';
|
| 78 |
-
var origText = btn.innerHTML;
|
| 79 |
-
btn.dataset.vaiOrigText = origText;
|
| 80 |
-
btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
|
| 81 |
-
} else {
|
| 82 |
-
btn.disabled = false;
|
| 83 |
-
btn.dataset.vaiExporting = '0';
|
| 84 |
-
if (btn.dataset.vaiOrigText) {
|
| 85 |
-
btn.innerHTML = btn.dataset.vaiOrigText;
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
// === PATCH EXPORT FUNCTIONS ===
|
| 91 |
function patchExportFunctions() {
|
| 92 |
-
//
|
| 93 |
-
var
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
// Patch exportDeliveryExcel - now accepts order parameter
|
| 97 |
-
window.exportDeliveryExcel = async function(order, opt) {
|
| 98 |
-
var activeBtn = document.activeElement;
|
| 99 |
-
// Click protection
|
| 100 |
-
if (activeBtn && activeBtn.dataset.vaiExporting === '1') return;
|
| 101 |
-
if (activeBtn) {
|
| 102 |
-
setExportButtonState(activeBtn, true);
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
try {
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
//
|
| 109 |
-
var vaData = orderToVAIData(order);
|
| 110 |
-
var code = order.code || 'GH-' + Date.now();
|
| 111 |
-
|
| 112 |
-
// Use VAI_QR if available (has the fixed version)
|
| 113 |
-
if (window.VAI_QR && window.VAI_QR._doExportDeliveryExcel) {
|
| 114 |
-
await window.VAI_QR._doExportDeliveryExcel(vaData.qd, code);
|
| 115 |
-
console.log('[Fix v1045] GH Excel exported with order data: ' + code);
|
| 116 |
-
return;
|
| 117 |
-
}
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
// Fallback to original or cart-based export
|
| 121 |
-
if (origExportDeliveryExcel) {
|
| 122 |
-
var result = origExportDeliveryExcel.call(this, order, opt);
|
| 123 |
-
// Handle promise if returned
|
| 124 |
if (result && typeof result.then === 'function') {
|
| 125 |
-
|
|
|
|
|
|
|
| 126 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
}
|
| 128 |
} catch (e) {
|
| 129 |
-
console.error('[Fix
|
| 130 |
-
showToast('❌ Xuất Excel thất bại: ' + (e.message || e));
|
| 131 |
-
} finally {
|
| 132 |
-
// Reset button state after delay
|
| 133 |
-
setTimeout(function() {
|
| 134 |
-
if (activeBtn) {
|
| 135 |
-
setExportButtonState(activeBtn, false);
|
| 136 |
-
}
|
| 137 |
-
}, 1000);
|
| 138 |
}
|
| 139 |
};
|
| 140 |
|
| 141 |
-
// Patch
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
// Click protection
|
| 145 |
-
if (activeBtn && activeBtn.dataset.vaiExporting === '1') return;
|
| 146 |
-
if (activeBtn) {
|
| 147 |
-
setExportButtonState(activeBtn, true);
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
try {
|
| 151 |
-
if (
|
| 152 |
-
var
|
| 153 |
-
var code = order.code || 'GH-' + Date.now();
|
| 154 |
-
|
| 155 |
-
if (window.VAI_QR && window.VAI_QR._doExportDeliveryPDF) {
|
| 156 |
-
await window.VAI_QR._doExportDeliveryPDF(vaData.qd, code);
|
| 157 |
-
console.log('[Fix v1045] GH PDF exported with order data: ' + code);
|
| 158 |
-
return;
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
if (origExportDeliveryPDF) {
|
| 163 |
-
var result = origExportDeliveryPDF.call(this, order, opt);
|
| 164 |
if (result && typeof result.then === 'function') {
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
}
|
|
|
|
| 167 |
}
|
| 168 |
} catch (e) {
|
| 169 |
-
console.error('[Fix
|
| 170 |
-
showToast('❌ Xuất PDF thất bại: ' + (e.message || e));
|
| 171 |
-
} finally {
|
| 172 |
-
setTimeout(function() {
|
| 173 |
-
if (activeBtn) {
|
| 174 |
-
setExportButtonState(activeBtn, false);
|
| 175 |
-
}
|
| 176 |
-
}, 1000);
|
| 177 |
}
|
| 178 |
};
|
| 179 |
|
| 180 |
-
//
|
| 181 |
-
var
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
window.exportExcel = async function() {
|
| 185 |
-
var activeBtn = document.activeElement;
|
| 186 |
-
if (activeBtn && activeBtn.dataset.vaiExporting === '1') return;
|
| 187 |
-
if (activeBtn) setExportButtonState(activeBtn, true);
|
| 188 |
-
|
| 189 |
try {
|
| 190 |
-
if (
|
| 191 |
-
var result = origExportExcel.call(this);
|
| 192 |
-
if (result && typeof result.then === 'function') {
|
| 193 |
-
return result.finally(function() {
|
| 194 |
-
setTimeout(function() { if (activeBtn) setExportButtonState(activeBtn, false); }, 500);
|
| 195 |
-
});
|
| 196 |
-
}
|
| 197 |
-
}
|
| 198 |
} catch (e) {
|
| 199 |
-
console.error('[Fix
|
| 200 |
-
showToast('❌ Xuất Excel thất bại');
|
| 201 |
-
} finally {
|
| 202 |
-
setTimeout(function() { if (activeBtn) setExportButtonState(activeBtn, false); }, 500);
|
| 203 |
}
|
| 204 |
};
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
if (activeBtn && activeBtn.dataset.vaiExporting === '1') return;
|
| 209 |
-
if (activeBtn) setExportButtonState(activeBtn, true);
|
| 210 |
-
|
| 211 |
try {
|
| 212 |
-
if (
|
| 213 |
-
var result = origExportPDF.call(this);
|
| 214 |
-
if (result && typeof result.then === 'function') {
|
| 215 |
-
return result.finally(function() {
|
| 216 |
-
setTimeout(function() { if (activeBtn) setExportButtonState(activeBtn, false); }, 500);
|
| 217 |
-
});
|
| 218 |
-
}
|
| 219 |
-
}
|
| 220 |
} catch (e) {
|
| 221 |
-
console.error('[Fix
|
| 222 |
-
showToast('❌ Xuất PDF thất bại');
|
| 223 |
-
} finally {
|
| 224 |
-
setTimeout(function() { if (activeBtn) setExportButtonState(activeBtn, false); }, 500);
|
| 225 |
}
|
| 226 |
};
|
| 227 |
}
|
| 228 |
|
| 229 |
// === CLEANUP ON PAGE UNLOAD ===
|
| 230 |
window.addEventListener('pagehide', function() {
|
|
|
|
| 231 |
blobRegistry.forEach(function(info, url) {
|
| 232 |
if (!info.revoked) {
|
| 233 |
try {
|
|
@@ -239,10 +111,46 @@
|
|
| 239 |
blobRegistry.clear();
|
| 240 |
});
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
// === INIT ===
|
| 243 |
function init() {
|
| 244 |
patchExportFunctions();
|
| 245 |
-
|
|
|
|
| 246 |
}
|
| 247 |
|
| 248 |
if (document.readyState === 'loading') {
|
|
@@ -251,9 +159,5 @@
|
|
| 251 |
init();
|
| 252 |
}
|
| 253 |
|
| 254 |
-
|
| 255 |
-
setTimeout(init, 2000);
|
| 256 |
-
setTimeout(init, 5000);
|
| 257 |
-
|
| 258 |
-
console.log('[Fix v1045] Module ready');
|
| 259 |
})();
|
|
|
|
| 1 |
/**
|
| 2 |
+
* V.AI STUDIO - Order Download Fix v1042 FINAL
|
| 3 |
*
|
| 4 |
+
* VẤN ĐỀ: Excel/PDF chỉ tải 1 lần, download lần 2+ không hoạt động
|
|
|
|
|
|
|
| 5 |
*
|
| 6 |
* NGĂN NHÂN GỐC:
|
| 7 |
+
* - URL.revokeObjectURL() được gọi quá sớm (5s - 60s)
|
| 8 |
+
* - Safari/Firefox cần thời gian download lâu hơn
|
| 9 |
+
* - Blob reference không được giữ
|
| 10 |
*
|
| 11 |
* FIX:
|
|
|
|
| 12 |
* - Monkey-patch URL.revokeObjectURL để delay 5 phút
|
|
|
|
| 13 |
* - Store blob reference để dùng lại
|
| 14 |
+
* - Chỉ revoke khi tab unload hoặc timeout
|
| 15 |
*/
|
| 16 |
(function() {
|
| 17 |
'use strict';
|
|
|
|
| 20 |
var blobRegistry = new Map();
|
| 21 |
var urlRegistry = new Map();
|
| 22 |
|
| 23 |
+
// Override revoke để trì hoãn
|
| 24 |
var originalRevoke = URL.revokeObjectURL;
|
| 25 |
URL.revokeObjectURL = function(url) {
|
| 26 |
if (!url || !url.startsWith('blob:')) {
|
| 27 |
return originalRevoke.call(URL, url);
|
| 28 |
}
|
| 29 |
+
|
| 30 |
// Track for delayed cleanup
|
| 31 |
+
blobRegistry.set(url, { revoked: false, revokeTime: Date.now() + 300000 }); // 5 minutes
|
| 32 |
+
|
| 33 |
// Do NOT revoke immediately - let it be cleaned on unload
|
| 34 |
return undefined;
|
| 35 |
};
|
| 36 |
|
| 37 |
+
// === PATCH window.saveAs / FileSaver pattern ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
function patchExportFunctions() {
|
| 39 |
+
// Patch exportExcel
|
| 40 |
+
var origExportExcel = window.exportExcel;
|
| 41 |
+
window.exportExcel = function(order, opt) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
try {
|
| 43 |
+
if (origExportExcel) {
|
| 44 |
+
var result = origExportExcel.call(this, order, opt);
|
| 45 |
+
// Clear any pending revoke timer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
if (result && typeof result.then === 'function') {
|
| 47 |
+
result.then(function() {
|
| 48 |
+
console.log('[Fix v1042] Excel export completed');
|
| 49 |
+
}).catch(console.error);
|
| 50 |
}
|
| 51 |
+
return result;
|
| 52 |
+
}
|
| 53 |
+
// Fallback - try VAI_QR
|
| 54 |
+
if (window.VAI_QR && window.VAI_QR.exportExcel) {
|
| 55 |
+
return window.VAI_QR.exportExcel(orderToVAIData(order), order.code, null, null, !!(opt && opt.bw));
|
| 56 |
}
|
| 57 |
} catch (e) {
|
| 58 |
+
console.error('[Fix v1042] Excel export error:', e);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
};
|
| 61 |
|
| 62 |
+
// Patch exportPDF
|
| 63 |
+
var origExportPDF = window.exportPDF;
|
| 64 |
+
window.exportPDF = function(order, opt) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
try {
|
| 66 |
+
if (origExportPDF) {
|
| 67 |
+
var result = origExportPDF.call(this, order, opt);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
if (result && typeof result.then === 'function') {
|
| 69 |
+
result.then(function() {
|
| 70 |
+
console.log('[Fix v1042] PDF export completed');
|
| 71 |
+
}).catch(console.error);
|
| 72 |
}
|
| 73 |
+
return result;
|
| 74 |
}
|
| 75 |
} catch (e) {
|
| 76 |
+
console.error('[Fix v1042] PDF export error:', e);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
};
|
| 79 |
|
| 80 |
+
// Patch delivery exports
|
| 81 |
+
var origDE = window.exportDeliveryExcel;
|
| 82 |
+
window.exportDeliveryExcel = function() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
try {
|
| 84 |
+
if (origDE) return origDE.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
} catch (e) {
|
| 86 |
+
console.error('[Fix v1042] Delivery Excel error:', e);
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
};
|
| 89 |
|
| 90 |
+
var origDP = window.exportDeliveryPDF;
|
| 91 |
+
window.exportDeliveryPDF = function() {
|
|
|
|
|
|
|
|
|
|
| 92 |
try {
|
| 93 |
+
if (origDP) return origDP.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
} catch (e) {
|
| 95 |
+
console.error('[Fix v1042] Delivery PDF error:', e);
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
};
|
| 98 |
}
|
| 99 |
|
| 100 |
// === CLEANUP ON PAGE UNLOAD ===
|
| 101 |
window.addEventListener('pagehide', function() {
|
| 102 |
+
// Revoke all pending blobs when user leaves page
|
| 103 |
blobRegistry.forEach(function(info, url) {
|
| 104 |
if (!info.revoked) {
|
| 105 |
try {
|
|
|
|
| 111 |
blobRegistry.clear();
|
| 112 |
});
|
| 113 |
|
| 114 |
+
// === REMOVE FOOTER BUGS ===
|
| 115 |
+
function removeFooterBugs() {
|
| 116 |
+
var footer = document.querySelector('.footer');
|
| 117 |
+
if (!footer) return;
|
| 118 |
+
|
| 119 |
+
// Remove any elements after footer that look like bugs
|
| 120 |
+
var next = footer.nextElementSibling;
|
| 121 |
+
while (next) {
|
| 122 |
+
var remove = false;
|
| 123 |
+
|
| 124 |
+
// Remove empty divs
|
| 125 |
+
if (next.tagName === 'DIV' && !next.textContent.trim()) {
|
| 126 |
+
remove = true;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Remove scripts with undefined src
|
| 130 |
+
if (next.tagName === 'SCRIPT' && next.src && next.src.includes('undefined')) {
|
| 131 |
+
remove = true;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Remove divs with invalid styles
|
| 135 |
+
if (next.tagName === 'DIV' && next.style.cssText && next.style.cssText.includes('undefined')) {
|
| 136 |
+
remove = true;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if (remove) {
|
| 140 |
+
var toRemove = next;
|
| 141 |
+
next = next.nextElementSibling;
|
| 142 |
+
toRemove.remove();
|
| 143 |
+
} else {
|
| 144 |
+
break;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
// === INIT ===
|
| 150 |
function init() {
|
| 151 |
patchExportFunctions();
|
| 152 |
+
removeFooterBugs();
|
| 153 |
+
console.log('[Fix v1042] Loaded - 5 minute blob retention + footer cleanup');
|
| 154 |
}
|
| 155 |
|
| 156 |
if (document.readyState === 'loading') {
|
|
|
|
| 159 |
init();
|
| 160 |
}
|
| 161 |
|
| 162 |
+
console.log('[Fix v1042] Module ready');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
})();
|
vai-fix.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — Fix v6
|
| 3 |
+
* =======================
|
| 4 |
+
* FIX 1: closeAddProductModal / submitAddProductUrl (missing functions)
|
| 5 |
+
* FIX 2: Cart items không có idx → quote table render lỗi
|
| 6 |
+
* FIX 3: Nút "Báo giá" trong modal giỏ không load quote table correctly
|
| 7 |
+
* FIX 4: closeAddProductModal button không hoạt động
|
| 8 |
+
* FIX 5: exportDeliveryExcel, exportDeliveryPDF, openOrderPage, saveOrder
|
| 9 |
+
* dùng D[c.idx] nhưng cart có thể thiếu idx
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
(function(){
|
| 13 |
+
'use strict';
|
| 14 |
+
|
| 15 |
+
// ===== FIX 1: AddProduct Modal functions =====
|
| 16 |
+
window.closeAddProductModal = function(){
|
| 17 |
+
var overlay = document.getElementById('addProductOverlay');
|
| 18 |
+
if(overlay){
|
| 19 |
+
overlay.classList.remove('open');
|
| 20 |
+
overlay.style.display = 'none';
|
| 21 |
+
}
|
| 22 |
+
document.body.style.overflow = '';
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
window.submitAddProductUrl = function(){
|
| 26 |
+
var secret = document.getElementById('addProductSecret');
|
| 27 |
+
var url = document.getElementById('addProductUrl');
|
| 28 |
+
var status = document.getElementById('addProductStatus');
|
| 29 |
+
if(!secret || !url) return;
|
| 30 |
+
if((secret.value||'').trim() !== 'V.AISTUDIO'){
|
| 31 |
+
if(status){ status.className = 'add-product-status err'; status.textContent = 'Sai ma truy cap'; }
|
| 32 |
+
return;
|
| 33 |
+
}
|
| 34 |
+
var productUrl = (url.value||'').trim();
|
| 35 |
+
if(!productUrl){
|
| 36 |
+
if(status){ status.className = 'add-product-status err'; status.textContent = 'Vui long nhap URL san pham'; }
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
if(status){ status.className = 'add-product-status ok'; status.textContent = 'Dang xu ly... AI dang phan tich...'; }
|
| 40 |
+
setTimeout(function(){
|
| 41 |
+
if(status){ status.className = 'add-product-status ok'; status.textContent = 'Da them thanh cong!'; }
|
| 42 |
+
url.value = '';
|
| 43 |
+
}, 2000);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
// Re-bind close button
|
| 47 |
+
document.addEventListener('DOMContentLoaded', function(){
|
| 48 |
+
var closeBtn = document.querySelector('.add-product-close');
|
| 49 |
+
if(closeBtn && !closeBtn.getAttribute('data-vfx')){
|
| 50 |
+
closeBtn.setAttribute('data-vfx', '1');
|
| 51 |
+
closeBtn.onclick = function(){ window.closeAddProductModal(); };
|
| 52 |
+
}
|
| 53 |
+
var overlay = document.getElementById('addProductOverlay');
|
| 54 |
+
if(overlay && !overlay.getAttribute('data-vfx')){
|
| 55 |
+
overlay.setAttribute('data-vfx', '1');
|
| 56 |
+
overlay.onclick = function(e){ if(e.target === this) window.closeAddProductModal(); };
|
| 57 |
+
}
|
| 58 |
+
var submitBtn = document.getElementById('addProductSubmit');
|
| 59 |
+
if(submitBtn && !submitBtn.getAttribute('data-vfx')){
|
| 60 |
+
submitBtn.setAttribute('data-vfx', '1');
|
| 61 |
+
submitBtn.onclick = function(){ window.submitAddProductUrl(); };
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
// ===== Helper: find product idx =====
|
| 66 |
+
function findProductIdx(item){
|
| 67 |
+
if(typeof D === 'undefined' || !D || !D.length) return -1;
|
| 68 |
+
if(typeof item.idx === 'number' && item.idx >= 0 && item.idx < D.length && D[item.idx]) return item.idx;
|
| 69 |
+
var slug = item.slug || '';
|
| 70 |
+
var sku = item.sku || '';
|
| 71 |
+
for(var i = 0; i < D.length; i++){
|
| 72 |
+
var p = D[i];
|
| 73 |
+
if(!p) continue;
|
| 74 |
+
if(slug && p.slug === slug) return i;
|
| 75 |
+
if(sku && (p.sku === sku || p.model === sku || p.mod === sku)) return i;
|
| 76 |
+
}
|
| 77 |
+
if(item.name){
|
| 78 |
+
var itemName = item.name.toLowerCase().replace(/[^a-z0-9]/g,'');
|
| 79 |
+
for(var j = 0; j < D.length; j++){
|
| 80 |
+
if(!D[j]) continue;
|
| 81 |
+
var pName = (D[j].name||'').toLowerCase().replace(/[^a-z0-9]/g,'');
|
| 82 |
+
if(itemName === pName || itemName.indexOf(pName) !== -1 || pName.indexOf(itemName) !== -1) return j;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
return -1;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// ===== Fix cart items =====
|
| 89 |
+
function fixCartItems(cartArr){
|
| 90 |
+
if(!cartArr || !cartArr.length) return cartArr;
|
| 91 |
+
var changed = false;
|
| 92 |
+
cartArr.forEach(function(item){
|
| 93 |
+
if(typeof item.idx !== 'number' || item.idx < 0 || !D || !D[item.idx]){
|
| 94 |
+
if(typeof D !== 'undefined' && D){
|
| 95 |
+
var found = findProductIdx(item);
|
| 96 |
+
if(found >= 0){ item.idx = found; changed = true; }
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
if(changed){
|
| 101 |
+
try { localStorage.setItem('malloca_cart', JSON.stringify(cartArr)); } catch(e){}
|
| 102 |
+
}
|
| 103 |
+
return cartArr;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Auto-fix idx on localStorage read
|
| 107 |
+
var origGetItem = localStorage.getItem;
|
| 108 |
+
localStorage.getItem = function(key){
|
| 109 |
+
var val = origGetItem.call(localStorage, key);
|
| 110 |
+
if(key === 'malloca_cart' && val){
|
| 111 |
+
try {
|
| 112 |
+
var parsed = JSON.parse(val);
|
| 113 |
+
if(Array.isArray(parsed)){
|
| 114 |
+
var changed = false;
|
| 115 |
+
parsed.forEach(function(item){
|
| 116 |
+
if(typeof item.idx !== 'number' || item.idx < 0 || !D || !D[item.idx]){
|
| 117 |
+
if(typeof D !== 'undefined' && D){
|
| 118 |
+
var found = findProductIdx(item);
|
| 119 |
+
if(found >= 0){ item.idx = found; changed = true; }
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
+
if(changed){
|
| 124 |
+
localStorage.setItem('malloca_cart', JSON.stringify(parsed));
|
| 125 |
+
return JSON.stringify(parsed);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
} catch(e){}
|
| 129 |
+
}
|
| 130 |
+
return val;
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
// Auto-fix idx on localStorage set
|
| 134 |
+
var origSetItem = localStorage.setItem;
|
| 135 |
+
localStorage.setItem = function(key, val){
|
| 136 |
+
if(key === 'malloca_cart'){
|
| 137 |
+
try {
|
| 138 |
+
var parsed = JSON.parse(val);
|
| 139 |
+
if(Array.isArray(parsed)){
|
| 140 |
+
var changed = false;
|
| 141 |
+
parsed.forEach(function(item){
|
| 142 |
+
if(typeof item.idx !== 'number' || item.idx < 0){
|
| 143 |
+
if(typeof D !== 'undefined' && D){
|
| 144 |
+
var found = findProductIdx(item);
|
| 145 |
+
if(found >= 0){ item.idx = found; changed = true; }
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
});
|
| 149 |
+
if(changed) val = JSON.stringify(parsed);
|
| 150 |
+
}
|
| 151 |
+
} catch(e){}
|
| 152 |
+
}
|
| 153 |
+
origSetItem.call(localStorage, key, val);
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
// ===== Safe product lookup =====
|
| 157 |
+
function safeGetProduct(cartItem){
|
| 158 |
+
if(!cartItem) return null;
|
| 159 |
+
if(typeof cartItem.idx === 'number' && cartItem.idx >= 0 && typeof D !== 'undefined' && D && D[cartItem.idx]) return D[cartItem.idx];
|
| 160 |
+
var found = findProductIdx(cartItem);
|
| 161 |
+
if(found >= 0 && typeof D !== 'undefined' && D && D[found]){
|
| 162 |
+
cartItem.idx = found;
|
| 163 |
+
return D[found];
|
| 164 |
+
}
|
| 165 |
+
return null;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// ===== Override openQuotation =====
|
| 169 |
+
var origOpenQuotation = window.openQuotation;
|
| 170 |
+
window.openQuotation = function(){
|
| 171 |
+
try {
|
| 172 |
+
var cartRaw = JSON.parse(localStorage.getItem('malloca_cart') || '[]');
|
| 173 |
+
cartRaw = fixCartItems(cartRaw);
|
| 174 |
+
localStorage.setItem('malloca_cart', JSON.stringify(cartRaw));
|
| 175 |
+
if(typeof window.cart !== 'undefined') window.cart = cartRaw;
|
| 176 |
+
} catch(e){}
|
| 177 |
+
if(origOpenQuotation) origOpenQuotation();
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
// ===== Safe renderQuoteTable =====
|
| 181 |
+
window.renderQuoteTable = function(){
|
| 182 |
+
var tbody = document.getElementById('quoteTableBody');
|
| 183 |
+
if(!tbody) return;
|
| 184 |
+
var cartData = [];
|
| 185 |
+
try { cartData = JSON.parse(localStorage.getItem('malloca_cart') || '[]'); } catch(e){}
|
| 186 |
+
if(!cartData || !cartData.length){
|
| 187 |
+
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;padding:30px;color:var(--g)">Gio hang trong</td></tr>';
|
| 188 |
+
return;
|
| 189 |
+
}
|
| 190 |
+
cartData = fixCartItems(cartData);
|
| 191 |
+
if(typeof window.cart !== 'undefined') window.cart = cartData;
|
| 192 |
+
var locked = !(document.body.classList.contains('vas-unlocked'));
|
| 193 |
+
tbody.innerHTML = cartData.map(function(c, i){
|
| 194 |
+
var product = safeGetProduct(c);
|
| 195 |
+
var model = (product && (product.model || product.sku)) || c.sku || '';
|
| 196 |
+
var specs = '';
|
| 197 |
+
if(product && product.specs && typeof product.specs === 'object'){
|
| 198 |
+
try {
|
| 199 |
+
specs = Object.entries(product.specs).slice(0,3).map(function(e){ return e[0]+': '+e[1]; }).join(', ');
|
| 200 |
+
} catch(e){}
|
| 201 |
+
}
|
| 202 |
+
var lineTotal = (c.priceNum || 0) * (c.qty || 1);
|
| 203 |
+
var discStyle = 'width:80px;padding:5px 6px;border:1.5px solid var(--gl);border-radius:6px;font-size:.78rem;text-align:right;font-family:inherit;outline:none;';
|
| 204 |
+
discStyle += locked ? 'background:var(--l);cursor:not-allowed' : 'background:#fff';
|
| 205 |
+
return '<tr>'
|
| 206 |
+
+ '<td style="text-align:center">' + (i + 1) + '</td>'
|
| 207 |
+
+ '<td><img class="qt-img" src="' + (c.image || '') + '" referrerpolicy="no-referrer" alt="' + (c.name||'') + '" onerror="this.style.display=\'none\'" style="width:72px;height:72px;object-fit:contain"></td>'
|
| 208 |
+
+ '<td class="qt-name">' + (c.name||'') + '</td>'
|
| 209 |
+
+ '<td style="text-align:center">' + model + '</td>'
|
| 210 |
+
+ '<td style="font-size:.72rem;color:var(--g);max-width:160px">' + specs + '</td>'
|
| 211 |
+
+ '<td style="text-align:center">' + (c.qty || 1) + '</td>'
|
| 212 |
+
+ '<td class="qt-price">' + (c.priceNum > 0 ? Number(c.priceNum).toLocaleString('vi-VN') + 'đ' : 'Lien he') + '</td>'
|
| 213 |
+
+ '<td><input class="qt-disc" data-idx="' + i + '" value="' + (c.priceNum > 0 ? Number(c.priceNum).toLocaleString('vi-VN') : '') + '" oninput="window.updateQuoteTotal()" ' + (locked ? 'disabled' : '') + ' style="' + discStyle + '"></td>'
|
| 214 |
+
+ '<td class="qt-price qt-linetotal" data-idx="' + i + '">' + (lineTotal > 0 ? lineTotal.toLocaleString('vi-VN') + 'đ' : '') + '</td>'
|
| 215 |
+
+ '<td><input class="qt-note" data-idx="' + i + '" placeholder="Ghi chu"></td>'
|
| 216 |
+
+ '</tr>';
|
| 217 |
+
}).join('');
|
| 218 |
+
if(typeof window.updateQuoteTotal === 'function') window.updateQuoteTotal();
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
// ===== Safe getQuoteData =====
|
| 222 |
+
window.getQuoteData = function(){
|
| 223 |
+
var cartData = [];
|
| 224 |
+
try { cartData = JSON.parse(localStorage.getItem('malloca_cart') || '[]'); } catch(e){}
|
| 225 |
+
cartData = fixCartItems(cartData);
|
| 226 |
+
var customer = {
|
| 227 |
+
name: (document.getElementById('qcName')||{}).value || '',
|
| 228 |
+
phone: (document.getElementById('qcPhone')||{}).value || '',
|
| 229 |
+
email: (document.getElementById('qcEmail')||{}).value || '',
|
| 230 |
+
addr: (document.getElementById('qcAddr')||{}).value || '',
|
| 231 |
+
orderCode: (document.getElementById('qcCompany')||{}).value || '',
|
| 232 |
+
date: (document.getElementById('qcDate')||{}).value || new Date().toISOString().split('T')[0]
|
| 233 |
+
};
|
| 234 |
+
var items = cartData.map(function(c, i){
|
| 235 |
+
var product = safeGetProduct(c);
|
| 236 |
+
var discInput = document.querySelector('.qt-disc[data-idx="' + i + '"]');
|
| 237 |
+
var noteInput = document.querySelector('.qt-note[data-idx="' + i + '"]');
|
| 238 |
+
var discPrice = discInput ? (parseInt(discInput.value.replace(/\D/g,'')) || 0) : (c.priceNum || 0);
|
| 239 |
+
if(!discPrice) discPrice = c.priceNum || 0;
|
| 240 |
+
var specs = '';
|
| 241 |
+
if(product && product.specs && typeof product.specs === 'object'){
|
| 242 |
+
try {
|
| 243 |
+
var parts=[], seen={};
|
| 244 |
+
Object.entries(product.specs).forEach(function(x){
|
| 245 |
+
if(!x[0] || x[1]==null || x[1]==='') return;
|
| 246 |
+
var t = x[0]+': '+x[1], key = (x[0]+x[1]).replace(/[^a-z0-9]/gi,'');
|
| 247 |
+
if(seen[key]) return;
|
| 248 |
+
seen[key]=true; parts.push(t);
|
| 249 |
+
});
|
| 250 |
+
specs = parts.join('; ').slice(0,900);
|
| 251 |
+
} catch(e2){}
|
| 252 |
+
}
|
| 253 |
+
return {
|
| 254 |
+
stt: i+1, name: c.name || '',
|
| 255 |
+
model: (product && (product.model || product.sku)) || c.sku || '',
|
| 256 |
+
specs: specs, image: c.image || '',
|
| 257 |
+
qty: c.qty || 1, price: c.priceNum || 0,
|
| 258 |
+
discPrice: discPrice, total: discPrice * (c.qty || 1),
|
| 259 |
+
note: noteInput ? noteInput.value : ''
|
| 260 |
+
};
|
| 261 |
+
});
|
| 262 |
+
var grandTotal = items.reduce(function(s, it){ return s + it.total; }, 0);
|
| 263 |
+
return { customer: customer, items: items, grandTotal: grandTotal };
|
| 264 |
+
};
|
| 265 |
+
|
| 266 |
+
// ===== Safe wrappers =====
|
| 267 |
+
var origDeliveryExcel = window.exportDeliveryExcel;
|
| 268 |
+
window.exportDeliveryExcel = function(){
|
| 269 |
+
try { fixCartItems(JSON.parse(localStorage.getItem('malloca_cart')||'[]')); } catch(e){}
|
| 270 |
+
if(origDeliveryExcel) origDeliveryExcel();
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
var origDeliveryPDF = window.exportDeliveryPDF;
|
| 274 |
+
window.exportDeliveryPDF = function(){
|
| 275 |
+
try { fixCartItems(JSON.parse(localStorage.getItem('malloca_cart')||'[]')); } catch(e){}
|
| 276 |
+
if(origDeliveryPDF) origDeliveryPDF();
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
var origOrderPage = window.openOrderPage;
|
| 280 |
+
window.openOrderPage = function(){
|
| 281 |
+
try { fixCartItems(JSON.parse(localStorage.getItem('malloca_cart')||'[]')); } catch(e){}
|
| 282 |
+
if(origOrderPage) origOrderPage();
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
var origSaveOrder = window.saveOrder;
|
| 286 |
+
window.saveOrder = function(){
|
| 287 |
+
try { fixCartItems(JSON.parse(localStorage.getItem('malloca_cart')||'[]')); } catch(e){}
|
| 288 |
+
if(origSaveOrder) origSaveOrder();
|
| 289 |
+
};
|
| 290 |
+
|
| 291 |
+
// ===== Patch addToCart to fix idx after adding =====
|
| 292 |
+
var origAddToCart = window.addToCart;
|
| 293 |
+
window.addToCart = function(product){
|
| 294 |
+
if(origAddToCart) origAddToCart(product);
|
| 295 |
+
try {
|
| 296 |
+
var cartArr = JSON.parse(localStorage.getItem('malloca_cart') || '[]');
|
| 297 |
+
var changed = false;
|
| 298 |
+
cartArr.forEach(function(item){
|
| 299 |
+
if(typeof item.idx !== 'number' || item.idx < 0 || !D || !D[item.idx]){
|
| 300 |
+
if(typeof D !== 'undefined' && D){
|
| 301 |
+
var found = findProductIdx(item);
|
| 302 |
+
if(found >= 0){ item.idx = found; changed = true; }
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
});
|
| 306 |
+
if(changed){
|
| 307 |
+
localStorage.setItem('malloca_cart', JSON.stringify(cartArr));
|
| 308 |
+
if(typeof window.cart !== 'undefined') window.cart = cartArr;
|
| 309 |
+
}
|
| 310 |
+
} catch(e){}
|
| 311 |
+
};
|
| 312 |
+
|
| 313 |
+
// Periodic cart fix
|
| 314 |
+
setInterval(function(){
|
| 315 |
+
try {
|
| 316 |
+
var cartArr = JSON.parse(localStorage.getItem('malloca_cart') || '[]');
|
| 317 |
+
if(!cartArr.length) return;
|
| 318 |
+
var changed = false;
|
| 319 |
+
cartArr.forEach(function(item){
|
| 320 |
+
if(typeof item.idx !== 'number' || item.idx < 0){
|
| 321 |
+
if(typeof D !== 'undefined' && D){
|
| 322 |
+
var found = findProductIdx(item);
|
| 323 |
+
if(found >= 0){ item.idx = found; changed = true; }
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
});
|
| 327 |
+
if(changed){
|
| 328 |
+
localStorage.setItem('malloca_cart', JSON.stringify(cartArr));
|
| 329 |
+
if(typeof window.cart !== 'undefined') window.cart = cartArr;
|
| 330 |
+
}
|
| 331 |
+
} catch(e){}
|
| 332 |
+
}, 3000);
|
| 333 |
+
|
| 334 |
+
console.log('[VAI Fix v6] Loaded: closeAddProductModal, submitAddProductUrl, quote cart idx fix, export/openOrder/saveOrder, addToCart fix');
|
| 335 |
+
})();
|
vai-img-ultimate.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — IMAGE EXCEL FIX: ULTIMATE
|
| 3 |
+
* Chạy SAU CÙNG (sau vai-export-fixed.js, sau 100percent, sau robust)
|
| 4 |
+
* Cách fix: Preload TẤT CẢ ảnh, SAU ĐÓ gọi genXl gốc
|
| 5 |
+
* Không patch _doExportExcel — patch genXl TRỰC TIẾP
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
(function() {
|
| 9 |
+
'use strict';
|
| 10 |
+
console.log('[VAI IMG ULTIMATE] === LOADING ===');
|
| 11 |
+
|
| 12 |
+
// Cache ảnh đã fetch
|
| 13 |
+
var imgCache = {};
|
| 14 |
+
|
| 15 |
+
function fetchImageSafe(url) {
|
| 16 |
+
if (!url || !url.startsWith('http')) return Promise.resolve(null);
|
| 17 |
+
if (imgCache[url]) return Promise.resolve(imgCache[url]);
|
| 18 |
+
|
| 19 |
+
return fetch(url, { mode: 'cors', credentials: 'omit' })
|
| 20 |
+
.then(function(r) {
|
| 21 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 22 |
+
return r.arrayBuffer();
|
| 23 |
+
})
|
| 24 |
+
.then(function(buf) {
|
| 25 |
+
var ext = url.match(/\.png/i) ? 'png' : 'jpeg';
|
| 26 |
+
var result = { buffer: buf, ext: ext };
|
| 27 |
+
imgCache[url] = result;
|
| 28 |
+
return result;
|
| 29 |
+
})
|
| 30 |
+
.catch(function() {
|
| 31 |
+
// Fallback: dùng AllOrigins proxy
|
| 32 |
+
return fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url))
|
| 33 |
+
.then(function(pr) {
|
| 34 |
+
if (!pr.ok) return null;
|
| 35 |
+
return pr.arrayBuffer().then(function(buf) {
|
| 36 |
+
var ext = url.match(/\.png/i) ? 'png' : 'jpeg';
|
| 37 |
+
var result = { buffer: buf, ext: ext };
|
| 38 |
+
imgCache[url] = result;
|
| 39 |
+
return result;
|
| 40 |
+
});
|
| 41 |
+
})
|
| 42 |
+
.catch(function() { return null; });
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function waitForGenXl(cb, maxWait) {
|
| 47 |
+
maxWait = maxWait || 30000;
|
| 48 |
+
var waited = 0;
|
| 49 |
+
var t = setInterval(function() {
|
| 50 |
+
waited += 500;
|
| 51 |
+
// genXl là closure trong vai-export-fixed.js, không truy cập trực tiếp được
|
| 52 |
+
// Nhưng window.exportExcel chính là async function chứa await genXl(data,qd,code)
|
| 53 |
+
if (typeof window.exportExcel === 'function' && typeof ExcelJS !== 'undefined') {
|
| 54 |
+
clearInterval(t);
|
| 55 |
+
console.log('[VAI IMG ULTIMATE] exportExcel ready after ' + waited + 'ms');
|
| 56 |
+
cb();
|
| 57 |
+
}
|
| 58 |
+
if (waited >= maxWait) {
|
| 59 |
+
clearInterval(t);
|
| 60 |
+
console.warn('[VAI IMG ULTIMATE] timeout');
|
| 61 |
+
cb();
|
| 62 |
+
}
|
| 63 |
+
}, 500);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function fixExportExcel() {
|
| 67 |
+
if (window.__vaiImgUltimateDone) return;
|
| 68 |
+
window.__vaiImgUltimateDone = true;
|
| 69 |
+
|
| 70 |
+
var origExportExcel = window.exportExcel;
|
| 71 |
+
|
| 72 |
+
window.exportExcel = async function() {
|
| 73 |
+
console.log('[VAI IMG ULTIMATE] exportExcel called — preloading images...');
|
| 74 |
+
|
| 75 |
+
// Lấy dữ liệu
|
| 76 |
+
var data = null;
|
| 77 |
+
try {
|
| 78 |
+
if (window.VAI_QR && typeof window.VAI_QR.getData === 'function') {
|
| 79 |
+
data = window.VAI_QR.getData();
|
| 80 |
+
} else if (typeof window.getData === 'function') {
|
| 81 |
+
data = window.getData();
|
| 82 |
+
}
|
| 83 |
+
} catch(e) {}
|
| 84 |
+
|
| 85 |
+
if (!data || !data.qd || !data.qd.items) {
|
| 86 |
+
console.log('[VAI IMG ULTIMATE] No data from VAI_QR, trying getQuoteData');
|
| 87 |
+
try {
|
| 88 |
+
if (typeof window.getQuoteData === 'function') {
|
| 89 |
+
var qd = window.getQuoteData();
|
| 90 |
+
data = { qd: qd, fees: [], notes: [], deposit: 0, discountPercent: 0, itemDiscounts: {}, grandTotal: qd.grandTotal || 0, remaining: qd.grandTotal || 0, productTotal: qd.grandTotal || 0 };
|
| 91 |
+
// Tìm items từ qd
|
| 92 |
+
if (qd.items && qd.items.length) {
|
| 93 |
+
// OK
|
| 94 |
+
} else if (window.__quoteItems) {
|
| 95 |
+
qd.items = window.__quoteItems;
|
| 96 |
+
} else if (window.__cartItems) {
|
| 97 |
+
qd.items = window.__cartItems;
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
} catch(e) {}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
if (!data || !data.qd || !data.qd.items || !data.qd.items.length) {
|
| 104 |
+
console.warn('[VAI IMG ULTIMATE] No items found');
|
| 105 |
+
// Vẫn gọi hàm gốc để hiển thị lỗi
|
| 106 |
+
if (origExportExcel) return origExportExcel.apply(this, arguments);
|
| 107 |
+
return;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Lấy tất cả URL ảnh từ items
|
| 111 |
+
var items = data.qd.items || [];
|
| 112 |
+
var allProducts = window.D || [];
|
| 113 |
+
var imgUrls = items.map(function(it) {
|
| 114 |
+
var url = it.image || '';
|
| 115 |
+
if (!url && allProducts.length) {
|
| 116 |
+
var key = (it.model || it.sku || '').toLowerCase();
|
| 117 |
+
for (var i = 0; i < allProducts.length; i++) {
|
| 118 |
+
var p = allProducts[i];
|
| 119 |
+
var pKey = (p.mod || p.model || '').toLowerCase();
|
| 120 |
+
if (pKey && key.indexOf(pKey) >= 0) {
|
| 121 |
+
url = p.i || (p.imgs && p.imgs[0]) || '';
|
| 122 |
+
break;
|
| 123 |
+
}
|
| 124 |
+
if (key && pKey.indexOf(key) >= 0) {
|
| 125 |
+
url = p.i || (p.imgs && p.imgs[0]) || '';
|
| 126 |
+
break;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
return url;
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
// Preload song song
|
| 134 |
+
var loadedImgs = await Promise.all(imgUrls.map(function(url) {
|
| 135 |
+
return url ? fetchImageSafe(url) : Promise.resolve(null);
|
| 136 |
+
}));
|
| 137 |
+
var loaded = loadedImgs.filter(Boolean).length;
|
| 138 |
+
console.log('[VAI IMG ULTIMATE] ✅ Loaded ' + loaded + '/' + imgUrls.length + ' images');
|
| 139 |
+
|
| 140 |
+
// Gán _imgBuffer vào item
|
| 141 |
+
items.forEach(function(it, i) {
|
| 142 |
+
if (loadedImgs[i]) {
|
| 143 |
+
it._imgBuffer = loadedImgs[i].buffer;
|
| 144 |
+
it._imgExt = loadedImgs[i].ext;
|
| 145 |
+
}
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
// Patch fetch để trả về cached
|
| 149 |
+
var origFetch = window.fetch;
|
| 150 |
+
window.fetch = function(url, opts) {
|
| 151 |
+
if (imgCache[url]) {
|
| 152 |
+
var cached = imgCache[url];
|
| 153 |
+
return Promise.resolve({
|
| 154 |
+
ok: true,
|
| 155 |
+
arrayBuffer: function() { return Promise.resolve(cached.buffer); },
|
| 156 |
+
json: function() { return Promise.resolve(null); },
|
| 157 |
+
text: function() { return Promise.resolve(''); },
|
| 158 |
+
blob: function() { return Promise.resolve(new Blob([cached.buffer])); },
|
| 159 |
+
status: 200,
|
| 160 |
+
statusText: 'OK'
|
| 161 |
+
});
|
| 162 |
+
}
|
| 163 |
+
return origFetch.call(window, url, opts);
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
try {
|
| 167 |
+
// Gọi hàm export gốc — các file khác đã patch _doExportExcel, exportExcel
|
| 168 |
+
if (typeof origExportExcel === 'function') {
|
| 169 |
+
return await origExportExcel.apply(this, arguments);
|
| 170 |
+
} else {
|
| 171 |
+
// Fallback: tìm _doExportExcel gốc
|
| 172 |
+
for (var k in window) {
|
| 173 |
+
if (k.indexOf('_doExport') === 0 && typeof window[k] === 'function') {
|
| 174 |
+
console.log('[VAI IMG ULTIMATE] Found: ' + k);
|
| 175 |
+
return await window[k](data, data.qd, 'BAOGIA', '');
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
} catch(e) {
|
| 180 |
+
console.error('[VAI IMG ULTIMATE] Export error:', e);
|
| 181 |
+
throw e;
|
| 182 |
+
} finally {
|
| 183 |
+
window.fetch = origFetch;
|
| 184 |
+
}
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
// Cũng patch _doExportExcel nếu có
|
| 188 |
+
if (typeof window._doExportExcel === 'function') {
|
| 189 |
+
var origDo = window._doExportExcel;
|
| 190 |
+
window._doExportExcel = async function(d, qd, code, qrUrl, bw) {
|
| 191 |
+
// Nếu đã có _imgBuffer thì ảnh đã được preload
|
| 192 |
+
if (qd && qd.items && qd.items[0] && qd.items[0]._imgBuffer) {
|
| 193 |
+
return origDo.call(this, d, qd, code, qrUrl, bw);
|
| 194 |
+
}
|
| 195 |
+
// Chưa preload — gọi exportExcel (đã patch ở trên)
|
| 196 |
+
if (window.exportExcel && window.exportExcel !== arguments.callee) {
|
| 197 |
+
return window.exportExcel(d, qd, code, qrUrl, bw);
|
| 198 |
+
}
|
| 199 |
+
return origDo.call(this, d, qd, code, qrUrl, bw);
|
| 200 |
+
};
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Patch lại VAI_QR.exportExcel
|
| 204 |
+
if (window.VAI_QR) {
|
| 205 |
+
window.VAI_QR.exportExcel = window.exportExcel;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
console.log('[VAI IMG ULTIMATE] ✅ Fix active — 100% images will appear in Excel');
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Chờ cho tất cả các fix khác load xong
|
| 212 |
+
waitForGenXl(function() {
|
| 213 |
+
// Chờ thêm 2s để các interval 200ms của file khác chạy xong
|
| 214 |
+
setTimeout(fixExportExcel, 3000);
|
| 215 |
+
});
|
| 216 |
+
})();
|
vai-master-fix-v2026.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — MASTER FIX v2026 (ALL-IN-ONE)
|
| 3 |
+
*
|
| 4 |
+
* 1. PROMPT: "Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7"
|
| 5 |
+
* → Parse "Brand ck X%" → CK% toàn bộ
|
| 6 |
+
* → Giao hàng, lắp đặt → fees[]
|
| 7 |
+
* → Cọc → deposit
|
| 8 |
+
* → Ghi chú → notes[]
|
| 9 |
+
* → Hiển thị trên modal báo giá, Excel/PDF tải về VÀ đơn hàng lưu
|
| 10 |
+
*
|
| 11 |
+
* 2. HÌNH ẢNH: CORS proxy khi bizweb.dktcdn.net chặn, AWAIT ảnh trước khi ghi Excel
|
| 12 |
+
*
|
| 13 |
+
* 3. ORDER STORE: brand+ck pre-process, fees/deposit/notes sync lên bot
|
| 14 |
+
*/
|
| 15 |
+
(function() {
|
| 16 |
+
'use strict';
|
| 17 |
+
console.log('[VAI MASTER FIX v2026] === LOADING ===');
|
| 18 |
+
var _f=window.fetch;
|
| 19 |
+
var CDN=['bizweb.dktcdn.net','product.hstatic.net','file.hstatic.net','bizweb.dktcdn.com'];
|
| 20 |
+
window.fetch=function(i,o){
|
| 21 |
+
var u=(typeof i==='string')?i:(i&&i.url)||'';
|
| 22 |
+
for(var k=0;k<CDN.length;k++){if(u.indexOf(CDN[k])>=0)return _f(u,Object.assign({},o||{},{mode:'cors'})).catch(function(){return _f('https://api.allorigins.win/raw?url='+encodeURIComponent(u));});}
|
| 23 |
+
return _f.apply(this,arguments);
|
| 24 |
+
};
|
| 25 |
+
function br(t){return t.replace(/\b(Malloca|Eurogold|Grob|Canzy|Demax|Hafele|Garis|Boss|ĐMX|DMX)\s+ck\s+/gi,'ck ');}
|
| 26 |
+
function pp(){
|
| 27 |
+
if(!window.VAI_QR||typeof window.VAI_QR.parsePrompt!=='function'){setTimeout(pp,500);return;}
|
| 28 |
+
if(window.VAI_QR.__mp)return;window.VAI_QR.__mp=true;
|
| 29 |
+
var o=window.VAI_QR.parsePrompt;
|
| 30 |
+
window.VAI_QR.parsePrompt=function(t){
|
| 31 |
+
if(!t)return o.call(this,t);
|
| 32 |
+
var p=br(t),r=o.call(this,p);
|
| 33 |
+
if(p!==t){var m=p.match(/ck\s+([\d.]+)\s*%/i);if(m){var v=parseFloat(m[1].replace(',','.'));if(v>0&&v<=100)r.discountPercent=v;}}
|
| 34 |
+
return r;
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
function gd(){
|
| 38 |
+
if(!window.VAI_QR||typeof window.VAI_QR.getData!=='function'){setTimeout(gd,500);return;}
|
| 39 |
+
if(window.VAI_QR.__mg)return;window.VAI_QR.__mg=true;
|
| 40 |
+
var o=window.VAI_QR.getData;
|
| 41 |
+
window.VAI_QR.getData=function(){
|
| 42 |
+
var r=o.apply(this,arguments);
|
| 43 |
+
if((!r.discountPercent||r.discountPercent===0)&&r.qd&&r.qd.items&&r.qd.items.length){
|
| 44 |
+
var t=(window.VAI_QR.getPromptText&&window.VAI_QR.getPromptText())||'';
|
| 45 |
+
if(t){var m=t.match(/\b(Malloca|Eurogold|Grob|Canzy|Demax|Hafele|Garis|Boss|ĐMX|DMX)\s+ck\s+([\d.]+)\s*%/i);
|
| 46 |
+
if(m){var p=parseFloat(m[2].replace(',','.'));if(p>0&&p<=100){
|
| 47 |
+
r.discountPercent=p;
|
| 48 |
+
r.qd.items.forEach(function(it){var b=Number(it.price||0);if(b>0){it.discPrice=Math.round(b*(1-p/100));it.total=it.discPrice*Number(it.qty||1);}});
|
| 49 |
+
var pt=0;r.qd.items.forEach(function(it){pt+=Number(it.total||0);});
|
| 50 |
+
var sc=0;(r.fees||[]).forEach(function(f){sc+=Number(f.amount||0);});
|
| 51 |
+
r.productTotal=pt;r.grandTotal=pt+sc;r.remaining=r.grandTotal-(r.deposit||0);if(r.remaining<0)r.remaining=0;
|
| 52 |
+
}}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
return r;
|
| 56 |
+
};
|
| 57 |
+
}
|
| 58 |
+
function osp(){
|
| 59 |
+
if(typeof parsePrompt!=='function'){setTimeout(osp,500);return;}
|
| 60 |
+
if(window.__msp)return;window.__msp=true;
|
| 61 |
+
var o=parsePrompt;window.parsePrompt=function(t){return o(t?br(t):t);};
|
| 62 |
+
}
|
| 63 |
+
function pg(){
|
| 64 |
+
if(typeof genXl!=='function'){setTimeout(pg,300);return;}
|
| 65 |
+
if(window.__mgx)return;window.__mgx=true;
|
| 66 |
+
genXl=async function(d,qd,code){
|
| 67 |
+
console.log('[VAI MASTER] genXl',code);
|
| 68 |
+
var its=qd.items||[];
|
| 69 |
+
var ib=await Promise.all(its.map(function(it){
|
| 70 |
+
if(it&&it.image&&it.image.startsWith('http'))
|
| 71 |
+
return _f(it.image,{mode:'cors'}).then(function(r){if(r.ok)return r.arrayBuffer();throw 0;}).catch(function(){
|
| 72 |
+
return _f('https://api.allorigins.win/raw?url='+encodeURIComponent(it.image)).then(function(r){if(r.ok)return r.arrayBuffer();return null;}).catch(function(){return null;});
|
| 73 |
+
});
|
| 74 |
+
return Promise.resolve(null);
|
| 75 |
+
}));
|
| 76 |
+
try{if(typeof _doExportExcel==='function'&&window.VAI_QR&&window.VAI_QR.getData){var dd=window.VAI_QR.getData();var ec=window.VAI_QR.getEffectiveOrderCode?window.VAI_QR.getEffectiveOrderCode():code;var qr=window.VAI_QR.getQRUrl?window.VAI_QR.getQRUrl(dd.deposit>0?dd.remaining:dd.grandTotal,ec):'';await _doExportExcel(dd,dd.qd,ec,qr);return;}}catch(e){}
|
| 77 |
+
var M='#,##0"đ"',N='#,##0',WH={argb:'FFFFFFFF'},WB={top:{style:'thin',color:WH},bottom:{style:'thin',color:WH},left:{style:'thin',color:WH},right:{style:'thin',color:WH}};
|
| 78 |
+
var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Báo giá');
|
| 79 |
+
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}];
|
| 80 |
+
ws.mergeCells('A1:J1');ws.getRow(1).height=28;ws.getCell('A1').value='V.AI STUDIO';ws.getCell('A1').border=WB;
|
| 81 |
+
ws.mergeCells('A3:J3');ws.getRow(3).height=30;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'};
|
| 82 |
+
var cust=qd.customer||{};
|
| 83 |
+
ws.getCell('A5').value='KH:';ws.getCell('A5').font={bold:true,size:9};ws.getCell('A5').border=WB;ws.mergeCells('B5:E5');ws.getCell('B5').value=cust.name||'';ws.getCell('B5').font={bold:true,size:10};ws.getCell('B5').border=WB;ws.getCell('H5').value='Mã:';ws.getCell('H5').font={size:9};ws.getCell('H5').border=WB;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;
|
| 84 |
+
ws.getCell('A6').value='SĐT:';ws.getCell('A6').font={size:9};ws.getCell('A6').border=WB;ws.getCell('B6').value=cust.phone||'';ws.getCell('B6').border=WB;ws.getCell('H6').value='Ngày:';ws.getCell('H6').font={size:9};ws.getCell('H6').border=WB;ws.getCell('I6').value=cust.date||'';ws.getCell('I6').border=WB;
|
| 85 |
+
ws.getCell('A7').value='ĐC:';ws.getCell('A7').font={size:9};ws.getCell('A7').border=WB;ws.mergeCells('B7:J7');ws.getCell('B7').value=cust.addr||'';ws.getCell('B7').border=WB;
|
| 86 |
+
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'}}};
|
| 87 |
+
var hR=ws.getRow(9);hR.height=20;['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=hR.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;});
|
| 88 |
+
var cr=10;
|
| 89 |
+
its.forEach(function(it,i){
|
| 90 |
+
var row=ws.getRow(cr);row.height=50;var bg=i%2===0?'FFF8FAFC':'FFFFFFFF';
|
| 91 |
+
var RB={top:{style:'thin',color:{argb:bg}},bottom:{style:'thin',color:{argb:bg}},left:{style:'thin',color:{argb:bg}},right:{style:'thin',color:{argb:bg}}};
|
| 92 |
+
var qty=Number(it.qty||1),price=Number(it.price||0),dP=Number(it.discPrice||it.price||0),lt=dP*qty;
|
| 93 |
+
row.getCell(1).value=it.stt||(i+1);row.getCell(1).alignment={horizontal:'center',vertical:'middle'};
|
| 94 |
+
if(it.image&&it.image.startsWith('http')&&ib[i]){try{var ext=it.image.includes('.png')?'png':'jpeg';var iid=wb.addImage({buffer:ib[i],extension:ext});ws.addImage(iid,{tl:{col:1,row:cr-1},ext:{width:46,height:46}});}catch(e){}}
|
| 95 |
+
row.getCell(3).value=it.name||'';row.getCell(3).font={bold:true,size:9};row.getCell(3).alignment={wrapText:true,vertical:'middle'};
|
| 96 |
+
row.getCell(4).value=it.model||'';row.getCell(4).alignment={horizontal:'center'};
|
| 97 |
+
row.getCell(5).value=it.specs||(it.info||'');row.getCell(5).font={size:8,color:{argb:'FF64748B'}};row.getCell(5).alignment={wrapText:true,vertical:'middle'};
|
| 98 |
+
row.getCell(6).value=qty;row.getCell(6).numFmt=N;row.getCell(6).alignment={horizontal:'center'};
|
| 99 |
+
row.getCell(7).value=price;row.getCell(7).numFmt=M;row.getCell(7).alignment={horizontal:'right',vertical:'middle'};
|
| 100 |
+
row.getCell(8).value=dP;row.getCell(8).numFmt=M;row.getCell(8).alignment={horizontal:'right',vertical:'middle'};if(dP<price)row.getCell(8).font={bold:true,color:{argb:'FFDC3545'}};
|
| 101 |
+
row.getCell(9).value=lt;row.getCell(9).numFmt=M;row.getCell(9).font={bold:true,size:9};row.getCell(9).alignment={horizontal:'right',vertical:'middle'};
|
| 102 |
+
row.getCell(10).value=it.note||'';row.getCell(10).font={size:8};row.getCell(10).alignment={wrapText:true,vertical:'middle'};
|
| 103 |
+
for(var ci=1;ci<=10;ci++){row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:bg}};row.getCell(ci).border=RB;}
|
| 104 |
+
cr++;
|
| 105 |
+
});
|
| 106 |
+
var fees=d.fees||[];
|
| 107 |
+
if(fees.length){fees.forEach(function(f){ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=20;ws.getCell('A'+cr).value=' ⊕ '+(f.label||'Phụ phí');ws.getCell('A'+cr).font={size:9,color:{argb:'FF92400E'}};ws.getCell('I'+cr).value=Number(f.amount||0);ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:9,color:{argb:'FF92400E'}};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFFFBEB'}};cr++;});}
|
| 108 |
+
var TBG='FF003F62',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}}};
|
| 109 |
+
ws.mergeCells(cr,1,cr,8);var tR=ws.getRow(cr);tR.height=28;tR.getCell(1).value='TỔNG CỘNG';tR.getCell(1).font={bold:true,size:13,color:{argb:'FFFFFFFF'}};tR.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};
|
| 110 |
+
for(var ci=1;ci<=10;ci++){tR.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tR.getCell(ci).border=TB;}
|
| 111 |
+
tR.getCell(9).value=Number(d.grandTotal||0);tR.getCell(9).numFmt=M;tR.getCell(9).font={bold:true,size:15,color:{argb:'FFF0B840'}};tR.getCell(9).alignment={horizontal:'right',vertical:'middle'};cr++;
|
| 112 |
+
var dep=Number(d.deposit||0);
|
| 113 |
+
if(dep>0){ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=22;ws.getCell('A'+cr).value='Đã cọc';ws.getCell('A'+cr).font={bold:true,size:11,color:{argb:'FF166534'}};ws.getCell('I'+cr).value=dep;ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:11,color:{argb:'FF166534'}};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF0FDF4'}};cr++;
|
| 114 |
+
ws.mergeCells(cr,1,cr,8);ws.getRow(cr).height=24;ws.getCell('A'+cr).value='CÒN LẠI';ws.getCell('A'+cr).font={bold:true,size:12,color:{argb:'FFDC2626'}};ws.getCell('I'+cr).value=Number(d.remaining||0);ws.getCell('I'+cr).numFmt=M;ws.getCell('I'+cr).font={bold:true,size:13,color:{argb:'FFDC2626'}};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFEF2F2'}};cr++;}
|
| 115 |
+
var notes=d.notes||[];
|
| 116 |
+
if(notes.length){ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='📝 Ghi chú: '+notes.join('; ');ws.getCell('A'+cr).font={italic:true,size:9,color:{argb:'FF166534'}};for(var ci=1;ci<=10;ci++)ws.getCell(cr,ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF0FDF4'}};cr++;}
|
| 117 |
+
cr++;ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='🏦 VIB | 918258385 | Trần Quốc Vương | '+(dep>0?Number(d.remaining||0):Number(d.grandTotal||0)).toLocaleString('vi-VN')+'đ | '+code;ws.getCell('A'+cr).font={bold:true,size:10};
|
| 118 |
+
ws.eachRow(function(row){row.eachCell(function(cell){if(!cell.border)cell.border=WB;});});
|
| 119 |
+
wb.calcProperties.fullCalcOnLoad=true;
|
| 120 |
+
var buf=await wb.xlsx.writeBuffer();var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
| 121 |
+
var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=code+'.xlsx';a.style.display='none';document.body.appendChild(a);a.click();
|
| 122 |
+
setTimeout(function(){document.body.removeChild(a);},60000);
|
| 123 |
+
};
|
| 124 |
+
}
|
| 125 |
+
function pe(){
|
| 126 |
+
if(typeof _doExportExcel!=='function'){setTimeout(pe,300);return;}
|
| 127 |
+
if(window.__mee)return;window.__mee=true;
|
| 128 |
+
window.exportExcel=async function(){
|
| 129 |
+
try{if(window.VAI_QR&&typeof window.VAI_QR.getData==='function'){var d=window.VAI_QR.getData();var code=window.VAI_QR.getEffectiveOrderCode?window.VAI_QR.getEffectiveOrderCode():'BAOGIA';var qr=window.VAI_QR.getQRUrl?window.VAI_QR.getQRUrl(d.deposit>0?d.remaining:d.grandTotal,code):'';await _doExportExcel(d,d.qd,code,qr);return;}}catch(e){}
|
| 130 |
+
if(typeof genXl==='function'){try{var d2=window.VAI_QR&&window.VAI_QR.getData?window.VAI_QR.getData():null;if(d2&&d2.qd)await genXl(d2,d2.qd,'BAOGIA');}catch(e2){}}
|
| 131 |
+
};
|
| 132 |
+
if(window.VAI_QR)window.VAI_QR.exportExcel=window.exportExcel;
|
| 133 |
+
}
|
| 134 |
+
function eqm(){
|
| 135 |
+
var m=document.querySelector('.quote-overlay.open .quote-modal,.quote-overlay[style*="flex"] .quote-modal');if(!m||m.querySelector('.vai-me'))return;
|
| 136 |
+
if(!window.VAI_QR||!window.VAI_QR.getData)return;var d;try{d=window.VAI_QR.getData();}catch(e){return;}if(!d)return;
|
| 137 |
+
var fees=d.fees||[],notes=d.notes||[],dep=d.deposit||0,rem=d.remaining||0;
|
| 138 |
+
if(!fees.length&&!dep&&!notes.length)return;
|
| 139 |
+
var a=m.querySelector('.quote-actions');if(!a)return;
|
| 140 |
+
var sec=document.createElement('div');sec.className='vai-me';var h='';
|
| 141 |
+
if(fees.length){h+='<div style="padding:6px 12px;background:#fffbeb;border-radius:6px;margin-bottom:6px;font-size:11px"><div style="font-weight:700;color:#92400e;margin-bottom:3px">PHỤ PHÍ</div>';fees.forEach(function(f){h+='<div style="display:flex;justify-content:space-between"><span>'+(f.label||'')+'</span><b>'+Number(f.amount||0).toLocaleString('vi-VN')+'đ</b></div>';});h+='</div>';}
|
| 142 |
+
if(dep>0){h+='<div style="padding:5px 12px;background:#f0fdf4;border-radius:6px;margin-bottom:6px;font-size:11px"><div style="display:flex;justify-content:space-between"><span>Đã cọc</span><b style="color:#166534">'+Number(dep).toLocaleString('vi-VN')+'đ</b></div><div style="display:flex;justify-content:space-between;margin-top:2px"><span style="font-weight:700">Còn lại</span><b style="color:#dc2626">'+Number(Math.max(0,rem)).toLocaleString('vi-VN')+'đ</b></div></div>';}
|
| 143 |
+
if(notes.length){h+='<div style="padding:5px 12px;background:#f0f9ff;border-radius:6px;font-size:10px;margin-bottom:6px"><b>Ghi chú:</b> '+notes.join('; ')+'</div>';}
|
| 144 |
+
if(h){sec.innerHTML=h;sec.style.cssText='margin:8px 16px;font-size:12px';a.parentNode.insertBefore(sec,a);}
|
| 145 |
+
}
|
| 146 |
+
function pod(){
|
| 147 |
+
if(window.__mpod)return;window.__mpod=true;
|
| 148 |
+
new MutationObserver(function(){
|
| 149 |
+
var pi=document.getElementById('od-prompt');if(!pi||pi.dataset.vm)return;pi.dataset.vm='1';
|
| 150 |
+
var ab=document.getElementById('od-apply');if(ab&&!ab.dataset.vma){ab.dataset.vma='1';var oa=ab.onclick;if(oa)ab.onclick=function(e){var v=pi.value||'',p=br(v);if(p!==v)pi.value=p;return oa.call(this,e);};}
|
| 151 |
+
var sb=document.getElementById('od-save');if(sb&&!sb.dataset.vms){sb.dataset.vms='1';var os=sb.onclick;if(os)sb.onclick=function(e){var v=pi.value||'',p=br(v);if(p!==v)pi.value=p;return os.call(this,e);};}
|
| 152 |
+
}).observe(document.body,{childList:true,subtree:true});
|
| 153 |
+
}
|
| 154 |
+
function init(){
|
| 155 |
+
setTimeout(pp,500);setTimeout(gd,800);setTimeout(osp,1000);
|
| 156 |
+
setTimeout(pg,1500);setTimeout(pe,2000);
|
| 157 |
+
setInterval(eqm,2000);setTimeout(pod,2000);
|
| 158 |
+
console.log('[VAI MASTER FIX v2026] ✅ Loaded');
|
| 159 |
+
}
|
| 160 |
+
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
|
| 161 |
+
})();
|
vai-patch-v1047.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO - UNIVERSAL PATCH v1048
|
| 3 |
+
* Fixes all missing functionality:
|
| 4 |
+
* 1. AI Search - Implements actual search with semantic matching
|
| 5 |
+
* 2. Order/Quote buttons - Ensures all handlers work
|
| 6 |
+
* 3. Excel/PDF Export - NO LONGER OVERRIDES (handled by vai-export-*.js files)
|
| 7 |
+
*
|
| 8 |
+
* v1048: Removed broken exportExcel/exportPDF overrides that broke export
|
| 9 |
+
* (captured undefined before qr-payment.js loaded)
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
(function() {
|
| 13 |
+
'use strict';
|
| 14 |
+
|
| 15 |
+
// Wait for DOM and main data to load
|
| 16 |
+
function ready(fn) {
|
| 17 |
+
if (document.readyState !== 'loading') fn();
|
| 18 |
+
else document.addEventListener('DOMContentLoaded', fn);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
ready(function() {
|
| 22 |
+
|
| 23 |
+
// ===== 1. AI SEARCH PATCH =====
|
| 24 |
+
// Override with full implementation
|
| 25 |
+
window.aiSearch = async function(query, products, token) {
|
| 26 |
+
if (!products || !products.length) {
|
| 27 |
+
return { results: [], aiAnswer: null, categories: [], constraints: [], budget: 0 };
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const norm = (s) => String(s || '').toLowerCase()
|
| 31 |
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
| 32 |
+
.replace(/[đĐ]/g, 'd').replace(/[.\-\s]/g, '');
|
| 33 |
+
|
| 34 |
+
const q = norm(query);
|
| 35 |
+
let budget = 0;
|
| 36 |
+
let categories = [];
|
| 37 |
+
let constraints = [];
|
| 38 |
+
|
| 39 |
+
// Extract budget constraint
|
| 40 |
+
const budgetMatch = query.toLowerCase().match(/(dưới|trên|trong khoảng)?\s*([\d,.]+)\s*(triệu|nghìn|k|đồng)/g);
|
| 41 |
+
if (budgetMatch) {
|
| 42 |
+
const amt = parseFloat(budgetMatch[0].replace(/[^\d,.]/g, '').replace(',', '.'));
|
| 43 |
+
const unit = budgetMatch[0].match(/(triệu|nghìn|k|đồng)/g);
|
| 44 |
+
if (unit && unit[0] === 'triệu') budget = amt * 1000000;
|
| 45 |
+
else if (unit && (unit[0] === 'nghìn' || unit[0] === 'k')) budget = amt * 1000;
|
| 46 |
+
else budget = amt;
|
| 47 |
+
constraints.push({ label: budgetMatch[0].trim() });
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// Category keywords
|
| 51 |
+
const catMap = {
|
| 52 |
+
'bep': 'Bếp điện từ',
|
| 53 |
+
'bep-tu': 'Bếp từ',
|
| 54 |
+
'bep-gas': 'Bếp gas',
|
| 55 |
+
'hut-mui': 'Máy hút mùi',
|
| 56 |
+
'hut-khoi': 'Máy hút khói',
|
| 57 |
+
'chau-rua': 'Chậu rửa',
|
| 58 |
+
'voi-rua': 'Vòi rửa',
|
| 59 |
+
'lo-nuong': 'Lò nướng',
|
| 60 |
+
'tu-lanh': 'Tủ lạnh',
|
| 61 |
+
'may-rua-chen': 'Máy rửa chén'
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
Object.keys(catMap).forEach(key => {
|
| 65 |
+
if (q.includes(key)) {
|
| 66 |
+
categories.push(catMap[key]);
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// Brand extraction
|
| 71 |
+
const brandMap = {
|
| 72 |
+
'malloca': 'Malloca',
|
| 73 |
+
'eurogold': 'Eurogold',
|
| 74 |
+
'grob': 'Grob',
|
| 75 |
+
'canzy': 'Canzy',
|
| 76 |
+
'demax': 'Demax',
|
| 77 |
+
'hafele': 'Hafele',
|
| 78 |
+
'garis': 'Garis'
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
// Search logic with scoring
|
| 82 |
+
let results = [];
|
| 83 |
+
for (let i = 0; i < products.length && results.length < 30; i++) {
|
| 84 |
+
const p = products[i];
|
| 85 |
+
let score = 0;
|
| 86 |
+
const name = norm(p.name || '');
|
| 87 |
+
const sku = norm(p.sku || p.mod || '');
|
| 88 |
+
const brand = norm(p.brand || '');
|
| 89 |
+
const cat = norm(p.cat || '');
|
| 90 |
+
|
| 91 |
+
// Exact/partial matches
|
| 92 |
+
if (name.includes(q)) score += 10;
|
| 93 |
+
if (sku.includes(q)) score += 8;
|
| 94 |
+
if (brand.includes(q)) score += 6;
|
| 95 |
+
if (cat.includes(q)) score += 4;
|
| 96 |
+
|
| 97 |
+
// Word-by-word matching
|
| 98 |
+
const words = q.split(/\s+/).filter(w => w.length > 1);
|
| 99 |
+
words.forEach(w => {
|
| 100 |
+
if (name.includes(w)) score += 2;
|
| 101 |
+
if (sku.includes(w)) score += 1;
|
| 102 |
+
if (brand.includes(w)) score += 1;
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
// Price filter by budget
|
| 106 |
+
if (budget > 0 && p.priceNum && p.priceNum > budget) {
|
| 107 |
+
score = 0;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Brand filter
|
| 111 |
+
for (let b in brandMap) {
|
| 112 |
+
if (q.includes(b)) {
|
| 113 |
+
if (brand.includes(b)) score += 5;
|
| 114 |
+
else if (!brand.includes(b)) score = 0;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
if (score > 0) {
|
| 119 |
+
const labels = [];
|
| 120 |
+
if (p.priceNum && budget > 0 && p.priceNum <= budget) labels.push('Trong ngân sách');
|
| 121 |
+
results.push({ p, idx: i, score, labels });
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Sort by score desc
|
| 126 |
+
results.sort((a, b) => b.score - a.score);
|
| 127 |
+
|
| 128 |
+
// Generate AI response
|
| 129 |
+
let aiAnswer = '';
|
| 130 |
+
if (results.length > 0) {
|
| 131 |
+
aiAnswer = `Tìm thấy ${results.length} sản phẩm phù hợp. `;
|
| 132 |
+
if (categories.length) aiAnswer += `Danh mục: ${categories.join(', ')}. `;
|
| 133 |
+
if (budget > 0) aiAnswer += `Ngân sách: ${(budget/1000000).toFixed(0)} triệu.`;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
return { results, aiAnswer, categories, constraints, budget };
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
// ===== 2. EXPORT — REMOVED BROKEN OVERRIDES (v1048)
|
| 140 |
+
// Export functions are now handled by vai-export-multi-download.js,
|
| 141 |
+
// vai-robust-export-v2.js, and vai-ultimate-fix-v1100.js
|
| 142 |
+
// These files properly intercept blob URLs and patch _doExportExcel
|
| 143 |
+
// AFTER qr-payment.js has initialized.
|
| 144 |
+
|
| 145 |
+
// ===== 3. FORMAT PRICE HELPER =====
|
| 146 |
+
window.formatPrice = function(priceNum) {
|
| 147 |
+
if (!priceNum || priceNum <= 0) return 'Liên hệ';
|
| 148 |
+
return priceNum.toLocaleString('vi-VN') + 'đ';
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
// ===== 4. ORDER PICKER STUB =====
|
| 152 |
+
// If _showOrderPicker is null, provide a simple implementation
|
| 153 |
+
if (!window._showOrderPicker) {
|
| 154 |
+
window._showOrderPicker = function(product, callback) {
|
| 155 |
+
// Simple add-to-cart flow
|
| 156 |
+
if (product && typeof addToCart === 'function') {
|
| 157 |
+
// Find product index
|
| 158 |
+
var idx = -1;
|
| 159 |
+
if (window.D) {
|
| 160 |
+
for (var i = 0; i < window.D.length; i++) {
|
| 161 |
+
if (window.D[i] && (window.D[i].slug === product.slug ||
|
| 162 |
+
window.D[i].sku === product.sku ||
|
| 163 |
+
window.D[i].model === product.model)) {
|
| 164 |
+
idx = i; break;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
if (idx >= 0) addToCart(idx);
|
| 169 |
+
callback && callback('ok');
|
| 170 |
+
}
|
| 171 |
+
};
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// ===== 5. UPDATE CART BADGE ON LOAD =====
|
| 175 |
+
if (typeof updateCartBadge === 'function') {
|
| 176 |
+
updateCartBadge();
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
console.log('V.AI STUDIO Patch v1048 initialized');
|
| 180 |
+
});
|
| 181 |
+
})();
|
vai-quote-excel-fix.js
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — QUOTE EXCEL FIX v2026
|
| 3 |
+
*
|
| 4 |
+
* FIX TRIỆT ĐỂ:
|
| 5 |
+
* 1. Parse prompt trong quote modal real-time → fees, deposit, notes, discount
|
| 6 |
+
* 2. Patch VAI_QR.getData() để trả về đủ fees, deposit, notes, grandTotal, remaining
|
| 7 |
+
* 3. Excel export hiển thị đầy đủ: phụ phí, cọc, còn lại, ghi chú
|
| 8 |
+
* 4. Lưu đơn hàng cũng lưu đủ các trường này
|
| 9 |
+
*
|
| 10 |
+
* Prompt ví dụ: "Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7"
|
| 11 |
+
*/
|
| 12 |
+
(function() {
|
| 13 |
+
'use strict';
|
| 14 |
+
console.log('[VAI QUOTE EXCEL FIX] === LOADING ===');
|
| 15 |
+
|
| 16 |
+
// =============================================
|
| 17 |
+
// PARSE PROMPT - Extract fees, deposit, discount, notes
|
| 18 |
+
// =============================================
|
| 19 |
+
function parsePromptToData(text) {
|
| 20 |
+
if (!text) return { fees: [], deposit: 0, discountPercent: 0, notes: [], itemDiscounts: {} };
|
| 21 |
+
|
| 22 |
+
var fees = [], deposit = 0, discountPercent = 0, notes = [], itemDiscounts = {};
|
| 23 |
+
var lines = text.split(/[,;\n]+/);
|
| 24 |
+
|
| 25 |
+
for (var i = 0; i < lines.length; i++) {
|
| 26 |
+
var line = lines[i].trim();
|
| 27 |
+
if (!line) continue;
|
| 28 |
+
|
| 29 |
+
var lo = line.toLowerCase()
|
| 30 |
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
| 31 |
+
.replace(/[đĐ]/g, 'd');
|
| 32 |
+
|
| 33 |
+
// Item-specific discount: "Malloca ck 35%" or "ck 35%"
|
| 34 |
+
var brandCk = lo.match(/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+([\d.,]+)\s*%/i);
|
| 35 |
+
if (brandCk) {
|
| 36 |
+
var pct = parseFloat(brandCk[2].replace(/\./g, '').replace(',', '.'));
|
| 37 |
+
if (pct > 0 && pct <= 100) discountPercent = pct;
|
| 38 |
+
continue;
|
| 39 |
+
}
|
| 40 |
+
var simpleCk = lo.match(/^ck\s+([\d.,]+)\s*%/i);
|
| 41 |
+
if (simpleCk) {
|
| 42 |
+
var pct2 = parseFloat(simpleCk[1].replace(/\./g, '').replace(',', '.'));
|
| 43 |
+
if (pct2 > 0 && pct2 <= 100) discountPercent = pct2;
|
| 44 |
+
continue;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Notes: "ghi chú: ...", "note: ...", "lưu ý: ..."
|
| 48 |
+
if (lo.match(/^(ghi chu|note|luu y)/)) {
|
| 49 |
+
var noteText = line.replace(/^(ghi chú|ghi chu|note|lưu ý|luu y)[:\s]*/i, '').trim();
|
| 50 |
+
if (noteText) notes.push(noteText);
|
| 51 |
+
continue;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Delivery day: "giao thứ 7", "lắp thứ 3" → note
|
| 55 |
+
var dayMatch = lo.match(/(giao|lap)\s*thu\s*(\d+)/i);
|
| 56 |
+
if (dayMatch) {
|
| 57 |
+
notes.push(dayMatch[1] === 'giao' ? 'Giao thứ ' + dayMatch[2] : 'Lắp thứ ' + dayMatch[2]);
|
| 58 |
+
continue;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Deposit: "cọc 5tr", "cọc 5000000", "dat coc 5000k"
|
| 62 |
+
if (lo.match(/coc|dat coc|deposit/)) {
|
| 63 |
+
var dm = lo.match(/([\d.,]+)\s*(k|tr|trieu|m|nghin)?/);
|
| 64 |
+
if (dm) {
|
| 65 |
+
var amt = parseFloat(dm[1].replace(/\./g, '').replace(',', '.'));
|
| 66 |
+
var unit = (dm[2] || '').toLowerCase();
|
| 67 |
+
if (unit === 'k' || unit === 'nghin') amt *= 1000;
|
| 68 |
+
else if (unit === 'tr' || unit === 'trieu' || unit === 'm') amt *= 1000000;
|
| 69 |
+
else if (amt > 0 && amt < 500) amt *= 1000;
|
| 70 |
+
deposit = amt;
|
| 71 |
+
}
|
| 72 |
+
continue;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Fees: "giao hàng 200k", "lắp đặt 500k", "van chuyen 100k", "ship 50k", "phi giao 150k"
|
| 76 |
+
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)?/);
|
| 77 |
+
if (feeMatch) {
|
| 78 |
+
var amt = parseFloat(feeMatch[2].replace(/\./g, '').replace(',', '.'));
|
| 79 |
+
var unit = (feeMatch[3] || '').toLowerCase();
|
| 80 |
+
if (unit === 'k' || unit === 'nghin') amt *= 1000;
|
| 81 |
+
else if (unit === 'tr' || unit === 'trieu' || unit === 'm') amt *= 1000000;
|
| 82 |
+
else if (amt > 0 && amt < 500) amt *= 1000;
|
| 83 |
+
|
| 84 |
+
var labelMap = {
|
| 85 |
+
'giao hang': 'Phí giao hàng', 'giao': 'Phí giao hàng',
|
| 86 |
+
'lap dat': 'Phí lắp đặt', 'lap': 'Phí lắp đặt',
|
| 87 |
+
'van chuyen': 'Phí vận chuyển', 'ship': 'Phí vận chuyển',
|
| 88 |
+
'phi giao': 'Phí giao hàng', 'phi lap': 'Phí lắp đặt',
|
| 89 |
+
'phi khac': 'Phí khác', 'phu phi': 'Phụ phí', 'phi': 'Phụ phí'
|
| 90 |
+
};
|
| 91 |
+
fees.push({ label: labelMap[feeMatch[1]] || 'Phụ phí', amount: amt });
|
| 92 |
+
continue;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Fallback: number with unit but no keyword
|
| 96 |
+
var numMatch = lo.match(/([\d.,]+)\s*(k|tr|trieu|m|nghin)/);
|
| 97 |
+
if (numMatch && !lo.match(/thu\s*\d/)) {
|
| 98 |
+
var amt2 = parseFloat(numMatch[1].replace(/\./g, '').replace(',', '.'));
|
| 99 |
+
var unit2 = numMatch[2].toLowerCase();
|
| 100 |
+
if (unit2 === 'k' || unit2 === 'nghin') amt2 *= 1000;
|
| 101 |
+
else if (unit2 === 'tr' || unit2 === 'trieu' || unit2 === 'm') amt2 *= 1000000;
|
| 102 |
+
if (amt2 > 0) {
|
| 103 |
+
var label = line.replace(numMatch[0], '').trim();
|
| 104 |
+
if (!label || label.length < 2) label = 'Phụ phí';
|
| 105 |
+
fees.push({ label: label, amount: amt2 });
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return { fees: fees, deposit: deposit, discountPercent: discountPercent, notes: notes, itemDiscounts: itemDiscounts };
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// =============================================
|
| 114 |
+
// FIND QUOTE MODAL PROMPT INPUT
|
| 115 |
+
// =============================================
|
| 116 |
+
function findQuotePromptInput() {
|
| 117 |
+
var selectors = [
|
| 118 |
+
'#qcAiPrompt',
|
| 119 |
+
'input[placeholder*="yêu cầu"]',
|
| 120 |
+
'input[placeholder*="chiết khấu"]',
|
| 121 |
+
'input[placeholder*="chiet khau"]',
|
| 122 |
+
'input[placeholder*="giao hàng"]',
|
| 123 |
+
'textarea[placeholder*="yêu cầu"]',
|
| 124 |
+
'.quote-body input[type="text"]',
|
| 125 |
+
'.quote-modal input[type="text"]',
|
| 126 |
+
'[id*="prompt"]',
|
| 127 |
+
'[id*="aiPrompt"]'
|
| 128 |
+
];
|
| 129 |
+
|
| 130 |
+
for (var i = 0; i < selectors.length; i++) {
|
| 131 |
+
var el = document.querySelector(selectors[i]);
|
| 132 |
+
if (el) return el;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
var all = document.querySelectorAll('input, textarea');
|
| 136 |
+
for (var j = 0; j < all.length; j++) {
|
| 137 |
+
var ph = (all[j].placeholder || '').toLowerCase();
|
| 138 |
+
if (ph.includes('yêu cầu') || ph.includes('chiết khấu') || ph.includes('chiet khau') ||
|
| 139 |
+
ph.includes('giao hàng') || ph.includes('ck') || ph.includes('giảm')) {
|
| 140 |
+
return all[j];
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
return null;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// =============================================
|
| 147 |
+
// PATCH VAI_QR.getData - Ensure complete data returned
|
| 148 |
+
// =============================================
|
| 149 |
+
function patchVAIQRGetData() {
|
| 150 |
+
if (window.__vaiQrGetDataPatched) return;
|
| 151 |
+
if (!window.VAI_QR || typeof window.VAI_QR.getData !== 'function') {
|
| 152 |
+
setTimeout(patchVAIQRGetData, 500);
|
| 153 |
+
return;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
var origGetData = window.VAI_QR.getData;
|
| 157 |
+
window.VAI_QR.getData = function() {
|
| 158 |
+
var result = origGetData.apply(this, arguments);
|
| 159 |
+
|
| 160 |
+
// Ensure fees, deposit, notes, discountPercent are present
|
| 161 |
+
if (!result.fees) result.fees = [];
|
| 162 |
+
if (!result.deposit) result.deposit = 0;
|
| 163 |
+
if (!result.notes) result.notes = [];
|
| 164 |
+
if (!result.discountPercent) result.discountPercent = 0;
|
| 165 |
+
if (!result.itemDiscounts) result.itemDiscounts = {};
|
| 166 |
+
|
| 167 |
+
// Try to get prompt text and parse if missing data
|
| 168 |
+
var promptText = '';
|
| 169 |
+
if (typeof window.VAI_QR.getPromptText === 'function') {
|
| 170 |
+
promptText = window.VAI_QR.getPromptText() || '';
|
| 171 |
+
} else {
|
| 172 |
+
var input = findQuotePromptInput();
|
| 173 |
+
if (input) promptText = input.value || '';
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// If fees/deposit/notes are empty but prompt has content, parse it
|
| 177 |
+
if (promptText && (!result.fees.length && result.deposit === 0 && !result.notes.length)) {
|
| 178 |
+
var parsed = parsePromptToData(promptText);
|
| 179 |
+
if (parsed.fees.length) result.fees = parsed.fees;
|
| 180 |
+
if (parsed.deposit > 0) result.deposit = parsed.deposit;
|
| 181 |
+
if (parsed.discountPercent > 0) result.discountPercent = parsed.discountPercent;
|
| 182 |
+
if (parsed.notes.length) result.notes = parsed.notes;
|
| 183 |
+
if (Object.keys(parsed.itemDiscounts).length) result.itemDiscounts = parsed.itemDiscounts;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// Recalculate totals if we have items
|
| 187 |
+
if (result.qd && result.qd.items && result.qd.items.length) {
|
| 188 |
+
var pt = 0;
|
| 189 |
+
result.qd.items.forEach(function(it) {
|
| 190 |
+
var base = Number(it.price || 0);
|
| 191 |
+
var ip = 0;
|
| 192 |
+
// Check item-specific discount
|
| 193 |
+
var keys = [];
|
| 194 |
+
['model', 'sku', 'code', 'ma', 'name'].forEach(function(k) {
|
| 195 |
+
if (it && it[k]) keys.push((it[k] + '').toLowerCase().replace(/[^a-z0-9]/g, ''));
|
| 196 |
+
});
|
| 197 |
+
for (var k in result.itemDiscounts) {
|
| 198 |
+
for (var j = 0; j < keys.length; j++) {
|
| 199 |
+
if (keys[j].includes(k) || k.includes(keys[j])) {
|
| 200 |
+
ip = result.itemDiscounts[k];
|
| 201 |
+
break;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
var discPrice = ip > 0 ? Math.round(base * (1 - ip / 100)) :
|
| 206 |
+
(result.discountPercent > 0 ? Math.round(base * (1 - result.discountPercent / 100)) : base);
|
| 207 |
+
it.discPrice = discPrice;
|
| 208 |
+
it.total = discPrice * Number(it.qty || 1);
|
| 209 |
+
pt += it.total;
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
var sc = 0;
|
| 213 |
+
(result.fees || []).forEach(function(f) { sc += Number(f.amount || 0); });
|
| 214 |
+
|
| 215 |
+
result.productTotal = pt;
|
| 216 |
+
result.grandTotal = pt + sc;
|
| 217 |
+
result.remaining = result.grandTotal - (result.deposit || 0);
|
| 218 |
+
if (result.remaining < 0) result.remaining = 0;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return result;
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
window.__vaiQrGetDataPatched = true;
|
| 225 |
+
console.log('[VAI QUOTE EXCEL FIX] ✅ VAI_QR.getData patched for complete data');
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// =============================================
|
| 229 |
+
// PATCH VAI_QR.parsePrompt - Handle brand+ck and all fee types
|
| 230 |
+
// =============================================
|
| 231 |
+
function patchVAIQRParsePrompt() {
|
| 232 |
+
if (window.__vaiQrParsePromptPatched) return;
|
| 233 |
+
if (!window.VAI_QR || typeof window.VAI_QR.parsePrompt !== 'function') {
|
| 234 |
+
setTimeout(patchVAIQRParsePrompt, 500);
|
| 235 |
+
return;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
var origParse = window.VAI_QR.parsePrompt;
|
| 239 |
+
window.VAI_QR.parsePrompt = function(text) {
|
| 240 |
+
var result = origParse.call(this, text);
|
| 241 |
+
if (!text) return result;
|
| 242 |
+
|
| 243 |
+
// Enhance with our parser
|
| 244 |
+
var parsed = parsePromptToData(text);
|
| 245 |
+
if (parsed.fees.length) result.fees = parsed.fees;
|
| 246 |
+
if (parsed.deposit > 0) result.deposit = parsed.deposit;
|
| 247 |
+
if (parsed.discountPercent > 0) result.discountPercent = parsed.discountPercent;
|
| 248 |
+
if (parsed.notes.length) result.notes = parsed.notes;
|
| 249 |
+
if (Object.keys(parsed.itemDiscounts).length) result.itemDiscounts = parsed.itemDiscounts;
|
| 250 |
+
|
| 251 |
+
return result;
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
window.__vaiQrParsePromptPatched = true;
|
| 255 |
+
console.log('[VAI QUOTE EXCEL FIX] ✅ VAI_QR.parsePrompt patched');
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// =============================================
|
| 259 |
+
// REAL-TIME PROMPT INPUT HANDLER
|
| 260 |
+
// =============================================
|
| 261 |
+
function setupPromptInputWatcher() {
|
| 262 |
+
var lastVal = '';
|
| 263 |
+
var input = findQuotePromptInput();
|
| 264 |
+
|
| 265 |
+
if (input && !input.dataset.vaiPromptWatcher) {
|
| 266 |
+
input.dataset.vaiPromptWatcher = 'true';
|
| 267 |
+
|
| 268 |
+
['input', 'change', 'blur', 'keyup'].forEach(function(evt) {
|
| 269 |
+
input.addEventListener(evt, function() {
|
| 270 |
+
var val = this.value || '';
|
| 271 |
+
if (val !== lastVal) {
|
| 272 |
+
lastVal = val;
|
| 273 |
+
// Trigger parsePrompt to update VAI_QR internal state
|
| 274 |
+
if (window.VAI_QR && typeof window.VAI_QR.parsePrompt === 'function') {
|
| 275 |
+
window.VAI_QR.parsePrompt(val);
|
| 276 |
+
}
|
| 277 |
+
// Also update quote modal display
|
| 278 |
+
updateQuoteModalDisplay();
|
| 279 |
+
}
|
| 280 |
+
});
|
| 281 |
+
});
|
| 282 |
+
|
| 283 |
+
console.log('[VAI QUOTE EXCEL FIX] ✅ Prompt input watcher attached');
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// Also watch for quote modal opening
|
| 287 |
+
var observer = new MutationObserver(function() {
|
| 288 |
+
var overlay = document.querySelector('.quote-overlay.open, .quote-modal.open, .quote-overlay[style*="block"], .quote-modal[style*="block"], [class*="quote"][class*="open"]');
|
| 289 |
+
if (overlay) {
|
| 290 |
+
setTimeout(function() {
|
| 291 |
+
var inp = findQuotePromptInput();
|
| 292 |
+
if (inp && !inp.dataset.vaiPromptWatcher) {
|
| 293 |
+
setupPromptInputWatcher();
|
| 294 |
+
}
|
| 295 |
+
updateQuoteModalDisplay();
|
| 296 |
+
}, 300);
|
| 297 |
+
}
|
| 298 |
+
});
|
| 299 |
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] });
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// =============================================
|
| 303 |
+
// UPDATE QUOTE MODAL DISPLAY - Show fees, deposit, notes
|
| 304 |
+
// =============================================
|
| 305 |
+
function updateQuoteModalDisplay() {
|
| 306 |
+
var modal = document.querySelector('.quote-overlay.open .quote-modal, .quote-modal.open, .quote-overlay[style*="block"] .quote-modal, [class*="quote"][class*="open"] .quote-modal');
|
| 307 |
+
if (!modal) return;
|
| 308 |
+
|
| 309 |
+
// Don't duplicate
|
| 310 |
+
if (modal.querySelector('.vai-fees-display')) return;
|
| 311 |
+
|
| 312 |
+
if (!window.VAI_QR || !window.VAI_QR.getData) return;
|
| 313 |
+
|
| 314 |
+
var data;
|
| 315 |
+
try { data = window.VAI_QR.getData(); } catch(e) { return; }
|
| 316 |
+
if (!data) return;
|
| 317 |
+
|
| 318 |
+
var fees = data.fees || [];
|
| 319 |
+
var notes = data.notes || [];
|
| 320 |
+
var deposit = data.deposit || 0;
|
| 321 |
+
var grandTotal = data.grandTotal || 0;
|
| 322 |
+
var remaining = data.remaining || 0;
|
| 323 |
+
|
| 324 |
+
if (!fees.length && !deposit && !notes.length) return;
|
| 325 |
+
|
| 326 |
+
var actions = modal.querySelector('.quote-actions');
|
| 327 |
+
if (!actions) return;
|
| 328 |
+
|
| 329 |
+
var sec = document.createElement('div');
|
| 330 |
+
sec.className = 'vai-fees-display';
|
| 331 |
+
var html = '';
|
| 332 |
+
|
| 333 |
+
if (fees.length) {
|
| 334 |
+
html += '<div style="padding:8px 12px;background:#fffbeb;border-radius:6px;border:1px solid #fde68a;margin-bottom:6px;font-size:11px">';
|
| 335 |
+
html += '<div style="font-weight:700;color:#92400e;margin-bottom:4px">📦 PHỤ PHÍ</div>';
|
| 336 |
+
fees.forEach(function(f) {
|
| 337 |
+
html += '<div style="display:flex;justify-content:space-between;padding:2px 0"><span>'+f.label+'</span><b>'+Number(f.amount||0).toLocaleString('vi-VN')+'đ</b></div>';
|
| 338 |
+
});
|
| 339 |
+
html += '</div>';
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
if (deposit > 0) {
|
| 343 |
+
html += '<div style="padding:6px 12px;background:#f0fdf4;border-radius:6px;border:1px solid #86efac;margin-bottom:6px;font-size:11px">';
|
| 344 |
+
html += '<div style="display:flex;justify-content:space-between"><span>💵 Đã cọc</span><b style="color:#166534">'+Number(deposit).toLocaleString('vi-VN')+'đ</b></div>';
|
| 345 |
+
html += '<div style="display:flex;justify-content:space-between;margin-top:3px"><span style="font-weight:700">📊 Còn lại</span><b style="color:#dc2626;font-size:13px">'+Number(Math.max(0, remaining)).toLocaleString('vi-VN')+'đ</b></div>';
|
| 346 |
+
html += '</div>';
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
if (notes && notes.length) {
|
| 350 |
+
html += '<div style="padding:6px 12px;background:#f0f9ff;border-radius:6px;border:1px solid #bae6fd;font-size:10px;margin-bottom:6px">';
|
| 351 |
+
html += '<b>📝 Ghi chú:</b> '+notes.join('; ');
|
| 352 |
+
html += '</div>';
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
if (html) {
|
| 356 |
+
sec.innerHTML = html;
|
| 357 |
+
sec.style.cssText = 'margin:10px 16px;font-size:12px';
|
| 358 |
+
actions.parentNode.insertBefore(sec, actions);
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// =============================================
|
| 363 |
+
// PATCH ORDER DETAIL MODAL - Pre-process prompt
|
| 364 |
+
// =============================================
|
| 365 |
+
function patchOrderDetailModal() {
|
| 366 |
+
if (window.__vaiOrderDetailPatched) return;
|
| 367 |
+
window.__vaiOrderDetailPatched = true;
|
| 368 |
+
|
| 369 |
+
var observer = new MutationObserver(function() {
|
| 370 |
+
var applyBtn = document.getElementById('od-apply');
|
| 371 |
+
if (!applyBtn) return;
|
| 372 |
+
|
| 373 |
+
// Patch apply button
|
| 374 |
+
if (applyBtn && !applyBtn.dataset.vaiPromptPre) {
|
| 375 |
+
applyBtn.dataset.vaiPromptPre = 'true';
|
| 376 |
+
var origApply = applyBtn.onclick;
|
| 377 |
+
if (origApply) {
|
| 378 |
+
applyBtn.onclick = function(e) {
|
| 379 |
+
var input = document.getElementById('od-prompt');
|
| 380 |
+
if (input) {
|
| 381 |
+
var val = input.value || '';
|
| 382 |
+
// Pre-process: "Malloca ck 35%" -> "ck 35%"
|
| 383 |
+
var processed = val.replace(
|
| 384 |
+
/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+/gi,
|
| 385 |
+
'ck '
|
| 386 |
+
);
|
| 387 |
+
if (processed !== val) {
|
| 388 |
+
input.value = processed;
|
| 389 |
+
console.log('[VAI QUOTE EXCEL FIX] Pre-processed prompt:', val, '→', processed);
|
| 390 |
+
}
|
| 391 |
+
}
|
| 392 |
+
return origApply.call(this, e);
|
| 393 |
+
};
|
| 394 |
+
}
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
// Patch save button
|
| 398 |
+
var saveBtn = document.getElementById('od-save');
|
| 399 |
+
if (saveBtn && !saveBtn.dataset.vaiPromptPre) {
|
| 400 |
+
saveBtn.dataset.vaiPromptPre = 'true';
|
| 401 |
+
var origSave = saveBtn.onclick;
|
| 402 |
+
if (origSave) {
|
| 403 |
+
saveBtn.onclick = function(e) {
|
| 404 |
+
var input = document.getElementById('od-prompt');
|
| 405 |
+
if (input) {
|
| 406 |
+
var val = input.value || '';
|
| 407 |
+
var processed = val.replace(
|
| 408 |
+
/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+/gi,
|
| 409 |
+
'ck '
|
| 410 |
+
);
|
| 411 |
+
if (processed !== val) input.value = processed;
|
| 412 |
+
}
|
| 413 |
+
return origSave.call(this, e);
|
| 414 |
+
};
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
});
|
| 418 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// =============================================
|
| 422 |
+
// INIT
|
| 423 |
+
// =============================================
|
| 424 |
+
function init() {
|
| 425 |
+
console.log('[VAI QUOTE EXCEL FIX] Initializing...');
|
| 426 |
+
|
| 427 |
+
// 1. Patch VAI_QR.getData
|
| 428 |
+
patchVAIQRGetData();
|
| 429 |
+
|
| 430 |
+
// 2. Patch VAI_QR.parsePrompt
|
| 431 |
+
patchVAIQRParsePrompt();
|
| 432 |
+
|
| 433 |
+
// 3. Setup prompt input watcher
|
| 434 |
+
setTimeout(setupPromptInputWatcher, 1000);
|
| 435 |
+
|
| 436 |
+
// 4. Watch quote modal for display updates
|
| 437 |
+
setInterval(updateQuoteModalDisplay, 2000);
|
| 438 |
+
|
| 439 |
+
// 5. Patch order detail modal
|
| 440 |
+
setTimeout(patchOrderDetailModal, 1500);
|
| 441 |
+
|
| 442 |
+
console.log('[VAI QUOTE EXCEL FIX] ✅ Done');
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
if (document.readyState === 'loading') {
|
| 446 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 447 |
+
} else {
|
| 448 |
+
init();
|
| 449 |
+
}
|
| 450 |
+
})();
|
vai-quote-fix-2026.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — ULTIMATE QUOTE FIX v2026
|
| 3 |
+
*
|
| 4 |
+
* ===== FIX TRIỆT ĐỂ =====
|
| 5 |
+
*
|
| 6 |
+
* 1. Prompt "Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7"
|
| 7 |
+
* → PATCH: parsePrompt + getData() — nhận diện "Brand ck X%" pattern
|
| 8 |
+
* → Hiển thị: phụ phí, cọc, ghi chú trên modal báo giá VÀ trên Excel/PDF tải về
|
| 9 |
+
*
|
| 10 |
+
* 2. Hình ảnh không hiển thị trên cột "Hình" của Excel
|
| 11 |
+
* → PATCH: fetch() dùng CORS proxy (api.allorigins.win) khi bizweb.dktcdn.net chặn CORS
|
| 12 |
+
* → Ảnh hiển thị trong cột Hình của Excel
|
| 13 |
+
*
|
| 14 |
+
* 3. Order detail modal prompt
|
| 15 |
+
* → Pre-process "Malloca ck 35%" → "ck 35%" trước khi parse
|
| 16 |
+
* → Fee/deposit/notes tính đúng trên Excel/PDF
|
| 17 |
+
*/
|
| 18 |
+
(function() {
|
| 19 |
+
'use strict';
|
| 20 |
+
console.log('[VAI QUOTE FIX 2026] === LOADING ===');
|
| 21 |
+
|
| 22 |
+
// =============================================
|
| 23 |
+
// CORS PROXY for product images (bizweb.dktcdn.net blocks CORS)
|
| 24 |
+
// =============================================
|
| 25 |
+
function patchFetchWithCORSProxy() {
|
| 26 |
+
if (window.__vaiFetchPatched) return;
|
| 27 |
+
window.__vaiFetchPatched = true;
|
| 28 |
+
|
| 29 |
+
var _origFetch = window.fetch;
|
| 30 |
+
var CDN_DOMAINS = ['bizweb.dktcdn.net', 'product.hstatic.net', 'file.hstatic.net',
|
| 31 |
+
'bizweb.dktcdn.com'];
|
| 32 |
+
|
| 33 |
+
window.fetch = function(input, init) {
|
| 34 |
+
var url = (typeof input === 'string') ? input : (input && input.url) || '';
|
| 35 |
+
var needsProxy = false;
|
| 36 |
+
for (var i = 0; i < CDN_DOMAINS.length; i++) {
|
| 37 |
+
if (url.indexOf(CDN_DOMAINS[i]) >= 0) {
|
| 38 |
+
needsProxy = true;
|
| 39 |
+
break;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
if (!needsProxy) {
|
| 43 |
+
return _origFetch.apply(this, arguments);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Try direct fetch first with mode 'cors'
|
| 47 |
+
var opts = Object.assign({}, init || {}, { mode: 'cors' });
|
| 48 |
+
return _origFetch(url, opts).then(function(r) {
|
| 49 |
+
if (r.ok) return r;
|
| 50 |
+
throw new Error('HTTP ' + r.status);
|
| 51 |
+
}).catch(function() {
|
| 52 |
+
// Fallback: try CORS proxy
|
| 53 |
+
var proxyUrl = 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url);
|
| 54 |
+
return _origFetch(proxyUrl);
|
| 55 |
+
});
|
| 56 |
+
};
|
| 57 |
+
console.log('[VAI QUOTE FIX] ✅ fetch patched — CORS proxy for images');
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// =============================================
|
| 61 |
+
// PATCH VAI_QR.getData — handle "Brand ck X%" pattern
|
| 62 |
+
// =============================================
|
| 63 |
+
function patchGetData() {
|
| 64 |
+
if (window.__vaiGetDataFixed) return;
|
| 65 |
+
if (!window.VAI_QR || typeof window.VAI_QR.getData !== 'function') {
|
| 66 |
+
setTimeout(patchGetData, 500);
|
| 67 |
+
return;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
var origGetData = window.VAI_QR.getData;
|
| 71 |
+
window.VAI_QR.getData = function() {
|
| 72 |
+
var result = origGetData.apply(this, arguments);
|
| 73 |
+
|
| 74 |
+
// If no discount yet, try to parse brand+ck pattern
|
| 75 |
+
if ((!result.discountPercent || result.discountPercent === 0) && result.qd && result.qd.items && result.qd.items.length) {
|
| 76 |
+
var text = (window.VAI_QR.getPromptText && window.VAI_QR.getPromptText()) || '';
|
| 77 |
+
if (text) {
|
| 78 |
+
var lines = text.split(/[,;\n]+/);
|
| 79 |
+
for (var i = 0; i < lines.length; i++) {
|
| 80 |
+
var line = lines[i].trim();
|
| 81 |
+
if (!line) continue;
|
| 82 |
+
// Match: "Malloca ck 35%", "Eurogold CK 20", "Grob ck 15" etc.
|
| 83 |
+
var m = line.match(/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+([\d.]+)\s*%/i);
|
| 84 |
+
if (m) {
|
| 85 |
+
var pct = parseFloat(m[2].replace(',', '.'));
|
| 86 |
+
if (pct > 0 && pct <= 100) {
|
| 87 |
+
result.discountPercent = pct;
|
| 88 |
+
// Apply to items
|
| 89 |
+
if (result.qd && result.qd.items) {
|
| 90 |
+
result.qd.items.forEach(function(it) {
|
| 91 |
+
var base = Number(it.price || 0);
|
| 92 |
+
if (base > 0) {
|
| 93 |
+
it.discPrice = Math.round(base * (1 - pct / 100));
|
| 94 |
+
it.total = it.discPrice * Number(it.qty || 1);
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
// Recalc grand total
|
| 98 |
+
var pt = 0;
|
| 99 |
+
result.qd.items.forEach(function(it) { pt += Number(it.total || 0); });
|
| 100 |
+
var sc = 0;
|
| 101 |
+
(result.fees || []).forEach(function(f) { sc += Number(f.amount || 0); });
|
| 102 |
+
result.productTotal = pt;
|
| 103 |
+
result.grandTotal = pt + sc;
|
| 104 |
+
result.remaining = result.grandTotal - (result.deposit || 0);
|
| 105 |
+
if (result.remaining < 0) result.remaining = 0;
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
break;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
// Ensure notes are preserved
|
| 114 |
+
if (result.notes && result.notes.length) {
|
| 115 |
+
// Already set — keep as-is
|
| 116 |
+
}
|
| 117 |
+
return result;
|
| 118 |
+
};
|
| 119 |
+
window.__vaiGetDataFixed = true;
|
| 120 |
+
console.log('[VAI QUOTE FIX] ✅ VAI_QR.getData patched for brand+ck');
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// =============================================
|
| 124 |
+
// ENHANCE QUOTE MODAL — show fees/deposit/notes
|
| 125 |
+
// =============================================
|
| 126 |
+
function enhanceQuoteModal() {
|
| 127 |
+
var modal = document.querySelector('.quote-overlay.open .quote-modal');
|
| 128 |
+
if (!modal) return;
|
| 129 |
+
if (modal.querySelector('.vai-enhanced-fees')) return;
|
| 130 |
+
|
| 131 |
+
setTimeout(function() {
|
| 132 |
+
if (modal.querySelector('.vai-enhanced-fees')) return;
|
| 133 |
+
if (!window.VAI_QR || !window.VAI_QR.getData) return;
|
| 134 |
+
|
| 135 |
+
var data;
|
| 136 |
+
try { data = window.VAI_QR.getData(); } catch(e) { return; }
|
| 137 |
+
if (!data) return;
|
| 138 |
+
|
| 139 |
+
var fees = data.fees || [];
|
| 140 |
+
var notes = data.notes || [];
|
| 141 |
+
var deposit = data.deposit || 0;
|
| 142 |
+
var grandTotal = data.grandTotal || 0;
|
| 143 |
+
var remaining = data.remaining || 0;
|
| 144 |
+
|
| 145 |
+
if (!fees.length && !deposit && !notes.length) return;
|
| 146 |
+
|
| 147 |
+
var actions = modal.querySelector('.quote-actions');
|
| 148 |
+
if (!actions) return;
|
| 149 |
+
|
| 150 |
+
var sec = document.createElement('div');
|
| 151 |
+
sec.className = 'vai-enhanced-fees';
|
| 152 |
+
var html = '';
|
| 153 |
+
|
| 154 |
+
if (fees.length) {
|
| 155 |
+
html += '<div style="padding:6px 12px;background:#fffbeb;border-radius:6px;border:1px solid #fde68a;margin-bottom:6px;font-size:11px">';
|
| 156 |
+
html += '<div style="font-weight:700;color:#92400e;margin-bottom:3px">📦 PHỤ PHÍ</div>';
|
| 157 |
+
fees.forEach(function(f) {
|
| 158 |
+
html += '<div style="display:flex;justify-content:space-between;padding:1px 0"><span>'+f.label+'</span><b>'+Number(f.amount||0).toLocaleString('vi-VN')+'đ</b></div>';
|
| 159 |
+
});
|
| 160 |
+
html += '</div>';
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
if (deposit > 0) {
|
| 164 |
+
html += '<div style="padding:5px 12px;background:#f0fdf4;border-radius:6px;border:1px solid #86efac;margin-bottom:6px;font-size:11px">';
|
| 165 |
+
html += '<div style="display:flex;justify-content:space-between"><span>💵 Đã cọc</span><b style="color:#166534">'+Number(deposit).toLocaleString('vi-VN')+'đ</b></div>';
|
| 166 |
+
html += '<div style="display:flex;justify-content:space-between;margin-top:2px"><span style="font-weight:700">📊 Còn lại</span><b style="color:#dc2626;font-size:13px">'+Number(Math.max(0, remaining)).toLocaleString('vi-VN')+'đ</b></div>';
|
| 167 |
+
html += '</div>';
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if (notes && notes.length) {
|
| 171 |
+
html += '<div style="padding:5px 12px;background:#f0f9ff;border-radius:6px;border:1px solid #bae6fd;font-size:10px;margin-bottom:6px">';
|
| 172 |
+
html += '<b>📝 Ghi chú:</b> '+notes.join('; ');
|
| 173 |
+
html += '</div>';
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if (html) {
|
| 177 |
+
sec.innerHTML = html;
|
| 178 |
+
sec.style.cssText = 'margin:8px 16px;font-size:12px';
|
| 179 |
+
actions.parentNode.insertBefore(sec, actions);
|
| 180 |
+
}
|
| 181 |
+
}, 700);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// =============================================
|
| 185 |
+
// PATCH ORDER DETAIL MODAL — pre-process prompt
|
| 186 |
+
// =============================================
|
| 187 |
+
function patchOrderDetailPrompt() {
|
| 188 |
+
if (window.__vaiOrderPromptFixed) return;
|
| 189 |
+
window.__vaiOrderPromptFixed = true;
|
| 190 |
+
|
| 191 |
+
var observer = new MutationObserver(function() {
|
| 192 |
+
var applyBtn = document.getElementById('od-apply');
|
| 193 |
+
if (!applyBtn) return;
|
| 194 |
+
|
| 195 |
+
// Patch apply button
|
| 196 |
+
if (applyBtn && !applyBtn.dataset.vaiPromptPre) {
|
| 197 |
+
applyBtn.dataset.vaiPromptPre = 'true';
|
| 198 |
+
var origApply = applyBtn.onclick;
|
| 199 |
+
if (origApply) {
|
| 200 |
+
applyBtn.onclick = function(e) {
|
| 201 |
+
var input = document.getElementById('od-prompt');
|
| 202 |
+
if (input) {
|
| 203 |
+
var val = input.value || '';
|
| 204 |
+
var processed = val.replace(
|
| 205 |
+
/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+/gi,
|
| 206 |
+
'ck '
|
| 207 |
+
);
|
| 208 |
+
if (processed !== val) {
|
| 209 |
+
input.value = processed;
|
| 210 |
+
console.log('[VAI QUOTE FIX] Pre-processed prompt: "'+val+'" → "'+processed+'"');
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
return origApply.call(this, e);
|
| 214 |
+
};
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Patch save button
|
| 219 |
+
var saveBtn = document.getElementById('od-save');
|
| 220 |
+
if (saveBtn && !saveBtn.dataset.vaiPromptPre) {
|
| 221 |
+
saveBtn.dataset.vaiPromptPre = 'true';
|
| 222 |
+
var origSave = saveBtn.onclick;
|
| 223 |
+
if (origSave) {
|
| 224 |
+
saveBtn.onclick = function(e) {
|
| 225 |
+
var input = document.getElementById('od-prompt');
|
| 226 |
+
if (input) {
|
| 227 |
+
var val = input.value || '';
|
| 228 |
+
var processed = val.replace(
|
| 229 |
+
/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+/gi,
|
| 230 |
+
'ck '
|
| 231 |
+
);
|
| 232 |
+
if (processed !== val) input.value = processed;
|
| 233 |
+
}
|
| 234 |
+
return origSave.call(this, e);
|
| 235 |
+
};
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
});
|
| 239 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// =============================================
|
| 243 |
+
// WATCH PROMPT CHANGE — re-render fee section
|
| 244 |
+
// =============================================
|
| 245 |
+
function watchPromptChange() {
|
| 246 |
+
var lastVal = '';
|
| 247 |
+
setInterval(function() {
|
| 248 |
+
var input = document.getElementById('qcAiPrompt');
|
| 249 |
+
if (!input) {
|
| 250 |
+
var all = document.querySelectorAll('input,textarea');
|
| 251 |
+
for (var i = 0; i < all.length; i++) {
|
| 252 |
+
var ph = (all[i].placeholder || '').toLowerCase();
|
| 253 |
+
if (ph.indexOf('yêu cầu') >= 0 || ph.indexOf('chiết khấu') >= 0 ||
|
| 254 |
+
ph.indexOf('ck') >= 0 || ph.indexOf('giao hàng') >= 0) {
|
| 255 |
+
input = all[i];
|
| 256 |
+
break;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
if (!input) return;
|
| 261 |
+
if (input.value !== lastVal) {
|
| 262 |
+
lastVal = input.value;
|
| 263 |
+
var old = document.querySelector('.vai-enhanced-fees');
|
| 264 |
+
if (old) old.remove();
|
| 265 |
+
enhanceQuoteModal();
|
| 266 |
+
}
|
| 267 |
+
}, 800);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// =============================================
|
| 271 |
+
// PATCH parsePrompt to handle brand+ck pattern
|
| 272 |
+
// =============================================
|
| 273 |
+
function patchParsePrompt() {
|
| 274 |
+
if (window.__vaiParsePromptFixed) return;
|
| 275 |
+
if (!window.VAI_QR || typeof window.VAI_QR.parsePrompt !== 'function') {
|
| 276 |
+
setTimeout(patchParsePrompt, 500);
|
| 277 |
+
return;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
var origParse = window.VAI_QR.parsePrompt;
|
| 281 |
+
var patchedParse = function(text) {
|
| 282 |
+
var result = origParse.call(this, text);
|
| 283 |
+
if (!text) return result;
|
| 284 |
+
var lines = text.split(/[,;\n]+/);
|
| 285 |
+
for (var i = 0; i < lines.length; i++) {
|
| 286 |
+
var line = lines[i].trim();
|
| 287 |
+
if (!line) continue;
|
| 288 |
+
// Match: "Malloca ck 35%" etc. — treat as global CK
|
| 289 |
+
var m = line.match(/(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\s+ck\s+([\d.]+)\s*%/i);
|
| 290 |
+
if (m) {
|
| 291 |
+
var pct = parseFloat(m[2].replace(',', '.'));
|
| 292 |
+
if (pct > 0 && pct <= 100) {
|
| 293 |
+
result.discountPercent = pct;
|
| 294 |
+
}
|
| 295 |
+
break;
|
| 296 |
+
}
|
| 297 |
+
// Also handle just "ck X%" at start
|
| 298 |
+
var ckM = line.match(/^ck\s+([\d.]+)\s*%/i);
|
| 299 |
+
if (ckM) {
|
| 300 |
+
var cpct = parseFloat(ckM[1].replace(',', '.'));
|
| 301 |
+
if (cpct > 0 && cpct <= 100) {
|
| 302 |
+
result.discountPercent = cpct;
|
| 303 |
+
}
|
| 304 |
+
break;
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
return result;
|
| 308 |
+
};
|
| 309 |
+
|
| 310 |
+
window.VAI_QR.parsePrompt = patchedParse;
|
| 311 |
+
window.__vaiParsePromptFixed = true;
|
| 312 |
+
console.log('[VAI QUOTE FIX] ✅ VAI_QR.parsePrompt patched');
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// =============================================
|
| 316 |
+
// OVERRIDE _doExportExcel with image-fixed version
|
| 317 |
+
// NOT overriding — the CORS proxy patch to fetch() handles this
|
| 318 |
+
// =============================================
|
| 319 |
+
|
| 320 |
+
// =============================================
|
| 321 |
+
// INIT
|
| 322 |
+
// =============================================
|
| 323 |
+
function init() {
|
| 324 |
+
console.log('[VAI QUOTE FIX 2026] Initializing...');
|
| 325 |
+
|
| 326 |
+
// Fix 1: Patch fetch for CORS images (applies to Excel export)
|
| 327 |
+
patchFetchWithCORSProxy();
|
| 328 |
+
|
| 329 |
+
// Fix 2: Patch VAI_QR.getData for brand+ck parsing
|
| 330 |
+
setTimeout(patchGetData, 1000);
|
| 331 |
+
|
| 332 |
+
// Fix 3: Patch parsePrompt for brand+ck
|
| 333 |
+
setTimeout(patchParsePrompt, 1000);
|
| 334 |
+
|
| 335 |
+
// Fix 4: Enhance quote modal with fees/deposit/notes
|
| 336 |
+
setInterval(enhanceQuoteModal, 2000);
|
| 337 |
+
|
| 338 |
+
// Fix 5: Watch prompt changes and re-render
|
| 339 |
+
setTimeout(watchPromptChange, 2000);
|
| 340 |
+
|
| 341 |
+
// Fix 6: Patch order detail modal prompt pre-processing
|
| 342 |
+
setTimeout(patchOrderDetailPrompt, 1500);
|
| 343 |
+
|
| 344 |
+
console.log('[VAI QUOTE FIX 2026] ✅ Done');
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
if (document.readyState === 'loading') {
|
| 348 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 349 |
+
} else {
|
| 350 |
+
init();
|
| 351 |
+
}
|
| 352 |
+
})();
|
vai-robust-export-v2.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — Robust Export v2 (ULTIMATE FIX)
|
| 3 |
+
*
|
| 4 |
+
* VẤN ĐỀ GỐC:
|
| 5 |
+
* 1. Blob URL bị revoke sau 60s → không tải lại lần 2 được
|
| 6 |
+
* 2. qr-payment.js override window.exportExcel (no params)
|
| 7 |
+
* nhưng order-store.js gọi exportExcel(order, opt) → routing sai
|
| 8 |
+
* 3. Excel/PDF từ quote modal gọi sai function export
|
| 9 |
+
* 4. getData() có thể trả về undefined khi quote modal đã đóng
|
| 10 |
+
*
|
| 11 |
+
* FIX:
|
| 12 |
+
* - Giữ blob URLs vĩnh viễn (chỉ cleanup khi page unload)
|
| 13 |
+
* - Cache blob URLs để tải lại nhiều lần
|
| 14 |
+
* - Đồng bộ routing: window.exportExcel → luôn hoạt động
|
| 15 |
+
* - Export từ quote modal và order detail modal đều OK
|
| 16 |
+
* - Hỗ trợ tải file từ history (lịch sử đã export)
|
| 17 |
+
*/
|
| 18 |
+
(function() {
|
| 19 |
+
'use strict';
|
| 20 |
+
|
| 21 |
+
// === BLOB URL CACHE - GIEEP VINH VIEN ===
|
| 22 |
+
var blobCache = {};
|
| 23 |
+
var urlCache = {};
|
| 24 |
+
var EXPORT_HISTORY_KEY = 'vai_export_history';
|
| 25 |
+
|
| 26 |
+
function getExportHistory() {
|
| 27 |
+
try { return JSON.parse(localStorage.getItem(EXPORT_HISTORY_KEY) || '[]'); } catch(e) { return []; }
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function addExportHistory(code, type, fileName) {
|
| 31 |
+
var hist = getExportHistory();
|
| 32 |
+
hist.unshift({ code: code, type: type, fileName: fileName, date: new Date().toISOString() });
|
| 33 |
+
if (hist.length > 50) hist = hist.slice(0, 50);
|
| 34 |
+
try { localStorage.setItem(EXPORT_HISTORY_KEY, JSON.stringify(hist)); } catch(e) {}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// NEVER revoke blob URLs for our exports
|
| 38 |
+
var _origRevoke = URL.revokeObjectURL;
|
| 39 |
+
URL.revokeObjectURL = function(url) {
|
| 40 |
+
// Never revoke blob URLs that we created for exports
|
| 41 |
+
if (url && url.startsWith('blob:') && urlCache[url]) {
|
| 42 |
+
// Keep it alive - just log
|
| 43 |
+
console.log('[RobustExport] Blocked revoke for cached blob:', url.substring(0, 30) + '...');
|
| 44 |
+
return undefined;
|
| 45 |
+
}
|
| 46 |
+
return _origRevoke.call(URL, url);
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
// Create blob URL with caching - multi-download support
|
| 50 |
+
function createCachedBlobURL(blob, key) {
|
| 51 |
+
var cacheKey = key || blob.size + '-' + blob.type;
|
| 52 |
+
// Return existing URL if same blob
|
| 53 |
+
if (blobCache[cacheKey]) {
|
| 54 |
+
return blobCache[cacheKey];
|
| 55 |
+
}
|
| 56 |
+
var url = URL.createObjectURL(blob);
|
| 57 |
+
blobCache[cacheKey] = url;
|
| 58 |
+
urlCache[url] = { key: cacheKey, createdAt: Date.now() };
|
| 59 |
+
return url;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// === ROBUST DOWNLOAD - multiple times ===
|
| 63 |
+
function robustDownload(blob, fileName, code, type) {
|
| 64 |
+
// Create cached URL
|
| 65 |
+
var url = createCachedBlobURL(blob, fileName);
|
| 66 |
+
|
| 67 |
+
// Method 1: Download via <a> tag
|
| 68 |
+
var a = document.createElement('a');
|
| 69 |
+
a.href = url;
|
| 70 |
+
a.download = fileName;
|
| 71 |
+
a.style.display = 'none';
|
| 72 |
+
document.body.appendChild(a);
|
| 73 |
+
a.click();
|
| 74 |
+
|
| 75 |
+
// Keep the element for a moment then remove
|
| 76 |
+
setTimeout(function() {
|
| 77 |
+
document.body.removeChild(a);
|
| 78 |
+
}, 1000);
|
| 79 |
+
|
| 80 |
+
console.log('[RobustExport] ✅ ' + fileName + ' — blob URL cached, re-download available until page unload');
|
| 81 |
+
|
| 82 |
+
// Add to export history
|
| 83 |
+
addExportHistory(code, type, fileName);
|
| 84 |
+
|
| 85 |
+
// Store in window for re-download
|
| 86 |
+
if (!window._vaiExportCache) window._vaiExportCache = {};
|
| 87 |
+
window._vaiExportCache[code] = window._vaiExportCache[code] || {};
|
| 88 |
+
window._vaiExportCache[code][type] = { url: url, fileName: fileName, blob: blob };
|
| 89 |
+
|
| 90 |
+
return url;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Re-download a previously exported file
|
| 94 |
+
window._vaiReDownload = function(code, type) {
|
| 95 |
+
if (!window._vaiExportCache || !window._vaiExportCache[code]) {
|
| 96 |
+
console.warn('[RobustExport] No cached export for', code, type);
|
| 97 |
+
return false;
|
| 98 |
+
}
|
| 99 |
+
var entry = window._vaiExportCache[code][type];
|
| 100 |
+
if (!entry) {
|
| 101 |
+
console.warn('[RobustExport] No cached export type', type, 'for', code);
|
| 102 |
+
return false;
|
| 103 |
+
}
|
| 104 |
+
var a = document.createElement('a');
|
| 105 |
+
a.href = entry.url;
|
| 106 |
+
a.download = entry.fileName;
|
| 107 |
+
a.style.display = 'none';
|
| 108 |
+
document.body.appendChild(a);
|
| 109 |
+
a.click();
|
| 110 |
+
setTimeout(function() { document.body.removeChild(a); }, 1000);
|
| 111 |
+
console.log('[RobustExport] Re-download:', entry.fileName);
|
| 112 |
+
return true;
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
// === PATCH ORDER-STORE EXPORT FUNCTIONS ===
|
| 116 |
+
function patchOrderStoreExports() {
|
| 117 |
+
// Override exportExcel in order-store to actually work
|
| 118 |
+
window.exportExcel = window.exportExcel || function() {};
|
| 119 |
+
window.exportPDF = window.exportPDF || function() {};
|
| 120 |
+
window.exportDeliveryExcel = window.exportDeliveryExcel || function() {};
|
| 121 |
+
window.exportDeliveryPDF = window.exportDeliveryPDF || function() {};
|
| 122 |
+
|
| 123 |
+
var origOE = window.exportExcel;
|
| 124 |
+
window.exportExcel = async function(order, opt) {
|
| 125 |
+
console.log('[RobustExport] exportExcel called', order ? order.code : 'from quote modal');
|
| 126 |
+
try {
|
| 127 |
+
// If from quote modal (no order arg) — use VAI_QR
|
| 128 |
+
if (!order) {
|
| 129 |
+
if (window.VAI_QR && window.VAI_QR.exportExcel) {
|
| 130 |
+
try {
|
| 131 |
+
var d = window.VAI_QR.getData ? window.VAI_QR.getData() : null;
|
| 132 |
+
if (d && d.qd) {
|
| 133 |
+
var code = window.VAI_QR.getEffectiveOrderCode ? window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 134 |
+
var qrUrl = window.VAI_QR.getQRUrl ? window.VAI_QR.getQRUrl(d.deposit > 0 ? d.remaining : d.grandTotal, code) : '';
|
| 135 |
+
if (typeof _doExportExcel === 'function') {
|
| 136 |
+
await _doExportExcel(d, d.qd, code, qrUrl, !!(opt && opt.bw));
|
| 137 |
+
}
|
| 138 |
+
return;
|
| 139 |
+
}
|
| 140 |
+
} catch(e) {
|
| 141 |
+
console.error('[RobustExport] Quote modal export failed:', e);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
// Fallback: call the qr-payment global exportExcel
|
| 145 |
+
if (typeof window.exportExcel === 'function' && window.exportExcel !== arguments.callee) {
|
| 146 |
+
await window.exportExcel.call(this, order, opt);
|
| 147 |
+
}
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// From order detail modal — route to VAI_QR
|
| 152 |
+
if (window.VAI_QR && window.VAI_QR.exportExcel) {
|
| 153 |
+
var d = window.VAI_QR.getData ? window.VAI_QR.getData() : null;
|
| 154 |
+
if (d && d.qd) {
|
| 155 |
+
var code = window.VAI_QR.getEffectiveOrderCode ? window.VAI_QR.getEffectiveOrderCode() : order.code;
|
| 156 |
+
var qrUrl = window.VAI_QR.getQRUrl ? window.VAI_QR.getQRUrl(order.remaining || order.grandTotal, code) : '';
|
| 157 |
+
if (typeof _doExportExcel === 'function') {
|
| 158 |
+
await _doExportExcel(d, d.qd, code, qrUrl, !!(opt && opt.bw));
|
| 159 |
+
}
|
| 160 |
+
} else if (typeof _doExportExcel === 'function' && order.items) {
|
| 161 |
+
// Create data from order directly
|
| 162 |
+
var qd = { customer: order, items: order.items || [] };
|
| 163 |
+
var code = order.code || 'BAOGIA';
|
| 164 |
+
var qrUrl = '';
|
| 165 |
+
await _doExportExcel({qd: qd, fees: order.fees || [], discountPercent: order.discountPercent || 0, itemDiscounts: order.itemDiscounts || {}, deposit: order.deposit || 0, grandTotal: order.grandTotal || 0, remaining: order.remaining || 0, notes: []}, qd, code, qrUrl, !!(opt && opt.bw));
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
} catch(e) {
|
| 169 |
+
console.error('[RobustExport] exportExcel error:', e);
|
| 170 |
+
}
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
window.exportPDF = async function(order, opt) {
|
| 174 |
+
try {
|
| 175 |
+
if (!order) {
|
| 176 |
+
if (window.VAI_QR && window.VAI_QR.exportPDF) {
|
| 177 |
+
try {
|
| 178 |
+
var d = window.VAI_QR.getData ? window.VAI_QR.getData() : null;
|
| 179 |
+
if (d) {
|
| 180 |
+
await window.VAI_QR.exportPDF(d, window.VAI_QR.getEffectiveOrderCode ? window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA');
|
| 181 |
+
return;
|
| 182 |
+
}
|
| 183 |
+
} catch(e) {}
|
| 184 |
+
}
|
| 185 |
+
return;
|
| 186 |
+
}
|
| 187 |
+
if (window.VAI_QR && window.VAI_QR.exportPDF) {
|
| 188 |
+
var d = window.VAI_QR.getData ? window.VAI_QR.getData() : null;
|
| 189 |
+
if (d) await window.VAI_QR.exportPDF(d, order.code);
|
| 190 |
+
}
|
| 191 |
+
} catch(e) {
|
| 192 |
+
console.error('[RobustExport] exportPDF error:', e);
|
| 193 |
+
}
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
// PATCH: GH Excel
|
| 197 |
+
window.exportDeliveryExcel = async function(order, opt) {
|
| 198 |
+
console.log('[RobustExport] exportDeliveryExcel called');
|
| 199 |
+
try {
|
| 200 |
+
if (!order) {
|
| 201 |
+
if (window.VAI_QR && window.VAI_QR.exportDeliveryExcel) {
|
| 202 |
+
try {
|
| 203 |
+
var qd = window._origGetQuoteData ? window._origGetQuoteData() : null;
|
| 204 |
+
if (qd) {
|
| 205 |
+
await window.VAI_QR.exportDeliveryExcel(qd, window.VAI_QR.getEffectiveOrderCode ? window.VAI_QR.getEffectiveOrderCode() : 'GH-BAOGIA');
|
| 206 |
+
return;
|
| 207 |
+
}
|
| 208 |
+
} catch(e) {}
|
| 209 |
+
}
|
| 210 |
+
return;
|
| 211 |
+
}
|
| 212 |
+
if (window.VAI_QR && window.VAI_QR.exportDeliveryExcel) {
|
| 213 |
+
var qd = { customer: order, items: order.items || [] };
|
| 214 |
+
await window.VAI_QR.exportDeliveryExcel(qd, order.code || 'GH-ORDER');
|
| 215 |
+
}
|
| 216 |
+
} catch(e) {
|
| 217 |
+
console.error('[RobustExport] exportDeliveryExcel error:', e);
|
| 218 |
+
}
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
// PATCH: GH PDF
|
| 222 |
+
window.exportDeliveryPDF = async function(order, opt) {
|
| 223 |
+
console.log('[RobustExport] exportDeliveryPDF called');
|
| 224 |
+
try {
|
| 225 |
+
if (!order) {
|
| 226 |
+
if (window.VAI_QR && window.VAI_QR.exportDeliveryPDF) {
|
| 227 |
+
try {
|
| 228 |
+
var qd = window._origGetQuoteData ? window._origGetQuoteData() : null;
|
| 229 |
+
if (qd) {
|
| 230 |
+
await window.VAI_QR.exportDeliveryPDF(qd, window.VAI_QR.getEffectiveOrderCode ? window.VAI_QR.getEffectiveOrderCode() : 'GH-BAOGIA');
|
| 231 |
+
return;
|
| 232 |
+
}
|
| 233 |
+
} catch(e) {}
|
| 234 |
+
}
|
| 235 |
+
return;
|
| 236 |
+
}
|
| 237 |
+
if (window.VAI_QR && window.VAI_QR.exportDeliveryPDF) {
|
| 238 |
+
var qd = { customer: order, items: order.items || [] };
|
| 239 |
+
await window.VAI_QR.exportDeliveryPDF(qd, order.code || 'GH-ORDER');
|
| 240 |
+
}
|
| 241 |
+
} catch(e) {
|
| 242 |
+
console.error('[RobustExport] exportDeliveryPDF error:', e);
|
| 243 |
+
}
|
| 244 |
+
};
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// === WRAP _doExportExcel TO USE ROBUST DOWNLOAD ===
|
| 248 |
+
function wrapDoExportExcel() {
|
| 249 |
+
if (typeof _doExportExcel !== 'function' || window._vaiWrappedDoExportExcel) return;
|
| 250 |
+
window._vaiWrappedDoExportExcel = true;
|
| 251 |
+
|
| 252 |
+
var orig = _doExportExcel;
|
| 253 |
+
window.origDoExportExcel = orig;
|
| 254 |
+
|
| 255 |
+
// Replace _doExportExcel with wrapped version that uses robust download
|
| 256 |
+
_doExportExcel = async function(d, qd, code, qrUrl, bw) {
|
| 257 |
+
try {
|
| 258 |
+
// Call original to create the buffer
|
| 259 |
+
await orig(d, qd, code, qrUrl, bw);
|
| 260 |
+
console.log('[RobustExport] Excel export completed for', code);
|
| 261 |
+
} catch(e) {
|
| 262 |
+
console.error('[RobustExport] Excel export error:', e);
|
| 263 |
+
|
| 264 |
+
// Emergency fallback: manual Excel via table
|
| 265 |
+
try {
|
| 266 |
+
console.log('[RobustExport] Fallback: manual table-to-Excel');
|
| 267 |
+
var html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>BaoGia</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body><table>';
|
| 268 |
+
html += '<tr><th colspan="10" style="font-size:18px;color:#db9815;text-align:center">BẢNG BÁO GIÁ</th></tr>';
|
| 269 |
+
html += '<tr><td colspan="2"><b>Khách hàng:</b> '+(qd.customer?qd.customer.name:'')+'</td><td colspan="3"><b>Mã đơn:</b> '+code+'</td></tr>';
|
| 270 |
+
html += '<tr><th style="background:#003f62;color:#fff">STT</th><th style="background:#003f62;color:#fff">Tên SP</th><th style="background:#003f62;color:#fff">Mã</th><th style="background:#003f62;color:#fff">SL</th><th style="background:#003f62;color:#fff">Đơn giá</th><th style="background:#003f62;color:#fff">Giá CK</th><th style="background:#003f62;color:#fff">TT</th></tr>';
|
| 271 |
+
(qd.items||[]).forEach(function(it,i){
|
| 272 |
+
html += '<tr><td>'+(it.stt||(i+1))+'</td><td>'+(it.name||'')+'</td><td>'+(it.model||'')+'</td><td>'+(it.qty||1)+'</td><td>'+(it.price||0)+'</td><td>'+(it.discPrice||it.price||0)+'</td><td>'+(it.total||(it.discPrice||it.price||0)*(it.qty||1))+'</td></tr>';
|
| 273 |
+
});
|
| 274 |
+
html += '<tr><td colspan="6" style="font-weight:bold;text-align:right">TỔNG CỘNG</td><td style="font-weight:bold">'+(d.grandTotal||0)+'</td></tr>';
|
| 275 |
+
html += '</table></body></html>';
|
| 276 |
+
|
| 277 |
+
var blob = new Blob([html], {type:'application/vnd.ms-excel'});
|
| 278 |
+
robustDownload(blob, code+'.xls', code, 'Excel');
|
| 279 |
+
} catch(e2) {
|
| 280 |
+
console.error('[RobustExport] Fallback also failed:', e2);
|
| 281 |
+
alert('❌ Lỗi xuất Excel, thử lại sau');
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
};
|
| 285 |
+
|
| 286 |
+
// Also wrap delivery export
|
| 287 |
+
if (typeof _doExportDeliveryExcel === 'function') {
|
| 288 |
+
var origDE = _doExportDeliveryExcel;
|
| 289 |
+
_doExportDeliveryExcel = async function(qd, code) {
|
| 290 |
+
try {
|
| 291 |
+
await origDE(qd, code);
|
| 292 |
+
} catch(e) {
|
| 293 |
+
console.error('[RobustExport] Delivery Excel error:', e);
|
| 294 |
+
}
|
| 295 |
+
};
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// Wrap PDF export
|
| 299 |
+
if (typeof _doExportPDF === 'function') {
|
| 300 |
+
var origPDF = _doExportPDF;
|
| 301 |
+
_doExportPDF = async function(d, code) {
|
| 302 |
+
try {
|
| 303 |
+
await origPDF(d, code);
|
| 304 |
+
} catch(e) {
|
| 305 |
+
console.error('[RobustExport] PDF error:', e);
|
| 306 |
+
}
|
| 307 |
+
};
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// Wrap delivery PDF
|
| 311 |
+
if (typeof _doExportDeliveryPDF === 'function') {
|
| 312 |
+
var origDPDF = _doExportDeliveryPDF;
|
| 313 |
+
_doExportDeliveryPDF = async function(qd, code) {
|
| 314 |
+
try {
|
| 315 |
+
await origDPDF(qd, code);
|
| 316 |
+
} catch(e) {
|
| 317 |
+
console.error('[RobustExport] Delivery PDF error:', e);
|
| 318 |
+
}
|
| 319 |
+
};
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
console.log('[RobustExport] ✅ Wrapped _doExportExcel for robust download');
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// === RE-DOWNLOAD UI BUTTONS ===
|
| 326 |
+
function injectRedownloadUI() {
|
| 327 |
+
var modal = document.getElementById('vai-order-detail-modal');
|
| 328 |
+
if (!modal) return;
|
| 329 |
+
|
| 330 |
+
var existing = modal.querySelector('.vai-redownload-section');
|
| 331 |
+
if (existing) existing.remove();
|
| 332 |
+
|
| 333 |
+
var code = null;
|
| 334 |
+
var hd = modal.querySelector('[style*="background:#003f62"],[style*="background:#003f62"]');
|
| 335 |
+
if (hd) {
|
| 336 |
+
var txt = hd.textContent || '';
|
| 337 |
+
var m = txt.match(/(VAS\d+|DH\d+|BAOGIA)/);
|
| 338 |
+
if (m) code = m[1];
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
if (!code || !window._vaiExportCache || !window._vaiExportCache[code]) return;
|
| 342 |
+
|
| 343 |
+
var exports = window._vaiExportCache[code];
|
| 344 |
+
var types = Object.keys(exports);
|
| 345 |
+
if (!types.length) return;
|
| 346 |
+
|
| 347 |
+
var sec = document.createElement('div');
|
| 348 |
+
sec.className = 'vai-redownload-section';
|
| 349 |
+
sec.style.cssText = 'margin:8px 0;padding:6px 10px;background:#f0fdf4;border-radius:8px;border:1px solid #86efac';
|
| 350 |
+
sec.innerHTML = '<div style="font-size:10px;font-weight:700;color:#166534;margin-bottom:4px">📥 Tải lại file đã xuất:</div>' +
|
| 351 |
+
types.map(function(t) {
|
| 352 |
+
return '<button class="vai-redl-btn" data-code="'+code+'" data-type="'+t+'" style="padding:4px 8px;background:#16a34a;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:10px;font-weight:700;margin-right:4px;margin-bottom:4px">⬇ ' + t + '</button>';
|
| 353 |
+
}).join('');
|
| 354 |
+
|
| 355 |
+
var actionsArea = modal.querySelector('[class*="action"],[style*="flex-wrap"]');
|
| 356 |
+
if (actionsArea) {
|
| 357 |
+
actionsArea.parentNode.insertBefore(sec, actionsArea.nextSibling);
|
| 358 |
+
} else {
|
| 359 |
+
var lastDiv = modal.querySelector('div:last-child');
|
| 360 |
+
if (lastDiv) lastDiv.appendChild(sec);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
sec.querySelectorAll('.vai-redl-btn').forEach(function(btn) {
|
| 364 |
+
btn.onclick = function() {
|
| 365 |
+
window._vaiReDownload(this.dataset.code, this.dataset.type);
|
| 366 |
+
};
|
| 367 |
+
});
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
setInterval(injectRedownloadUI, 2000);
|
| 371 |
+
|
| 372 |
+
// === PATCH BUTTON CLICKS (quote modal) ===
|
| 373 |
+
function patchQuoteModalButtons() {
|
| 374 |
+
document.querySelectorAll('.quote-btn-excel').forEach(function(btn) {
|
| 375 |
+
if (btn.dataset.vaiPatched) return;
|
| 376 |
+
btn.dataset.vaiPatched = '1';
|
| 377 |
+
btn.onclick = function(e) {
|
| 378 |
+
e.preventDefault();
|
| 379 |
+
e.stopPropagation();
|
| 380 |
+
console.log('[RobustExport] Quote modal Excel button clicked');
|
| 381 |
+
window.exportExcel();
|
| 382 |
+
return false;
|
| 383 |
+
};
|
| 384 |
+
});
|
| 385 |
+
document.querySelectorAll('.quote-btn-pdf').forEach(function(btn) {
|
| 386 |
+
if (btn.dataset.vaiPatched) return;
|
| 387 |
+
btn.dataset.vaiPatched = '1';
|
| 388 |
+
btn.onclick = function(e) {
|
| 389 |
+
e.preventDefault();
|
| 390 |
+
e.stopPropagation();
|
| 391 |
+
console.log('[RobustExport] Quote modal PDF button clicked');
|
| 392 |
+
window.exportPDF();
|
| 393 |
+
return false;
|
| 394 |
+
};
|
| 395 |
+
});
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
setInterval(patchQuoteModalButtons, 1500);
|
| 399 |
+
|
| 400 |
+
// === INIT ===
|
| 401 |
+
function init() {
|
| 402 |
+
patchOrderStoreExports();
|
| 403 |
+
console.log('[RobustExport v2] ✅ Loaded — blob URLs cached forever, multi-download OK, export routing fixed');
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
if (document.readyState === 'loading') {
|
| 407 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 408 |
+
} else {
|
| 409 |
+
init();
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Wrap export functions when they become available
|
| 413 |
+
var maxRetry = 30;
|
| 414 |
+
var retryCount = 0;
|
| 415 |
+
var retryTimer = setInterval(function() {
|
| 416 |
+
wrapDoExportExcel();
|
| 417 |
+
retryCount++;
|
| 418 |
+
if (retryCount >= maxRetry) clearInterval(retryTimer);
|
| 419 |
+
}, 500);
|
| 420 |
+
|
| 421 |
+
// Cleanup blob URLs on page unload (not sooner!)
|
| 422 |
+
window.addEventListener('pagehide', function() {
|
| 423 |
+
Object.keys(blobCache).forEach(function(key) {
|
| 424 |
+
var url = blobCache[key];
|
| 425 |
+
try { _origRevoke.call(URL, url); } catch(e) {}
|
| 426 |
+
});
|
| 427 |
+
blobCache = {};
|
| 428 |
+
urlCache = {};
|
| 429 |
+
});
|
| 430 |
+
})();
|
vai-ultimate-fix-v1100.js
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — ULTIMATE FIX v1100 (FINAL)
|
| 3 |
+
*
|
| 4 |
+
* ===== VẤN ĐỀ =====
|
| 5 |
+
* 1. _doExportExcel trong qr-payment.js tạo blob URL và revoke sau 60s → không tải lại
|
| 6 |
+
* 2. 3 file fix cũ (v1042, robust-v2, multi-download) ghi đè lẫn nhau → conflict
|
| 7 |
+
* 3. Nut Excell/PDF trên modal không bind đúng hoặc bị ghi đè nhiều lần
|
| 8 |
+
* 4. Window._vaiExportCache bị các file fix cũ ghi đè sai → không tìm thấy file đã xuất
|
| 9 |
+
*
|
| 10 |
+
* ===== FIX TRIỆT ĐỂ =====
|
| 11 |
+
* 1. Intercept URL.createObjectURL + URL.revokeObjectURL — GIỮ blob VĨNH VIỄN
|
| 12 |
+
* 2. Lưu tất cả blob export vào _vaiFinalExportStore (dùng array, ko dùng object dễ bị ghi đè)
|
| 13 |
+
* 3. Thêm UI "Tải lại file đã xuất" vào cả modal báo giá và modal chi tiết đơn hàng
|
| 14 |
+
* 4. Bind trực tiếp các nút export (ko qua trung gian)
|
| 15 |
+
* 5. Xuất lại file mới nếu blob cũ không còn (tự động fallback)
|
| 16 |
+
*/
|
| 17 |
+
(function() {
|
| 18 |
+
'use strict';
|
| 19 |
+
console.log('[VAI ULTIMATE FIX v1100] === LOADING ===');
|
| 20 |
+
|
| 21 |
+
// =============================================
|
| 22 |
+
// 1. GLOBAL EXPORT STORE (ARRAY — không bị ghi đè)
|
| 23 |
+
// =============================================
|
| 24 |
+
window._vaiFinalExportStore = window._vaiFinalExportStore || [];
|
| 25 |
+
window._vaiFinalExportStore.all = function() { return this; };
|
| 26 |
+
window._vaiFinalExportStore.add = function(entry) { this.push(entry); };
|
| 27 |
+
window._vaiFinalExportStore.getByCode = function(code) {
|
| 28 |
+
return this.filter(function(e) { return e.code === code; });
|
| 29 |
+
};
|
| 30 |
+
window._vaiFinalExportStore.clear = function() { this.length = 0; };
|
| 31 |
+
|
| 32 |
+
// =============================================
|
| 33 |
+
// 2. INTERCEPT URL.createObjectURL + revokeObjectURL
|
| 34 |
+
// =============================================
|
| 35 |
+
if (!window._vaiFinalBlobIntercept) {
|
| 36 |
+
window._vaiFinalBlobIntercept = true;
|
| 37 |
+
|
| 38 |
+
var _origCreate = URL.createObjectURL;
|
| 39 |
+
var _origRevoke = URL.revokeObjectURL;
|
| 40 |
+
var _blobMap = {}; // url -> blob
|
| 41 |
+
|
| 42 |
+
URL.createObjectURL = function(blob) {
|
| 43 |
+
var url = _origCreate.call(URL, blob);
|
| 44 |
+
if (blob instanceof Blob) {
|
| 45 |
+
_blobMap[url] = blob;
|
| 46 |
+
// Auto-register if it looks like an export (xlsx, xls, pdf)
|
| 47 |
+
var isExport = false;
|
| 48 |
+
if (blob.type) {
|
| 49 |
+
if (blob.type.indexOf('spreadsheet') >= 0 ||
|
| 50 |
+
blob.type.indexOf('excel') >= 0 ||
|
| 51 |
+
blob.type.indexOf('openxml') >= 0 ||
|
| 52 |
+
blob.type.indexOf('pdf') >= 0) {
|
| 53 |
+
isExport = true;
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
if (!isExport && blob.size > 512) {
|
| 57 |
+
isExport = true; // conservative: any non-tiny blob
|
| 58 |
+
}
|
| 59 |
+
console.log('[VAI ULTIMATE] 📦 Blob stored [' + (isExport ? 'EXPORT' : 'other') + '] size=' +
|
| 60 |
+
(blob.size/1024).toFixed(1) + 'KB type=' + blob.type);
|
| 61 |
+
}
|
| 62 |
+
return url;
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
URL.revokeObjectURL = function(url) {
|
| 66 |
+
if (url && url.startsWith('blob:') && _blobMap[url]) {
|
| 67 |
+
// NEVER revoke — keep forever
|
| 68 |
+
return undefined;
|
| 69 |
+
}
|
| 70 |
+
// For non-cached blobs, still block to be safe
|
| 71 |
+
if (url && url.startsWith('blob:')) {
|
| 72 |
+
return undefined;
|
| 73 |
+
}
|
| 74 |
+
return _origRevoke.call(URL, url);
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
// Cleanup only on page unload
|
| 78 |
+
window.addEventListener('pagehide', function() {
|
| 79 |
+
var keys = Object.keys(_blobMap);
|
| 80 |
+
keys.forEach(function(url) {
|
| 81 |
+
try { _origRevoke.call(URL, url); } catch(e) {}
|
| 82 |
+
});
|
| 83 |
+
_blobMap = {};
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
window._vaiFinalBlobMap = _blobMap;
|
| 87 |
+
window._vaiFinalOrigRevoke = _origRevoke;
|
| 88 |
+
|
| 89 |
+
console.log('[VAI ULTIMATE] ✅ URL.createObjectURL/revokeObjectURL intercepted');
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// =============================================
|
| 93 |
+
// 3. REGISTER EXPORT — lưu vào store
|
| 94 |
+
// =============================================
|
| 95 |
+
function registerExport(fileName, code, type, url) {
|
| 96 |
+
if (!code) code = 'BAOGIA';
|
| 97 |
+
if (!type) type = 'Excel';
|
| 98 |
+
|
| 99 |
+
// Remove old entry for same code+type (if exists)
|
| 100 |
+
var store = window._vaiFinalExportStore;
|
| 101 |
+
for (var i = 0; i < store.length; i++) {
|
| 102 |
+
if (store[i].code === code && store[i].type === type) {
|
| 103 |
+
store.splice(i, 1);
|
| 104 |
+
break;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
store.add({
|
| 109 |
+
code: code,
|
| 110 |
+
type: type,
|
| 111 |
+
fileName: fileName,
|
| 112 |
+
url: url,
|
| 113 |
+
date: new Date().toISOString()
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
console.log('[VAI ULTIMATE] ✅ Export registered: [' + type + '] ' + code + ' → ' + fileName);
|
| 117 |
+
|
| 118 |
+
// Also set on window._vaiExportCache for backward compat
|
| 119 |
+
if (!window._vaiExportCache) window._vaiExportCache = {};
|
| 120 |
+
if (!window._vaiExportCache[code]) window._vaiExportCache[code] = {};
|
| 121 |
+
window._vaiExportCache[code][type] = { url: url, fileName: fileName, date: new Date().toISOString() };
|
| 122 |
+
|
| 123 |
+
// Inject re-download UI
|
| 124 |
+
setTimeout(injectRedownloadUI, 100);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// =============================================
|
| 128 |
+
// 4. WRAP EXPORT FUNCTIONS
|
| 129 |
+
// =============================================
|
| 130 |
+
var _wrapAttempts = 0;
|
| 131 |
+
var _wrapTimer = setInterval(function() {
|
| 132 |
+
_wrapAttempts++;
|
| 133 |
+
|
| 134 |
+
// Wrap _doExportExcel
|
| 135 |
+
if (typeof _doExportExcel === 'function' && !window._vaiUltimateWrappedExcel) {
|
| 136 |
+
window._vaiUltimateWrappedExcel = true;
|
| 137 |
+
var origExcel = _doExportExcel;
|
| 138 |
+
_doExportExcel = async function(d, qd, code, qrUrl, bw) {
|
| 139 |
+
try {
|
| 140 |
+
await origExcel(d, qd, code, qrUrl, bw);
|
| 141 |
+
// Find the blob URL that was just created
|
| 142 |
+
setTimeout(function() {
|
| 143 |
+
var url = findLatestBlobURL(code);
|
| 144 |
+
if (url) {
|
| 145 |
+
registerExport(code + '.xlsx', code, 'Excel', url);
|
| 146 |
+
}
|
| 147 |
+
}, 100);
|
| 148 |
+
} catch(e) {
|
| 149 |
+
console.error('[VAI ULTIMATE] Excel error:', e);
|
| 150 |
+
// Fallback: simple HTML table export
|
| 151 |
+
try {
|
| 152 |
+
await fallbackHTMLTableExport(qd, code, 'Excel');
|
| 153 |
+
} catch(e2) {
|
| 154 |
+
console.error('[VAI ULTIMATE] Fallback failed:', e2);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
};
|
| 158 |
+
console.log('[VAI ULTIMATE] ✅ Wrapped _doExportExcel');
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// Wrap _doExportDeliveryExcel
|
| 162 |
+
if (typeof _doExportDeliveryExcel === 'function' && !window._vaiUltimateWrappedDelivery) {
|
| 163 |
+
window._vaiUltimateWrappedDelivery = true;
|
| 164 |
+
var origDel = _doExportDeliveryExcel;
|
| 165 |
+
_doExportDeliveryExcel = async function(qd, code) {
|
| 166 |
+
try {
|
| 167 |
+
await origDel(qd, code);
|
| 168 |
+
setTimeout(function() {
|
| 169 |
+
var url = findLatestBlobURL(code);
|
| 170 |
+
if (url) {
|
| 171 |
+
registerExport('GH-' + code + '.xlsx', code, 'GiaoHang', url);
|
| 172 |
+
}
|
| 173 |
+
}, 100);
|
| 174 |
+
} catch(e) {
|
| 175 |
+
console.error('[VAI ULTIMATE] Delivery Excel error:', e);
|
| 176 |
+
}
|
| 177 |
+
};
|
| 178 |
+
console.log('[VAI ULTIMATE] ✅ Wrapped _doExportDeliveryExcel');
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Wrap _doExportPDF
|
| 182 |
+
if (typeof _doExportPDF === 'function' && !window._vaiUltimateWrappedPDF) {
|
| 183 |
+
window._vaiUltimateWrappedPDF = true;
|
| 184 |
+
var origPDF = _doExportPDF;
|
| 185 |
+
_doExportPDF = async function(d, code) {
|
| 186 |
+
try {
|
| 187 |
+
await origPDF(d, code);
|
| 188 |
+
setTimeout(function() {
|
| 189 |
+
var url = findLatestBlobURL(code);
|
| 190 |
+
if (url) {
|
| 191 |
+
registerExport(code + '.pdf', code, 'PDF', url);
|
| 192 |
+
}
|
| 193 |
+
}, 100);
|
| 194 |
+
} catch(e) {
|
| 195 |
+
console.error('[VAI ULTIMATE] PDF error:', e);
|
| 196 |
+
}
|
| 197 |
+
};
|
| 198 |
+
console.log('[VAI ULTIMATE] ✅ Wrapped _doExportPDF');
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// Wrap _doExportDeliveryPDF
|
| 202 |
+
if (typeof _doExportDeliveryPDF === 'function' && !window._vaiUltimateWrappedDelPDF) {
|
| 203 |
+
window._vaiUltimateWrappedDelPDF = true;
|
| 204 |
+
var origDelPDF = _doExportDeliveryPDF;
|
| 205 |
+
_doExportDeliveryPDF = async function(qd, code) {
|
| 206 |
+
try {
|
| 207 |
+
await origDelPDF(qd, code);
|
| 208 |
+
setTimeout(function() {
|
| 209 |
+
var url = findLatestBlobURL(code);
|
| 210 |
+
if (url) {
|
| 211 |
+
registerExport('GH-' + code + '.pdf', code, 'GH-PDF', url);
|
| 212 |
+
}
|
| 213 |
+
}, 100);
|
| 214 |
+
} catch(e) {
|
| 215 |
+
console.error('[VAI ULTIMATE] Delivery PDF error:', e);
|
| 216 |
+
}
|
| 217 |
+
};
|
| 218 |
+
console.log('[VAI ULTIMATE] ✅ Wrapped _doExportDeliveryPDF');
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// Stop after 30 attempts (15s)
|
| 222 |
+
if (_wrapAttempts >= 30) {
|
| 223 |
+
clearInterval(_wrapTimer);
|
| 224 |
+
console.log('[VAI ULTIMATE] Wrap attempts finished (' + _wrapAttempts + ')');
|
| 225 |
+
}
|
| 226 |
+
}, 500);
|
| 227 |
+
|
| 228 |
+
// =============================================
|
| 229 |
+
// 5. FIND LATEST BLOB URL
|
| 230 |
+
// =============================================
|
| 231 |
+
function findLatestBlobURL(code) {
|
| 232 |
+
var map = window._vaiFinalBlobMap || {};
|
| 233 |
+
var urls = Object.keys(map);
|
| 234 |
+
// Look for the most recent blob matching export characteristics
|
| 235 |
+
for (var i = urls.length - 1; i >= 0; i--) {
|
| 236 |
+
var url = urls[i];
|
| 237 |
+
var blob = map[url];
|
| 238 |
+
if (blob && blob.type) {
|
| 239 |
+
var t = blob.type;
|
| 240 |
+
if (t.indexOf('spreadsheet') >= 0 || t.indexOf('excel') >= 0 ||
|
| 241 |
+
t.indexOf('openxml') >= 0 || t.indexOf('pdf') >= 0) {
|
| 242 |
+
// Check if this blob is already registered
|
| 243 |
+
var already = false;
|
| 244 |
+
var store = window._vaiFinalExportStore;
|
| 245 |
+
for (var j = 0; j < store.length; j++) {
|
| 246 |
+
if (store[j].url === url) { already = true; break; }
|
| 247 |
+
}
|
| 248 |
+
if (!already) return url;
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
// Fallback: return any unregistered blob
|
| 253 |
+
var store = window._vaiFinalExportStore;
|
| 254 |
+
for (var k = urls.length - 1; k >= 0; k--) {
|
| 255 |
+
var u = urls[k];
|
| 256 |
+
var found = false;
|
| 257 |
+
for (var l = 0; l < store.length; l++) {
|
| 258 |
+
if (store[l].url === u) { found = true; break; }
|
| 259 |
+
}
|
| 260 |
+
if (!found) return u;
|
| 261 |
+
}
|
| 262 |
+
return null;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// =============================================
|
| 266 |
+
// 6. FALLBACK EXPORT (HTML table → xls)
|
| 267 |
+
// =============================================
|
| 268 |
+
async function fallbackHTMLTableExport(qd, code, type) {
|
| 269 |
+
var html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>BaoGia</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body><table>';
|
| 270 |
+
html += '<tr><th colspan="10" style="font-size:18px;color:#db9815;text-align:center;background:#003f62;color:#fff">BẢNG BÁO GIÁ</th></tr>';
|
| 271 |
+
html += '<tr><td colspan="2"><b>KH:</b> ' + (qd.customer ? qd.customer.name : '') + '</td><td colspan="3"><b>Mã:</b> ' + code + '</td></tr>';
|
| 272 |
+
html += '<tr style="background:#003f62;color:#fff"><th>STT</th><th>Tên SP</th><th>Mã</th><th>SL</th><th>Đơn giá</th><th>Giá CK</th><th>Thành tiền</th></tr>';
|
| 273 |
+
(qd.items || []).forEach(function(it, i) {
|
| 274 |
+
html += '<tr><td>' + (it.stt || (i + 1)) + '</td><td>' + (it.name || '') + '</td><td>' + (it.model || '') + '</td><td>' + (it.qty || 1) + '</td><td>' + Number(it.price || 0).toLocaleString('vi-VN') + '</td><td>' + Number(it.discPrice || it.price || 0).toLocaleString('vi-VN') + '</td><td>' + Number((it.discPrice || it.price || 0) * (it.qty || 1)).toLocaleString('vi-VN') + '</td></tr>';
|
| 275 |
+
});
|
| 276 |
+
var total = (qd.items || []).reduce(function(s, it) { return s + Number((it.discPrice || it.price || 0) * (it.qty || 1)); }, 0);
|
| 277 |
+
html += '<tr style="font-weight:bold;background:#003f62;color:#fff"><td colspan="6" style="text-align:right">TỔNG CỘNG</td><td style="color:#f0b840">' + total.toLocaleString('vi-VN') + 'đ</td></tr>';
|
| 278 |
+
html += '</table></body></html>';
|
| 279 |
+
|
| 280 |
+
var blob = new Blob([html], { type: 'application/vnd.ms-excel' });
|
| 281 |
+
var url = URL.createObjectURL(blob);
|
| 282 |
+
registerExport(code + '.xls', code, 'Excel', url);
|
| 283 |
+
|
| 284 |
+
downloadFromURL(url, code + '.xls');
|
| 285 |
+
console.log('[VAI ULTIMATE] Fallback export done: ' + code);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// =============================================
|
| 289 |
+
// 7. DOWNLOAD HELPER
|
| 290 |
+
// =============================================
|
| 291 |
+
function downloadFromURL(url, fileName) {
|
| 292 |
+
var a = document.createElement('a');
|
| 293 |
+
a.href = url;
|
| 294 |
+
a.download = fileName;
|
| 295 |
+
a.style.display = 'none';
|
| 296 |
+
a.target = '_blank';
|
| 297 |
+
document.body.appendChild(a);
|
| 298 |
+
a.click();
|
| 299 |
+
setTimeout(function() {
|
| 300 |
+
try { document.body.removeChild(a); } catch(e) {}
|
| 301 |
+
}, 500);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// =============================================
|
| 305 |
+
// 8. INJECT RE-DOWNLOAD UI
|
| 306 |
+
// =============================================
|
| 307 |
+
function injectRedownloadUI() {
|
| 308 |
+
// Find the currently open modal
|
| 309 |
+
var modal = findOpenModal();
|
| 310 |
+
if (!modal) return;
|
| 311 |
+
|
| 312 |
+
// Get the order code from the modal
|
| 313 |
+
var code = extractCodeFromModal(modal);
|
| 314 |
+
if (!code) return;
|
| 315 |
+
|
| 316 |
+
// Check if we have exports for this code
|
| 317 |
+
var exports = window._vaiFinalExportStore.getByCode(code);
|
| 318 |
+
if (!exports.length) {
|
| 319 |
+
// Also check _vaiExportCache (backward compat)
|
| 320 |
+
if (window._vaiExportCache && window._vaiExportCache[code]) {
|
| 321 |
+
var cache = window._vaiExportCache[code];
|
| 322 |
+
Object.keys(cache).forEach(function(type) {
|
| 323 |
+
if (cache[type] && cache[type].url) {
|
| 324 |
+
exports.push({
|
| 325 |
+
code: code,
|
| 326 |
+
type: type,
|
| 327 |
+
fileName: cache[type].fileName || (code + '.' + (type === 'PDF' ? 'pdf' : 'xlsx')),
|
| 328 |
+
url: cache[type].url,
|
| 329 |
+
date: cache[type].date || ''
|
| 330 |
+
});
|
| 331 |
+
}
|
| 332 |
+
});
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
if (!exports.length) return;
|
| 337 |
+
|
| 338 |
+
// Remove old section
|
| 339 |
+
var oldSec = modal.querySelector('.vai-ultimate-redownload');
|
| 340 |
+
if (oldSec) oldSec.remove();
|
| 341 |
+
|
| 342 |
+
// Build section
|
| 343 |
+
var sec = document.createElement('div');
|
| 344 |
+
sec.className = 'vai-ultimate-redownload';
|
| 345 |
+
sec.style.cssText = 'margin:8px 14px;padding:8px 12px;background:#f0fdf4;border-radius:8px;border:2px solid #86efac;font-family:Inter,system-ui,sans-serif';
|
| 346 |
+
|
| 347 |
+
var labels = {
|
| 348 |
+
'Excel': '📊 Báo giá Excel',
|
| 349 |
+
'PDF': '📄 Báo giá PDF',
|
| 350 |
+
'GiaoHang': '📦 Phiếu giao hàng',
|
| 351 |
+
'GH-PDF': '📦 Phiếu giao hàng PDF'
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
var html = '<div style="font-size:10px;font-weight:700;color:#166534;margin-bottom:5px">📥 <b>Tải lại file đã xuất</b> (không giới hạn số lần):</div><div style="display:flex;flex-wrap:wrap;gap:4px">';
|
| 355 |
+
|
| 356 |
+
exports.forEach(function(exp) {
|
| 357 |
+
var label = labels[exp.type] || ('📎 ' + exp.type);
|
| 358 |
+
html += '<button class="vai-ultimate-redl-btn" data-code="' + code + '" data-type="' + exp.type + '" data-url="' + (exp.url || '') + '" data-fn="' + (exp.fileName || code + '.xlsx') + '" style="padding:5px 10px;background:#16a34a;color:#fff;border:none;border-radius:5px;cursor:pointer;font-size:10px;font-weight:700;display:inline-flex;align-items:center;gap:3px">⬇ ' + label + '</button>';
|
| 359 |
+
});
|
| 360 |
+
|
| 361 |
+
html += '</div>';
|
| 362 |
+
sec.innerHTML = html;
|
| 363 |
+
|
| 364 |
+
// Find insertion point — after quote-actions or in the modal body
|
| 365 |
+
var actions = modal.querySelector('.quote-actions, .modal-footer, [class*="footer"], [class*="action"]');
|
| 366 |
+
if (actions && actions.parentNode) {
|
| 367 |
+
actions.parentNode.insertBefore(sec, actions.nextSibling);
|
| 368 |
+
} else {
|
| 369 |
+
var body = modal.querySelector('.quote-body, .modal-body, .modal-content') || modal;
|
| 370 |
+
body.appendChild(sec);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
// Bind click events
|
| 374 |
+
sec.querySelectorAll('.vai-ultimate-redl-btn').forEach(function(btn) {
|
| 375 |
+
btn.onclick = function(e) {
|
| 376 |
+
e.preventDefault();
|
| 377 |
+
e.stopPropagation();
|
| 378 |
+
|
| 379 |
+
var url = this.dataset.url;
|
| 380 |
+
var fn = this.dataset.fn;
|
| 381 |
+
var c = this.dataset.code;
|
| 382 |
+
var t = this.dataset.type;
|
| 383 |
+
|
| 384 |
+
// Try to download from cached URL
|
| 385 |
+
if (url && url.startsWith('blob:')) {
|
| 386 |
+
downloadFromURL(url, fn);
|
| 387 |
+
// Visual feedback
|
| 388 |
+
var origText = this.innerHTML;
|
| 389 |
+
this.innerHTML = '✅ Đã tải!';
|
| 390 |
+
this.style.background = '#15803d';
|
| 391 |
+
setTimeout(function() {
|
| 392 |
+
if (btn) {
|
| 393 |
+
var labels = {
|
| 394 |
+
'Excel': '📊 Báo giá Excel',
|
| 395 |
+
'PDF': '📄 Báo giá PDF',
|
| 396 |
+
'GiaoHang': '📦 Phiếu giao hàng',
|
| 397 |
+
'GH-PDF': '📦 Phiếu giao hàng PDF'
|
| 398 |
+
};
|
| 399 |
+
btn.innerHTML = '⬇ ' + (labels[t] || '📎 ' + t);
|
| 400 |
+
btn.style.background = '#16a34a';
|
| 401 |
+
}
|
| 402 |
+
}, 1500);
|
| 403 |
+
return false;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// If URL is gone, try to re-export
|
| 407 |
+
alert('⚠️ File đã xuất không còn trong bộ nhớ. Đang xuất lại...');
|
| 408 |
+
if (window.VAI_QR) {
|
| 409 |
+
if (t === 'Excel' && typeof window.VAI_QR.exportExcel === 'function') {
|
| 410 |
+
window.VAI_QR.exportExcel();
|
| 411 |
+
} else if (t === 'PDF' && typeof window.VAI_QR.exportPDF === 'function') {
|
| 412 |
+
window.VAI_QR.exportPDF();
|
| 413 |
+
} else if (t === 'GiaoHang' && typeof window.VAI_QR.exportDeliveryExcel === 'function') {
|
| 414 |
+
window.VAI_QR.exportDeliveryExcel();
|
| 415 |
+
}
|
| 416 |
+
} else {
|
| 417 |
+
// Try via window functions
|
| 418 |
+
if (t === 'Excel' && typeof window.exportExcel === 'function') {
|
| 419 |
+
window.exportExcel();
|
| 420 |
+
} else if (t === 'PDF' && typeof window.exportPDF === 'function') {
|
| 421 |
+
window.exportPDF();
|
| 422 |
+
} else if (t === 'GiaoHang' && typeof window.exportDeliveryExcel === 'function') {
|
| 423 |
+
window.exportDeliveryExcel();
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
return false;
|
| 427 |
+
};
|
| 428 |
+
});
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
// =============================================
|
| 432 |
+
// 9. FIND OPEN MODAL
|
| 433 |
+
// =============================================
|
| 434 |
+
function findOpenModal() {
|
| 435 |
+
// 1. Quote modal
|
| 436 |
+
var quoteModal = document.querySelector('.quote-overlay.open .quote-modal, .quote-overlay[style*="flex"] .quote-modal');
|
| 437 |
+
if (quoteModal) return quoteModal;
|
| 438 |
+
|
| 439 |
+
// 2. Order detail modal
|
| 440 |
+
var orderModal = document.getElementById('vai-order-detail-modal');
|
| 441 |
+
if (orderModal && orderModal.offsetParent !== null) {
|
| 442 |
+
var modalContent = orderModal.querySelector('div[style*="max-width"]');
|
| 443 |
+
if (modalContent) return modalContent;
|
| 444 |
+
return orderModal;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// 3. Any visible modal
|
| 448 |
+
var modals = document.querySelectorAll('[class*="modal"][style*="block"], [class*="modal"][style*="flex"]');
|
| 449 |
+
for (var i = 0; i < modals.length; i++) {
|
| 450 |
+
if (modals[i].offsetWidth > 0 || modals[i].offsetHeight > 0) {
|
| 451 |
+
return modals[i];
|
| 452 |
+
}
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
return null;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
// =============================================
|
| 459 |
+
// 10. EXTRACT CODE FROM MODAL
|
| 460 |
+
// =============================================
|
| 461 |
+
function extractCodeFromModal(modal) {
|
| 462 |
+
if (!modal) return null;
|
| 463 |
+
var text = modal.textContent || '';
|
| 464 |
+
|
| 465 |
+
// Try VAS code pattern
|
| 466 |
+
var m = text.match(/VAS[A-Z0-9]{5,}/);
|
| 467 |
+
if (m) return m[0];
|
| 468 |
+
|
| 469 |
+
// Try DH pattern
|
| 470 |
+
m = text.match(/DH\d{3,}/);
|
| 471 |
+
if (m) return m[0];
|
| 472 |
+
|
| 473 |
+
// Try BAOGIA
|
| 474 |
+
if (text.indexOf('BÁO GIÁ') >= 0 || text.indexOf('Bao gia') >= 0) {
|
| 475 |
+
return 'BAOGIA';
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
// Try to find customer name + generate code
|
| 479 |
+
var inputs = modal.querySelectorAll('input');
|
| 480 |
+
for (var j = 0; j < inputs.length; j++) {
|
| 481 |
+
var ph = (inputs[j].placeholder || '').toLowerCase();
|
| 482 |
+
var val = (inputs[j].value || '').trim();
|
| 483 |
+
if ((ph.indexOf('khách') >= 0 || ph.indexOf('khach') >= 0 || ph.indexOf('tên') >= 0) && val.length > 1) {
|
| 484 |
+
// Generate code from name
|
| 485 |
+
var now = new Date();
|
| 486 |
+
var dd = String(now.getDate()).padStart(2, '0');
|
| 487 |
+
var mm = String(now.getMonth() + 1).padStart(2, '0');
|
| 488 |
+
var yy = String(now.getFullYear()).slice(-2);
|
| 489 |
+
var name = val.trim();
|
| 490 |
+
var ini = '';
|
| 491 |
+
name.split(/\s+/).forEach(function(w) {
|
| 492 |
+
if (w) {
|
| 493 |
+
var ch = w.charAt(0).toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
| 494 |
+
if (/[A-Z]/.test(ch)) ini += ch;
|
| 495 |
+
}
|
| 496 |
+
});
|
| 497 |
+
return 'VAS' + (ini || 'X') + dd + mm + yy;
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// Fallback to any code-like word
|
| 502 |
+
m = text.match(/\b[A-Z]{2,}\d{3,}\b/);
|
| 503 |
+
if (m) return m[0];
|
| 504 |
+
|
| 505 |
+
return null;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
// =============================================
|
| 509 |
+
// 11. PATCH EXPORT BUTTONS DIRECTLY
|
| 510 |
+
// =============================================
|
| 511 |
+
function patchAllExportButtons() {
|
| 512 |
+
document.querySelectorAll('button, a, [role="button"]').forEach(function(btn) {
|
| 513 |
+
var text = (btn.textContent || '').toLowerCase();
|
| 514 |
+
var cls = (btn.className || '').toLowerCase();
|
| 515 |
+
|
| 516 |
+
// Excel export buttons
|
| 517 |
+
if ((text.indexOf('excel') >= 0 || cls.indexOf('excel') >= 0) &&
|
| 518 |
+
(text.indexOf('xuất') >= 0 || text.indexOf('export') >= 0 || cls.indexOf('quote-btn-excel') >= 0)) {
|
| 519 |
+
btn.dataset.vaiUltimateFixed = 'true';
|
| 520 |
+
btn.onclick = function(e) {
|
| 521 |
+
e.preventDefault();
|
| 522 |
+
e.stopPropagation();
|
| 523 |
+
console.log('[VAI ULTIMATE] Excel button clicked');
|
| 524 |
+
|
| 525 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportExcel === 'function') {
|
| 526 |
+
window.VAI_QR.exportExcel();
|
| 527 |
+
} else if (typeof window.exportExcel === 'function') {
|
| 528 |
+
window.exportExcel();
|
| 529 |
+
} else if (typeof _doExportExcel === 'function') {
|
| 530 |
+
var d = (typeof getData === 'function') ? getData() : null;
|
| 531 |
+
if (d) {
|
| 532 |
+
var code = (window.VAI_QR && window.VAI_QR.getEffectiveOrderCode) ?
|
| 533 |
+
window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 534 |
+
var qrUrl = (window.VAI_QR && window.VAI_QR.getQRUrl) ?
|
| 535 |
+
window.VAI_QR.getQRUrl(d.deposit > 0 ? d.remaining : d.grandTotal, code) : '';
|
| 536 |
+
_doExportExcel(d, d.qd, code, qrUrl);
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
return false;
|
| 540 |
+
};
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
// PDF export buttons
|
| 544 |
+
if ((text.indexOf('pdf') >= 0 || cls.indexOf('pdf') >= 0) &&
|
| 545 |
+
(text.indexOf('xuất') >= 0 || text.indexOf('export') >= 0 || cls.indexOf('quote-btn-pdf') >= 0)) {
|
| 546 |
+
btn.dataset.vaiUltimatePDFFixed = 'true';
|
| 547 |
+
btn.onclick = function(e) {
|
| 548 |
+
e.preventDefault();
|
| 549 |
+
e.stopPropagation();
|
| 550 |
+
console.log('[VAI ULTIMATE] PDF button clicked');
|
| 551 |
+
|
| 552 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportPDF === 'function') {
|
| 553 |
+
window.VAI_QR.exportPDF();
|
| 554 |
+
} else if (typeof window.exportPDF === 'function') {
|
| 555 |
+
window.exportPDF();
|
| 556 |
+
} else if (typeof _doExportPDF === 'function') {
|
| 557 |
+
var d = (typeof getData === 'function') ? getData() : null;
|
| 558 |
+
if (d) {
|
| 559 |
+
var code = (window.VAI_QR && window.VAI_QR.getEffectiveOrderCode) ?
|
| 560 |
+
window.VAI_QR.getEffectiveOrderCode() : 'BAOGIA';
|
| 561 |
+
_doExportPDF(d, code);
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
return false;
|
| 565 |
+
};
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
// Delivery Excel buttons
|
| 569 |
+
if ((text.indexOf('giao hàng') >= 0 || text.indexOf('gh') >= 0) &&
|
| 570 |
+
text.indexOf('excel') >= 0) {
|
| 571 |
+
btn.dataset.vaiUltimateGHFixed = 'true';
|
| 572 |
+
btn.onclick = function(e) {
|
| 573 |
+
e.preventDefault();
|
| 574 |
+
e.stopPropagation();
|
| 575 |
+
console.log('[VAI ULTIMATE] GH Excel button clicked');
|
| 576 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryExcel === 'function') {
|
| 577 |
+
window.VAI_QR.exportDeliveryExcel();
|
| 578 |
+
} else if (typeof window.exportDeliveryExcel === 'function') {
|
| 579 |
+
window.exportDeliveryExcel();
|
| 580 |
+
}
|
| 581 |
+
return false;
|
| 582 |
+
};
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
// Delivery PDF buttons
|
| 586 |
+
if ((text.indexOf('giao hàng') >= 0 || text.indexOf('gh') >= 0) &&
|
| 587 |
+
text.indexOf('pdf') >= 0) {
|
| 588 |
+
btn.dataset.vaiUltimateGHPDFFixed = 'true';
|
| 589 |
+
btn.onclick = function(e) {
|
| 590 |
+
e.preventDefault();
|
| 591 |
+
e.stopPropagation();
|
| 592 |
+
console.log('[VAI ULTIMATE] GH PDF button clicked');
|
| 593 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryPDF === 'function') {
|
| 594 |
+
window.VAI_QR.exportDeliveryPDF();
|
| 595 |
+
} else if (typeof window.exportDeliveryPDF === 'function') {
|
| 596 |
+
window.exportDeliveryPDF();
|
| 597 |
+
}
|
| 598 |
+
return false;
|
| 599 |
+
};
|
| 600 |
+
}
|
| 601 |
+
});
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
// =============================================
|
| 605 |
+
// 12. PATCH ORDER DETAIL MODAL EXPORT BUTTONS
|
| 606 |
+
// =============================================
|
| 607 |
+
function patchOrderDetailButtons() {
|
| 608 |
+
var modal = document.getElementById('vai-order-detail-modal');
|
| 609 |
+
if (!modal) return;
|
| 610 |
+
|
| 611 |
+
var btnIds = ['od-xl', 'od-pdf', 'od-gh-xl', 'od-gh'];
|
| 612 |
+
var handlers = {
|
| 613 |
+
'od-xl': function() {
|
| 614 |
+
console.log('[VAI ULTIMATE] Order detail Excel');
|
| 615 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportExcel === 'function') {
|
| 616 |
+
window.VAI_QR.exportExcel();
|
| 617 |
+
}
|
| 618 |
+
},
|
| 619 |
+
'od-pdf': function() {
|
| 620 |
+
console.log('[VAI ULTIMATE] Order detail PDF');
|
| 621 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportPDF === 'function') {
|
| 622 |
+
window.VAI_QR.exportPDF();
|
| 623 |
+
}
|
| 624 |
+
},
|
| 625 |
+
'od-gh-xl': function() {
|
| 626 |
+
console.log('[VAI ULTIMATE] Order detail GH Excel');
|
| 627 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryExcel === 'function') {
|
| 628 |
+
window.VAI_QR.exportDeliveryExcel();
|
| 629 |
+
}
|
| 630 |
+
},
|
| 631 |
+
'od-gh': function() {
|
| 632 |
+
console.log('[VAI ULTIMATE] Order detail GH PDF');
|
| 633 |
+
if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryPDF === 'function') {
|
| 634 |
+
window.VAI_QR.exportDeliveryPDF();
|
| 635 |
+
}
|
| 636 |
+
}
|
| 637 |
+
};
|
| 638 |
+
|
| 639 |
+
btnIds.forEach(function(id) {
|
| 640 |
+
var btn = document.getElementById(id);
|
| 641 |
+
if (btn) {
|
| 642 |
+
btn.onclick = function(e) {
|
| 643 |
+
e.preventDefault();
|
| 644 |
+
e.stopPropagation();
|
| 645 |
+
if (handlers[id]) handlers[id]();
|
| 646 |
+
return false;
|
| 647 |
+
};
|
| 648 |
+
}
|
| 649 |
+
});
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
// =============================================
|
| 653 |
+
// 13. INIT
|
| 654 |
+
// =============================================
|
| 655 |
+
function init() {
|
| 656 |
+
console.log('[VAI ULTIMATE] Initializing...');
|
| 657 |
+
patchAllExportButtons();
|
| 658 |
+
console.log('[VAI ULTIMATE] ✅ Patch buttons done');
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
// Start on DOM ready
|
| 662 |
+
if (document.readyState === 'loading') {
|
| 663 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 664 |
+
} else {
|
| 665 |
+
init();
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
// =============================================
|
| 669 |
+
// 14. POLLING LOOP — keep everything working
|
| 670 |
+
// =============================================
|
| 671 |
+
|
| 672 |
+
// Re-inject re-download UI every 2s
|
| 673 |
+
setInterval(function() {
|
| 674 |
+
injectRedownloadUI();
|
| 675 |
+
}, 2000);
|
| 676 |
+
|
| 677 |
+
// Re-patch buttons every 3s (for dynamic buttons)
|
| 678 |
+
setInterval(function() {
|
| 679 |
+
patchAllExportButtons();
|
| 680 |
+
}, 3000);
|
| 681 |
+
|
| 682 |
+
// Re-patch order detail buttons every 2s
|
| 683 |
+
setInterval(function() {
|
| 684 |
+
patchOrderDetailButtons();
|
| 685 |
+
}, 2000);
|
| 686 |
+
|
| 687 |
+
console.log('[VAI ULTIMATE v1100] ✅ FULLY LOADED — multi-download working, all buttons bound');
|
| 688 |
+
})();
|