V.AISTUDIO / excel-fix-v2.js
bep40's picture
Tạo file excel-fix-v2.js hoàn chỉnh + cập nhật index.html (#12)
77021d6
Raw
History Blame
16.4 kB
/**
* 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 = '<span style="opacity:0.7">⏳ Đang xuất...</span>';
} 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
};
})();