Spaces:
Running
Running
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- index.html +3 -4
- src/cart-quote.js +146 -46
- src/greeting-news.js +71 -48
- 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-
|
| 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=
|
| 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=
|
| 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 }).
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1031 |
-
if (
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
| 1122 |
if (it.image && it.image.indexOf('http') === 0) {
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
} catch(e1) {
|
| 1130 |
try {
|
| 1131 |
-
var
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 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"
|
| 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"
|
| 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"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1316 |
-
|
|
|
|
| 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"
|
| 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"
|
| 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"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1058 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
} catch(e1) {
|
| 1122 |
try {
|
| 1123 |
-
var
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 1202 |
-
a.href = url; a.download = built.code + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
| 1203 |
-
|
| 1204 |
-
toast('✅ Đã
|
| 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="' +
|
| 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"
|
| 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"
|
| 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 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 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:
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1340 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1371 |
-
|
|
|
|
| 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
|
| 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 |
-
|
| 1438 |
-
a.href = url; a.download = 'GH-' + code + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
| 1439 |
-
|
| 1440 |
-
toast('✅ Đã
|
| 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 |
-
|
| 1495 |
-
|
|
|
|
| 1496 |
div.remove();
|
| 1497 |
})();
|
| 1498 |
}
|
| 1499 |
|
| 1500 |
|
| 1501 |
function printOrder(order) {
|
|
|
|
|
|
|
| 1502 |
var w = window.open('', '_blank');
|
| 1503 |
-
if (!w) {
|
| 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 || '') + ' <b>SĐT:</b> ' + esc(order.phone || '') + '<br><b>Email:</b> ' + esc(order.email || '') + ' <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 || '') + ' <b>SĐT:</b> ' + esc(order.phone || '') + '<br><b>Email:</b> ' + esc(order.email || '') + ' <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 = "
|
| 3 |
-
window.VAI_AVATAR_CACHE = "
|
|
|
|
| 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();
|