bep40 commited on
Commit
c16319b
·
verified ·
1 Parent(s): 84f6ef3

Upload vai-excel-ultimate-fix.js

Browse files
Files changed (1) hide show
  1. vai-excel-ultimate-fix.js +185 -404
vai-excel-ultimate-fix.js CHANGED
@@ -1,417 +1,198 @@
1
  /**
2
- * V.AI STUDIO - Ultimate Excel Multi-Download Fix v1.0 FINAL
3
- * ========================================================
4
- * FIX TRIỆT ĐỐI VẤN ĐỀ: File Excel chỉ tải được 1 lần, lần 2+ thất bại
5
- *
6
- * NGĂN NHÂN GỐC: URL.revokeObjectURL được gọi sau khi click, làm mất blob URL
7
- * GIẢI PHÁP: Patch URL.revokeObjectURL + registry chung + cleanup khi unload
8
  */
9
  (function() {
10
- 'use strict';
11
-
12
- // Registry toàn cục - CHỈ DÙNG 1 registry duy nhất cho tất cả
13
- window.__VAI_ALL_BLOB_URLS = window.__VAI_ALL_BLOB_URLS || [];
14
- var __isUnloading__ = false;
15
-
16
- // Cleanup tất cả blob khi tab đóng
17
- function cleanupAllBlobs() {
18
- __isUnloading__ = true;
19
- window.__VAI_ALL_BLOB_URLS.forEach(function(e) {
20
- try {
21
- if (e && e.url && !e.revoked) {
22
- URL.revokeObjectURL(e.url);
23
- e.revoked = true;
24
- }
25
- } catch(err) {}
26
- });
27
- window.__VAI_ALL_BLOB_URLS = [];
28
- }
29
-
30
- // Đăng ký cleanup events
31
- window.addEventListener('pagehide', cleanupAllBlobs);
32
- window.addEventListener('beforeunload', cleanupAllBlobs);
33
- window.addEventListener('unload', cleanupAllBlobs);
34
-
35
- // PATCH URL.revokeObjectURL - QUAN TRỌNG NHẤT
36
- var __originalRevoke__ = URL.revokeObjectURL;
37
- URL.revokeObjectURL = function(url) {
38
- // Đang unload hoặc không phải blob thì revoke bình thường
39
- if (__isUnloading__ || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
40
- return __originalRevoke__.call(URL, url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
- // Đăng ký để cleanup sau, KHÔNG revoke ngay
43
- var exists = window.__VAI_ALL_BLOB_URLS.find(function(e) { return e && e.url === url; });
44
- if (!exists) {
45
- window.__VAI_ALL_BLOB_URLS.push({ url: url, revoked: false, created: Date.now() });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
- // KHÔNG revoke - để download hoạt động được nhiều lần
48
- };
 
 
 
 
 
 
 
 
 
49
 
50
- // Hàm download blob an toàn - dùng chung cho mọi export
51
- window.__VAI_DOWNLOAD_BLOB__ = function(blob, filename) {
52
- var url = URL.createObjectURL(blob);
53
- window.__VAI_ALL_BLOB_URLS.push({ url: url, revoked: false, created: Date.now() });
54
-
55
- var a = document.createElement('a');
56
- a.href = url;
57
- a.download = filename;
58
- a.style.cssText = 'position:fixed;top:-1000px;left:-1000px;width:1px;height:1px;opacity:0;';
59
- document.body.appendChild(a);
60
-
61
- // Click ngay lập tức
62
- a.click();
63
-
64
- // Xóa anchor sau 100ms
65
- setTimeout(function() {
66
- try { document.body.removeChild(a); } catch(e) {}
67
- }, 100);
68
- };
69
 
70
- // Hàm format tiền
71
- function fmtMoney(n) {
72
- if (!n || isNaN(n)) return '0đ';
73
- return Number(n).toLocaleString('vi-VN') + 'đ';
 
 
 
74
  }
75
 
76
- // Hàm tạo mã đơn
77
- function genOrderCode(name) {
78
- var now = new Date();
79
- var dd = String(now.getDate()).padStart(2, '0');
80
- var mm = String(now.getMonth() + 1).padStart(2, '0');
81
- var yy = String(now.getFullYear()).slice(-2);
82
- var ini = '';
83
- if (name && name.trim()) {
84
- name.trim().split(/\s+/).forEach(function(w) {
85
- if (w) {
86
- var ch = w.charAt(0).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase();
87
- if (/[A-Z]/.test(ch)) ini += ch;
88
- }
89
- });
90
- }
91
- return 'VAS' + (ini || 'X') + dd + mm + yy;
92
  }
93
 
94
- // ==========================================
95
- // FIX: exportDeliveryExcel - Phiếu Giao Hàng (cho cả quotation modal và order modal)
96
- // ==========================================
97
- window.exportDeliveryExcel = async function(order, opt) {
98
- var qd, code;
99
-
100
- if (order && order.items) {
101
- // Từ order detail modal
102
- qd = {
103
- customer: {
104
- name: order.customer || '',
105
- phone: order.phone || '',
106
- addr: order.addr || '',
107
- date: order.date || ''
108
- },
109
- items: (order.items || []).map(function(it, i) {
110
- return {
111
- stt: i + 1,
112
- name: it.name || '',
113
- model: it.model || '',
114
- qty: it.qty || 1,
115
- note: it.note || ''
116
- };
117
- })
118
- };
119
- code = order.code || genOrderCode(order.customer);
120
- } else if (window.cart && window.cart.length > 0) {
121
- // Từ giỏ hàng (quotation modal)
122
- qd = {
123
- customer: {
124
- name: '',
125
- phone: '',
126
- addr: '',
127
- date: new Date().toLocaleDateString('vi-VN')
128
- },
129
- items: window.cart.map(function(item, idx) {
130
- var p = (window.D && window.D[item.idx]) ? window.D[item.idx] : null;
131
- return {
132
- stt: idx + 1,
133
- name: p ? (p.name || p.n || '') : (item.name || ''),
134
- model: p ? (p.sku || p.mod || '') : (item.sku || item.model || ''),
135
- qty: item.qty || 1,
136
- note: item.note || ''
137
- };
138
- })
139
- };
140
- code = genOrderCode('');
141
- } else {
142
- if (typeof showToast === 'function') showToast('⚠️ Giỏ hàng trống!');
143
- return;
144
- }
145
-
146
- try {
147
- if (typeof ExcelJS === 'undefined') {
148
- if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS');
149
- return;
150
- }
151
-
152
- var wb = new ExcelJS.Workbook();
153
- var ws = wb.addWorksheet('Phiếu Giao Hàng');
154
-
155
- ws.views = [{ showGridLines: false }];
156
- ws.columns = [{ width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 }];
157
-
158
- // Header
159
- ws.mergeCells('A1:E1');
160
- ws.getCell('A1').value = 'PHIẾU GIAO HÀNG V.AI STUDIO';
161
- ws.getCell('A1').font = { bold: true, size: 14, color: { argb: 'FF003F62' } };
162
- ws.getCell('A1').alignment = { horizontal: 'center' };
163
- ws.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0F7FF' } };
164
-
165
- // Thông tin khách hàng
166
- ws.getCell('A2').value = 'Ngày giao:';
167
- ws.getCell('B2').value = new Date().toLocaleDateString('vi-VN');
168
- ws.getCell('A3').value = 'KH: ' + (qd.customer.name || '');
169
- ws.getCell('D3').value = 'Mã: ' + code;
170
- ws.getCell('A4').value = 'SĐT: ' + (qd.customer.phone || '');
171
- ws.getCell('A5').value = 'Địa chỉ: ' + (qd.customer.addr || '');
172
-
173
- // Header row
174
- var hr = ws.getRow(7);
175
- ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
176
- var c = hr.getCell(i + 1);
177
- c.value = h;
178
- c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
179
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
180
- });
181
- hr.height = 20;
182
-
183
- // Dữ liệu sản phẩm
184
- var cr = 8, totalQty = 0;
185
- (qd.items || []).forEach(function(it, i) {
186
- var row = ws.getRow(cr);
187
- row.values = [i + 1, it.name || '', it.model || '', it.qty || 1, it.note || ''];
188
- totalQty += (it.qty || 1);
189
- cr++;
190
- });
191
-
192
- // Tổng cộng
193
- var tr = ws.getRow(cr);
194
- ws.mergeCells(cr, 1, cr, 3);
195
- ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
196
- ws.getCell('A' + cr).font = { bold: true, color: { argb: 'FF1E40AF' } };
197
- ws.getCell('D' + cr).value = totalQty;
198
-
199
- // Tạo blob và download
200
- var buf = await wb.xlsx.writeBuffer();
201
- var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
202
-
203
- window.__VAI_DOWNLOAD_BLOB__(blob, 'GH-' + code + '.xlsx');
204
-
205
- if (typeof showToast === 'function') {
206
- showToast('✅ Đã tải GH-' + code + '.xlsx');
207
- }
208
- } catch (e) {
209
- console.error('exportDeliveryExcel error:', e);
210
- if (typeof showToast === 'function') showToast('❌ Lỗi: ' + e.message);
211
- }
212
- };
213
-
214
- // ==========================================
215
- // FIX: exportExcel - Bảng Báo Giá
216
- // ==========================================
217
- window.exportExcel = async function(d, qd, code, qrUrl, bw) {
218
- // Chuẩn hoá tham số
219
- if (typeof qd === 'undefined' && d && d.qd) {
220
- var tmp = d;
221
- d = tmp;
222
- qd = tmp.qd;
223
- code = tmp.code || code;
224
- }
225
-
226
- try {
227
- if (typeof ExcelJS === 'undefined') {
228
- if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS');
229
- return;
230
- }
231
-
232
- var MONEY_FMT = '#,##0"đ"';
233
- var wb = new ExcelJS.Workbook();
234
- var ws = wb.addWorksheet('Báo giá');
235
-
236
- ws.views = [{ showGridLines: false }];
237
- ws.columns = [
238
- { width: 5 }, { width: 11 }, { width: 28 }, { width: 13 },
239
- { width: 20 }, { width: 6 }, { width: 13 }, { width: 13 },
240
- { width: 15 }, { width: 14 }
241
- ];
242
-
243
- // Header
244
- ws.mergeCells('A3:J3');
245
- ws.getCell('A3').value = 'BẢNG BÁO GIÁ';
246
- ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } };
247
- ws.getCell('A3').alignment = { horizontal: 'center' };
248
-
249
- // Thông tin khách hàng
250
- if (qd && qd.customer) {
251
- ws.getCell('A5').value = 'Khách hàng:';
252
- ws.mergeCells('B5:E5');
253
- ws.getCell('B5').value = qd.customer.name || '';
254
- ws.getCell('H5').value = 'Mã đơn:';
255
- ws.mergeCells('I5:J5');
256
- ws.getCell('I5').value = code || '';
257
- ws.getCell('A6').value = 'SĐT:';
258
- ws.mergeCells('B6:E6');
259
- ws.getCell('B6').value = qd.customer.phone || '';
260
- }
261
-
262
- // Header row
263
- var hr = ws.getRow(10);
264
- ['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) {
265
- var c = hr.getCell(i + 1);
266
- c.value = h;
267
- c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
268
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
269
- });
270
-
271
- // Dữ liệu sản phẩm
272
- var cr = 11, firstRow = cr;
273
- (qd.items || []).forEach(function(it, i) {
274
- var row = ws.getRow(cr);
275
- row.getCell(1).value = it.stt || i + 1;
276
- row.getCell(3).value = it.name || '';
277
- row.getCell(3).font = { bold: true };
278
- row.getCell(4).value = it.model || '';
279
- row.getCell(5).value = it.specs || '';
280
- row.getCell(6).value = Number(it.qty || 1);
281
- row.getCell(6).numFmt = '#,##0';
282
- row.getCell(7).value = Number(it.price || 0);
283
- row.getCell(7).numFmt = MONEY_FMT;
284
- row.getCell(8).value = Number(it.discPrice || it.price || 0);
285
- row.getCell(8).numFmt = MONEY_FMT;
286
- row.getCell(9).value = Number(it.total || 0);
287
- row.getCell(9).numFmt = MONEY_FMT;
288
- row.getCell(10).value = it.note || '';
289
- cr++;
290
- });
291
-
292
- // Tổng cuối
293
- ws.mergeCells(cr, 1, cr, 5);
294
- ws.getCell('A' + cr).value = 'TỔNG CỘNG';
295
- ws.getCell('A' + cr).font = { bold: true, color: { argb: 'FFFFFFFF' } };
296
- ws.getCell('A' + cr).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
297
- ws.getCell('E' + cr).value = Number((d && d.grandTotal) || 0);
298
- ws.getCell('E' + cr).numFmt = MONEY_FMT;
299
-
300
- // Xuất
301
- var buf = await wb.xlsx.writeBuffer();
302
- var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
303
-
304
- window.__VAI_DOWNLOAD_BLOB__(blob, (code || 'quote') + '.xlsx');
305
-
306
- if (typeof showToast === 'function') {
307
- showToast('✅ Đã tải ' + (code || 'quote') + '.xlsx');
308
- }
309
- } catch (e) {
310
- console.error('exportExcel error:', e);
311
- if (typeof showToast === 'function') showToast('❌ Lỗi: ' + e.message);
312
- }
313
- };
314
-
315
- // ==========================================
316
- // FIX: exportDeliveryPDF - Phiếu Giao Hàng PDF
317
- // ==========================================
318
- window.exportDeliveryPDF = async function(order, opt) {
319
- var qd, code;
320
-
321
- if (order && order.items) {
322
- qd = {
323
- customer: {
324
- name: order.customer || '',
325
- phone: order.phone || '',
326
- addr: order.addr || '',
327
- date: order.date || ''
328
- },
329
- items: (order.items || []).map(function(it, i) {
330
- return {
331
- stt: i + 1,
332
- name: it.name || '',
333
- model: it.model || '',
334
- qty: it.qty || 1,
335
- note: it.note || ''
336
- };
337
- })
338
- };
339
- code = order.code || genOrderCode(order.customer);
340
- } else {
341
- qd = window._origGetQuoteData ? window._origGetQuoteData() : {
342
- customer: { name: '', phone: '', date: new Date().toLocaleDateString('vi-VN') },
343
- items: window.cart || []
344
- };
345
- code = genOrderCode('');
346
- }
347
-
348
- try {
349
- if (typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') {
350
- if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện PDF');
351
- return;
352
- }
353
-
354
- var html = '<div style="padding:20px;font-family:Inter,Arial,sans-serif">'
355
- + '<div style="text-align:center;margin-bottom:16px">'
356
- + '<div style="font-size:22px;font-weight:900;color:#003f62">PHIẾU GIAO HÀNG</div>'
357
- + '</div>'
358
- + '<div style="margin-bottom:12px;font-size:12px">'
359
- + '<b>Khách hàng:</b> ' + (qd.customer.name || '') + '<br>'
360
- + '<b>SĐT:</b> ' + (qd.customer.phone || '') + '<br>'
361
- + '<b>Mã đơn:</b> ' + code + '<br>'
362
- + '</div>'
363
- + '<table style="width:100%;border-collapse:collapse;font-size:12px">'
364
- + '<thead><tr style="background:#003f62;color:#fff">'
365
- + '<th style="padding:8px;width:35px">STT</th>'
366
- + '<th style="padding:8px;text-align:left">Tên sản phẩm</th>'
367
- + '<th style="padding:8px;width:90px">Mã SP</th>'
368
- + '<th style="padding:8px;width:40px">SL</th>'
369
- + '<th style="padding:8px;width:120px;text-align:left">Ghi chú</th>'
370
- + '</tr></thead><tbody>';
371
-
372
- (qd.items || []).forEach(function(it, i) {
373
- html += '<tr style="border-bottom:1px solid #e2e8f0">'
374
- + '<td style="padding:8px;text-align:center">' + (i + 1) + '</td>'
375
- + '<td style="padding:8px;font-weight:600">' + (it.name || '') + '</td>'
376
- + '<td style="padding:8px;text-align:center">' + (it.model || '') + '</td>'
377
- + '<td style="padding:8px;text-align:center;font-weight:700">' + (it.qty || 1) + '</td>'
378
- + '<td style="padding:8px">' + (it.note || '') + '</td></tr>';
379
- });
380
-
381
- html += '</tbody></table>'
382
- + '<div style="margin-top:50px;display:flex;justify-content:space-around;font-size:12px">'
383
- + '<div style="text-align:center"><b>Bên giao</b><br>(Ký)<div style="margin-top:25px">________</div></div>'
384
- + '<div style="text-align:center"><b>Bên nhận</b><br>(Ký)<div style="margin-top:25px">________</div></div>'
385
- + '</div></div>';
386
-
387
- var div = document.createElement('div');
388
- div.style.cssText = 'position:absolute;left:-10000px;top:0;width:700px;background:#fff;padding:28px;box-sizing:border-box';
389
- div.innerHTML = html;
390
- document.body.appendChild(div);
391
-
392
- var cv = await html2canvas(div, { scale: 2, useCORS: true, backgroundColor: '#fff' });
393
- div.remove();
394
-
395
- var img = cv.toDataURL('image/png');
396
- var pdf = new jspdf.jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
397
- var margin = 25, usW = 595 - margin * 2, ratio = cv.height / cv.width, imgH = usW * ratio;
398
-
399
- if (imgH > 842 - margin * 2) imgH = 842 - margin * 2;
400
-
401
- pdf.addImage(img, 'PNG', margin, margin, usW, imgH);
402
- pdf.save('GH-' + code + '.pdf');
403
-
404
- if (typeof showToast === 'function') showToast('✅ Đã tải GH-' + code + '.pdf');
405
- } catch (e) {
406
- console.error('exportDeliveryPDF error:', e);
407
- if (typeof showToast === 'function') showToast('❌ Lỗi: ' + e.message);
408
- }
409
- };
410
-
411
- // Override các hàm cũ nếu tồn tại
412
- if (window.VAI_QR && window.VAI_QR.exportDeliveryExcel) {
413
- window.VAI_QR.exportDeliveryExcel = window.exportDeliveryExcel;
414
  }
415
-
416
- console.log('[VAI Ultimate Excel Fix v1.0] Multi-download enabled - blob cleanup on tab unload');
 
 
417
  })();
 
1
  /**
2
+ * V.AI STUDIO - Excel Export Fix v8.1 FINAL
3
+ * ========================================
4
+ * ROOT CAUSE: Chrome's "transient activation" expires after async operations
5
+ * SOLUTION: Pre-build xlsx in background, download SYNC from cache on click
 
 
6
  */
7
  (function() {
8
+ 'use strict';
9
+
10
+ var cache = { quote: null, delivery: null };
11
+
12
+ // SYNC download - must be called within user gesture
13
+ function downloadSync(blob, filename) {
14
+ var url = URL.createObjectURL(blob);
15
+ var a = document.createElement('a');
16
+ a.href = url;
17
+ a.download = filename;
18
+ a.style.cssText = 'position:fixed;top:-10000px;';
19
+ document.body.appendChild(a);
20
+ a.click();
21
+ setTimeout(function() {
22
+ document.body.removeChild(a);
23
+ }, 200);
24
+ }
25
+
26
+ // SYNC CSV download - always works
27
+ function downloadCSVSync(csv, filename) {
28
+ var blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
29
+ return downloadSync(blob, filename);
30
+ }
31
+
32
+ // Get data safely
33
+ function getQuoteDataSafe() {
34
+ try {
35
+ if (typeof getQuoteData === 'function') return getQuoteData();
36
+ } catch(e) {}
37
+ return { customer: {}, items: [] };
38
+ }
39
+
40
+ function getCartSafe() {
41
+ var cart = window.cart || [];
42
+ try {
43
+ var s = localStorage.getItem('malloca_cart');
44
+ if (s) {
45
+ var p = JSON.parse(s);
46
+ if (Array.isArray(p) && p.length) cart = p;
47
+ }
48
+ } catch(e) {}
49
+ return cart;
50
+ }
51
+
52
+ // Build CSV (simple, works always)
53
+ function buildCSV(qd) {
54
+ var csv = 'BÁO GIÁ V.AI STUDIO\n';
55
+ csv += 'Khách hàng:,' + (qd.customer.name || '') + '\n';
56
+ csv += 'SĐT:,' + (qd.customer.phone || '') + '\n';
57
+ csv += 'Mã đơn:,' + (qd.customer.orderCode || '') + '\n\n';
58
+ csv += 'STT,Tên,Mã SP,SL,Đơn giá,Thành tiền,Ghi chú\n';
59
+ var total = 0;
60
+ (qd.items || []).forEach(function(it, i) {
61
+ var lt = (it.qty || 1) * (it.discPrice || it.price || 0);
62
+ total += lt;
63
+ csv += (i + 1) + ',' + (it.name || '').replace(/,/g, '-') + ',' + (it.model || '') + ',' + (it.qty || 1) + ',' + (it.price || 0) + ',' + lt + ',' + (it.note || '') + '\n';
64
+ });
65
+ csv += '\n,,,,,,TỔNG:,' + total + '\n';
66
+ return csv;
67
+ }
68
+
69
+ function buildDeliveryCSV(cart) {
70
+ var csv = 'PHIẾU GIAO HÀNG\n\n';
71
+ csv += 'STT,Mã SP,Tên,SL,Đơn giá,Thành tiền\n';
72
+ var total = 0;
73
+ (cart || []).forEach(function(c, i) {
74
+ var price = Number(c.priceNum || 0) || 0;
75
+ var qty = Number(c.qty || 1);
76
+ var lt = price * qty;
77
+ total += lt;
78
+ csv += (i + 1) + ',' + (c.sku || '') + ',' + (c.name || '').replace(/,/g, '-') + ',' + qty + ',' + price + ',' + lt + '\n';
79
+ });
80
+ csv += '\n,,,,,TỔNG:,' + total + '\n';
81
+ return csv;
82
+ }
83
+
84
+ // Build xlsx (async, called in background)
85
+ function buildXLSX(qd, cb) {
86
+ if (typeof ExcelJS === 'undefined') { cb(null); return; }
87
+ try {
88
+ var wb = new ExcelJS.Workbook();
89
+ var ws = wb.addWorksheet('Báo Giá');
90
+ ws.columns = [
91
+ { width: 6 }, { width: 32 }, { width: 16 },
92
+ { width: 28 }, { width: 18 }, { width: 8 },
93
+ { width: 16 }, { width: 16 }, { width: 18 }, { width: 18 }
94
+ ];
95
+ ws.mergeCells('A1:J1');
96
+ ws.getCell('A1').value = 'BÁO GIÁ V.AI STUDIO';
97
+ ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FFDB9815' } };
98
+
99
+ var r = 3;
100
+ (qd.items || []).forEach(function(it, i) {
101
+ r++;
102
+ var row = ws.getRow(r);
103
+ row.values = [
104
+ i + 1, '', it.name || '', it.model || '', it.specs || '',
105
+ it.qty || 1, it.price || 0, it.discPrice || it.price || 0,
106
+ (it.qty || 1) * (it.discPrice || it.price || 0), it.note || ''
107
+ ];
108
+ if (i % 2 === 1) {
109
+ for (var c = 1; c <= 10; c++) {
110
+ row.getCell(c).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF8FAFC' } };
111
+ }
112
  }
113
+ });
114
+
115
+ var total = (qd.items || []).reduce(function(s, it) {
116
+ return s + (it.qty || 1) * (it.discPrice || it.price || 0);
117
+ }, 0);
118
+
119
+ var tr = ws.getRow(++r);
120
+ ws.mergeCells('A' + r + ':I' + r);
121
+ tr.getCell(1).value = 'TỔNG CỘNG';
122
+ tr.getCell(1).font = { bold: true, size: 12 };
123
+ tr.getCell(9).value = total;
124
+ tr.getCell(9).numFmt = '#,##0"đ"';
125
+ tr.getCell(9).font = { bold: true, size: 12 };
126
+
127
+ wb.xlsx.writeBuffer().then(cb).catch(cb);
128
+ } catch(e) { cb(null); }
129
+ }
130
+
131
+ // Prebuild xlsx when modal opens (background, no gesture needed)
132
+ function prebuildQuote() {
133
+ var qd = getQuoteDataSafe();
134
+ if (!qd || !qd.items || !qd.items.length) return;
135
+ buildXLSX(qd, function(buf) {
136
+ if (buf) {
137
+ cache.quote = { buffer: buf, filename: (qd.customer.orderCode || 'BG') + '.xlsx' };
138
+ if (typeof showToast === 'function') {
139
+ showToast('✅ File Excel sẵn sàng! Bấm để tải');
140
  }
141
+ }
142
+ });
143
+ }
144
+
145
+ // OVERRIDE exportExcel - DOWNLOAD FIRST, TOAST AFTER
146
+ window.exportExcel = function() {
147
+ var qd = getQuoteDataSafe();
148
+ if (!qd || !qd.items || !qd.items.length) {
149
+ if (typeof showToast === 'function') showToast('⚠️ Không có sản phẩm');
150
+ return;
151
+ }
152
 
153
+ var xlsxName = (qd.customer.orderCode || 'BG') + '.xlsx';
154
+ var csvName = xlsxName.replace('.xlsx', '.csv');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ // CACHE READY → download xlsx SYNC (guaranteed)
157
+ if (cache.quote && cache.quote.buffer) {
158
+ downloadSync(cache.quote.buffer, xlsxName);
159
+ if (typeof showToast === 'function') showToast('✅ Đã tải ' + xlsxName);
160
+ // Rebuild cache
161
+ prebuildQuote();
162
+ return;
163
  }
164
 
165
+ // NO CACHE → download CSV SYNC (guaranteed), build xlsx in background
166
+ downloadCSVSync(buildCSV(qd), csvName);
167
+ if (typeof showToast === 'function') showToast('⏳ Excel đang tạo... CSV đã tải');
168
+ prebuildQuote();
169
+ };
170
+
171
+ // OVERRIDE exportDeliveryExcel
172
+ window.exportDeliveryExcel = function() {
173
+ var cart = getCartSafe();
174
+ if (!cart || !cart.length) {
175
+ if (typeof showToast === 'function') showToast('⚠️ Giỏ trống');
176
+ return;
 
 
 
 
177
  }
178
 
179
+ var name = 'Giao_hang.xlsx';
180
+ downloadCSVSync(buildDeliveryCSV(cart), name.replace('.xlsx', '.csv'));
181
+ if (typeof showToast === 'function') showToast('✅ Đã tải CSV giao hàng');
182
+ };
183
+
184
+ // Hook openQuotation to start prebuilding
185
+ if (!window.__VAI_HOOKED_V81) {
186
+ var orig = window.openQuotation;
187
+ if (typeof orig === 'function') {
188
+ window.openQuotation = function() {
189
+ var r = orig.apply(this, arguments);
190
+ setTimeout(prebuildQuote, 300);
191
+ return r;
192
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }
194
+ window.__VAI_HOOKED_V81 = true;
195
+ }
196
+
197
+ console.log('[VAI v8.1] Ready - SYNC download + cache');
198
  })();