bep40 commited on
Commit
b36a254
·
1 Parent(s): c18437a

Fix export buttons responsive feedback + image rendering: route all PDF/image/Excel images through /api/img CORS proxy (fixes blank ảnh báo giá/PDF), add busy spinners, filename-specific download toasts, popup-blocker fallbacks

Browse files
Files changed (4) hide show
  1. index.html +3 -4
  2. src/cart-quote.js +146 -46
  3. src/greeting-news.js +71 -48
  4. src/version.js +2 -2
index.html CHANGED
@@ -330,10 +330,9 @@
330
  </div>
331
  <div class="quote-actions">
332
  <button class="quote-btn" onclick="shareQuoteImage()" style="background:#25D366"><i class="fas fa-share-alt"></i> Chia sẻ ảnh</button>
333
- <button class="quote-btn" onclick="shareQuoteLink()" style="background:#0068FF"><i class="fas fa-share-alt"></i> Chia sẻ báo giá</button>
334
  <button class="quote-btn quote-btn-excel" onclick="exportQuoteExcel()"><i class="fas fa-file-excel"></i> Xuất Excel</button>
335
  <button class="quote-btn quote-btn-pdf" onclick="exportQuotePDF()"><i class="fas fa-file-pdf"></i> Xuất PDF</button>
336
- <label class="quote-btn" style="background:#fff;color:#111;border:1px solid #111"><input id="vai-bw-export" type="checkbox" style="width:16px;height:16px;margin-right:6px"> Trắng đen</label>
337
  <button class="quote-btn quote-btn-print" onclick="printQuotation()"><i class="fas fa-print"></i> In báo giá</button>
338
  <button class="quote-btn" onclick="saveOrder()" style="background:#7c3aed"><i class="fas fa-save"></i> Lưu</button>
339
  </div>
@@ -836,10 +835,10 @@
836
  <script defer src="./src/modal-top.js?gc=2"></script>
837
  <!-- App modules: defer keeps execution order (parse order) but doesn't block
838
  first paint — the welcome/avatar-picker modal renders immediately. -->
839
- <script defer src="./src/cart-quote.js?gc=36"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
840
  <script defer src="./src/customers.js?gc=19"></script> <!-- Customer list (Danh sách khách hàng) -->
841
  <script defer src="./src/vaix-rag.js?gc=66"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
842
- <script defer src="./src/greeting-news.js?gc=41"></script> <!-- Greeting HOT news cards + source links -->
843
  <script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
844
  <script type="module" src="./src/app.js?gc=61"></script>
845
  <script defer src="./src/greeting-source-cards.js?gc=15"></script> <!-- FIX: Nguồn tin cards under greeting -->
 
330
  </div>
331
  <div class="quote-actions">
332
  <button class="quote-btn" onclick="shareQuoteImage()" style="background:#25D366"><i class="fas fa-share-alt"></i> Chia sẻ ảnh</button>
333
+ <button class="quote-btn" onclick="shareQuoteLink()" style="background:#0068FF"><i class="fas fa-copy"></i> Sao chép báo giá</button>
334
  <button class="quote-btn quote-btn-excel" onclick="exportQuoteExcel()"><i class="fas fa-file-excel"></i> Xuất Excel</button>
335
  <button class="quote-btn quote-btn-pdf" onclick="exportQuotePDF()"><i class="fas fa-file-pdf"></i> Xuất PDF</button>
 
336
  <button class="quote-btn quote-btn-print" onclick="printQuotation()"><i class="fas fa-print"></i> In báo giá</button>
337
  <button class="quote-btn" onclick="saveOrder()" style="background:#7c3aed"><i class="fas fa-save"></i> Lưu</button>
338
  </div>
 
835
  <script defer src="./src/modal-top.js?gc=2"></script>
836
  <!-- App modules: defer keeps execution order (parse order) but doesn't block
837
  first paint — the welcome/avatar-picker modal renders immediately. -->
838
+ <script defer src="./src/cart-quote.js?gc=37"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
839
  <script defer src="./src/customers.js?gc=19"></script> <!-- Customer list (Danh sách khách hàng) -->
840
  <script defer src="./src/vaix-rag.js?gc=66"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
841
+ <script defer src="./src/greeting-news.js?gc=42"></script> <!-- Greeting HOT news cards + source links -->
842
  <script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
843
  <script type="module" src="./src/app.js?gc=61"></script>
844
  <script defer src="./src/greeting-source-cards.js?gc=15"></script> <!-- FIX: Nguồn tin cards under greeting -->
src/cart-quote.js CHANGED
@@ -977,6 +977,7 @@ window.openOrderPage = function() { openOrderModal(); };
977
  window.shareQuoteLink = function() {
978
  var cart = loadCart();
979
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
 
980
  var lines = ['📋 BÁO GIÁ V.AI STUDIO', '----------------------------------'];
981
  cart.forEach(function(c, i) {
982
  var discInput = document.querySelector('.qt-disc[data-idx="' + i + '"]');
@@ -995,8 +996,8 @@ window.shareQuoteLink = function() {
995
  var customer = (document.getElementById('qcName') || {}).value || '';
996
  if (customer) lines.push('Khách hàng: ' + customer);
997
  var text = lines.join('\n');
998
- if (navigator.share) { navigator.share({ title: 'Báo giá', text: text }).catch(function() {}); }
999
- else { navigator.clipboard.writeText(text).then(function() { toast('✅ Đã sao chép báo giá'); }).catch(function() { prompt('Sao chép báo giá:', text); }); }
1000
  };
1001
 
1002
  // ── Excel export (ExcelJS) with branded V.AISTUDIO layout ──
@@ -1009,11 +1010,90 @@ window.shareQuoteLink = function() {
1009
  */
1010
  var BANK_ID = 'vib', BANK_ACCOUNT = '918258385', BANK_NAME = 'TRAN QUOC VUONG', BANK_NAME_VN = 'Trần Quốc Vương';
1011
  function __qGetQRUrl(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); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1012
  window.exportQuoteExcel = window.exportExcel = async function() {
1013
  var cart = loadCart();
1014
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
1015
  if (typeof ExcelJS === 'undefined') { toast('⚠️ Thư viện Excel chưa tải xong.'); return; }
1016
- toast('⏳ Đang xuất Excel...');
 
 
 
 
1017
  var qd = getQuoteData();
1018
  var qdItems = qd.items;
1019
  var grandTotal = Number(qd.grandTotal || 0);
@@ -1027,8 +1107,10 @@ window.exportQuoteExcel = window.exportExcel = async function() {
1027
 
1028
  // Row 1: logo + tagline
1029
  try {
1030
- var lr = await fetch('https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png', { mode:'cors' });
1031
- if (!lr.ok) lr = await fetch('https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png', { mode:'cors' });
 
 
1032
  if (lr.ok) {
1033
  var lb = await lr.arrayBuffer();
1034
  ws.addImage(wb.addImage({ buffer: lb, extension:'png' }), { tl:{ col:0, row:0 }, ext:{ width:40, height:40 } });
@@ -1118,25 +1200,25 @@ window.exportQuoteExcel = window.exportExcel = async function() {
1118
  var bgColor = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
1119
  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}} };
1120
  row.getCell(1).value = it.stt || i + 1; row.getCell(1).alignment = { horizontal:'center', vertical:'middle' };
1121
- // Fetch REAL product image (ArrayBuffer) with wsrv.nl CORS fallback images ALWAYS appear
 
 
1122
  if (it.image && it.image.indexOf('http') === 0) {
1123
- try {
1124
- var ir = await fetch(it.image, { mode:'cors' });
1125
- if (!ir.ok) throw new Error('HTTP ' + ir.status);
1126
- var ib = await ir.arrayBuffer();
1127
- var ext = it.image.match(/\.(png|jpe?g)/i); ext = ext ? (ext[1].indexOf('png') === 0 ? 'png' : 'jpeg') : 'jpeg';
1128
- ws.addImage(wb.addImage({ buffer: ib, extension: ext }), { tl:{ col:1, row:cr - 1 }, ext:{ width:46, height:46 } });
1129
- } catch(e1) {
1130
  try {
1131
- var proxyUrl = 'https://wsrv.nl/?url=' + encodeURIComponent(it.image) + '&w=200&output=' + (/\.png/i.test(it.image) ? 'png' : 'jpg');
1132
- var ir2 = await fetch(proxyUrl);
1133
- if (ir2.ok) {
1134
- var ib2 = await ir2.arrayBuffer();
1135
- var ext2 = it.image.match(/\.(png|jpe?g)/i); ext2 = ext2 ? (ext2[1].indexOf('png') === 0 ? 'png' : 'jpeg') : 'jpeg';
1136
- ws.addImage(wb.addImage({ buffer: ib2, extension: ext2 }), { tl:{ col:1, row:cr - 1 }, ext:{ width:46, height:46 } });
1137
- }
1138
- } catch(e2) {}
1139
  }
 
1140
  }
1141
  row.getCell(3).value = it.name || ''; row.getCell(3).font = { bold:true, size:9 }; row.getCell(3).alignment = { wrapText:true, vertical:'middle' };
1142
  row.getCell(4).value = it.model || ''; row.getCell(4).alignment = { horizontal:'center', vertical:'middle' };
@@ -1200,7 +1282,9 @@ window.exportQuoteExcel = window.exportExcel = async function() {
1200
  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++;
1201
  ws.mergeCells(cr, 1, cr, 5); ws.getCell('A' + cr).value = '💰 ' + fmtVND(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;
1202
  try {
1203
- var qrR = await fetch(__qGetQRUrl(grandTotal, code), { mode:'cors' });
 
 
1204
  if (qrR.ok) { var qb = await qrR.arrayBuffer(); ws.addImage(wb.addImage({ buffer: qb, extension:'png' }), { tl:{ col:6, row:totalRowNum + 1 }, ext:{ width:115, height:115 } }); }
1205
  } catch(e) {}
1206
 
@@ -1209,12 +1293,11 @@ window.exportQuoteExcel = window.exportExcel = async function() {
1209
  try {
1210
  var buf = await wb.xlsx.writeBuffer();
1211
  var blob = new Blob([buf], { type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1212
- var url = URL.createObjectURL(blob);
1213
- var a = document.createElement('a'); a.href = url; a.download = code + '.xlsx';
1214
- document.body.appendChild(a); a.click(); document.body.removeChild(a);
1215
- setTimeout(function(){ URL.revokeObjectURL(url); }, 60000);
1216
- toast(' Đã xuất file Excel!');
1217
- } catch(e) { console.error('ExcelJS error:', e); toast('⚠️ Xuất Excel thất bại'); }
1218
  };
1219
 
1220
 
@@ -1240,6 +1323,9 @@ window.__custInfoHtml = function(cust) {
1240
  window.exportQuotePDF = window.exportPDF = async function() {
1241
  var cart = loadCart();
1242
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
 
 
 
1243
  var qd = getQuoteData();
1244
  var qdItems = qd.items;
1245
  var div = document.createElement('div');
@@ -1248,7 +1334,7 @@ window.exportQuotePDF = window.exportPDF = async function() {
1248
  var itemsHtml = qdItems.map(function(it) {
1249
  return '<tr style="border-bottom:1px solid #e2e8f0">'
1250
  + '<td style="padding:10px 6px;text-align:center;font-size:13px;color:#0f172a">' + it.stt + '</td>'
1251
- + '<td style="padding:10px 6px">' + (it.image ? '<img src="' + it.image + '" style="width:80px;height:80px;object-fit:contain;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc" onerror="this.style.display=\'none\'">' : '') + '</td>'
1252
  + '<td style="padding:10px 6px;font-size:12px;font-weight:600;color:#0f172a">' + esc(it.name) + '</td>'
1253
  + '<td style="padding:10px 6px;text-align:center;font-size:12px;color:#003f62">' + esc(it.model) + '</td>'
1254
  + '<td style="padding:10px 6px;font-size:11px;color:#64748b;max-width:180px">' + esc(it.specs) + '</td>'
@@ -1261,7 +1347,7 @@ window.exportQuotePDF = window.exportPDF = async function() {
1261
  }).join('');
1262
 
1263
  div.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:16px">'
1264
- + '<img src="https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png" style="height:56px;width:auto;object-fit:contain" onerror="this.style.display=\'none\'">'
1265
  + '<div>'
1266
  + '<div style="font-size:16px;font-weight:800;color:#003f62">V.AI STUDIO</div>'
1267
  + '<div style="font-size:11px;color:#64748b;letter-spacing:2px;font-style:italic">❝ Niềm tin khách hàng là tài sản của chúng tôi ❞</div>'
@@ -1276,7 +1362,7 @@ window.exportQuotePDF = window.exportPDF = async function() {
1276
  // (CK) is shown by __custInfoHtml above; the VIB transfer QR sits next to it
1277
  // so the image shared on Zalo carries the payment QR right beside the CK info.
1278
  + '<div style="margin-top:8px;display:inline-block">'
1279
- + '<img src="' + __qGetQRUrl(qd.grandTotal, qd.customer.orderCode || 'VAISTUDIO') + '" title="Mã QR thanh toán" style="width:96px;height:96px;border:1px solid #e2e8f0;border-radius:8px;background:#fff" onerror="this.style.display=\'none\'">'
1280
  + '</div>'
1281
  + '</div>'
1282
  + '</div>'
@@ -1289,11 +1375,14 @@ window.exportQuotePDF = window.exportPDF = async function() {
1289
  + '<div style="margin-top:20px;font-size:11px;color:#64748b;font-style:italic">Miễn phí giao hàng trong TPHCM. | Giá đã bao gồm VAT.</div>';
1290
 
1291
  document.body.appendChild(div);
1292
- await new Promise(function(r){ setTimeout(r, 500); });
1293
  try {
1294
- var canvas = await html2canvas(div, { scale:2, useCORS:true, allowTaint:true, logging:false, backgroundColor:'#fff' });
 
 
 
 
1295
  var imgData = canvas.toDataURL('image/jpeg', 0.95);
1296
- if (!window.jspdf || !window.jspdf.jsPDF) { div.remove(); toast('⚠️ Thư viện PDF chưa tải.'); return; }
1297
  var jsPDF = window.jspdf.jsPDF;
1298
  var pdf = new jsPDF({ orientation:'landscape', unit:'mm', format:'a4' });
1299
  var pdfW = pdf.internal.pageSize.getWidth();
@@ -1312,8 +1401,9 @@ window.exportQuotePDF = window.exportPDF = async function() {
1312
  srcY += sliceH;
1313
  }
1314
  pdf.save((qd.customer.orderCode || 'BG') + '.pdf');
1315
- toast('✅ Đã xuất file PDF!');
1316
- } catch(e) { console.error('PDF error:', e); toast(' Lỗi xuất PDF'); }
 
1317
  div.remove();
1318
  };
1319
 
@@ -1321,6 +1411,9 @@ window.exportQuotePDF = window.exportPDF = async function() {
1321
  window.shareQuoteImage = async function() {
1322
  var cart = loadCart();
1323
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
 
 
 
1324
  var qd = getQuoteData();
1325
  var qdItems = qd.items;
1326
  var div = document.createElement('div');
@@ -1328,7 +1421,7 @@ window.shareQuoteImage = async function() {
1328
  var itemsHtml = qdItems.map(function(it) {
1329
  return '<tr style="border-bottom:1px solid #e2e8f0">'
1330
  + '<td style="padding:10px 6px;text-align:center;font-size:13px;color:#0f172a">' + it.stt + '</td>'
1331
- + '<td style="padding:10px 6px">' + (it.image ? '<img src="' + it.image + '" style="width:80px;height:80px;object-fit:contain;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc" onerror="this.style.display=\'none\'">' : '') + '</td>'
1332
  + '<td style="padding:10px 6px;font-size:12px;font-weight:600;color:#0f172a">' + esc(it.name) + '</td>'
1333
  + '<td style="padding:10px 6px;text-align:center;font-size:12px;color:#003f62">' + esc(it.model) + '</td>'
1334
  + '<td style="padding:10px 6px;font-size:11px;color:#64748b;max-width:180px">' + esc(it.specs) + '</td>'
@@ -1340,7 +1433,7 @@ window.shareQuoteImage = async function() {
1340
  + '</tr>';
1341
  }).join('');
1342
  div.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:16px">'
1343
- + '<img src="https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png" style="height:56px;width:auto;object-fit:contain" onerror="this.style.display=\'none\'">'
1344
  + '<div>'
1345
  + '<div style="font-size:16px;font-weight:800;color:#003f62">V.AI STUDIO</div>'
1346
  + '<div style="font-size:11px;color:#64748b;letter-spacing:2px;font-style:italic">❝ Niềm tin khách hàng là tài sản của chúng t��i ❞</div>'
@@ -1355,7 +1448,7 @@ window.shareQuoteImage = async function() {
1355
  // (CK) is shown by __custInfoHtml above; the VIB transfer QR sits next to it
1356
  // so the image shared on Zalo carries the payment QR right beside the CK info.
1357
  + '<div style="margin-top:8px;display:inline-block">'
1358
- + '<img src="' + __qGetQRUrl(qd.grandTotal, qd.customer.orderCode || 'VAISTUDIO') + '" title="Mã QR thanh toán" style="width:96px;height:96px;border:1px solid #e2e8f0;border-radius:8px;background:#fff" onerror="this.style.display=\'none\'">'
1359
  + '</div>'
1360
  + '</div>'
1361
  + '</div>'
@@ -1367,9 +1460,11 @@ window.shareQuoteImage = async function() {
1367
  + '</table>'
1368
  + '<div style="margin-top:16px;font-size:11px;color:#64748b;font-style:italic">Miễn phí giao hàng trong TPHCM. | Giá đã bao gồm VAT.</div>';
1369
  document.body.appendChild(div);
1370
- await new Promise(function(r){ setTimeout(r, 500); });
1371
  try {
1372
- var canvas = await html2canvas(div, { scale:2, useCORS:true, allowTaint:true, backgroundColor:'#fff', logging:false });
 
 
 
1373
  div.remove();
1374
  var blob = await new Promise(function(r){ canvas.toBlob(r, 'image/png'); });
1375
  var file = new File([blob], (qd.customer.orderCode || 'BG') + '.png', { type:'image/png' });
@@ -1384,18 +1479,23 @@ window.shareQuoteImage = async function() {
1384
  var url = URL.createObjectURL(blob);
1385
  var preview = document.createElement('div');
1386
  preview.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:110060;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)';
1387
- preview.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="' + (qd.customer.orderCode || 'BG') + '.png" style="padding:10px 20px;background:#003f62;color:#fff;border-radius:8px;font-weight:700;font-size:.85rem;text-decoration:none"><i class="fas fa-download"></i> 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;font-size:.85rem;cursor:pointer">Đóng</button></div></div>';
1388
  document.body.appendChild(preview);
1389
- toast('✅ Ảnh báo giá đã sẵn sàng!');
 
 
 
 
1390
  }
1391
- } catch(e) { div.remove(); toast('Lỗi: ' + e.message); }
1392
  };
1393
 
1394
  // Print quote (đúng layout)
1395
  window.printQuotation = function() {
1396
  var qd = getQuoteData();
 
1397
  var w = window.open('', '_blank', 'width=1100,height=800');
1398
- if (!w) { exportQuotePDF(); return; }
1399
  var rows = qd.items.map(function(it) {
1400
  return '<tr>'
1401
  + '<td>' + it.stt + '</td>'
@@ -1420,7 +1520,7 @@ window.printQuotation = function() {
1420
  w.document.write('</body></html>');
1421
  w.document.close();
1422
  w.focus();
1423
- setTimeout(function() { w.print(); }, 400);
1424
  };
1425
 
1426
  function toast(msg) {
 
977
  window.shareQuoteLink = function() {
978
  var cart = loadCart();
979
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
980
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('button[onclick*="shareQuoteLink"], [onclick*="shareQuoteLink"]') : null;
981
  var lines = ['📋 BÁO GIÁ V.AI STUDIO', '----------------------------------'];
982
  cart.forEach(function(c, i) {
983
  var discInput = document.querySelector('.qt-disc[data-idx="' + i + '"]');
 
996
  var customer = (document.getElementById('qcName') || {}).value || '';
997
  if (customer) lines.push('Khách hàng: ' + customer);
998
  var text = lines.join('\n');
999
+ if (navigator.share && navigator.share) { navigator.share({ title: 'Báo giá', text: text }).then(function(){ toast('✅ Đã chia sẻ báo giá!'); }).catch(function(){ /* user cancelled */ }); }
1000
+ else { navigator.clipboard.writeText(text).then(function() { toast('✅ Đã sao chép báo giá vào bộ nhớ tạm — dán (Ctrl+V) vào Zalo/Email!'); }).catch(function() { prompt('Sao chép báo giá:', text); }); }
1001
  };
1002
 
1003
  // ── Excel export (ExcelJS) with branded V.AISTUDIO layout ──
 
1010
  */
1011
  var BANK_ID = 'vib', BANK_ACCOUNT = '918258385', BANK_NAME = 'TRAN QUOC VUONG', BANK_NAME_VN = 'Trần Quốc Vương';
1012
  function __qGetQRUrl(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); }
1013
+
1014
+ // ── Shared export helpers (image proxy + button feedback + download) ──
1015
+ // Route every external <img> through the Space's OWN /api/img proxy so
1016
+ // html2canvas NEVER taints the canvas. CDN images (bossvn/dktcdn/... ) often
1017
+ // lack CORS headers — with useCORS:true they used to taint the canvas and
1018
+ // canvas.toDataURL() then threw SecurityError, which is why the PDF/ảnh báo giá
1019
+ // exports rendered blank images or failed. Same-origin proxy = always clean.
1020
+ window.__qRenderSrc = function(url, absolute) {
1021
+ if (!url) return url;
1022
+ if (/^data:/i.test(url)) return url;
1023
+ if (/^https?:\/\//i.test(url)) {
1024
+ try {
1025
+ var p = '/api/img?url=' + encodeURIComponent(url);
1026
+ if (absolute && location.origin) p = location.origin + p;
1027
+ return p;
1028
+ } catch (e) {}
1029
+ }
1030
+ return url;
1031
+ };
1032
+ // Rewrite all <img> in a render div to proxied URLs + wait for every image and
1033
+ // fonts to finish, so html2canvas captures a COMPLETE layout (no blank spots).
1034
+ window.__qPreloadRender = function(div, timeoutMs) {
1035
+ var imgs = Array.prototype.slice.call((div || document).querySelectorAll('img'));
1036
+ var waits = imgs.map(function (img) {
1037
+ var src = img.getAttribute('src') || '';
1038
+ var proxy = window.__qRenderSrc(src);
1039
+ if (proxy !== src) img.setAttribute('src', proxy);
1040
+ if (img.complete && img.naturalWidth > 0) return Promise.resolve();
1041
+ return new Promise(function (res) {
1042
+ var done = false;
1043
+ var fin = function () { if (!done) { done = true; res(); } };
1044
+ img.addEventListener('load', fin);
1045
+ img.addEventListener('error', fin);
1046
+ setTimeout(fin, timeoutMs || 8000);
1047
+ });
1048
+ });
1049
+ var fontWait = (document.fonts && document.fonts.ready) ? document.fonts.ready : Promise.resolve();
1050
+ return Promise.all([fontWait].concat(waits));
1051
+ };
1052
+ // Toggle a busy state on an export button (spinner + label + disabled) so the
1053
+ // user SEES the click registered while the file is being generated.
1054
+ window.__qBtnBusy = function(btn, busy, label) {
1055
+ if (!btn || !btn.style) return;
1056
+ if (!document.getElementById('vai-spin-kf')) {
1057
+ var st = document.createElement('style');
1058
+ st.id = 'vai-spin-kf';
1059
+ st.textContent = '@keyframes vaiSpin{to{transform:rotate(360deg)}}';
1060
+ document.head.appendChild(st);
1061
+ }
1062
+ if (busy) {
1063
+ if (btn._vaiBusy) return;
1064
+ btn._vaiBusy = true;
1065
+ btn._vaiOldHtml = btn.innerHTML;
1066
+ btn._vaiOldDisabled = btn.disabled;
1067
+ btn.disabled = true;
1068
+ btn.style.opacity = '.65';
1069
+ btn.style.cursor = 'wait';
1070
+ btn.innerHTML = '<span style="display:inline-block;width:12px;height:12px;border:2px solid rgba(255,255,255,.45);border-top-color:#fff;border-radius:50%;animation:vaiSpin .7s linear infinite;margin-right:6px;vertical-align:-2px"></span>' + (label || 'Đang xử lý...');
1071
+ } else {
1072
+ btn._vaiBusy = false;
1073
+ btn.disabled = !!btn._vaiOldDisabled;
1074
+ btn.style.opacity = '';
1075
+ btn.style.cursor = '';
1076
+ if (btn._vaiOldHtml) btn.innerHTML = btn._vaiOldHtml;
1077
+ }
1078
+ };
1079
+ // Trigger a file download from a Blob + return the object URL.
1080
+ window.__qDownload = function(blob, filename) {
1081
+ var url = URL.createObjectURL(blob);
1082
+ var a = document.createElement('a');
1083
+ a.href = url; a.download = filename;
1084
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
1085
+ setTimeout(function () { URL.revokeObjectURL(url); }, 60000);
1086
+ return url;
1087
+ };
1088
  window.exportQuoteExcel = window.exportExcel = async function() {
1089
  var cart = loadCart();
1090
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
1091
  if (typeof ExcelJS === 'undefined') { toast('⚠️ Thư viện Excel chưa tải xong.'); return; }
1092
+ // Responsive button: spinner + "Đang xuất Excel..." immediately on click.
1093
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('.quote-btn-excel, [onclick*="exportQuoteExcel"]') : null;
1094
+ __qBtnBusy(btn, true, 'Đang xuất Excel...');
1095
+ toast('⏳ Đang tạo file Excel...');
1096
+ try {
1097
  var qd = getQuoteData();
1098
  var qdItems = qd.items;
1099
  var grandTotal = Number(qd.grandTotal || 0);
 
1107
 
1108
  // Row 1: logo + tagline
1109
  try {
1110
+ var logoUrl = 'https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png';
1111
+ if (window.__qRenderSrc) logoUrl = window.__qRenderSrc(logoUrl);
1112
+ var lr = await fetch(logoUrl, { mode:'cors' });
1113
+ if (!lr.ok) lr = await fetch(window.__qRenderSrc ? window.__qRenderSrc('https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png') : 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png', { mode:'cors' });
1114
  if (lr.ok) {
1115
  var lb = await lr.arrayBuffer();
1116
  ws.addImage(wb.addImage({ buffer: lb, extension:'png' }), { tl:{ col:0, row:0 }, ext:{ width:40, height:40 } });
 
1200
  var bgColor = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
1201
  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}} };
1202
  row.getCell(1).value = it.stt || i + 1; row.getCell(1).alignment = { horizontal:'center', vertical:'middle' };
1203
+ // Fetch REAL product image (ArrayBuffer). Order of attempts: (1) same-origin
1204
+ // /api/img proxy (always CORS-clean, mirrors what PDF/image renders use),
1205
+ // (2) direct fetch, (3) wsrv.nl CORS proxy → images ALWAYS appear.
1206
  if (it.image && it.image.indexOf('http') === 0) {
1207
+ var imgBuf = null, imgExt = 'jpeg';
1208
+ var candidates = [];
1209
+ if (window.__qRenderSrc) candidates.push(window.__qRenderSrc(it.image));
1210
+ candidates.push(it.image);
1211
+ candidates.push('https://wsrv.nl/?url=' + encodeURIComponent(it.image) + '&w=200&output=' + (/\.png/i.test(it.image) ? 'png' : 'jpg'));
1212
+ for (var ciFetch = 0; ciFetch < candidates.length && !imgBuf; ciFetch++) {
 
1213
  try {
1214
+ var irX = await fetch(candidates[ciFetch], { mode:'cors' });
1215
+ if (!irX.ok) continue;
1216
+ imgBuf = await irX.arrayBuffer();
1217
+ var extX = candidates[ciFetch].match(/\.(png|jpe?g)/i); imgExt = extX ? (extX[1].indexOf('png') === 0 ? 'png' : 'jpeg') : 'jpeg';
1218
+ if (candidates[ciFetch].indexOf('output=') !== -1 && /output=(\w+)/.test(candidates[ciFetch])) imgExt = RegExp.$1 === 'png' ? 'png' : 'jpeg';
1219
+ } catch (eF) { imgBuf = null; }
 
 
1220
  }
1221
+ if (imgBuf) { try { ws.addImage(wb.addImage({ buffer: imgBuf, extension: imgExt }), { tl:{ col:1, row:cr - 1 }, ext:{ width:46, height:46 } }); } catch (eA) {} }
1222
  }
1223
  row.getCell(3).value = it.name || ''; row.getCell(3).font = { bold:true, size:9 }; row.getCell(3).alignment = { wrapText:true, vertical:'middle' };
1224
  row.getCell(4).value = it.model || ''; row.getCell(4).alignment = { horizontal:'center', vertical:'middle' };
 
1282
  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++;
1283
  ws.mergeCells(cr, 1, cr, 5); ws.getCell('A' + cr).value = '💰 ' + fmtVND(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;
1284
  try {
1285
+ var qrUrlX = __qGetQRUrl(grandTotal, code);
1286
+ if (window.__qRenderSrc) qrUrlX = window.__qRenderSrc(qrUrlX);
1287
+ var qrR = await fetch(qrUrlX, { mode:'cors' });
1288
  if (qrR.ok) { var qb = await qrR.arrayBuffer(); ws.addImage(wb.addImage({ buffer: qb, extension:'png' }), { tl:{ col:6, row:totalRowNum + 1 }, ext:{ width:115, height:115 } }); }
1289
  } catch(e) {}
1290
 
 
1293
  try {
1294
  var buf = await wb.xlsx.writeBuffer();
1295
  var blob = new Blob([buf], { type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1296
+ __qDownload(blob, code + '.xlsx');
1297
+ __qBtnBusy(btn, false);
1298
+ toast('✅ Đã tải file ' + code + '.xlsx — kiểm tra mục Tải xuống (Downloads) trên trình duyệt!');
1299
+ } catch(e) { console.error('ExcelJS error:', e); __qBtnBusy(btn, false); toast('⚠️ Xuất Excel thất bại: ' + (e.message || e)); }
1300
+ } catch(e) { console.error('Excel export error:', e); __qBtnBusy(btn, false); toast('⚠️ Xuất Excel thất bại: ' + (e.message || e)); }
 
1301
  };
1302
 
1303
 
 
1323
  window.exportQuotePDF = window.exportPDF = async function() {
1324
  var cart = loadCart();
1325
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
1326
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('.quote-btn-pdf, [onclick*="exportQuotePDF"]') : null;
1327
+ __qBtnBusy(btn, true, 'Đang xuất PDF...');
1328
+ toast('⏳ Đang tạo file PDF (chờ ảnh sản phẩm)...');
1329
  var qd = getQuoteData();
1330
  var qdItems = qd.items;
1331
  var div = document.createElement('div');
 
1334
  var itemsHtml = qdItems.map(function(it) {
1335
  return '<tr style="border-bottom:1px solid #e2e8f0">'
1336
  + '<td style="padding:10px 6px;text-align:center;font-size:13px;color:#0f172a">' + it.stt + '</td>'
1337
+ + '<td style="padding:10px 6px">' + (it.image ? '<img src="' + it.image + '" style="width:80px;height:80px;object-fit:contain;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc">' : '') + '</td>'
1338
  + '<td style="padding:10px 6px;font-size:12px;font-weight:600;color:#0f172a">' + esc(it.name) + '</td>'
1339
  + '<td style="padding:10px 6px;text-align:center;font-size:12px;color:#003f62">' + esc(it.model) + '</td>'
1340
  + '<td style="padding:10px 6px;font-size:11px;color:#64748b;max-width:180px">' + esc(it.specs) + '</td>'
 
1347
  }).join('');
1348
 
1349
  div.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:16px">'
1350
+ + '<img src="' + __qRenderSrc('https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png') + '" style="height:56px;width:auto;object-fit:contain">'
1351
  + '<div>'
1352
  + '<div style="font-size:16px;font-weight:800;color:#003f62">V.AI STUDIO</div>'
1353
  + '<div style="font-size:11px;color:#64748b;letter-spacing:2px;font-style:italic">❝ Niềm tin khách hàng là tài sản của chúng tôi ❞</div>'
 
1362
  // (CK) is shown by __custInfoHtml above; the VIB transfer QR sits next to it
1363
  // so the image shared on Zalo carries the payment QR right beside the CK info.
1364
  + '<div style="margin-top:8px;display:inline-block">'
1365
+ + '<img src="' + __qRenderSrc(__qGetQRUrl(qd.grandTotal, qd.customer.orderCode || 'VAISTUDIO')) + '" title="Mã QR thanh toán" style="width:96px;height:96px;border:1px solid #e2e8f0;border-radius:8px;background:#fff">'
1366
  + '</div>'
1367
  + '</div>'
1368
  + '</div>'
 
1375
  + '<div style="margin-top:20px;font-size:11px;color:#64748b;font-style:italic">Miễn phí giao hàng trong TPHCM. | Giá đã bao gồm VAT.</div>';
1376
 
1377
  document.body.appendChild(div);
 
1378
  try {
1379
+ // Rewrite images through the /api/img CORS proxy + wait until all images
1380
+ // and fonts have loaded — this is the fix for blank/ảnh báo giá không hiện.
1381
+ await __qPreloadRender(div, 10000);
1382
+ await new Promise(function(r){ setTimeout(r, 150); });
1383
+ var canvas = await html2canvas(div, { scale:2, useCORS:true, allowTaint:false, logging:false, backgroundColor:'#fff' });
1384
  var imgData = canvas.toDataURL('image/jpeg', 0.95);
1385
+ if (!window.jspdf || !window.jspdf.jsPDF) { div.remove(); __qBtnBusy(btn, false); toast('⚠️ Thư viện PDF chưa tải.'); return; }
1386
  var jsPDF = window.jspdf.jsPDF;
1387
  var pdf = new jsPDF({ orientation:'landscape', unit:'mm', format:'a4' });
1388
  var pdfW = pdf.internal.pageSize.getWidth();
 
1401
  srcY += sliceH;
1402
  }
1403
  pdf.save((qd.customer.orderCode || 'BG') + '.pdf');
1404
+ __qBtnBusy(btn, false);
1405
+ toast('✅ Đã tải file ' + (qd.customer.orderCode || 'BG') + '.pdf — kiểm tra mục Tải xuống (Downloads)!');
1406
+ } catch(e) { console.error('PDF error:', e); __qBtnBusy(btn, false); toast('❌ Lỗi xuất PDF: ' + (e.message || e)); }
1407
  div.remove();
1408
  };
1409
 
 
1411
  window.shareQuoteImage = async function() {
1412
  var cart = loadCart();
1413
  if (!cart.length) { toast('Giỏ hàng trống!'); return; }
1414
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('.quote-btn[onclick*="shareQuoteImage"], [onclick*="shareQuoteImage"]') : null;
1415
+ __qBtnBusy(btn, true, 'Đang tạo ảnh...');
1416
+ toast('⏳ Đang tạo ảnh báo giá (chờ ảnh sản phẩm)...');
1417
  var qd = getQuoteData();
1418
  var qdItems = qd.items;
1419
  var div = document.createElement('div');
 
1421
  var itemsHtml = qdItems.map(function(it) {
1422
  return '<tr style="border-bottom:1px solid #e2e8f0">'
1423
  + '<td style="padding:10px 6px;text-align:center;font-size:13px;color:#0f172a">' + it.stt + '</td>'
1424
+ + '<td style="padding:10px 6px">' + (it.image ? '<img src="' + it.image + '" style="width:80px;height:80px;object-fit:contain;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc">' : '') + '</td>'
1425
  + '<td style="padding:10px 6px;font-size:12px;font-weight:600;color:#0f172a">' + esc(it.name) + '</td>'
1426
  + '<td style="padding:10px 6px;text-align:center;font-size:12px;color:#003f62">' + esc(it.model) + '</td>'
1427
  + '<td style="padding:10px 6px;font-size:11px;color:#64748b;max-width:180px">' + esc(it.specs) + '</td>'
 
1433
  + '</tr>';
1434
  }).join('');
1435
  div.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:16px">'
1436
+ + '<img src="' + __qRenderSrc('https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png') + '" style="height:56px;width:auto;object-fit:contain">'
1437
  + '<div>'
1438
  + '<div style="font-size:16px;font-weight:800;color:#003f62">V.AI STUDIO</div>'
1439
  + '<div style="font-size:11px;color:#64748b;letter-spacing:2px;font-style:italic">❝ Niềm tin khách hàng là tài sản của chúng t��i ❞</div>'
 
1448
  // (CK) is shown by __custInfoHtml above; the VIB transfer QR sits next to it
1449
  // so the image shared on Zalo carries the payment QR right beside the CK info.
1450
  + '<div style="margin-top:8px;display:inline-block">'
1451
+ + '<img src="' + __qRenderSrc(__qGetQRUrl(qd.grandTotal, qd.customer.orderCode || 'VAISTUDIO')) + '" title="Mã QR thanh toán" style="width:96px;height:96px;border:1px solid #e2e8f0;border-radius:8px;background:#fff">'
1452
  + '</div>'
1453
  + '</div>'
1454
  + '</div>'
 
1460
  + '</table>'
1461
  + '<div style="margin-top:16px;font-size:11px;color:#64748b;font-style:italic">Miễn phí giao hàng trong TPHCM. | Giá đã bao gồm VAT.</div>';
1462
  document.body.appendChild(div);
 
1463
  try {
1464
+ // CORS-proxy every image + wait for them to load canvas stays clean.
1465
+ await __qPreloadRender(div, 10000);
1466
+ await new Promise(function(r){ setTimeout(r, 150); });
1467
+ var canvas = await html2canvas(div, { scale:2, useCORS:true, allowTaint:false, backgroundColor:'#fff', logging:false });
1468
  div.remove();
1469
  var blob = await new Promise(function(r){ canvas.toBlob(r, 'image/png'); });
1470
  var file = new File([blob], (qd.customer.orderCode || 'BG') + '.png', { type:'image/png' });
 
1479
  var url = URL.createObjectURL(blob);
1480
  var preview = document.createElement('div');
1481
  preview.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:110060;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)';
1482
+ preview.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="' + (qd.customer.orderCode || 'BG') + '.png" style="padding:10px 20px;background:#003f62;color:#fff;border-radius:8px;font-weight:700;font-size:.85rem;text-decoration:none"><i class="fas fa-download"></i> Tải ảnh ' + (qd.customer.orderCode || 'BG') + '.png</a><button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:10px 20px;background:#e2e8f0;border:none;border-radius:8px;font-weight:700;font-size:.85rem;cursor:pointer">Đóng</button></div></div>';
1483
  document.body.appendChild(preview);
1484
+ __qBtnBusy(btn, false);
1485
+ toast('✅ Ảnh báo giá đã sẵn sàng! Bấm "Tải ảnh" để lưu file ' + (qd.customer.orderCode || 'BG') + '.png');
1486
+ } else {
1487
+ __qBtnBusy(btn, false);
1488
+ toast('✅ Đã chia sẻ ảnh báo giá!');
1489
  }
1490
+ } catch(e) { div.remove(); __qBtnBusy(btn, false); toast('Lỗi: ' + e.message); }
1491
  };
1492
 
1493
  // Print quote (đúng layout)
1494
  window.printQuotation = function() {
1495
  var qd = getQuoteData();
1496
+ toast('⏳ Đang mở cửa sổ in...');
1497
  var w = window.open('', '_blank', 'width=1100,height=800');
1498
+ if (!w) { toast('⚠️ Trình duyệt chặn popup — đang xuất PDF thay thế...'); exportQuotePDF(); return; }
1499
  var rows = qd.items.map(function(it) {
1500
  return '<tr>'
1501
  + '<td>' + it.stt + '</td>'
 
1520
  w.document.write('</body></html>');
1521
  w.document.close();
1522
  w.focus();
1523
+ setTimeout(function() { w.print(); toast('🖨️ Đã mở cửa sổ in — chọn máy in và nhấn In!'); }, 400);
1524
  };
1525
 
1526
  function toast(msg) {
src/greeting-news.js CHANGED
@@ -1054,8 +1054,14 @@
1054
  // Row 1: logo + tagline
1055
  imagePromises.push((async function addLogo(){
1056
  try {
1057
- var lr = await fetch('https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png', { mode:'cors' });
1058
- if (!lr.ok) lr = await fetch('https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png', { mode:'cors' });
 
 
 
 
 
 
1059
  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 } }); }
1060
  } catch(e) {}
1061
  })());
@@ -1112,19 +1118,21 @@
1112
  if (it.image && it.image.indexOf('http') === 0) {
1113
  (function(img, r2){
1114
  imagePromises.push((async function(){
1115
- try {
1116
- var ir = await fetch(img, { mode:'cors' });
1117
- if (!ir.ok) throw new Error('HTTP ' + ir.status);
1118
- var ib = await ir.arrayBuffer();
1119
- var ext = img.match(/\.(png|jpe?g)/i); ext = ext ? (ext[1].indexOf('png') === 0 ? 'png' : 'jpeg') : 'jpeg';
1120
- ws.addImage(wb.addImage({ buffer: ib, extension: ext }), { tl:{ col:1, row:r2 - 1 }, ext:{ width:46, height:46 } });
1121
- } catch(e1) {
1122
  try {
1123
- var proxyUrl = 'https://wsrv.nl/?url=' + encodeURIComponent(img) + '&w=200&output=' + (/\.png/i.test(img) ? 'png' : 'jpg');
1124
- var ir2 = await fetch(proxyUrl);
1125
- if (ir2.ok) { var ib2 = await ir2.arrayBuffer(); var ext2 = img.match(/\.(png|jpe?g)/i); ext2 = ext2 ? (ext2[1].indexOf('png') === 0 ? 'png' : 'jpeg') : 'jpeg'; ws.addImage(wb.addImage({ buffer: ib2, extension: ext2 }), { tl:{ col:1, row:r2 - 1 }, ext:{ width:46, height:46 } }); }
1126
- } catch(e2) {}
 
 
1127
  }
 
1128
  })());
1129
  })(it.image, cr);
1130
  }
@@ -1168,6 +1176,7 @@
1168
  imagePromises.push((async function addQR(){
1169
  try {
1170
  var qrUrl = 'https://img.vietqr.io/image/vib-918258385-compact2.png?amount=' + (Math.round(grandTotal) || 0) + '&addInfo=' + encodeURIComponent(code || 'VAISTUDIO') + '&accountName=' + encodeURIComponent('TRAN QUOC VUONG');
 
1171
  var qrR = await fetch(qrUrl, { mode:'cors' });
1172
  if (qrR.ok) { var qb2 = await qrR.arrayBuffer(); ws.addImage(wb.addImage({ buffer: qb2, extension:'png' }), { tl:{ col:6, row:totalRowNum + 1 }, ext:{ width:115, height:115 } }); }
1173
  } catch(e) {}
@@ -1193,16 +1202,18 @@
1193
  function exportOrderExcel(order) {
1194
  var toast = window.toast || function (m) { alert(m); };
1195
  if (typeof ExcelJS === 'undefined') { toast('⚠️ Thư viện Excel chưa tải'); return; }
 
 
1196
  (async function () {
1197
- toast('⏳ Đang xuất Excel đơn ' + order.code + '...');
1198
  var built = await orderBrandedSheet(order);
1199
  var buf = await built.wb.xlsx.writeBuffer();
1200
  var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1201
- var url = URL.createObjectURL(blob), a = document.createElement('a');
1202
- a.href = url; a.download = built.code + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a);
1203
- setTimeout(function () { URL.revokeObjectURL(url); }, 60000);
1204
- toast('✅ Đã xuất Excel đơn ' + order.code);
1205
- })().catch(function (e) { console.error(e); toast('⚠️ Xuất Excel thất bại'); });
1206
  }
1207
 
1208
  // Branded HTML used for both PDF and image export. Mirrors EXACTLY the Excel
@@ -1219,7 +1230,7 @@
1219
  var itemsHtml = items.map(function (it) {
1220
  return '<tr style="border-bottom:1px solid #e2e8f0">'
1221
  + '<td style="padding:10px 6px;text-align:center;font-size:13px;color:#0f172a">' + it.stt + '</td>'
1222
- + '<td style="padding:10px 6px">' + (it.image ? '<img src="' + esc(it.image) + '" style="width:80px;height:80px;object-fit:contain;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc" onerror="this.style.display=\'none\'">' : '') + '</td>'
1223
  + '<td style="padding:10px 6px;font-size:12px;font-weight:600;color:#0f172a">' + esc(it.name) + '</td>'
1224
  + '<td style="padding:10px 6px;text-align:center;font-size:12px;color:#003f62">' + esc(it.model) + '</td>'
1225
  + '<td style="padding:10px 6px;font-size:11px;color:#64748b;max-width:180px">' + esc(it.specs) + '</td>'
@@ -1233,11 +1244,11 @@
1233
  var qrImg = '';
1234
  try {
1235
  var qrUrl = 'https://img.vietqr.io/image/vib-918258385-compact2.png?amount=' + (Math.round(grandTotal) || 0) + '&addInfo=' + encodeURIComponent(code || 'VAISTUDIO') + '&accountName=' + encodeURIComponent('TRAN QUOC VUONG');
1236
- qrImg = '<img src="' + qrUrl + '" style="width:120px;height:120px;border:1px solid #e2e8f0;border-radius:8px;background:#fff" onerror="this.style.display=\'none\'">';
1237
  } catch (e) { qrImg = ''; }
1238
  // Logo (brand logo on navy card) + tagline.
1239
  var logoSrc = 'https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png';
1240
- var logoImg = '<img src="' + logoSrc + '" style="height:48px;width:auto;object-fit:contain" onerror="this.style.display=\'none\'">';
1241
  return '<div style="display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:10px">'
1242
  + logoImg
1243
  + '<div style="font-size:16px;font-weight:800;color:#003f62">V.AI STUDIO</div>'
@@ -1293,15 +1304,12 @@
1293
  return new Promise(function (resolve, reject) {
1294
  // Wait for every <img> inside the render node to finish loading so
1295
  // product images / layout render identically to the Excel every time.
1296
- var imgs = Array.prototype.slice.call(div.querySelectorAll('img'));
1297
- var imgWaits = imgs.map(function (img) {
1298
- if (img.complete && img.naturalWidth > 0) return Promise.resolve();
1299
- return new Promise(function (res) { img.onload = function(){ res(); }; img.onerror = function(){ res(); }; });
1300
- });
1301
- var fontWait = (document.fonts && document.fonts.ready) ? document.fonts.ready : Promise.resolve();
1302
- Promise.all([fontWait, Promise.all(imgWaits)]).then(function () {
1303
  setTimeout(function () {
1304
- html2canvas(div, { scale:2, useCORS:true, allowTaint:true, logging:false, backgroundColor:'#ffffff' })
1305
  .then(function (canvas) { div.remove(); resolve(canvas); })
1306
  .catch(function (e) { div.remove(); reject(e); });
1307
  }, 120);
@@ -1313,7 +1321,9 @@
1313
  var toast = window.toast || function (m) { alert(m); };
1314
  if ((typeof window.jspdf === 'undefined' && typeof window.jsPDF === 'undefined') || typeof html2canvas === 'undefined') { toast('⚠️ Thư viện PDF/html2canvas chưa tải'); return; }
1315
  var code = order.code || 'DH';
1316
- toast('⏳ Đang xuất PDF đơn ' + code + '...');
 
 
1317
  (async function () {
1318
  try {
1319
  var canvas = await renderOrderBrandedToCanvas(order);
@@ -1336,8 +1346,9 @@
1336
  srcY += sliceH;
1337
  }
1338
  pdf.save(code + '.pdf');
1339
- toast('✅ Đã xuất PDF đơn ' + code);
1340
- } catch(e) { console.error(e); toast(' Lỗi xuất PDF'); }
 
1341
  })();
1342
  }
1343
 
@@ -1345,7 +1356,9 @@
1345
  var toast = window.toast || function (m) { alert(m); };
1346
  if (typeof html2canvas === 'undefined') { toast('⚠️ Thư viện html2canvas chưa tải'); return; }
1347
  var code = order.code || 'DH';
1348
- toast('⏳ Đang tạo ảnh đơn ' + code + '...');
 
 
1349
  (async function () {
1350
  try {
1351
  var canvas = await renderOrderBrandedToCanvas(order);
@@ -1361,14 +1374,15 @@
1361
  try { await navigator.share({ title:'Đơn hàng V.AI STUDIO - ' + code, files:[file] }); shared = true; }
1362
  catch (e) { shared = false; /* user cancelled or API unavailable → preview */ }
1363
  }
1364
- if (shared) return;
1365
  var url = URL.createObjectURL(blob);
1366
  var preview = document.createElement('div');
1367
  preview.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:110062;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)';
1368
- preview.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;font-size:.85rem;text-decoration:none"><i class="fas fa-download"></i> 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;font-size:.85rem;cursor:pointer">Đóng</button></div></div>';
1369
  document.body.appendChild(preview);
1370
- toast('✅ Ảnh đơn hàng đã sẵn sàng!');
1371
- } catch(e) { toast('Lỗi: ' + e.message); }
 
1372
  })();
1373
  }
1374
 
@@ -1377,8 +1391,10 @@
1377
  if (typeof ExcelJS === 'undefined') { toast('⚠️ Thư viện Excel chưa tải'); return; }
1378
  var items = orderToQuoteData(order).items;
1379
  var code = order.code || 'DH';
 
 
1380
  (async function () {
1381
- toast('⏳ Đang xuất Phiếu giao hàng ' + code + '...');
1382
  // ── V.AISTUDIO PHIẾU GIAO HÀNG layout (STT|Mã SP|Tên sản phẩm|SL|Ghi chú)
1383
  // Price columns (Đơn giá / Thành tiền) removed per request; Ghi chú added. ──
1384
  var WHITE = { argb: 'FFFFFFFF' };
@@ -1434,11 +1450,11 @@
1434
  ws.eachRow(function (row) { row.eachCell(function (cell) { if (!cell.border) cell.border = WB; }); });
1435
  var buf = await wb.xlsx.writeBuffer();
1436
  var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1437
- var url = URL.createObjectURL(blob), a = document.createElement('a');
1438
- a.href = url; a.download = 'GH-' + code + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a);
1439
- setTimeout(function () { URL.revokeObjectURL(url); }, 300000);
1440
- toast('✅ Đã xuất Phiếu giao hàng ' + code);
1441
- })().catch(function (e) { console.error(e); toast('⚠️ Xuất Phiếu giao hàng thất bại'); });
1442
  }
1443
 
1444
 
@@ -1446,10 +1462,13 @@
1446
  var toast = window.toast || function (m) { alert(m); };
1447
  var qd = orderToQuoteData(order);
1448
  var code = order.code || 'DH';
 
 
1449
  // ── V.AISTUDIO PHIẾU GIAO HÀNG (html2canvas → PNG), columns:
1450
  // STT | Mã SP | Tên sản phẩm | SL | Ghi chú + TỔNG SL + signature.
1451
  // Price columns (Đơn giá / Thành tiền) removed per request. ──
1452
  if (typeof html2canvas === 'undefined') {
 
1453
  toast('⚠️ Thư viện html2canvas chưa tải');
1454
  return;
1455
  }
@@ -1482,7 +1501,7 @@
1482
  await new Promise(function (r) { setTimeout(r, 500); });
1483
  var cv = await html2canvas(div, { scale: 2, useCORS: true, allowTaint: false, backgroundColor: '#fff', width: 900, height: Math.max(div.scrollHeight, div.offsetHeight), windowWidth: 900, windowHeight: Math.max(div.scrollHeight, div.offsetHeight), scrollX: 0, scrollY: 0 });
1484
  var img = cv.toDataURL('image/png');
1485
- if (typeof window.jspdf === 'undefined' && typeof window.jsPDF === 'undefined') { div.remove(); toast('⚠️ Thư viện PDF chưa tải'); return; }
1486
  var PDF = (typeof window.jspdf !== 'undefined') ? window.jspdf.jsPDF : window.jsPDF;
1487
  var pdf = new PDF({ orientation: 'p', unit: 'pt', format: 'a4' });
1488
  var margin = 25, usableW = 595 - margin * 2;
@@ -1491,22 +1510,26 @@
1491
  if (imgH > 842 - margin * 2) { imgH = 842 - margin * 2; usableW = imgH / ratio; }
1492
  pdf.addImage(img, 'PNG', margin, margin, usableW, imgH);
1493
  pdf.save('GH-' + code + '.pdf');
1494
- toast('✅ Đã xuất Phiếu giao hàng ' + code);
1495
- } catch (e) { console.error(e); toast('⚠️ Xuất Phiếu giao hàng thất bại'); }
 
1496
  div.remove();
1497
  })();
1498
  }
1499
 
1500
 
1501
  function printOrder(order) {
 
 
1502
  var w = window.open('', '_blank');
1503
- if (!w) { alert('Vui lòng cho phép popup'); return; }
1504
  var rows = (order.items || []).map(function (it) {
1505
  var n = normItem(it);
1506
  return '<tr style="border-bottom:1px solid #e2e8f0"><td style="padding:8px;text-align:center">' + n.stt + '</td><td style="padding:8px">' + esc(n.name) + '</td><td style="padding:8px;text-align:center">' + n.qty + '</td><td style="padding:8px;text-align:right">' + money(n.price) + '</td><td style="padding:8px;text-align:right">' + money(n.discPrice) + '</td><td style="padding:8px;text-align:right">' + money(n.total) + '</td></tr>';
1507
  }).join('');
1508
  w.document.write('<html><head><title>Đơn ' + order.code + '</title><style>body{font-family:Arial;padding:30px}table{border-collapse:collapse;width:100%;font-size:13px}th,td{border-bottom:1px solid #e2e8f0}th{background:#003f62;color:#fff;padding:8px}</style></head><body><h2 style="color:#003f62">ĐƠN HÀNG ' + order.code + '</h2><p><b>Khách hàng:</b> ' + esc(order.customer || '') + ' &nbsp; <b>SĐT:</b> ' + esc(order.phone || '') + '<br><b>Email:</b> ' + esc(order.email || '') + ' &nbsp; <b>Ngày:</b> ' + esc(order.date || '') + '<br><b>Địa chỉ:</b> ' + esc(order.addr || '') + '</p><table><thead><tr><th style="text-align:center">STT</th><th style="text-align:left">Tên SP</th><th>SL</th><th style="text-align:right">Đơn giá</th><th style="text-align:right">Giá CK</th><th style="text-align:right">Thành tiền</th></tr></thead><tbody>' + rows + '</tbody></table><h3 style="text-align:right;color:#c2410c">TỔNG CỘNG: ' + money(order.grandTotal || order.total || 0) + '</h3><script>window.onload=function(){window.print()}<\/script></body></html>');
1509
  w.document.close();
 
1510
  }
1511
 
1512
  // Expose overrides + export helpers
 
1054
  // Row 1: logo + tagline
1055
  imagePromises.push((async function addLogo(){
1056
  try {
1057
+ var logoUrl = 'https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png';
1058
+ if (window.__qRenderSrc) logoUrl = window.__qRenderSrc(logoUrl);
1059
+ var lr = await fetch(logoUrl, { mode:'cors' });
1060
+ if (!lr.ok) {
1061
+ var logoUrl2 = 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/logo/logo_main.png';
1062
+ if (window.__qRenderSrc) logoUrl2 = window.__qRenderSrc(logoUrl2);
1063
+ lr = await fetch(logoUrl2, { mode:'cors' });
1064
+ }
1065
  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 } }); }
1066
  } catch(e) {}
1067
  })());
 
1118
  if (it.image && it.image.indexOf('http') === 0) {
1119
  (function(img, r2){
1120
  imagePromises.push((async function(){
1121
+ var buf = null, ext = 'jpeg';
1122
+ var candidates = [];
1123
+ if (window.__qRenderSrc) candidates.push(window.__qRenderSrc(img));
1124
+ candidates.push(img);
1125
+ candidates.push('https://wsrv.nl/?url=' + encodeURIComponent(img) + '&w=200&output=' + (/\.png/i.test(img) ? 'png' : 'jpg'));
1126
+ for (var cf = 0; cf < candidates.length && !buf; cf++) {
 
1127
  try {
1128
+ var ir = await fetch(candidates[cf], { mode:'cors' });
1129
+ if (!ir.ok) continue;
1130
+ buf = await ir.arrayBuffer();
1131
+ var ex = candidates[cf].match(/\.(png|jpe?g)/i); ext = ex ? (ex[1].indexOf('png') === 0 ? 'png' : 'jpeg') : 'jpeg';
1132
+ if (candidates[cf].indexOf('output=') !== -1 && /output=(\w+)/.test(candidates[cf])) ext = RegExp.$1 === 'png' ? 'png' : 'jpeg';
1133
+ } catch (eF) { buf = null; }
1134
  }
1135
+ if (buf) { try { ws.addImage(wb.addImage({ buffer: buf, extension: ext }), { tl:{ col:1, row:r2 - 1 }, ext:{ width:46, height:46 } }); } catch (eA) {} }
1136
  })());
1137
  })(it.image, cr);
1138
  }
 
1176
  imagePromises.push((async function addQR(){
1177
  try {
1178
  var qrUrl = 'https://img.vietqr.io/image/vib-918258385-compact2.png?amount=' + (Math.round(grandTotal) || 0) + '&addInfo=' + encodeURIComponent(code || 'VAISTUDIO') + '&accountName=' + encodeURIComponent('TRAN QUOC VUONG');
1179
+ if (window.__qRenderSrc) qrUrl = window.__qRenderSrc(qrUrl);
1180
  var qrR = await fetch(qrUrl, { mode:'cors' });
1181
  if (qrR.ok) { var qb2 = await qrR.arrayBuffer(); ws.addImage(wb.addImage({ buffer: qb2, extension:'png' }), { tl:{ col:6, row:totalRowNum + 1 }, ext:{ width:115, height:115 } }); }
1182
  } catch(e) {}
 
1202
  function exportOrderExcel(order) {
1203
  var toast = window.toast || function (m) { alert(m); };
1204
  if (typeof ExcelJS === 'undefined') { toast('⚠️ Thư viện Excel chưa tải'); return; }
1205
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('[data-ord="excel"]') : null;
1206
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, true, 'Đang xuất Excel...');
1207
  (async function () {
1208
+ toast('⏳ Đang tạo Excel đơn ' + order.code + '...');
1209
  var built = await orderBrandedSheet(order);
1210
  var buf = await built.wb.xlsx.writeBuffer();
1211
  var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1212
+ if (window.__qDownload) { window.__qDownload(blob, built.code + '.xlsx'); }
1213
+ else { var url = URL.createObjectURL(blob), a = document.createElement('a'); a.href = url; a.download = built.code + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 60000); }
1214
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, false);
1215
+ toast('✅ Đã tải file ' + built.code + '.xlsx — kiểm tra mục Tải xuống (Downloads)!');
1216
+ })().catch(function (e) { console.error(e); if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('⚠️ Xuất Excel thất bại: ' + (e.message || e)); });
1217
  }
1218
 
1219
  // Branded HTML used for both PDF and image export. Mirrors EXACTLY the Excel
 
1230
  var itemsHtml = items.map(function (it) {
1231
  return '<tr style="border-bottom:1px solid #e2e8f0">'
1232
  + '<td style="padding:10px 6px;text-align:center;font-size:13px;color:#0f172a">' + it.stt + '</td>'
1233
+ + '<td style="padding:10px 6px">' + (it.image ? '<img src="' + (window.__qRenderSrc ? window.__qRenderSrc(it.image) : it.image) + '" style="width:80px;height:80px;object-fit:contain;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc">' : '') + '</td>'
1234
  + '<td style="padding:10px 6px;font-size:12px;font-weight:600;color:#0f172a">' + esc(it.name) + '</td>'
1235
  + '<td style="padding:10px 6px;text-align:center;font-size:12px;color:#003f62">' + esc(it.model) + '</td>'
1236
  + '<td style="padding:10px 6px;font-size:11px;color:#64748b;max-width:180px">' + esc(it.specs) + '</td>'
 
1244
  var qrImg = '';
1245
  try {
1246
  var qrUrl = 'https://img.vietqr.io/image/vib-918258385-compact2.png?amount=' + (Math.round(grandTotal) || 0) + '&addInfo=' + encodeURIComponent(code || 'VAISTUDIO') + '&accountName=' + encodeURIComponent('TRAN QUOC VUONG');
1247
+ qrImg = '<img src="' + (window.__qRenderSrc ? window.__qRenderSrc(qrUrl) : qrUrl) + '" style="width:120px;height:120px;border:1px solid #e2e8f0;border-radius:8px;background:#fff">';
1248
  } catch (e) { qrImg = ''; }
1249
  // Logo (brand logo on navy card) + tagline.
1250
  var logoSrc = 'https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/public/quote_logo.png';
1251
+ var logoImg = '<img src="' + (window.__qRenderSrc ? window.__qRenderSrc(logoSrc) : logoSrc) + '" style="height:48px;width:auto;object-fit:contain">';
1252
  return '<div style="display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:10px">'
1253
  + logoImg
1254
  + '<div style="font-size:16px;font-weight:800;color:#003f62">V.AI STUDIO</div>'
 
1304
  return new Promise(function (resolve, reject) {
1305
  // Wait for every <img> inside the render node to finish loading so
1306
  // product images / layout render identically to the Excel every time.
1307
+ // Images are already routed through the /api/img CORS proxy by
1308
+ // orderBrandedHTML/__qRenderSrc so the canvas never taints.
1309
+ var ready = window.__qPreloadRender ? window.__qPreloadRender(div, 10000) : Promise.resolve();
1310
+ ready.then(function () {
 
 
 
1311
  setTimeout(function () {
1312
+ html2canvas(div, { scale:2, useCORS:true, allowTaint:false, logging:false, backgroundColor:'#ffffff' })
1313
  .then(function (canvas) { div.remove(); resolve(canvas); })
1314
  .catch(function (e) { div.remove(); reject(e); });
1315
  }, 120);
 
1321
  var toast = window.toast || function (m) { alert(m); };
1322
  if ((typeof window.jspdf === 'undefined' && typeof window.jsPDF === 'undefined') || typeof html2canvas === 'undefined') { toast('⚠️ Thư viện PDF/html2canvas chưa tải'); return; }
1323
  var code = order.code || 'DH';
1324
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('[data-ord="pdf"]') : null;
1325
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, true, 'Đang xuất PDF...');
1326
+ toast('⏳ Đang tạo PDF đơn ' + code + ' (chờ ảnh sản phẩm)...');
1327
  (async function () {
1328
  try {
1329
  var canvas = await renderOrderBrandedToCanvas(order);
 
1346
  srcY += sliceH;
1347
  }
1348
  pdf.save(code + '.pdf');
1349
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, false);
1350
+ toast('✅ Đã tải file ' + code + '.pdf — kiểm tra mục Tải xuống (Downloads)!');
1351
+ } catch(e) { console.error(e); if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('❌ Lỗi xuất PDF: ' + (e.message || e)); }
1352
  })();
1353
  }
1354
 
 
1356
  var toast = window.toast || function (m) { alert(m); };
1357
  if (typeof html2canvas === 'undefined') { toast('⚠️ Thư viện html2canvas chưa tải'); return; }
1358
  var code = order.code || 'DH';
1359
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('[data-ord="image"]') : null;
1360
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, true, 'Đang tạo ảnh...');
1361
+ toast('⏳ Đang tạo ảnh đơn ' + code + ' (chờ ảnh sản phẩm)...');
1362
  (async function () {
1363
  try {
1364
  var canvas = await renderOrderBrandedToCanvas(order);
 
1374
  try { await navigator.share({ title:'Đơn hàng V.AI STUDIO - ' + code, files:[file] }); shared = true; }
1375
  catch (e) { shared = false; /* user cancelled or API unavailable → preview */ }
1376
  }
1377
+ if (shared) { if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('✅ Đã chia sẻ ảnh đơn hàng!'); return; }
1378
  var url = URL.createObjectURL(blob);
1379
  var preview = document.createElement('div');
1380
  preview.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:110062;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)';
1381
+ preview.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;font-size:.85rem;text-decoration:none"><i class="fas fa-download"></i> Tải ảnh ' + code + '.png</a><button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:10px 20px;background:#e2e8f0;border:none;border-radius:8px;font-weight:700;font-size:.85rem;cursor:pointer">Đóng</button></div></div>';
1382
  document.body.appendChild(preview);
1383
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, false);
1384
+ toast(' Ảnh đơn hàng đã sẵn sàng! Bấm "Tải ảnh" để lưu file ' + code + '.png');
1385
+ } catch(e) { if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('Lỗi: ' + e.message); }
1386
  })();
1387
  }
1388
 
 
1391
  if (typeof ExcelJS === 'undefined') { toast('⚠️ Thư viện Excel chưa tải'); return; }
1392
  var items = orderToQuoteData(order).items;
1393
  var code = order.code || 'DH';
1394
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('[data-ord="delivery-excel"]') : null;
1395
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, true, 'Đang xuất...');
1396
  (async function () {
1397
+ toast('⏳ Đang tạo Phiếu giao hàng ' + code + '...');
1398
  // ── V.AISTUDIO PHIẾU GIAO HÀNG layout (STT|Mã SP|Tên sản phẩm|SL|Ghi chú)
1399
  // Price columns (Đơn giá / Thành tiền) removed per request; Ghi chú added. ──
1400
  var WHITE = { argb: 'FFFFFFFF' };
 
1450
  ws.eachRow(function (row) { row.eachCell(function (cell) { if (!cell.border) cell.border = WB; }); });
1451
  var buf = await wb.xlsx.writeBuffer();
1452
  var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
1453
+ if (window.__qDownload) { window.__qDownload(blob, 'GH-' + code + '.xlsx'); }
1454
+ else { var url = URL.createObjectURL(blob), a = document.createElement('a'); a.href = url; a.download = 'GH-' + code + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 300000); }
1455
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, false);
1456
+ toast('✅ Đã tải file GH-' + code + '.xlsx — kiểm tra mục Tải xuống (Downloads)!');
1457
+ })().catch(function (e) { console.error(e); if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('⚠️ Xuất Phiếu giao hàng thất bại: ' + (e.message || e)); });
1458
  }
1459
 
1460
 
 
1462
  var toast = window.toast || function (m) { alert(m); };
1463
  var qd = orderToQuoteData(order);
1464
  var code = order.code || 'DH';
1465
+ var btn = (event && event.target && event.target.closest) ? event.target.closest('[data-ord="delivery-pdf"]') : null;
1466
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, true, 'Đang xuất...');
1467
  // ── V.AISTUDIO PHIẾU GIAO HÀNG (html2canvas → PNG), columns:
1468
  // STT | Mã SP | Tên sản phẩm | SL | Ghi chú + TỔNG SL + signature.
1469
  // Price columns (Đơn giá / Thành tiền) removed per request. ──
1470
  if (typeof html2canvas === 'undefined') {
1471
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, false);
1472
  toast('⚠️ Thư viện html2canvas chưa tải');
1473
  return;
1474
  }
 
1501
  await new Promise(function (r) { setTimeout(r, 500); });
1502
  var cv = await html2canvas(div, { scale: 2, useCORS: true, allowTaint: false, backgroundColor: '#fff', width: 900, height: Math.max(div.scrollHeight, div.offsetHeight), windowWidth: 900, windowHeight: Math.max(div.scrollHeight, div.offsetHeight), scrollX: 0, scrollY: 0 });
1503
  var img = cv.toDataURL('image/png');
1504
+ if (typeof window.jspdf === 'undefined' && typeof window.jsPDF === 'undefined') { div.remove(); if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('⚠️ Thư viện PDF chưa tải'); return; }
1505
  var PDF = (typeof window.jspdf !== 'undefined') ? window.jspdf.jsPDF : window.jsPDF;
1506
  var pdf = new PDF({ orientation: 'p', unit: 'pt', format: 'a4' });
1507
  var margin = 25, usableW = 595 - margin * 2;
 
1510
  if (imgH > 842 - margin * 2) { imgH = 842 - margin * 2; usableW = imgH / ratio; }
1511
  pdf.addImage(img, 'PNG', margin, margin, usableW, imgH);
1512
  pdf.save('GH-' + code + '.pdf');
1513
+ if (window.__qBtnBusy) window.__qBtnBusy(btn, false);
1514
+ toast('✅ Đã tải file GH-' + code + '.pdf kiểm tra mục Tải xuống (Downloads)!');
1515
+ } catch (e) { console.error(e); if (window.__qBtnBusy) window.__qBtnBusy(btn, false); toast('⚠️ Xuất Phiếu giao hàng thất bại: ' + (e.message || e)); }
1516
  div.remove();
1517
  })();
1518
  }
1519
 
1520
 
1521
  function printOrder(order) {
1522
+ var toast = window.toast || function (m) { alert(m); };
1523
+ toast('⏳ Đang mở cửa sổ in...');
1524
  var w = window.open('', '_blank');
1525
+ if (!w) { toast('⚠️ Trình duyệt chặn popup — đang xuất PDF thay thế...'); exportOrderPDF(order); return; }
1526
  var rows = (order.items || []).map(function (it) {
1527
  var n = normItem(it);
1528
  return '<tr style="border-bottom:1px solid #e2e8f0"><td style="padding:8px;text-align:center">' + n.stt + '</td><td style="padding:8px">' + esc(n.name) + '</td><td style="padding:8px;text-align:center">' + n.qty + '</td><td style="padding:8px;text-align:right">' + money(n.price) + '</td><td style="padding:8px;text-align:right">' + money(n.discPrice) + '</td><td style="padding:8px;text-align:right">' + money(n.total) + '</td></tr>';
1529
  }).join('');
1530
  w.document.write('<html><head><title>Đơn ' + order.code + '</title><style>body{font-family:Arial;padding:30px}table{border-collapse:collapse;width:100%;font-size:13px}th,td{border-bottom:1px solid #e2e8f0}th{background:#003f62;color:#fff;padding:8px}</style></head><body><h2 style="color:#003f62">ĐƠN HÀNG ' + order.code + '</h2><p><b>Khách hàng:</b> ' + esc(order.customer || '') + ' &nbsp; <b>SĐT:</b> ' + esc(order.phone || '') + '<br><b>Email:</b> ' + esc(order.email || '') + ' &nbsp; <b>Ngày:</b> ' + esc(order.date || '') + '<br><b>Địa chỉ:</b> ' + esc(order.addr || '') + '</p><table><thead><tr><th style="text-align:center">STT</th><th style="text-align:left">Tên SP</th><th>SL</th><th style="text-align:right">Đơn giá</th><th style="text-align:right">Giá CK</th><th style="text-align:right">Thành tiền</th></tr></thead><tbody>' + rows + '</tbody></table><h3 style="text-align:right;color:#c2410c">TỔNG CỘNG: ' + money(order.grandTotal || order.total || 0) + '</h3><script>window.onload=function(){window.print()}<\/script></body></html>');
1531
  w.document.close();
1532
+ toast('🖨️ Đã mở cửa sổ in — chọn máy in và nhấn In!');
1533
  }
1534
 
1535
  // Expose overrides + export helpers
src/version.js CHANGED
@@ -1,3 +1,3 @@
1
  // V.AI AVATAR — version banner (used for cache-busting / diagnostics)
2
- window.VAI_AVATAR_VERSION = "v30-order-code-live";
3
- window.VAI_AVATAR_CACHE = "v26-gc-" + Date.now();
 
1
  // V.AI AVATAR — version banner (used for cache-busting / diagnostics)
2
+ window.VAI_AVATAR_VERSION = "v31-export-fix-live";
3
+ window.VAI_AVATAR_CACHE = "v27-gc-" + Date.now();