/** * V.AI STUDIO - Excel Multi-Download Fix v2 * FIX TRIỆT ĐỐI CHO SPACE CÓ DẤU CHẤM * - Registry toàn cục blob URLs * - Patch URL.revokeObjectURL - không revoke khi chưa unload * - Multi-download hoạt động 100% */ (function(){ "use strict"; // Global blob registry window.__VAI_BLOB_REG = window.__VAI_BLOB_REG || { blobs: [], isUnloading: false }; var REG = window.__VAI_BLOB_REG; // Cleanup on unload function cleanup() { REG.isUnloading = true; REG.blobs.forEach(function(e) { if (!e.revoked && e.url) { try { URL.revokeObjectURL(e.url); e.revoked = true; } catch(err) {} } }); REG.blobs = []; } window.addEventListener('pagehide', cleanup); window.addEventListener('beforeunload', cleanup); window.addEventListener('unload', cleanup); // PATCH URL.revokeObjectURL - QUAN TRỌNG NHẤT if (!window.__VAI_REVOKE_DONE) { var _origRevoke = URL.revokeObjectURL; URL.revokeObjectURL = function(url) { if (REG.isUnloading) return _origRevoke.call(this, url); if (!url || typeof url !== 'string' || !url.startsWith('blob:')) { return _origRevoke.call(this, url); } // Register nhưng đừng revoke - fix multi-download var exists = REG.blobs.find(function(e) { return e.url === url; }); if (!exists) { REG.blobs.push({ url: url, revoked: false, created: Date.now() }); } console.log('[VAI] Blob preserved for multi-download'); }; window.__VAI_REVOKE_DONE = true; console.log('[VAI] URL.revokeObjectURL PATCHED - Multi-download enabled'); } // Download function function dl(blob, name) { var url = URL.createObjectURL(blob); REG.blobs.push({ url: url, revoked: false }); var a = document.createElement('a'); a.href = url; a.download = name; a.style.cssText = 'position:absolute;left:-9999px;top:-9999px'; document.body.appendChild(a); a.click(); setTimeout(function() { try { document.body.removeChild(a); } catch(err) {} }, 100); } // Get quote data function getQD() { var d = { customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') }, items: [], grandTotal: 0 }; var cart = window.cart || []; var prods = window.D || []; cart.forEach(function(item, i) { var p = prods[item.idx] || (prods.find ? prods.find(function(x) { return x.id === item.id; }) : null); var pr = item.priceNum || item.price || (p ? p.price : 0) || 0; d.items.push({ stt: i + 1, image: p ? p.img : '', name: p ? (p.name || p.n || '') : (item.name || ''), model: p ? (p.sku || p.mod || '') : (item.sku || item.model || ''), specs: p ? (p.info || p.desc || '') : '', qty: item.qty || 1, price: pr, discPrice: pr, total: (item.qty || 1) * pr, note: '' }); }); var ns = ['#quoteName', '#quoteCustomerName', '#checkoutName']; for (var j = 0; j < ns.length; j++) { var el = document.querySelector(ns[j]); if (el && el.value && el.value.trim()) { d.customer.name = el.value.trim(); break; } } var ph = document.querySelector('#quotePhone, #checkoutPhone'); if (ph && ph.value) d.customer.phone = ph.value.trim(); var em = document.querySelector('#quoteEmail'); if (em && em.value) d.customer.email = em.value.trim(); var ad = document.querySelector('#quoteAddr, #checkoutAddr'); if (ad && ad.value) d.customer.addr = ad.value.trim(); d.grandTotal = d.items.reduce(function(s, i) { return s + (i.total || 0); }, 0); return d; } // Format function fmt(n) { return n && !isNaN(n) ? Number(n).toLocaleString('vi-VN') + 'đ' : '0đ'; } // Strip diacritics function strip(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D'); } // Generate code function genCode(name) { var d = new Date(); var dd = String(d.getDate()).padStart(2, '0'); var mm = String(d.getMonth() + 1).padStart(2, '0'); var yy = String(d.getFullYear()).slice(-2); var ini = ''; if (name && name.trim()) { name.trim().split(/\s+/).forEach(function(w) { if (w) { var c = strip(w.charAt(0)).toUpperCase(); if (/[A-Z]/.test(c)) ini += c; } }); } return 'VAS' + (ini || 'X') + dd + mm + yy; } // Button state function setBtn(b, exporting) { if (exporting) { b.disabled = true; b.dataset.vaiExporting = '1'; b.dataset.vaiOrig = b.innerHTML; b.innerHTML = ''; } else { b.disabled = false; b.dataset.vaiExporting = '0'; b.dataset.vaiOrig && (b.innerHTML = b.dataset.vaiOrig); } } // Export Excel async function exportExcel() { var qd = getQD(); var code = genCode(qd.customer.name || ''); if (!qd.items.length || typeof ExcelJS === 'undefined') { alert('⚠️ ' + (!qd.items.length ? 'Không có sản phẩm' : 'Chưa có ExcelJS')); return; } try { var wb = new ExcelJS.Workbook(); var ws = wb.addWorksheet('Báo giá'); ws.views = [{ showGridLines: false }]; ws.columns = [{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}]; ws.mergeCells('A3:J3'); ws.getCell('A3').value = 'BẢNG BÁO GIÁ'; ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } }; ws.getCell('A5').value = 'Khách hàng:'; ws.mergeCells('B5:E5'); ws.getCell('B5').value = qd.customer.name; ws.getCell('H5').value = 'Mã đơn:'; ws.mergeCells('I5:J5'); ws.getCell('I5').value = code; ws.getCell('A6').value = 'SĐT:'; ws.mergeCells('B6:E6'); ws.getCell('B6').value = qd.customer.phone; ws.getCell('H6').value = 'Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value = qd.customer.date; ws.getCell('A7').value = 'Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value = qd.customer.email; ws.getCell('A8').value = 'Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value = qd.customer.addr; var hr = ws.getRow(10); ['STT','Hình','Tên sản phẩm','Mã SP','Thông tin','SL','Đơn giá','Giá CK','Thành tiền','Ghi chú'].forEach(function(h,i){ var c = hr.getCell(i+1); c.value = h; c.font = { bold: true, color: { argb: 'FFFFFFFF' } }; c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; }); var cr = 11, firstRow = cr; qd.items.forEach(function(it, i) { var row = ws.getRow(cr); row.height = 50; var bg = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF'; row.getCell(1).value = it.stt; row.getCell(3).value = it.name; row.getCell(3).font = { bold: true, size: 9 }; row.getCell(4).value = it.model; row.getCell(5).value = it.specs; row.getCell(5).font = { size: 8, color: { argb: 'FF64748B' } }; row.getCell(6).value = Number(it.qty); row.getCell(7).value = Number(it.price); row.getCell(7).numFmt = '#,##0"đ"'; row.getCell(8).value = Number(it.discPrice); row.getCell(8).numFmt = '#,##0"đ"'; row.getCell(9).value = Number(it.total); row.getCell(9).numFmt = '#,##0"đ"'; row.getCell(9).font = { bold: true }; cr++; }); ws.getRow(cr).height = 28; ws.mergeCells(cr,1,cr,5); ws.getCell('A'+cr).value = 'TỔNG CỘNG'; ws.getCell('A'+cr).font = { bold: true, color: { argb: 'FFFFFFFF' } }; ws.getCell('A'+cr).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; ws.getCell('E'+cr).value = { formula: 'SUM(I'+firstRow+':I'+(cr-1)+')', result: qd.grandTotal }; ws.getCell('E'+cr).numFmt = '#,##0"đ"'; wb.calcProperties.fullCalcOnLoad = true; var buf = await wb.xlsx.writeBuffer(); var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); dl(blob, code + '.xlsx'); if (typeof showToast === 'function') showToast('✅ Excel báo giá tải thành công!'); } catch(err) { console.error('[VAI] Export error:', err); alert('❌ Lỗi: ' + err.message); } } // Export delivery async function exportDelivery() { var order = window.VAI_CURRENT_ORDER; var code = genCode((order && order.customer) ? order.customer : ''); var qd = order && order.items ? { customer: { name: order.customer || '', phone: order.phone || '', addr: order.addr || '', date: '' }, items: (order.items || []).map(function(it, i) { return { stt: i+1, name: it.name || '', model: it.model || '', qty: it.qty || 1, note: it.note || '', specs: it.specs || '', price: it.price || 0, discPrice: it.discPrice || it.price || 0, total: it.total || 0 }; }), grandTotal: order.grandTotal || 0 } : getQD(); if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'); if (!qd.items.length) return alert('⚠️ Không có sản phẩm'); try { var wb = new ExcelJS.Workbook(); var ws = wb.addWorksheet('Giao hàng'); ws.views = [{ showGridLines: false }]; ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}]; ws.mergeCells('A1:E1'); ws.getCell('A1').value = 'PHIẾU GIAO HÀNG'; ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } }; ws.getCell('A2').value = 'KH: ' + qd.customer.name; ws.getCell('D2').value = 'Mã: ' + code; ws.getCell('A3').value = 'SĐT: ' + qd.customer.phone; ws.getCell('A4').value = 'Địa chỉ: ' + qd.customer.addr; var hr = ws.getRow(6); ['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){ var c = hr.getCell(i+1); c.value = h; c.font = { bold: true, color: { argb: 'FFFFFFFF' } }; c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; }); var cr = 7, totalQty = 0; qd.items.forEach(function(it, i) { var row = ws.getRow(cr); row.height = 20; row.getCell(1).value = it.stt; row.getCell(2).value = it.name; row.getCell(3).value = it.model; row.getCell(4).value = it.qty; row.getCell(5).value = it.note; totalQty += Number(it.qty); cr++; }); ws.mergeCells(cr,1,cr,3); ws.getCell('A'+cr).value = 'TỔNG CỘNG SỐ LƯỢNG'; ws.getCell('D'+cr).value = totalQty; var buf = await wb.xlsx.writeBuffer(); var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); dl(blob, 'GH-' + code + '.xlsx'); if (typeof showToast === 'function') showToast('✅ Excel giao hàng tải thành công!'); } catch(err) { console.error('[VAI] Delivery export error:', err); alert('❌ Lỗi: ' + err.message); } } // Click handler function clickHdl(e) { var btn = e.target.closest('button, a'); if (!btn) return; var txt = (btn.textContent || '').toLowerCase(); var btnAct = document.activeElement; // Quote Excel if (txt.includes('excel') && !txt.includes('giao') && !txt.includes('gh-')) { e.preventDefault(); e.stopPropagation(); if (btn.dataset.vaiExporting === '1') return; setBtn(btn, true); exportExcel().finally(function() { setBtn(btn, false); }); return false; } // Delivery Excel if ((txt.includes('giao hàng') || txt.includes('gh')) && txt.includes('excel')) { e.preventDefault(); e.stopPropagation(); if (btn.dataset.vaiExporting === '1') return; setBtn(btn, true); exportDelivery().finally(function() { setBtn(btn, false); }); return false; } } // Init function init() { document.removeEventListener('click', clickHdl, true); document.addEventListener('click', clickHdl, true); console.log('[VAI v2] EXCEL MULTI-DOWNLOAD FIX ACTIVE'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } setTimeout(init, 2000); setTimeout(init, 5000); window.VAI_EXCEL_V2 = { init, exportExcel, exportDelivery, getQD }; })();