/** * 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); })();