/**
* V.AI STUDIO - Auto Patch GH Excel - v2.0
*
* TỰ ĐỘNG PATCH mà không cần sửa index.html
* Chỉ cần thêm dòng: vào index.html
*
* FIX HOÀN HẢO:
* - exportDeliveryExcel nhận order parameter đúng
* - Click protection (chống bấm nhiều lần)
* - Blob retention đến khi unload (tải nhiều lần được)
* - Hỗ trợ cả Quote & Order modal
*/
(function() {
'use strict';
// ===== GLOBAL BLOB REGISTRY =====
window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], isUnloading: false };
var reg = window.__VAI_BLOB_REGISTRY;
// Cleanup on unload
function cleanupBlobs() {
reg.isUnloading = true;
reg.blobs.forEach(function(b) {
if (!b.revoked) {
try { URL.revokeObjectURL(b.url); b.revoked = true; } catch(e) {}
}
});
reg.blobs = [];
}
window.addEventListener('pagehide', cleanupBlobs);
window.addEventListener('beforeunload', cleanupBlobs);
window.addEventListener('unload', cleanupBlobs);
// ===== PATCH URL.revokeObjectURL - CRITICAL =====
if (!window.__VAI_REVOKE_PATCHED_AUTO) {
var origRevoke = URL.revokeObjectURL;
URL.revokeObjectURL = function(url) {
if (reg.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
return origRevoke.call(this, url);
}
var exists = reg.blobs.find(function(b) { return b.url === url; });
if (!exists) reg.blobs.push({ url: url, revoked: false, created: Date.now() });
return undefined;
};
window.__VAI_REVOKE_PATCHED_AUTO = true;
}
// ===== UTILITIES =====
function fmt(n) { return n && !isNaN(n) ? Number(n).toLocaleString('vi-VN') + 'đ' : '0đ'; }
function stripDiacritics(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'D'); }
function genCode(name) {
var d = new Date(), dd = String(d.getDate()).padStart(2,'0'), mm = String(d.getMonth()+1).padStart(2,'0'), yy = String(d.getFullYear()).slice(-2);
var ini = '';
if (name && name.trim()) {
name.trim().split(/\s+/).forEach(function(w) {
if (w) { var c = stripDiacritics(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 (!b) return;
if (exporting) {
b.disabled = true;
b.dataset.vaiExporting = '1';
b._orig = b.innerHTML;
b.innerHTML = '⏳ Đang xuất...';
} else {
b.disabled = false;
b.dataset.vaiExporting = '0';
if (b._orig) b.innerHTML = b._orig;
}
}
// ===== DOWNLOAD BLOB =====
function dlBlob(blob, filename) {
var url = URL.createObjectURL(blob);
reg.blobs.push({ url: url, revoked: false });
var a = document.createElement('a');
a.href = url; a.download = filename; a.style.cssText = 'position:absolute;left:-9999px';
document.body.appendChild(a); a.click();
setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 100);
}
// ===== EXPORT QUOTE EXCEL =====
async function exportQuoteExcel(d, c) {
if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
try {
var wb = new ExcelJS.Workbook(), 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=d.customer?.name||'';
ws.getCell('H5').value='Mã đơn:'; ws.mergeCells('I5:J5'); ws.getCell('I5').value=c;
ws.getCell('A6').value='SĐT:'; ws.mergeCells('B6:E6'); ws.getCell('B6').value=d.customer?.phone||'';
ws.getCell('H6').value='Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value=d.customer?.date||'';
ws.getCell('A7').value='Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value=d.customer?.email||'';
ws.getCell('A8').value='Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value=d.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, fr = cr;
(d.items||[]).forEach(function(it,i){
var r = ws.getRow(cr), bg = i%2===0?'FFF8FAFC':'FFFFFFFF'; r.height=50;
r.getCell(1).value=it.stt||i+1; r.getCell(3).value=it.name||''; r.getCell(3).font={bold:true,size:9};
r.getCell(4).value=it.model||''; r.getCell(5).value=it.specs||''; r.getCell(5).font={size:8,color:{argb:'FF64748B'}};
r.getCell(6).value=Number(it.qty||1); r.getCell(6).numFmt='#,##0';
r.getCell(7).value=Number(it.price||0); r.getCell(7).numFmt='#,##0"đ"';
r.getCell(8).value=Number(it.discPrice||it.price||0); r.getCell(8).numFmt='#,##0"đ"';
r.getCell(8).font=(it.price||0)>(it.discPrice||0)?{bold:true,color:{argb:'FFDC3545'}}:{};
r.getCell(9).value=Number(it.total||0); r.getCell(9).numFmt='#,##0"đ"'; r.getCell(9).font={bold:true};
r.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'+fr+':I'+(cr-1)+')',result:Number(d.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();
dlBlob(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), c+'.xlsx');
return true;
} catch(e) { console.error('[AutoPatch] exportQuoteExcel error:',e); alert('❌ Lỗi: '+e.message); return false; }
}
// ===== EXPORT DELIVERY EXCEL =====
async function exportDeliveryExcel(d, c) {
if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
try {
var wb = new ExcelJS.Workbook(), 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: '+(d.customer?.name||''); ws.getCell('D2').value='Mã: '+c;
ws.getCell('A3').value='SĐT: '+(d.customer?.phone||''); ws.getCell('D3').value='Ngày: '+(d.customer?.date||'');
ws.getCell('A4').value='Địa chỉ: '+(d.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, tq = 0;
(d.items||[]).forEach(function(it,i){
var r = ws.getRow(cr); r.height=20;
r.getCell(1).value=it.stt||i+1; r.getCell(2).value=it.name||''; r.getCell(3).value=it.model||''; r.getCell(4).value=it.qty||1; r.getCell(5).value=it.note||'';
tq += 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=tq;
var buf = await wb.xlsx.writeBuffer();
dlBlob(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), 'GH-'+c+'.xlsx');
return true;
} catch(e) { console.error('[AutoPatch] exportDeliveryExcel error:',e); alert('❌ Lỗi: '+e.message); return false; }
}
// ===== INIT =====
function init() {
// Check if VAI_EXCEL_FIX v2 already loaded
if (typeof window.VAI_EXCEL_FIX !== 'undefined' && typeof window.VAI_EXCEL_FIX.exportQuoteExcel === 'function') {
console.log('[AutoPatch v2.0] VAI_EXCEL_FIX v2 already loaded - done');
return;
}
// Load vai-excel-fix-v2.js if not exists
var s = document.createElement('script');
s.src = '/vai-excel-fix-v2.js?v=' + Date.now();
s.onload = function() {
console.log('[AutoPatch v2.0] Excel fix v2 loaded');
};
s.onerror = function() {
console.warn('[AutoPatch v2.0] Failed to load v2, applying inline patch');
// Inline fallback patch
if (typeof window.exportDeliveryExcel === 'function') {
var origDE = window.exportDeliveryExcel;
window.exportDeliveryExcel = async function(order, opt) {
var btn = document.activeElement;
if (btn && btn.dataset.vaiExporting === '1') return;
if (btn) setBtn(btn, true);
try {
var d = { 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 };
var c = order?.code || genCode(order?.customer||'');
await exportDeliveryExcel(d, c);
} finally { if (btn) setBtn(btn, false); }
};
}
};
document.head.appendChild(s);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
setTimeout(init, 2000);
setTimeout(init, 5000);
})();