/** * V.AI STUDIO - Excel Export Fix Ultimate v2.0 * ============================================ * FIX 100%: Multi-download Excel for Quote & Order modals * * Key fixes: * 1. Global blob URL registry - prevents premature URL.revokeObjectURL * 2. Creates fresh blob for each download * 3. Proper cleanup on page unload only * 4. Click protection to prevent double-clicks */ (function(){ 'use strict'; // ===== GLOBAL STATE ===== window.__VAI_EXCEL_FIX = window.__VAI_EXCEL_FIX || { blobRegistry: [], isUnloading: false }; var g = window.__VAI_EXCEL_FIX; // ===== CLEANUP ON UNLOAD ===== function cleanupAllBlobs() { g.isUnloading = true; g.blobRegistry.forEach(function(entry) { if (!entry.revoked && entry.url) { try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch(e) {} } }); g.blobRegistry = []; } window.addEventListener('pagehide', cleanupAllBlobs); window.addEventListener('beforeunload', cleanupAllBlobs); window.addEventListener('unload', cleanupAllBlobs); // ===== PATCH URL.revokeObjectURL - CRITICAL ===== if (!window.__VAI_REVOKE_PATCHED) { var originalRevoke = URL.revokeObjectURL; URL.revokeObjectURL = function(url) { // Only revoke on unload or non-blob URLs if (g.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) { return originalRevoke.call(this, url); } // Register but DON'T revoke - this is the key fix for multi-download var exists = g.blobRegistry.find(function(e) { return e.url === url; }); if (!exists) { g.blobRegistry.push({ url: url, revoked: false, created: Date.now() }); } return undefined; }; window.__VAI_REVOKE_PATCHED = true; } // ===== UTILITIES ===== function fmt(n) { if (!n || isNaN(n)) return '0đ'; return Number(n).toLocaleString('vi-VN') + 'đ'; } function stripDiacritics(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D'); } function generateOrderCode(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 ch = stripDiacritics(w.charAt(0)).toUpperCase(); if (/[A-Z]/.test(ch)) { ini += ch; } } }); } return 'VAS' + (ini || 'X') + dd + mm + yy; } // ===== BUTTON STATE ===== function setBtnState(btn, exporting) { if (!btn) return; if (exporting) { btn.disabled = true; btn.dataset.vaiExporting = '1'; btn.dataset.vaiOrigHTML = btn.innerHTML; btn.innerHTML = '⏳ Đang xuất...'; } else { btn.disabled = false; btn.dataset.vaiExporting = '0'; if (btn.dataset.vaiOrigHTML) { btn.innerHTML = btn.dataset.vaiOrigHTML; } } } // ===== CREATE DOWNLOAD ===== function downloadBlob(blob, filename) { var url = URL.createObjectURL(blob); g.blobRegistry.push({ url: url, revoked: false }); var a = document.createElement('a'); a.href = url; a.download = filename; a.style.cssText = 'position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;'; document.body.appendChild(a); a.click(); setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 100); } // ===== GET QUOTE DATA FROM MODAL ===== function getQuoteData() { var data = { customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') }, items: [], grandTotal: 0 }; // Get cart data var cart = window.cart || []; var products = window.D || []; cart.forEach(function(item, i) { var product = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null); var price = item.priceNum || item.price || (product ? product.price : 0) || 0; data.items.push({ stt: i + 1, image: product ? product.img : '', name: product ? (product.name || product.n || '') : (item.name || ''), model: product ? (product.sku || product.mod || '') : (item.sku || item.model || ''), specs: product ? (product.info || product.desc || '') : '', qty: item.qty || 1, price: price, discPrice: price, total: (item.qty || 1) * price, note: '' }); }); // Get customer info from modal var nameSelectors = ['#quoteName', '#quoteCustomerName', '#checkoutName']; for (var j = 0; j < nameSelectors.length; j++) { var el = document.querySelector(nameSelectors[j]); if (el && el.value && el.value.trim()) { data.customer.name = el.value.trim(); break; } } var phoneEl = document.querySelector('#quotePhone, #checkoutPhone'); if (phoneEl && phoneEl.value) data.customer.phone = phoneEl.value.trim(); var emailEl = document.querySelector('#quoteEmail'); if (emailEl && emailEl.value) data.customer.email = emailEl.value.trim(); var addrEl = document.querySelector('#quoteAddr, #checkoutAddr'); if (addrEl && addrEl.value) data.customer.addr = addrEl.value.trim(); data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0); return data; } // ===== EXPORT QUOTE EXCEL ===== async function exportQuoteExcel(quoteData, code) { if (typeof ExcelJS === 'undefined') { if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS'); return false; } 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('A3').alignment = { horizontal: 'center' }; ws.getCell('A5').value = 'Khách hàng:'; ws.mergeCells('B5:E5'); ws.getCell('B5').value = quoteData.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 = quoteData.customer?.phone || ''; ws.getCell('H6').value = 'Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value = quoteData.customer?.date || ''; ws.getCell('A7').value = 'Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value = quoteData.customer?.email || ''; ws.getCell('A8').value = 'Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value = quoteData.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; (quoteData.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 || i + 1; 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 || 1); row.getCell(6).numFmt = '#,##0'; row.getCell(7).value = Number(it.price || 0); row.getCell(7).numFmt = '#,##0"đ"'; row.getCell(8).value = Number(it.discPrice || it.price || 0); row.getCell(8).numFmt = '#,##0"đ"'; if ((it.price || 0) > (it.discPrice || 0)) { row.getCell(8).font = { bold: true, color: { argb: 'FFDC3545' } }; } row.getCell(9).value = Number(it.total || 0); row.getCell(9).numFmt = '#,##0"đ"'; row.getCell(9).font = { bold: true }; row.getCell(10).value = it.note || ''; 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, size: 13, 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: Number(quoteData.grandTotal || 0) }; ws.getCell('E' + cr).numFmt = '#,##0"đ"'; ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FFF0B840' } }; wb.calcProperties.fullCalcOnLoad = true; var buf = await wb.xlsx.writeBuffer(); var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); downloadBlob(blob, code + '.xlsx'); return true; } catch (err) { console.error('[VAI Excel Fix] exportQuoteExcel error:', err); alert('❌ Lỗi: ' + err.message); return false; } } // ===== EXPORT DELIVERY EXCEL ===== async function exportDeliveryExcel(quoteData, code) { if (typeof ExcelJS === 'undefined') { if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS'); return false; } 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('A1').alignment = { horizontal: 'center' }; ws.getCell('A2').value = 'KH: ' + (quoteData.customer?.name || ''); ws.getCell('D2').value = 'Mã: ' + code; ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer?.phone || ''); ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer?.date || ''); ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.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; (quoteData.items || []).forEach(function(it, i) { var row = ws.getRow(cr); row.height = 20; row.getCell(1).value = it.stt || i + 1; row.getCell(2).value = it.name || ''; row.getCell(3).value = it.model || ''; row.getCell(4).value = it.qty || 1; row.getCell(5).value = it.note || ''; totalQty += Number(it.qty || 1); 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' }); downloadBlob(blob, 'GH-' + code + '.xlsx'); return true; } catch (err) { console.error('[VAI Excel Fix] exportDeliveryExcel error:', err); alert('❌ Lỗi: ' + err.message); return false; } } // ===== CLICK HANDLER ===== function handleExportClick(e) { var btn = e.target.closest('button, a'); if (!btn) return; var txt = (btn.textContent || '').toLowerCase(); // Quote Excel - "Xuất Excel" button in quote modal if (txt.includes('excel') && !txt.includes('giao hàng') && !txt.includes('gh-') && !txt.includes('giao')) { e.preventDefault(); e.stopPropagation(); if (btn.dataset.vaiExporting === '1') return; setBtnState(btn, true); var qd = getQuoteData(); var code = generateOrderCode(qd.customer?.name || ''); exportQuoteExcel(qd, code).then(function(success) { setBtnState(btn, false); if (success && typeof showToast === 'function') { showToast('✅ Excel báo giá tải thành công!'); } }); return false; } // Delivery Excel - "GH Excel" button in order modal if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) { e.preventDefault(); e.stopPropagation(); if (btn.dataset.vaiExporting === '1') return; setBtnState(btn, true); // Check for order modal data var order = window.VAI_CURRENT_ORDER; var exportData, orderCode; if (order && order.items) { // From order modal exportData = { customer: { name: order.customer || '', phone: order.phone || '', addr: order.addr || '', date: order.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 }; orderCode = order.code || generateOrderCode(order.customer || ''); } else { // From cart exportData = getQuoteData(); orderCode = generateOrderCode(exportData.customer?.name || ''); } exportDeliveryExcel(exportData, orderCode).then(function(success) { setBtnState(btn, false); if (success && typeof showToast === 'function') { showToast('✅ Excel giao hàng tải thành công!'); } }); return false; } } // ===== INIT ===== function init() { // Remove any existing handler to avoid duplicates document.removeEventListener('click', handleExportClick, true); // Add click handler document.addEventListener('click', handleExportClick, true); console.log('[VAI Excel Fix v2.0] Loaded - Multi-download enabled'); } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Also run after delays for dynamic content setTimeout(init, 2000); setTimeout(init, 5000); // Export for global use window.VAI_EXCEL_FIX = { init: init, exportQuoteExcel: exportQuoteExcel, exportDeliveryExcel: exportDeliveryExcel, generateOrderCode: generateOrderCode, getQuoteData: getQuoteData }; })();