File size: 11,297 Bytes
9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d 783bcfe 9186b1d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | /**
* 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: <script src="auto-patch-gh-excel.js"></script> 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 = '<span style="opacity:0.7">⏳ Đang xuất...</span>';
} 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);
})(); |