bep40 commited on
Commit
b9658ab
·
verified ·
1 Parent(s): c6a365a

Upload vai-excel-download-v2.js

Browse files
Files changed (1) hide show
  1. vai-excel-download-v2.js +243 -0
vai-excel-download-v2.js ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO - Excel Download Multi-Click Fix v2.0
3
+ *
4
+ * VẤN ĐỀ: File Excel chỉ tải được 1 lần, lần 2+ không hoạt động
5
+ *
6
+ * NGĂN NHÂN GỐC:
7
+ * 1. URL.revokeObjectURL() được gọi sau 60s (trong setTimeout)
8
+ * 2. Một số trình duyệt (Safari, Firefox) cần thời gian download lâu hơn
9
+ * 3. Blob URL bị thu hồi khiến download thất bại
10
+ *
11
+ * GIẢI PHÁP:
12
+ * 1. KHÔNG revoke ngay - để blob tự giải phóng khi tab đóng
13
+ * 2. TẠO MỚI blob hoàn toàn cho mỗi lần click
14
+ * 3. Cleanup blob khi unload để tránh rò rỉ bộ nhớ
15
+ * 4. Tương thích mọi trình duyệt
16
+ */
17
+ (function() {
18
+ 'use strict';
19
+
20
+ // Registry lưu blob URLs
21
+ var excelBlobRegistry = [];
22
+
23
+ // Cleanup khi rời trang
24
+ function cleanupBlobs() {
25
+ excelBlobRegistry.forEach(function(entry) {
26
+ if (!entry.revoked && entry.url) {
27
+ try {
28
+ URL.revokeObjectURL(entry.url);
29
+ entry.revoked = true;
30
+ } catch (e) {}
31
+ }
32
+ });
33
+ }
34
+
35
+ window.addEventListener('pagehide', cleanupBlobs);
36
+ window.addEventListener('beforeunload', cleanupBlobs);
37
+ window.addEventListener('unload', cleanupBlobs);
38
+
39
+ // PATCH URL.revokeObjectURL - hoàn toàn không revoke cho blob
40
+ var originalRevoke = URL.revokeObjectURL;
41
+ URL.revokeObjectURL = function(url) {
42
+ // Bỏ qua revoke cho blob URLs - để cleanup khi unload
43
+ if (url && typeof url === 'string' && url.startsWith('blob:')) {
44
+ // Chỉ đăng ký, không revoke
45
+ var exists = excelBlobRegistry.find(function(e) { return e.url === url; });
46
+ if (!exists) {
47
+ excelBlobRegistry.push({ url: url, revoked: false, created: Date.now() });
48
+ }
49
+ return;
50
+ }
51
+ return originalRevoke.call(this, url);
52
+ };
53
+
54
+ // PATCH window.exportDeliveryExcel để tạo blob mới mỗi lần
55
+ var originalExportDeliveryExcel = window.exportDeliveryExcel;
56
+ window.exportDeliveryExcel = async function() {
57
+ if (!window._origGetQuoteData) return;
58
+
59
+ var qd = window._origGetQuoteData();
60
+ var code = window.VAI_QR ? window.VAI_QR.getEffectiveOrderCode() : 'VAS';
61
+
62
+ if (typeof ExcelJS === 'undefined') return;
63
+
64
+ // TẠO WORKBOOK HOÀN TOÀN MỚI
65
+ var wb = new ExcelJS.Workbook();
66
+ var ws = wb.addWorksheet('Giao hàng');
67
+
68
+ // Style headers
69
+ ws.views = [{ showGridLines: false }];
70
+ ws.columns = [{ width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 }];
71
+
72
+ var WHITE = { argb: 'FFFFFFFF' };
73
+ var WB = {
74
+ top: { style: 'thin', color: WHITE },
75
+ bottom: { style: 'thin', color: WHITE },
76
+ left: { style: 'thin', color: WHITE },
77
+ right: { style: 'thin', color: WHITE }
78
+ };
79
+ var HB = {
80
+ top: { style: 'thin', color: { argb: 'FF003F62' } },
81
+ bottom: { style: 'thin', color: { argb: 'FF003F62' } },
82
+ left: { style: 'thin', color: { argb: 'FF003F62' } },
83
+ right: { style: 'thin', color: { argb: 'FF003F62' } }
84
+ };
85
+
86
+ // Header
87
+ ws.mergeCells('A1:E1');
88
+ ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
89
+ ws.getCell('A1').font = { bold: true, size: 15, color: { argb: 'FF003F62' } };
90
+ ws.getCell('A1').alignment = { horizontal: 'center' };
91
+ ws.getCell('A1').border = WB;
92
+ ws.getRow(1).height = 24;
93
+
94
+ // Customer info
95
+ ws.getCell('A2').value = 'KH: ' + (qd.customer ? qd.customer.name : '');
96
+ ws.getCell('A2').font = { bold: true, size: 10 };
97
+ ws.getCell('A2').border = WB;
98
+ ws.getCell('D2').value = 'Mã: ' + code;
99
+ ws.getCell('D2').font = { bold: true, size: 9, color: { argb: 'FF003F62' } };
100
+ ws.getCell('D2').border = WB;
101
+ ws.getCell('A3').value = 'SĐT: ' + (qd.customer ? qd.customer.phone : '');
102
+ ws.getCell('A3').border = WB;
103
+ ws.getCell('D3').value = 'Ngày: ' + (qd.customer ? qd.customer.date : '');
104
+ ws.getCell('D3').border = WB;
105
+ ws.getCell('A4').value = 'Địa chỉ: ' + (qd.customer ? (qd.customer.addr || '') : '');
106
+ ws.getCell('A4').border = WB;
107
+
108
+ ws.getRow(5).height = 4;
109
+
110
+ // Table header
111
+ var hr = ws.getRow(6);
112
+ ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
113
+ var c = hr.getCell(i + 1);
114
+ c.value = h;
115
+ c.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 10 };
116
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
117
+ c.alignment = { horizontal: 'center', vertical: 'middle' };
118
+ c.border = HB;
119
+ });
120
+ hr.height = 20;
121
+
122
+ // Items
123
+ var cr = 7;
124
+ var items = qd.items || [];
125
+ for (var i = 0; i < items.length; i++) {
126
+ var it = items[i];
127
+ var row = ws.getRow(cr);
128
+ row.height = 20;
129
+ var bgColor = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
130
+ var RB = {
131
+ top: { style: 'thin', color: { argb: bgColor } },
132
+ bottom: { style: 'thin', color: { argb: bgColor } },
133
+ left: { style: 'thin', color: { argb: bgColor } },
134
+ right: { style: 'thin', color: { argb: bgColor } }
135
+ };
136
+ row.getCell(1).value = it.stt || i + 1;
137
+ row.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' };
138
+ row.getCell(2).value = it.name || '';
139
+ row.getCell(2).font = { size: 10 };
140
+ row.getCell(3).value = it.model || '';
141
+ row.getCell(3).alignment = { horizontal: 'center', vertical: 'middle' };
142
+ row.getCell(4).value = it.qty || 1;
143
+ row.getCell(4).alignment = { horizontal: 'center', vertical: 'middle' };
144
+ row.getCell(4).font = { bold: true };
145
+ row.getCell(5).value = it.note || '';
146
+ for (var ci = 1; ci <= 5; ci++) {
147
+ row.getCell(ci).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: bgColor } };
148
+ row.getCell(ci).border = RB;
149
+ }
150
+ cr++;
151
+ }
152
+
153
+ // Total row
154
+ var totalQtyRow = cr;
155
+ var TQBG = 'FFEFF6FF';
156
+ var TQB = {
157
+ top: { style: 'thin', color: { argb: TQBG } },
158
+ bottom: { style: 'thin', color: { argb: TQBG } },
159
+ left: { style: 'thin', color: { argb: TQBG } },
160
+ right: { style: 'thin', color: { argb: TQBG } }
161
+ };
162
+ ws.mergeCells(totalQtyRow, 1, totalQtyRow, 3);
163
+ ws.getCell('A' + totalQtyRow).value = 'TỔNG CỘNG SỐ LƯỢNG';
164
+ ws.getCell('A' + totalQtyRow).font = { bold: true, size: 10, color: { argb: 'FF1E40AF' } };
165
+ ws.getCell('A' + totalQtyRow).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: TQBG } };
166
+ ws.getCell('A' + totalQtyRow).border = TQB;
167
+ ws.getCell('D' + totalQtyRow).value = {
168
+ formula: items.length ? 'SUM(D7:D' + (cr - 1) + ')' : '0',
169
+ result: items.reduce(function(s, it) { return s + Number(it.qty || 1); }, 0)
170
+ };
171
+ ws.getCell('D' + totalQtyRow).numFmt = '#,##0';
172
+ ws.getCell('D' + totalQtyRow).font = { bold: true, size: 10, color: { argb: 'FF1E40AF' } };
173
+ ws.getCell('D' + totalQtyRow).alignment = { horizontal: 'center' };
174
+ ws.getCell('D' + totalQtyRow).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: TQBG } };
175
+ ws.getCell('D' + totalQtyRow).border = TQB;
176
+ ws.getCell('E' + totalQtyRow).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: TQBG } };
177
+ ws.getCell('E' + totalQtyRow).border = TQB;
178
+ cr++;
179
+
180
+ // Signature rows
181
+ cr += 2;
182
+ ws.getCell('A' + cr).value = 'Bên giao: ________________';
183
+ ws.getCell('A' + cr).border = WB;
184
+ ws.getCell('D' + cr).value = 'Bên nhận: ________________';
185
+ ws.getCell('D' + cr).border = WB;
186
+ cr++;
187
+ ws.getCell('A' + cr).value = '(Ký, họ tên)';
188
+ ws.getCell('A' + cr).font = { italic: true, size: 8, color: { argb: 'FF64748B' } };
189
+ ws.getCell('A' + cr).border = WB;
190
+ ws.getCell('D' + cr).value = '(Ký, họ tên)';
191
+ ws.getCell('D' + cr).font = { italic: true, size: 8, color: { argb: 'FF64748B' } };
192
+ ws.getCell('D' + cr).border = WB;
193
+
194
+ // Apply borders to all cells
195
+ ws.eachRow(function(row) {
196
+ row.eachCell(function(cell) {
197
+ if (!cell.border) cell.border = WB;
198
+ });
199
+ });
200
+
201
+ // TẠO BLOB MỚI HOÀN TOÀN
202
+ var buf = await wb.xlsx.writeBuffer();
203
+ var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
204
+ var url = URL.createObjectURL(blob);
205
+
206
+ // Download NGAY LẬP TỨC
207
+ var a = document.createElement('a');
208
+ a.href = url;
209
+ a.download = 'GH-' + code + '.xlsx';
210
+ a.style.display = 'none';
211
+ document.body.appendChild(a);
212
+
213
+ // Force click
214
+ a.click();
215
+
216
+ // Cleanup link element (giữ blob sống)
217
+ setTimeout(function() {
218
+ try { document.body.removeChild(a); } catch (e) {}
219
+ }, 100);
220
+
221
+ // THÔNG BÁO
222
+ if (typeof showToast === 'function') {
223
+ showToast('📥 Đã tải GH-' + code + '.xlsx');
224
+ }
225
+ };
226
+
227
+ // PATCH window.exportExcel (báo giá)
228
+ var originalExportExcel = window.exportExcel;
229
+ window.exportExcel = async function() {
230
+ if (!window.VAI_QR || !window.VAI_QR.getData) return;
231
+
232
+ var d = window.VAI_QR.getData();
233
+ var code = window.VAI_QR.getEffectiveOrderCode();
234
+ var qrUrl = window.VAI_QR.getQRUrl ? window.VAI_QR.getQRUrl(d.remaining || d.grandTotal, code) : '';
235
+
236
+ // Gọi export gốc nếu có
237
+ if (window.VAI_QR.exportExcel) {
238
+ await window.VAI_QR.exportExcel(d, d.qd, code, qrUrl);
239
+ }
240
+ };
241
+
242
+ console.log('[Fix v2.0] Excel multi-download patch loaded - blob will persist until tab unload');
243
+ })();