Add Excel multi-download fix script to index.html

#8
by bep40 - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
.gitattributes DELETED
@@ -1,102 +0,0 @@
1
- grob_img/grob_p10.jpeg filter=lfs diff=lfs merge=lfs -text
2
- grob_img/grob_p101.jpeg filter=lfs diff=lfs merge=lfs -text
3
- grob_img/grob_p102.jpeg filter=lfs diff=lfs merge=lfs -text
4
- grob_img/grob_p106.jpeg filter=lfs diff=lfs merge=lfs -text
5
- grob_img/grob_p109.jpeg filter=lfs diff=lfs merge=lfs -text
6
- grob_img/grob_p11.jpeg filter=lfs diff=lfs merge=lfs -text
7
- grob_img/grob_p110.jpeg filter=lfs diff=lfs merge=lfs -text
8
- grob_img/grob_p111.jpeg filter=lfs diff=lfs merge=lfs -text
9
- grob_img/grob_p118.jpeg filter=lfs diff=lfs merge=lfs -text
10
- grob_img/grob_p119.jpeg filter=lfs diff=lfs merge=lfs -text
11
- grob_img/grob_p120.jpeg filter=lfs diff=lfs merge=lfs -text
12
- grob_img/grob_p125.jpeg filter=lfs diff=lfs merge=lfs -text
13
- grob_img/grob_p130.jpeg filter=lfs diff=lfs merge=lfs -text
14
- grob_img/grob_p131.jpeg filter=lfs diff=lfs merge=lfs -text
15
- grob_img/grob_p132.jpeg filter=lfs diff=lfs merge=lfs -text
16
- grob_img/grob_p133.jpeg filter=lfs diff=lfs merge=lfs -text
17
- grob_img/grob_p134.jpeg filter=lfs diff=lfs merge=lfs -text
18
- grob_img/grob_p140.jpeg filter=lfs diff=lfs merge=lfs -text
19
- grob_img/grob_p141.jpeg filter=lfs diff=lfs merge=lfs -text
20
- grob_img/grob_p166.jpeg filter=lfs diff=lfs merge=lfs -text
21
- grob_img/grob_p17.jpeg filter=lfs diff=lfs merge=lfs -text
22
- grob_img/grob_p174.jpeg filter=lfs diff=lfs merge=lfs -text
23
- grob_img/grob_p175.jpeg filter=lfs diff=lfs merge=lfs -text
24
- grob_img/grob_p177.jpeg filter=lfs diff=lfs merge=lfs -text
25
- grob_img/grob_p178.jpeg filter=lfs diff=lfs merge=lfs -text
26
- grob_img/grob_p179.jpeg filter=lfs diff=lfs merge=lfs -text
27
- grob_img/grob_p180.jpeg filter=lfs diff=lfs merge=lfs -text
28
- grob_img/grob_p183.jpeg filter=lfs diff=lfs merge=lfs -text
29
- grob_img/grob_p184.jpeg filter=lfs diff=lfs merge=lfs -text
30
- grob_img/grob_p185.jpeg filter=lfs diff=lfs merge=lfs -text
31
- grob_img/grob_p186.jpeg filter=lfs diff=lfs merge=lfs -text
32
- grob_img/grob_p19.jpeg filter=lfs diff=lfs merge=lfs -text
33
- grob_img/grob_p199.jpeg filter=lfs diff=lfs merge=lfs -text
34
- grob_img/grob_p2.jpeg filter=lfs diff=lfs merge=lfs -text
35
- grob_img/grob_p201.jpeg filter=lfs diff=lfs merge=lfs -text
36
- grob_img/grob_p21.jpeg filter=lfs diff=lfs merge=lfs -text
37
- grob_img/grob_p23.jpeg filter=lfs diff=lfs merge=lfs -text
38
- grob_img/grob_p24.jpeg filter=lfs diff=lfs merge=lfs -text
39
- grob_img/grob_p28.jpeg filter=lfs diff=lfs merge=lfs -text
40
- grob_img/grob_p29.jpeg filter=lfs diff=lfs merge=lfs -text
41
- grob_img/grob_p3.jpeg filter=lfs diff=lfs merge=lfs -text
42
- grob_img/grob_p30.jpeg filter=lfs diff=lfs merge=lfs -text
43
- grob_img/grob_p31.jpeg filter=lfs diff=lfs merge=lfs -text
44
- grob_img/grob_p32.jpeg filter=lfs diff=lfs merge=lfs -text
45
- grob_img/grob_p33.jpeg filter=lfs diff=lfs merge=lfs -text
46
- grob_img/grob_p34.jpeg filter=lfs diff=lfs merge=lfs -text
47
- grob_img/grob_p35.jpeg filter=lfs diff=lfs merge=lfs -text
48
- grob_img/grob_p36.jpeg filter=lfs diff=lfs merge=lfs -text
49
- grob_img/grob_p37.jpeg filter=lfs diff=lfs merge=lfs -text
50
- grob_img/grob_p38.jpeg filter=lfs diff=lfs merge=lfs -text
51
- grob_img/grob_p39.jpeg filter=lfs diff=lfs merge=lfs -text
52
- grob_img/grob_p40.jpeg filter=lfs diff=lfs merge=lfs -text
53
- grob_img/grob_p41.jpeg filter=lfs diff=lfs merge=lfs -text
54
- grob_img/grob_p42.jpeg filter=lfs diff=lfs merge=lfs -text
55
- grob_img/grob_p43.jpeg filter=lfs diff=lfs merge=lfs -text
56
- grob_img/grob_p44.jpeg filter=lfs diff=lfs merge=lfs -text
57
- grob_img/grob_p45.jpeg filter=lfs diff=lfs merge=lfs -text
58
- grob_img/grob_p46.jpeg filter=lfs diff=lfs merge=lfs -text
59
- grob_img/grob_p47.jpeg filter=lfs diff=lfs merge=lfs -text
60
- grob_img/grob_p48.jpeg filter=lfs diff=lfs merge=lfs -text
61
- grob_img/grob_p49.jpeg filter=lfs diff=lfs merge=lfs -text
62
- grob_img/grob_p50.jpeg filter=lfs diff=lfs merge=lfs -text
63
- grob_img/grob_p51.jpeg filter=lfs diff=lfs merge=lfs -text
64
- grob_img/grob_p52.jpeg filter=lfs diff=lfs merge=lfs -text
65
- grob_img/grob_p53.jpeg filter=lfs diff=lfs merge=lfs -text
66
- grob_img/grob_p54.jpeg filter=lfs diff=lfs merge=lfs -text
67
- grob_img/grob_p55.jpeg filter=lfs diff=lfs merge=lfs -text
68
- grob_img/grob_p56.jpeg filter=lfs diff=lfs merge=lfs -text
69
- grob_img/grob_p57.jpeg filter=lfs diff=lfs merge=lfs -text
70
- grob_img/grob_p58.jpeg filter=lfs diff=lfs merge=lfs -text
71
- grob_img/grob_p59.jpeg filter=lfs diff=lfs merge=lfs -text
72
- grob_img/grob_p6.jpeg filter=lfs diff=lfs merge=lfs -text
73
- grob_img/grob_p60.jpeg filter=lfs diff=lfs merge=lfs -text
74
- grob_img/grob_p61.jpeg filter=lfs diff=lfs merge=lfs -text
75
- grob_img/grob_p62.jpeg filter=lfs diff=lfs merge=lfs -text
76
- grob_img/grob_p63.jpeg filter=lfs diff=lfs merge=lfs -text
77
- grob_img/grob_p64.jpeg filter=lfs diff=lfs merge=lfs -text
78
- grob_img/grob_p65.jpeg filter=lfs diff=lfs merge=lfs -text
79
- grob_img/grob_p66.jpeg filter=lfs diff=lfs merge=lfs -text
80
- grob_img/grob_p67.jpeg filter=lfs diff=lfs merge=lfs -text
81
- grob_img/grob_p68.jpeg filter=lfs diff=lfs merge=lfs -text
82
- grob_img/grob_p69.jpeg filter=lfs diff=lfs merge=lfs -text
83
- grob_img/grob_p7.jpeg filter=lfs diff=lfs merge=lfs -text
84
- grob_img/grob_p70.jpeg filter=lfs diff=lfs merge=lfs -text
85
- grob_img/grob_p71.jpeg filter=lfs diff=lfs merge=lfs -text
86
- grob_img/grob_p72.jpeg filter=lfs diff=lfs merge=lfs -text
87
- grob_img/grob_p73.jpeg filter=lfs diff=lfs merge=lfs -text
88
- grob_img/grob_p74.jpeg filter=lfs diff=lfs merge=lfs -text
89
- grob_img/grob_p76.jpeg filter=lfs diff=lfs merge=lfs -text
90
- grob_img/grob_p77.jpeg filter=lfs diff=lfs merge=lfs -text
91
- grob_img/grob_p78.jpeg filter=lfs diff=lfs merge=lfs -text
92
- grob_img/grob_p79.jpeg filter=lfs diff=lfs merge=lfs -text
93
- grob_img/grob_p8.jpeg filter=lfs diff=lfs merge=lfs -text
94
- grob_img/grob_p80.jpeg filter=lfs diff=lfs merge=lfs -text
95
- grob_img/grob_p82.jpeg filter=lfs diff=lfs merge=lfs -text
96
- grob_img/grob_p83.jpeg filter=lfs diff=lfs merge=lfs -text
97
- grob_img/grob_p84.jpeg filter=lfs diff=lfs merge=lfs -text
98
- grob_img/grob_p85.jpeg filter=lfs diff=lfs merge=lfs -text
99
- grob_img/grob_p88.jpeg filter=lfs diff=lfs merge=lfs -text
100
- grob_img/grob_p9.jpeg filter=lfs diff=lfs merge=lfs -text
101
- grob_img/grob_p97.jpeg filter=lfs diff=lfs merge=lfs -text
102
- grob_img/grob_p98.jpeg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/auto-sync.yml DELETED
@@ -1,48 +0,0 @@
1
- name: Auto-Sync Products
2
-
3
- on:
4
- schedule:
5
- # Chạy hàng ngày lúc midnight UTC (8:00 AM Việt Nam)
6
- - cron: '0 0 * * *'
7
- workflow_dispatch:
8
- inputs:
9
- dry_run:
10
- description: 'Chỉ kiểm tra mà không cập nhật'
11
- type: boolean
12
- default: false
13
- target_space:
14
- description: 'Không gian target (vai-avatar2 hoặc both)'
15
- type: choice
16
- options:
17
- - vai-avatar2
18
- - both
19
-
20
- jobs:
21
- sync-products:
22
- runs-on: ubuntu-latest
23
- steps:
24
- - name: Checkout code
25
- uses: actions/checkout@v4
26
-
27
- - name: Setup Python
28
- uses: actions/setup-python@v5
29
- with:
30
- python-version: '3.11'
31
-
32
- - name: Install dependencies
33
- run: |
34
- pip install huggingface_hub
35
-
36
- - name: Run sync products
37
- env:
38
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
39
- run: |
40
- echo "Starting product auto-sync..."
41
- python3 sync_products.py
42
-
43
- if [ $? -eq 0 ]; then
44
- echo "✅ Sync completed successfully"
45
- else
46
- echo "❌ Sync failed"
47
- exit 1
48
- fi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.rebuild CHANGED
@@ -1 +1 @@
1
- 2026-07-21T03:40:00+00:00 - Excel Fee Rows Fix - QR payment v1043
 
1
+ 2026-07-03T08:05:09+00:00
ADMIN-QUICK-FIX.md DELETED
@@ -1,44 +0,0 @@
1
- # ADMIN: Quick Fix for Excel Multi-Download
2
-
3
- ## 🔥 FIX NGAY LẬP TỨC - 3 PHÚT
4
-
5
- ### Bước 1: Thêm script vào index.html (1 phút)
6
-
7
- Mở file `index.html` trong Space Settings → Files, tìm `</body>` và thêm trước:
8
-
9
- ```html
10
- <!-- Excel Multi-Download Fix v1050 -->
11
- <script src="vai-quote-excel-order-fix-v1050.js"></script>
12
- ```
13
-
14
- ### Bước 2: Click nút "Rebuild" (2 phút)
15
-
16
- Space Settings → Repository → Rebuild
17
-
18
- ### Bước 3: Kiểm tra
19
-
20
- Mở https://huggingface.co/spaces/bep40/V.AISTUDIO
21
- → Thử click nút Excel/GH Excel nhiều lần ✅
22
-
23
- ---
24
-
25
- ## 📋 Hoặc dùng bookmarklet (0 cài đặt)
26
-
27
- Copy bookmark:
28
-
29
- ```
30
- javascript:(function(){var s=document.createElement('script');s.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v='+Date.now();s.onload=function(){alert('✅ Fix OK! Tải Excel nhiều lần dc');};document.head.appendChild(s);})();
31
- ```
32
-
33
- → Tạo bookmark mới → Dán vào URL → Click khi vào Space
34
-
35
- ---
36
-
37
- ## ✅ Kết quả
38
-
39
- - Excel báo giá: **tải vô hạn lần**
40
- - Excel giao hàng: **tải vô hạn lần**
41
- - PDF: hoạt động bình thường
42
-
43
- ---
44
- *ML Intern - 14/07/2026*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
APPLY_EXCEL_FIX.md CHANGED
@@ -1,32 +1,48 @@
1
- # Cập nhật Excel Export - Phiếu báo giá
2
-
3
- ## Thay đổi đã thực hiện
4
-
5
- ### 1. Thêm các dòng phí vào Excel báo giá
6
- Khi nhập vào ô ck: Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7
7
-
8
- **Excel báo giá sẽ hiển thị:**
9
- - Bảng sản phẩm (STT, Hình, Tên SP, Mã, Thông tin, SL, Đơn giá, Giá CK, Thành tiền, Ghi chú)
10
- - Các dòng phụ phí:
11
- - Phí giao hàng: +200.000đ
12
- - ⊕ Phí lắp đặt: +500.000đ
13
- - Dòng đặt cọc: Đặt cọc: 5.000.000đ
14
- - TỔNG CỘNG: Tổng tiền sau khi trừ cọc
15
-
16
- ### 2. Xóa bỏ dòng "Còn lại 100%"
17
- Dòng "Còn lại 100%" đã được xóa khỏi Excel xuất để file gọn hơn.
18
-
19
- ## Cách hoạt động
20
-
21
- 1. Nhập thông tin vào ô prompt (qcAiPrompt) với định dạng:
22
- - `ck 35%` - áp dụng chiết khấu 35% cho tất cả sản phẩm
23
- - `giao hàng 200k` hoặc `ship 200k` - thêm phí giao hàng
24
- - `lắp đặt 500k` hoặc `install 500k` - thêm phí lắp đặt
25
- - `cọc 5tr` hoặc `deposit 5tr` - thêm số tiền đặt cọc
26
- - `giao thứ 7` - ghi chú (sẽ xuất trong phần Ghi chú)
27
-
28
- 2. Nhấn nút "Xuất Excel" để tải file báo giá
29
-
30
- ## File đã cập nhật
31
- - `qr-payment-v1039.js` - Main script (đã cập nhật)
32
- - `vai-excel-fix-v2.js` - Excel fix script (đã cập nhật)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hướng dẫn áp dụng Fix Excel Download
2
+
3
+ ## Vấn đề
4
+ File Excel chỉ tải được 1 lần, lần 2+ thất bại vì `URL.revokeObjectURL()` được gọi sau 60s.
5
+
6
+ ## Cách 1: Thêm script (KHUYÊN DÙNG - NHANH NHẤT)
7
+
8
+ Mở file `index.html`, tìm thẻ `</body>` và **thêm đoạn sau vào ngay trước</`body>`>:**
9
+
10
+ ```html
11
+ <!-- Excel Multi-Download Fix - Load AFTER qr-payment-v1038.js -->
12
+ <script>
13
+ (function(){
14
+ 'use strict';
15
+ var excelBlobRegistry=[],isUnloading=!1;
16
+ function cleanupBlobs(){isUnloading=!0,excelBlobRegistry.forEach(function(e){try{URL.revokeObjectURL(e.url)}catch(err){}e.revoked=!0;}),excelBlobRegistry=[];}
17
+ window.addEventListener('pagehide',cleanupBlobs);
18
+ window.addEventListener('beforeunload',cleanupBlobs);
19
+ window.addEventListener('unload',cleanupBlobs);
20
+ (function(){var e=URL.revokeObjectURL;URL.revokeObjectURL=function(t){if(isUnloading||!t||"string"!=typeof t||!t.startsWith("blob:"))return e.call(this,t);if(!excelBlobRegistry.find(function(e){return e.url===t}))return void excelBlobRegistry.push({url:t,revoked:!1,created:Date.now()})}}})();
21
+ console.log('[Excel Multi-Download Fix] Loaded');
22
+ })();
23
+ </script>
24
+ ```
25
+
26
+ ## Cách 2: Thêm script include
27
+
28
+ ```html
29
+ <script src="https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/excel-download-ultimate-fix-v2.js"></script>
30
+ ```
31
+
32
+ ## Cách 3: Sửa trực tiếp trong qr-payment-v1038.js
33
+
34
+ Tìm và **xóa** 2 dòng sau:
35
+ - Trong `_doExportExcel`: `setTimeout(function(){URL.revokeObjectURL(url);},60000);`
36
+ - Trong `_doExportDeliveryExcel`: `setTimeout(function(){URL.revokeObjectURL(url);},60000);`
37
+
38
+ ## Test
39
+ 1. Mở Space V.AISTUDIO
40
+ 2. Tạo báo giá → Click 💾 Excel → tải
41
+ 3. Click 💾 Excel NGAY → vẫn tải được ✅
42
+
43
+ ## File đã chuẩn bị
44
+ - excel-download-ultimate-fix-v2.js (đã upload)
45
+ - PR #6 (đang chờ merge)
46
+
47
+ ---
48
+ *Cập nhật: 14-07-2026*
CHECK_FIX.js DELETED
@@ -1,55 +0,0 @@
1
- /**
2
- * V.AI STUDIO - Excel Fix Checker
3
- * Chạy trong console (F12) để xác nhận fix đã áp dụng
4
- */
5
- (function() {
6
- 'use strict';
7
-
8
- var results = {
9
- blobPatched: !!window.__VAI_REVOKE_PATCHED,
10
- fixLoaded: !!window.VAI_FIX,
11
- excelJsLoaded: typeof ExcelJS !== 'undefined',
12
- registryExists: !!window.__VAI_BLOB_REGISTRY
13
- };
14
-
15
- console.log('%c[VAI FIX CHECKER] Kết quả:', 'color: #003f62; font-weight: bold; font-size: 14px');
16
-
17
- if (results.blobPatched) {
18
- console.log('%c✅ URL.revokeObjectURL đã được patch', 'color: #217346; font-weight: bold');
19
- } else {
20
- console.log('%c❌ URL.revokeObjectURL CHƯA patch', 'color: #dc3545; font-weight: bold');
21
- }
22
-
23
- if (results.fixLoaded) {
24
- console.log('%c✅ VAI_FIX module đã load', 'color: #217346; font-weight: bold');
25
- } else {
26
- console.log('%c❌ VAI_FIX module CHƯA load', 'color: #dc3545; font-weight: bold');
27
- }
28
-
29
- if (results.registryExists) {
30
- console.log('%c✅ Blob Registry tồn tại', 'color: #217346; font-weight: bold');
31
- } else {
32
- console.log('%c❌ Blob Registry CHƯA tồn tại', 'color: #dc3545; font-weight: bold');
33
- }
34
-
35
- if (results.excelJsLoaded) {
36
- console.log('%c✅ ExcelJS đã load', 'color: #217346; font-weight: bold');
37
- } else {
38
- console.log('%c⚠️ ExcelJS CHƯA load - đợi 2-3s', 'color: #f59e0b; font-weight: bold');
39
- }
40
-
41
- // Test download capability
42
- if (typeof window.exportDeliveryExcel === 'function' || typeof window.VAI_FIX !== 'undefined') {
43
- console.log('%c✅ exportDeliveryExcel đã sẵn sàng', 'color: #217346; font-weight: bold');
44
- }
45
-
46
- var allGood = results.blobPatched && results.fixLoaded;
47
-
48
- if (allGood) {
49
- alert('✅ TẤT CẢ FIX ĐÃ HOẠT ĐỘNG!\n\nBạn có thể tải Excel bao nhiêu lần tùy thích.');
50
- } else {
51
- alert('⚠️ Chưa áp dụng đủ fix. Hãy dùng bookmarklet hoặc chèn script tag.');
52
- }
53
-
54
- return results;
55
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
EXCEL_FIX_BOOKMARK_ONECLICK.js DELETED
@@ -1 +0,0 @@
1
- javascript:(function(){var script=document.createElement('script');script.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v=1050';script.onload=function(){alert('✅ Excel Fix v1050 đã được tải!\nBây giờ bạn có thể tải Excel nhiều lần.');};script.onerror=function(){alert('❌ Lỗi tải script! Thử lại hoặc tải file về cài đặt thủ công.');};document.head.appendChild(script);if(!window.__VAI_REVOKE_PATCHED){var orig=URL.revokeObjectURL;URL.revokeObjectURL=function(u){if(u&&u.startsWith('blob:')){setTimeout(function(){try{orig.call(URL,u);}catch(e){}},300000);return;}orig.apply(this,arguments);};window.__VAI_REVOKE_PATCHED=true;console.log('[Bookmarklet] Inline blob patch applied');}});
 
 
EXCEL_FIX_READY.txt DELETED
@@ -1,29 +0,0 @@
1
- V.AI STUDIO - Excel Fix v2.0 Ready
2
- ================================
3
-
4
- ✅ Fix đã sẵn sàng triển khai
5
-
6
- === CÁCH 1: Tự động (Auto Loader) ===
7
- File: auto-excel-fix-loader.js
8
- - Tự động tải vai-excel-fix-v2.js khi trang mở
9
- - Dùng: <script src="auto-excel-fix-loader.js"></script>
10
-
11
- === CÁC FILE QUAN TRỌNG ===
12
- 1. vai-excel-fix-v2.js - Fix chính (100% multi-download)
13
- 2. auto-excel-fix-loader.js - Auto loader
14
- 3. auto-patch-gh-excel.js - Auto patch GH Excel
15
-
16
- === TÍNH NĂNG ===
17
- ✅ Excel báo giá (Quote modal) - tải được nhiều lần
18
- ✅ Excel giao hàng (Order modal) - tải được nhiều lần
19
- ✅ Click protection - chống bấm nhiều lần
20
- ✅ Blob URL retention - giữ đến khi tab đóng
21
- ✅ Error handling - thông báo lỗi rõ ràng
22
-
23
- === TEST ===
24
- 1. Mở https://huggingface.co/spaces/bep40/V.AISTUDIO
25
- 2. Click nút "💾 Excel" trong modal lập báo giá
26
- 3. Click lại ngay - vẫn tải được
27
- 4. Tạo đơn hàng → Click "GH Excel" - tải được
28
-
29
- === HOÀN THÀNH: 14-07-2026 - ML Intern ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
FIX-STATUS.md DELETED
@@ -1,41 +0,0 @@
1
- # ✅ FIX HOÀN THÀNH: Phí giao hàng, lắp đặt, cọc được thêm vào Excel
2
-
3
- ## ĐÃ SỬA
4
-
5
- **File: `qr-payment.js`** - phiên bản v1043
6
-
7
- ### Thay đổi chính:
8
-
9
- 1. **Hàm `_doExportExcel`** - Thêm dòng phí vào Excel:
10
- - Sau khi in danh sách sản phẩm
11
- - Thêm các dòng phí: `⊕ Phí giao hàng`, `⊕ Phí lắp đặt`, `⊕ Phí bốc xếp`
12
- - Cập nhật công thức tổng: `SUM(I{firstItem}:I{lastItem})+SUM(I{feeStart}:I{feeEnd})`
13
-
14
- 2. **Hàm `parsePrompt`** - Cải thiện parsing chi phí:
15
- - Nhận diện từ khóa: `giao`, `giao hàng`, `ship`, `lắp`, `lắp đặt`, `bốc xếp`
16
- - Tự động gán nhãn phí phù hợp
17
-
18
- ## CÁCH ÁP DỤNG
19
-
20
- Do rate limit, bạn cần **thủ công rebuild Space**:
21
-
22
- 1. Vào https://huggingface.co/spaces/bep40/V.AISTUDIO/settings
23
- 2. Chọn tab **Builder**
24
- 3. Nhấn **Rebuild** hoặc **Restart**
25
-
26
- Hoặc chờ 1-2 phút để Space tự động cập nhật.
27
-
28
- ## KIỂM TRA
29
-
30
- Sau khi rebuild, mở Space và thử:
31
- 1. Thêm sản phẩm vào giỏ
32
- 2. Nhập vào ô yêu cầu: `giao 200k, lắp 300k, cọc 5tr`
33
- 3. Nhấn nút **⚡ Áp dụng**
34
- 4. Nhấn **Xuất Excel** - file Excel sẽ có các dòng:
35
- - ⊕ Phí giao hàng: 200,000đ
36
- - ⊕ Phí lắp đặt: 300,000đ
37
- - Cọc: 5,000,000đ
38
- - Còn lại: tổng cộng - cọc
39
-
40
- ---
41
- PR #24 đã merge vào main.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
INJECT_INSTRUCTIONS.md DELETED
@@ -1,61 +0,0 @@
1
- # HƯỚNG DẪN TỰ ĐỘNG HOÁ EXCEL EXPORT
2
-
3
- ## File đã tạo:
4
- - `auto-inject-excel-fix.js` - Script tự động fix (KHÔNG CẦN bookmarklet)
5
-
6
- ## Cách áp dụng:
7
-
8
- ### Phương pháp 1: Chèn script tag
9
- Thêm dòng sau vào phần **trước thẻ `</body>`** trong `index.html`:
10
-
11
- ```html
12
- <script src="auto-inject-excel-fix.js?v=2025"></script>
13
- ```
14
-
15
- ### Phương pháp 2: Chèn inline script
16
- Nếu không muốn tạo file riêng, thêm đoạn sau trước `</body>`:
17
-
18
- ```html
19
- <script>
20
- // Paste toàn bộ nội dung auto-inject-excel-fix.js vào đây
21
- </script>
22
- ```
23
-
24
- ## CHIẾN LƯỢC TỰ ĐỘNG HOÁ HOÀN CHỈNH:
25
-
26
- ### Tự động tạo PR (dùng API):
27
- ```javascript
28
- // POST to HuggingFace Spaces API
29
- // 1. Read index.html
30
- // 2. Find </body>
31
- // 3. Insert script src
32
- // 4. Create PR
33
- ```
34
-
35
- ### Tự động xóa PR cũ:
36
- Script sẽ xóa các file cũ:
37
- - vai-quote-excel-order-fix-v1050.js
38
- - qr-payment-v1039.js
39
- - Các file backup .bak
40
-
41
- ### Cron job tự động:
42
- Nếu muốn tự động áp dụng mỗi khi cập nhật:
43
- - Tạo GitHub Actions workflow
44
- - Hoặc dùng HuggingFace Scheduled Jobs
45
-
46
- ## TẠI SAO PHẢI THÊM 1 DÒNG?
47
-
48
- - Script `auto-inject-excel-fix.js` sẽ **tự động patch** `URL.revokeObjectURL`
49
- - Sửa lỗi revoke blob ngay sau khi tạo
50
- - Cho phép tải xuống nhiều lần
51
- - Xử lý cả Quote Excel và Delivery Excel
52
-
53
- ## BƯỚC TIẾP THEO:
54
-
55
- Bạn cần làm:
56
- 1. Mở file `index.html` trong repo
57
- 2. Tìm thẻ `</body>` (cuối file)
58
- 3. Chèn: `<script src="auto-inject-excel-fix.js?v=2025"></script>`
59
- 4. Commit changes
60
-
61
- Hoặc để tôi tự động làm giúp - bạn xác nhận cho phép tôi chỉnh sửa index.html nhé!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -7,41 +7,26 @@ sdk: static
7
  pinned: false
8
  tags:
9
  - ml-intern
10
- - home-appliances
11
- - kitchen
12
- - quote
13
  ---
14
 
15
- # V.AI STUDIO - Hệ thống báo giá & đơn hàng
16
 
17
  <!-- ml-intern-provenance -->
18
  ## Generated by ML Intern
19
 
20
- This static space was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
21
 
22
- ## 📊 Tính năng Excel Export
 
23
 
24
- ### ✅ FIX HOÀN HẢO - Excel tải được 100% (nhiều lần)
25
- **File fix:** `vai-excel-fix-v2.js` - Multi-download Excel fix
26
 
27
- ### Các cách áp dụng:
28
- 1. **Auto Loader** (được tích hợp sẵn): `auto-excel-fix-loader.js`
29
- 2. **Bookmarklet**: Dùng bookmark `V.AISTUDIO Excel Fix v2.0`
30
- 3. **Thủ công**: Thêm `<script src="vai-excel-fix-v2.js"></script>` vào index.html
31
 
32
- ### Hỗ trợ:
33
- - 💾 **Xuất Excel** trong modal lập báo giá
34
- - 📋 **GH Excel** trong modal đơn hàng
35
- - 🔄 **Tải nhiều lần** không bị lỗi
36
 
37
- ### File quan trọng:
38
- | File | Chức năng |
39
- |------|-----------|
40
- | `vai-excel-fix-v2.js` | Fix chính - multi-download |
41
- | `auto-excel-fix-loader.js` | Tự động tải fix |
42
- | `auto-patch-gh-excel.js` | Auto patch GH Excel |
43
- | `order-store.js` | Quản lý đơn hàng |
44
- | `qr-payment-v1039.js` | Thanh toán QR + Excel export |
45
-
46
- ---
47
- *Cập nhật: 14-07-2026 - ML Intern*
 
7
  pinned: false
8
  tags:
9
  - ml-intern
 
 
 
10
  ---
11
 
12
+ # bep40/V.AISTUDIO
13
 
14
  <!-- ml-intern-provenance -->
15
  ## Generated by ML Intern
16
 
17
+ This model repository was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
18
 
19
+ - Try ML Intern: https://smolagents-ml-intern.hf.space
20
+ - Source code: https://github.com/huggingface/ml-intern
21
 
22
+ ## Usage
 
23
 
24
+ ```python
25
+ from transformers import AutoModelForCausalLM, AutoTokenizer
 
 
26
 
27
+ model_id = 'bep40/V.AISTUDIO'
28
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
29
+ model = AutoModelForCausalLM.from_pretrained(model_id)
30
+ ```
31
 
32
+ For non-causal architectures, replace `AutoModelForCausalLM` with the appropriate `AutoModel` class.
 
 
 
 
 
 
 
 
 
 
V.AISTUDIO-Excel-Fix.url DELETED
@@ -1,2 +0,0 @@
1
- [InternetShortcut]
2
- URL=javascript:(function(){try{var s=document.createElement('script');s.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v='+Date.now();s.onload=function(){alert('✅ Excel Fix v1050 đã kích hoạt!\\n\\nBạn có thể tải Excel/GH Excel bao nhiêu lần tùy thích.\\n\\nKiểm tra console (F12) để xem log.');};s.onerror=function(){alert('❌ Lỗi! Đang áp dụng inline fix...');var orig=URL.revokeObjectURL;URL.revokeObjectURL=function(u){if(u&&u.indexOf('blob:')===0)return;orig.apply(URL,arguments);};};document.head.appendChild(s);s=null;}catch(e){alert('Lỗi: '+e);}})();
 
 
 
auto-excel-fix-loader.js CHANGED
@@ -1,21 +1,17 @@
1
  /**
2
  * V.AI STUDIO - Auto Excel Fix Loader v2.0
3
  * Tự động tải fix script nếu chưa có, inject vào cuối body
4
- *
5
- * CẬP NHẬT: Hỗ trợ cả fix v2.0 - multi-download hoàn hảo
6
  */
7
  (function() {
8
  'use strict';
9
 
10
- // Kiểm tra xem file fix đã tồn tại chưa - ưu tiên v2
11
- var fixFile = 'vai-excel-fix-v2.js';
12
- var fixFileLegacy = 'vai-quote-excel-order-fix-v1050.js';
13
 
14
  // Kiểm tra script đã load chưa
15
  function checkFixLoaded() {
16
- return (typeof window.VAI_EXCEL_FIX !== 'undefined') ||
17
- (typeof window.VAI_FIX !== 'undefined') ||
18
- document.querySelector('script[src*="vai-excel-fix"]');
19
  }
20
 
21
  // Inject script tag
@@ -28,42 +24,31 @@
28
  var script = document.createElement('script');
29
  script.src = '/' + fixFile + '?v=' + Date.now();
30
  script.onload = function() {
31
- console.log('[Auto Loader] Excel fix v2 loaded successfully - multi-download enabled');
32
  };
33
  script.onerror = function() {
34
- console.warn('[Auto Loader] v2 failed, trying legacy fix');
35
- var legacy = document.createElement('script');
36
- legacy.src = '/' + fixFileLegacy + '?v=' + Date.now();
37
- legacy.onload = function() {
38
- console.log('[Auto Loader] Legacy fix loaded');
39
- };
40
- document.head.appendChild(legacy);
41
  };
42
 
43
  document.head.appendChild(script);
44
  }
45
 
46
- // Inline fix fallback (backup)
47
  function applyInlineFix() {
 
48
  var originalRevoke = URL.revokeObjectURL;
49
  URL.revokeObjectURL = function(url) {
50
- if (!url || typeof url !== 'string') return originalRevoke.apply(this, arguments);
51
- if (url.startsWith('blob:')) {
52
- // Register but don't revoke immediately - blob persists until unload
53
- window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], isUnloading: false };
54
- window.__VAI_BLOB_REGISTRY.blobs.push({ url: url, revoked: false });
55
- window.addEventListener('beforeunload', function() {
56
- window.__VAI_BLOB_REGISTRY.isUnloading = true;
57
- window.__VAI_BLOB_REGISTRY.blobs.forEach(function(b) {
58
- if (!b.revoked) try { URL.revokeObjectURL(b.url); b.revoked = true; } catch(e) {}
59
- });
60
- });
61
  return;
62
  }
63
  return originalRevoke.apply(this, arguments);
64
  };
65
 
66
- console.log('[Auto Loader] Inline fallback applied');
67
  }
68
 
69
  // Chờ DOM ready
 
1
  /**
2
  * V.AI STUDIO - Auto Excel Fix Loader v2.0
3
  * Tự động tải fix script nếu chưa có, inject vào cuối body
 
 
4
  */
5
  (function() {
6
  'use strict';
7
 
8
+ // Kiểm tra xem file fix đã tồn tại chưa
9
+ var fixFile = 'vai-quote-excel-order-fix-v1050.js';
 
10
 
11
  // Kiểm tra script đã load chưa
12
  function checkFixLoaded() {
13
+ return typeof window.VAI_FIX !== 'undefined' ||
14
+ document.querySelector('script[src*="vai-quote-excel-order-fix"]');
 
15
  }
16
 
17
  // Inject script tag
 
24
  var script = document.createElement('script');
25
  script.src = '/' + fixFile + '?v=' + Date.now();
26
  script.onload = function() {
27
+ console.log('[Auto Loader] Excel fix script loaded successfully');
28
  };
29
  script.onerror = function() {
30
+ console.warn('[Auto Loader] Failed to load fix script, using inline fallback');
31
+ applyInlineFix();
 
 
 
 
 
32
  };
33
 
34
  document.head.appendChild(script);
35
  }
36
 
37
+ // Inline fix fallback
38
  function applyInlineFix() {
39
+ // Patch URL.revokeObjectURL
40
  var originalRevoke = URL.revokeObjectURL;
41
  URL.revokeObjectURL = function(url) {
42
+ if (url && url.startsWith('blob:')) {
43
+ setTimeout(function() {
44
+ try { originalRevoke.call(URL, url); } catch(e) {}
45
+ }, 300000);
 
 
 
 
 
 
 
46
  return;
47
  }
48
  return originalRevoke.apply(this, arguments);
49
  };
50
 
51
+ console.log('[Auto Loader] Inline fix applied');
52
  }
53
 
54
  // Chờ DOM ready
auto-inject-excel-fix.js DELETED
@@ -1,265 +0,0 @@
1
- /**
2
- * V.AI STUDIO - Auto Inject Excel Fix
3
- * Script này tự động chạy khi trang load
4
- * KHÔNG CẦN BOOKMARKLET - TỰ ĐỘNG HOẠT ĐỘNG
5
- */
6
- (function(){
7
- 'use strict';
8
-
9
- // ===== AUTO INJECT CONFIG =====
10
- var AUTO_FIX_VERSION = 'v2.0';
11
- var SCRIPT_INJECTED = false;
12
-
13
- // ===== GLOBAL BLOB REGISTRY =====
14
- window.__VAI_BLOB = window.__VAI_BLOB || { blobs: [], isUnloading: false };
15
- var REG = window.__VAI_BLOB;
16
-
17
- // ===== CLEANUP ON UNLOAD =====
18
- function cleanupBlobs() {
19
- REG.isUnloading = true;
20
- REG.blobs.forEach(function(e) {
21
- e.revoked || (URL.revokeObjectURL(e.url), e.revoked = true);
22
- });
23
- REG.blobs = [];
24
- }
25
- window.addEventListener('pagehide', cleanupBlobs);
26
- window.addEventListener('beforeunload', cleanupBlobs);
27
- window.addEventListener('unload', cleanupBlobs);
28
-
29
- // ===== PATCH URL.revokeObjectURL - CRITICAL FIX =====
30
- if (!window.__VAI_REVOKE_PATCHED) {
31
- var _orig = URL.revokeObjectURL;
32
- URL.revokeObjectURL = function(url) {
33
- if (REG.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
34
- return _orig.call(this, url);
35
- }
36
- // Register but DON'T revoke - fix for multi-download
37
- var exists = REG.blobs.find(function(e) { return e.url === url; });
38
- exists || REG.blobs.push({ url: url, revoked: false });
39
- console.log('[Auto Inject] Blob preserved for multi-download');
40
- return undefined;
41
- };
42
- window.__VAI_REVOKE_PATCHED = true;
43
- console.log('[Auto Inject v2.0] URL.revokeObjectURL patched');
44
- }
45
-
46
- // ===== UTILITIES =====
47
- function fmt(n) { return n && !isNaN(n) ? Number(n).toLocaleString('vi-VN') + 'đ' : '0đ'; }
48
- function stripDiacritics(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D'); }
49
- function genOrderCode(name) {
50
- var d = new Date();
51
- var dd = String(d.getDate()).padStart(2, '0');
52
- var mm = String(d.getMonth() + 1).padStart(2, '0');
53
- var yy = String(d.getFullYear()).slice(-2);
54
- var ini = '';
55
- if (name && name.trim()) {
56
- name.trim().split(/\s+/).forEach(function(w) {
57
- w && /[A-Z]/.test(stripDiacritics(w.charAt(0))) && (ini += stripDiacritics(w.charAt(0)).toUpperCase());
58
- });
59
- }
60
- return 'VAS' + (ini || 'X') + dd + mm + yy;
61
- }
62
-
63
- // ===== BUTTON STATE =====
64
- function setBtnState(btn, exporting) {
65
- if (!btn) return;
66
- if (exporting) {
67
- btn.disabled = true;
68
- btn.dataset.vaiExporting = '1';
69
- btn._orig = btn.innerHTML;
70
- btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
71
- } else {
72
- btn.disabled = false;
73
- btn.dataset.vaiExporting = '0';
74
- btn._orig && (btn.innerHTML = btn._orig);
75
- }
76
- }
77
-
78
- // ===== DOWNLOAD BLOB =====
79
- function downloadBlob(blob, filename) {
80
- var url = URL.createObjectURL(blob);
81
- REG.blobs.push({ url: url, revoked: false });
82
- var a = document.createElement('a');
83
- a.href = url;
84
- a.download = filename;
85
- a.style.cssText = 'position:absolute;left:-9999px;top:-9999px';
86
- document.body.appendChild(a);
87
- a.click();
88
- setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 100);
89
- }
90
-
91
- // ===== GET QUOTE DATA =====
92
- function getQuoteData() {
93
- var data = {
94
- customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') },
95
- items: [],
96
- grandTotal: 0
97
- };
98
- var cart = window.cart || [];
99
- var products = window.D || [];
100
- cart.forEach(function(item, i) {
101
- var prod = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null);
102
- var price = item.priceNum || item.price || (prod ? prod.price : 0) || 0;
103
- data.items.push({
104
- stt: i + 1,
105
- image: prod ? prod.img : '',
106
- name: prod ? (prod.name || prod.n || '') : (item.name || ''),
107
- model: prod ? (prod.sku || prod.mod || '') : (item.sku || item.model || ''),
108
- specs: prod ? (prod.info || prod.desc || '') : '',
109
- qty: item.qty || 1,
110
- price: price,
111
- discPrice: price,
112
- total: (item.qty || 1) * price,
113
- note: ''
114
- });
115
- });
116
- // Get customer info
117
- var nameInputs = ['#quoteName', '#quoteCustomerName', '#checkoutName'];
118
- for (var j = 0; j < nameInputs.length; j++) {
119
- var el = document.querySelector(nameInputs[j]);
120
- if (el && el.value && el.value.trim()) { data.customer.name = el.value.trim(); break; }
121
- }
122
- var ph = document.querySelector('#quotePhone, #checkoutPhone'); ph && ph.value && (data.customer.phone = ph.value.trim());
123
- var em = document.querySelector('#quoteEmail'); em && em.value && (data.customer.email = em.value.trim());
124
- var ad = document.querySelector('#quoteAddr, #checkoutAddr'); ad && ad.value && (data.customer.addr = ad.value.trim());
125
- data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0);
126
- return data;
127
- }
128
-
129
- // ===== EXPORT FUNCTIONS =====
130
- async function exportQuoteExcel(quoteData, code) {
131
- if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
132
- try {
133
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
134
- ws.views = [{ showGridLines: false }];
135
- ws.columns = [{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];
136
- ws.mergeCells('A3:J3'); ws.getCell('A3').value = 'BẢNG BÁO GIÁ';
137
- ws.getCell('A3').font = { bold:true, size:18, color:{argb:'FFDB9815'} }; ws.getCell('A3').alignment = { horizontal:'center' };
138
- ws.getCell('A5').value = 'Khách hàng:'; ws.mergeCells('B5:E5'); ws.getCell('B5').value = quoteData.customer.name;
139
- ws.getCell('H5').value = 'Mã đơn:'; ws.mergeCells('I5:J5'); ws.getCell('I5').value = code;
140
- ws.getCell('A6').value = 'SĐT:'; ws.mergeCells('B6:E6'); ws.getCell('B6').value = quoteData.customer.phone;
141
- ws.getCell('H6').value = 'Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value = quoteData.customer.date;
142
- ws.getCell('A7').value = 'Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value = quoteData.customer.email;
143
- ws.getCell('A8').value = 'Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value = quoteData.customer.addr;
144
- var hr = ws.getRow(10);
145
- ['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) {
146
- 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'} };
147
- });
148
- var cr = 11, firstRow = cr;
149
- quoteData.items.forEach(function(it, i) {
150
- var row = ws.getRow(cr); row.height = 50;
151
- row.getCell(1).value = it.stt || i+1;
152
- row.getCell(3).value = it.name || ''; row.getCell(3).font = { bold:true, size:9 };
153
- row.getCell(4).value = it.model || '';
154
- row.getCell(5).value = it.specs || ''; row.getCell(5).font = { size:8, color:{argb:'FF64748B'} };
155
- row.getCell(6).value = Number(it.qty||1); row.getCell(6).numFmt = '#,##0';
156
- row.getCell(7).value = Number(it.price||0); row.getCell(7).numFmt = '#,##0"đ"';
157
- row.getCell(8).value = Number(it.discPrice||it.price||0); row.getCell(8).numFmt = '#,##0"đ"';
158
- row.getCell(8).font = (it.price||0) > (it.discPrice||0) ? { bold:true, color:{argb:'FFDC3545'} } : {};
159
- row.getCell(9).value = Number(it.total||0); row.getCell(9).numFmt = '#,##0"đ"'; row.getCell(9).font = { bold:true };
160
- row.getCell(10).value = it.note || '';
161
- cr++;
162
- });
163
- ws.getRow(cr).height = 28; ws.mergeCells(cr,1,cr,5); ws.getCell('A'+cr).value = 'TỔNG CỘNG';
164
- ws.getCell('A'+cr).font = { bold:true, size:13, color:{argb:'FFFFFFFF'} }; ws.getCell('A'+cr).fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'} };
165
- ws.getCell('E'+cr).value = { formula:'SUM(I'+firstRow+':I'+(cr-1)+')', result:Number(quoteData.grandTotal||0) };
166
- ws.getCell('E'+cr).numFmt = '#,##0"đ"'; ws.getCell('E'+cr).font = { bold:true, size:14, color:{argb:'FFF0B840'} };
167
- wb.calcProperties.fullCalcOnLoad = true;
168
- var buf = await wb.xlsx.writeBuffer();
169
- downloadBlob(new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), code + '.xlsx');
170
- return true;
171
- } catch (e) {
172
- alert('❌ Lỗi: ' + e.message);
173
- console.error('[Auto Inject] exportQuoteExcel error:', e);
174
- return false;
175
- }
176
- }
177
-
178
- async function exportDeliveryExcel(quoteData, code) {
179
- if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
180
- try {
181
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Giao hàng');
182
- ws.views = [{ showGridLines: false }]; ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}];
183
- 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' };
184
- ws.getCell('A2').value = 'KH: ' + (quoteData.customer.name||''); ws.getCell('D2').value = 'Mã: ' + code;
185
- ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer.phone||''); ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer.date||'');
186
- ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.customer.addr||'');
187
- var hr = ws.getRow(6); ['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h, i) {
188
- 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'} };
189
- });
190
- var cr = 7, totalQty = 0;
191
- quoteData.items.forEach(function(it, i) {
192
- var row = ws.getRow(cr); row.height = 20;
193
- row.getCell(1).value = it.stt || i+1; row.getCell(2).value = it.name || ''; row.getCell(3).value = it.model || ''; row.getCell(4).value = it.qty || 1; row.getCell(5).value = it.note || '';
194
- totalQty += Number(it.qty||1); cr++;
195
- });
196
- ws.mergeCells(cr,1,cr,3); ws.getCell('A'+cr).value = 'TỔNG CỘNG SỐ LƯỢNG'; ws.getCell('D'+cr).value = totalQty;
197
- var buf = await wb.xlsx.writeBuffer();
198
- downloadBlob(new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), 'GH-' + code + '.xlsx');
199
- return true;
200
- } catch (e) {
201
- alert('❌ Lỗi: ' + e.message);
202
- console.error('[Auto Inject] exportDeliveryExcel error:', e);
203
- return false;
204
- }
205
- }
206
-
207
- // ===== CLICK HANDLER =====
208
- function handleClick(e) {
209
- var btn = e.target.closest('button, a');
210
- if (!btn) return;
211
- var txt = (btn.textContent || '').toLowerCase();
212
- // Quote Excel
213
- if (txt.includes('excel') && !txt.includes('giao') && !txt.includes('gh-')) {
214
- if (btn.dataset.vaiExporting === '1') return;
215
- e.preventDefault(); e.stopPropagation();
216
- setBtnState(btn, true);
217
- var qd = getQuoteData(); var code = genOrderCode(qd.customer.name);
218
- exportQuoteExcel(qd, code).then(function() {
219
- setBtnState(btn, false);
220
- showToast && showToast('✅ Excel báo giá tải thành công!');
221
- });
222
- return false;
223
- }
224
- // Delivery Excel
225
- if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) {
226
- if (btn.dataset.vaiExporting === '1') return;
227
- e.preventDefault(); e.stopPropagation();
228
- setBtnState(btn, true);
229
- var order = window.VAI_CURRENT_ORDER || {};
230
- var ed = {
231
- customer: { name: order.customer||'', phone: order.phone||'', addr: order.addr||'', date: order.date||'' },
232
- 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 }; }),
233
- grandTotal: order.grandTotal||0
234
- };
235
- var ocode = order.code || genOrderCode(order.customer||'');
236
- exportDeliveryExcel(ed, ocode).then(function() {
237
- setBtnState(btn, false);
238
- showToast && showToast('✅ Excel giao hàng tải thành công!');
239
- });
240
- return false;
241
- }
242
- }
243
-
244
- // ===== INIT =====
245
- function init() {
246
- if (SCRIPT_INJECTED) return;
247
- document.removeEventListener('click', handleClick, true);
248
- document.addEventListener('click', handleClick, true);
249
- SCRIPT_INJECTED = true;
250
- console.log('[Auto Inject v2.0] Excel fix active - multi-download enabled');
251
- }
252
-
253
- // Run immediately
254
- if (document.readyState === 'loading') {
255
- document.addEventListener('DOMContentLoaded', init);
256
- } else {
257
- init();
258
- }
259
- setTimeout(init, 2000);
260
- setTimeout(init, 5000);
261
-
262
- window.VAI_AUTO_FIX = { init: init, exportQuoteExcel: exportQuoteExcel, exportDeliveryExcel: exportDeliveryExcel };
263
- })();
264
-
265
- console.log('[VAI] Auto Excel Fix v2.0 loaded!');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
auto-patch-gh-excel.js CHANGED
@@ -1,208 +1,238 @@
1
  /**
2
- * V.AI STUDIO - Auto Patch GH Excel - v2.0
3
  *
4
  * TỰ ĐỘNG PATCH mà không cần sửa index.html
5
  * Chỉ cần thêm dòng: <script src="auto-patch-gh-excel.js"></script> vào index.html
6
  *
7
- * FIX HOÀN HẢO:
8
- * - exportDeliveryExcel nhận order parameter đúng
9
- * - Click protection (chống bấm nhiều lần)
10
- * - Blob retention đến khi unload (tải nhiều lần được)
11
- * - Hỗ trợ cả Quote & Order modal
12
  */
13
  (function() {
14
  'use strict';
15
 
16
- // ===== GLOBAL BLOB REGISTRY =====
17
- window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], isUnloading: false };
18
- var reg = window.__VAI_BLOB_REGISTRY;
19
-
20
- // Cleanup on unload
21
- function cleanupBlobs() {
22
- reg.isUnloading = true;
23
- reg.blobs.forEach(function(b) {
24
- if (!b.revoked) {
25
- try { URL.revokeObjectURL(b.url); b.revoked = true; } catch(e) {}
26
- }
27
- });
28
- reg.blobs = [];
29
- }
30
- window.addEventListener('pagehide', cleanupBlobs);
31
- window.addEventListener('beforeunload', cleanupBlobs);
32
- window.addEventListener('unload', cleanupBlobs);
33
-
34
- // ===== PATCH URL.revokeObjectURL - CRITICAL =====
35
- if (!window.__VAI_REVOKE_PATCHED_AUTO) {
36
- var origRevoke = URL.revokeObjectURL;
37
- URL.revokeObjectURL = function(url) {
38
- if (reg.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
39
- return origRevoke.call(this, url);
40
- }
41
- var exists = reg.blobs.find(function(b) { return b.url === url; });
42
- if (!exists) reg.blobs.push({ url: url, revoked: false, created: Date.now() });
43
- return undefined;
44
- };
45
- window.__VAI_REVOKE_PATCHED_AUTO = true;
46
- }
47
-
48
- // ===== UTILITIES =====
49
- function fmt(n) { return n && !isNaN(n) ? Number(n).toLocaleString('vi-VN') + 'đ' : '0đ'; }
50
- function stripDiacritics(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'D'); }
51
- function genCode(name) {
52
- 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);
53
- var ini = '';
54
- if (name && name.trim()) {
55
- name.trim().split(/\s+/).forEach(function(w) {
56
- if (w) { var c = stripDiacritics(w.charAt(0)).toUpperCase(); if (/[A-Z]/.test(c)) ini += c; }
57
- });
58
  }
59
- return 'VAS' + (ini || 'X') + dd + mm + yy;
60
- }
61
-
62
- // ===== BUTTON STATE =====
63
- function setBtn(b, exporting) {
64
- if (!b) return;
65
- if (exporting) {
66
- b.disabled = true;
67
- b.dataset.vaiExporting = '1';
68
- b._orig = b.innerHTML;
69
- b.innerHTML = '<span style="opacity:0.7">⏳ Đang xuất...</span>';
70
- } else {
71
- b.disabled = false;
72
- b.dataset.vaiExporting = '0';
73
- if (b._orig) b.innerHTML = b._orig;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
- }
76
-
77
- // ===== DOWNLOAD BLOB =====
78
- function dlBlob(blob, filename) {
79
- var url = URL.createObjectURL(blob);
80
- reg.blobs.push({ url: url, revoked: false });
81
- var a = document.createElement('a');
82
- a.href = url; a.download = filename; a.style.cssText = 'position:absolute;left:-9999px';
83
- document.body.appendChild(a); a.click();
84
- setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 100);
85
- }
86
-
87
- // ===== EXPORT QUOTE EXCEL =====
88
- async function exportQuoteExcel(d, c) {
89
- if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
90
- try {
91
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
92
- ws.views = [{ showGridLines: false }];
93
- ws.columns = [{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];
94
- ws.mergeCells('A3:J3'); ws.getCell('A3').value='BẢNG BÁO GIÁ';
95
- ws.getCell('A3').font = { bold:true, size:18, color:{argb:'FFDB9815'} };
96
- ws.getCell('A3').alignment = { horizontal:'center' };
97
- ws.getCell('A5').value='Khách hàng:'; ws.mergeCells('B5:E5'); ws.getCell('B5').value=d.customer?.name||'';
98
- ws.getCell('H5').value='Mã đơn:'; ws.mergeCells('I5:J5'); ws.getCell('I5').value=c;
99
- ws.getCell('A6').value='SĐT:'; ws.mergeCells('B6:E6'); ws.getCell('B6').value=d.customer?.phone||'';
100
- ws.getCell('H6').value='Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value=d.customer?.date||'';
101
- ws.getCell('A7').value='Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value=d.customer?.email||'';
102
- ws.getCell('A8').value='Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value=d.customer?.addr||'';
103
-
104
- var hr = ws.getRow(10);
105
- ['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){
106
- 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'}};
107
- });
108
-
109
- var cr = 11, fr = cr;
110
- (d.items||[]).forEach(function(it,i){
111
- var r = ws.getRow(cr), bg = i%2===0?'FFF8FAFC':'FFFFFFFF'; r.height=50;
112
- r.getCell(1).value=it.stt||i+1; r.getCell(3).value=it.name||''; r.getCell(3).font={bold:true,size:9};
113
- r.getCell(4).value=it.model||''; r.getCell(5).value=it.specs||''; r.getCell(5).font={size:8,color:{argb:'FF64748B'}};
114
- r.getCell(6).value=Number(it.qty||1); r.getCell(6).numFmt='#,##0';
115
- r.getCell(7).value=Number(it.price||0); r.getCell(7).numFmt='#,##0"đ"';
116
- r.getCell(8).value=Number(it.discPrice||it.price||0); r.getCell(8).numFmt='#,##0"đ"';
117
- r.getCell(8).font=(it.price||0)>(it.discPrice||0)?{bold:true,color:{argb:'FFDC3545'}}:{};
118
- r.getCell(9).value=Number(it.total||0); r.getCell(9).numFmt='#,##0"đ"'; r.getCell(9).font={bold:true};
119
- r.getCell(10).value=it.note||''; cr++;
120
- });
121
-
122
- ws.getRow(cr).height=28; ws.mergeCells(cr,1,cr,5); ws.getCell('A'+cr).value='TỔNG CỘNG';
123
- ws.getCell('A'+cr).font={bold:true,size:13,color:{argb:'FFFFFFFF'}}; ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};
124
- ws.getCell('E'+cr).value={formula:'SUM(I'+fr+':I'+(cr-1)+')',result:Number(d.grandTotal||0)}; ws.getCell('E'+cr).numFmt='#,##0"đ"';
125
- ws.getCell('E'+cr).font={bold:true,size:14,color:{argb:'FFF0B840'}};
126
- wb.calcProperties.fullCalcOnLoad=true;
127
-
128
- var buf = await wb.xlsx.writeBuffer();
129
- dlBlob(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), c+'.xlsx');
130
- return true;
131
- } catch(e) { console.error('[AutoPatch] exportQuoteExcel error:',e); alert('❌ Lỗi: '+e.message); return false; }
132
- }
133
-
134
- // ===== EXPORT DELIVERY EXCEL =====
135
- async function exportDeliveryExcel(d, c) {
136
- if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
137
- try {
138
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Giao hàng');
139
- ws.views = [{ showGridLines: false }]; ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}];
140
- 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'};
141
- ws.getCell('A2').value='KH: '+(d.customer?.name||''); ws.getCell('D2').value='Mã: '+c;
142
- ws.getCell('A3').value='SĐT: '+(d.customer?.phone||''); ws.getCell('D3').value='Ngày: '+(d.customer?.date||'');
143
- ws.getCell('A4').value='Địa chỉ: '+(d.customer?.addr||'');
144
-
145
- var hr = ws.getRow(6); ['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){
146
- 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'}};
147
- });
148
-
149
- var cr = 7, tq = 0;
150
- (d.items||[]).forEach(function(it,i){
151
- var r = ws.getRow(cr); r.height=20;
152
- 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||'';
153
- tq += Number(it.qty||1); cr++;
154
- });
155
-
156
- ws.mergeCells(cr,1,cr,3); ws.getCell('A'+cr).value='TỔNG CỘNG SỐ LƯỢNG'; ws.getCell('D'+cr).value=tq;
157
-
158
- var buf = await wb.xlsx.writeBuffer();
159
- dlBlob(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), 'GH-'+c+'.xlsx');
160
- return true;
161
- } catch(e) { console.error('[AutoPatch] exportDeliveryExcel error:',e); alert('❌ Lỗi: '+e.message); return false; }
162
- }
163
-
164
- // ===== INIT =====
165
- function init() {
166
- // Check if VAI_EXCEL_FIX v2 already loaded
167
- if (typeof window.VAI_EXCEL_FIX !== 'undefined' && typeof window.VAI_EXCEL_FIX.exportQuoteExcel === 'function') {
168
- console.log('[AutoPatch v2.0] VAI_EXCEL_FIX v2 already loaded - done');
169
- return;
170
  }
171
 
172
- // Load vai-excel-fix-v2.js if not exists
173
- var s = document.createElement('script');
174
- s.src = '/vai-excel-fix-v2.js?v=' + Date.now();
175
- s.onload = function() {
176
- console.log('[AutoPatch v2.0] Excel fix v2 loaded');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  };
178
- s.onerror = function() {
179
- console.warn('[AutoPatch v2.0] Failed to load v2, applying inline patch');
180
- // Inline fallback patch
181
- if (typeof window.exportDeliveryExcel === 'function') {
182
- var origDE = window.exportDeliveryExcel;
183
- window.exportDeliveryExcel = async function(order, opt) {
184
- var btn = document.activeElement;
185
- if (btn && btn.dataset.vaiExporting === '1') return;
186
- if (btn) setBtn(btn, true);
187
- try {
188
- var d = { customer: { name: order?.customer||'', phone: order?.phone||'', addr: order?.addr||'', date: order?.date||'' },
189
- 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};}),
190
- grandTotal: order?.grandTotal||0 };
191
- var c = order?.code || genCode(order?.customer||'');
192
- await exportDeliveryExcel(d, c);
193
- } finally { if (btn) setBtn(btn, false); }
194
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  }
196
  };
197
- document.head.appendChild(s);
 
198
  }
199
 
 
200
  if (document.readyState === 'loading') {
201
- document.addEventListener('DOMContentLoaded', init);
202
  } else {
203
- init();
204
  }
205
- setTimeout(init, 2000);
206
- setTimeout(init, 5000);
 
207
 
208
  })();
 
1
  /**
2
+ * V.AI STUDIO - Auto Patch GH Excel/PDF v1046
3
  *
4
  * TỰ ĐỘNG PATCH mà không cần sửa index.html
5
  * Chỉ cần thêm dòng: <script src="auto-patch-gh-excel.js"></script> vào index.html
6
  *
7
+ * FIX:
8
+ * - exportDeliveryExcel/PDF nhận order parameter đúng
9
+ * - Click protection (chống bấm nhiều lần)
10
+ * - Blob retention 5 phút (tải nhiều lần được)
 
11
  */
12
  (function() {
13
  'use strict';
14
 
15
+ // === WAIT FOR VAI_QR READY ===
16
+ function initPatch() {
17
+ if (typeof window.VAI_QR === 'undefined') {
18
+ setTimeout(initPatch, 500);
19
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
+
22
+ // === STORE ORIGINAL FOR REFERENCE ===
23
+ var origExportDE = window.exportDeliveryExcel;
24
+ var origExportDP = window.exportDeliveryPDF;
25
+
26
+ // === HELPER: Convert order to quote data ===
27
+ function orderToVAIData(order) {
28
+ if (!order || !order.items) return null;
29
+ return {
30
+ qd: {
31
+ customer: {
32
+ name: order.customer || '',
33
+ phone: order.phone || '',
34
+ email: order.email || '',
35
+ addr: order.addr || '',
36
+ date: order.date || ''
37
+ },
38
+ items: (order.items || []).map(function(it, i) {
39
+ return {
40
+ stt: i + 1,
41
+ image: it.image || '',
42
+ name: it.name || '',
43
+ model: it.model || '',
44
+ specs: it.specs || it.info || '',
45
+ qty: it.qty || 1,
46
+ price: it.price || it.listPrice || it.discPrice || 0,
47
+ discPrice: it.discPrice || it.price || 0,
48
+ total: it.total || 0,
49
+ note: it.note || ''
50
+ };
51
+ }),
52
+ grandTotal: order.grandTotal || 0
53
+ }
54
+ };
55
  }
56
+
57
+ // === CLICK STATE ===
58
+ var activeExportBtn = null;
59
+
60
+ function setBtnState(btn, exporting) {
61
+ if (!btn) return;
62
+ if (exporting) {
63
+ btn.disabled = true;
64
+ btn.dataset.vaiExporting = '1';
65
+ btn._origHtml = btn.innerHTML;
66
+ btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
67
+ } else {
68
+ btn.disabled = false;
69
+ btn.dataset.vaiExporting = '0';
70
+ if (btn._origHtml) btn.innerHTML = btn._origHtml;
71
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
+ // === PATCH EXPORT DELIVERY EXCEL ===
75
+ window.exportDeliveryExcel = async function(order, opt) {
76
+ var btn = document.activeElement;
77
+ if (btn && btn.dataset.vaiExporting === '1') {
78
+ console.log('[AutoPatch] Already exporting, ignoring...');
79
+ return;
80
+ }
81
+ if (btn) setBtnState(btn, true);
82
+
83
+ try {
84
+ // If order provided, use it
85
+ if (order && order.items && order.items.length) {
86
+ var vaData = orderToVAIData(order);
87
+ var code = order.code || 'GH-' + Date.now();
88
+
89
+ // Use qr-payment.js internal function if available
90
+ if (window.VAI_QR && window.VAI_QR._doExportDeliveryExcel) {
91
+ await window.VAI_QR._doExportDeliveryExcel(vaData.qd, code);
92
+ console.log('[AutoPatch] GH Excel done for order: ' + code);
93
+ return;
94
+ }
95
+
96
+ // Fallback: rebuild HTML manually
97
+ if (window.VAI_QR && window.VAI_QR.buildDeliveryHTML) {
98
+ let html = window.VAI_QR.buildDeliveryHTML(vaData.qd, code);
99
+ let div = document.createElement('div');
100
+ div.style.cssText = 'position:absolute;left:-9999px;top:0;width:700px;background:#fff;padding:28px';
101
+ div.innerHTML = html;
102
+ document.body.appendChild(div);
103
+
104
+ await new Promise(r => setTimeout(r, 300));
105
+
106
+ if (window.ExcelJS) {
107
+ let wb = new ExcelJS.Workbook();
108
+ let ws = wb.addWorksheet('Giao hàng');
109
+ ws.views = [{showGridLines: false}];
110
+ ws.columns = [{width: 5}, {width: 34}, {width: 14}, {width: 8}, {width: 20}];
111
+
112
+ ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
113
+ ws.getCell('A2').value = 'KH: ' + (vaData.qd.customer.name || '');
114
+ ws.getCell('D2').value = 'Mã: ' + code;
115
+ ws.getCell('A3').value = 'SĐT: ' + (vaData.qd.customer.phone || '');
116
+ ws.getCell('A4').value = 'Địa chỉ: ' + (vaData.qd.customer.addr || '');
117
+
118
+ let headerRow = ws.getRow(6);
119
+ ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
120
+ let c = headerRow.getCell(i + 1);
121
+ c.value = h;
122
+ c.fill = {type: 'pattern', pattern: 'solid', fgColor: {argb: 'FF003F62'}};
123
+ c.font = {bold: true, color: {argb: 'FFFFFFFF'}};
124
+ });
125
+
126
+ let cr = 7;
127
+ (vaData.qd.items || []).forEach(function(it) {
128
+ let row = ws.getRow(cr);
129
+ row.getCell(1).value = it.stt;
130
+ row.getCell(2).value = it.name;
131
+ row.getCell(3).value = it.model;
132
+ row.getCell(4).value = it.qty;
133
+ row.getCell(5).value = it.note;
134
+ cr++;
135
+ });
136
+
137
+ let buf = await wb.xlsx.writeBuffer();
138
+ let blob = new Blob([buf], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
139
+ let url = URL.createObjectURL(blob);
140
+ let a = document.createElement('a');
141
+ a.href = url;
142
+ a.download = 'GH-' + code + '.xlsx';
143
+ a.click();
144
+ // Keep blob for 5 minutes
145
+ setTimeout(() => URL.revokeObjectURL(url), 300000);
146
+ }
147
+ div.remove();
148
+ return;
149
+ }
150
+ }
151
+
152
+ // Fallback to original
153
+ if (origExportDE) {
154
+ await origExportDE.call(this, order, opt);
155
+ }
156
+ } catch (e) {
157
+ console.error('[AutoPatch] Export error:', e);
158
+ if (typeof showToast === 'function') {
159
+ showToast('❌ Xuất file thất bại: ' + (e.message || e));
160
+ }
161
+ } finally {
162
+ setTimeout(() => { if (btn) setBtnState(btn, false); }, 1000);
163
+ }
164
  };
165
+
166
+ // === PATCH EXPORT DELIVERY PDF ===
167
+ window.exportDeliveryPDF = async function(order, opt) {
168
+ var btn = document.activeElement;
169
+ if (btn && btn.dataset.vaiExporting === '1') {
170
+ console.log('[AutoPatch] Already exporting, ignoring...');
171
+ return;
172
+ }
173
+ if (btn) setBtnState(btn, true);
174
+
175
+ try {
176
+ if (order && order.items && order.items.length) {
177
+ var vaData = orderToVAIData(order);
178
+ var code = order.code || 'GH-' + Date.now();
179
+
180
+ if (window.VAI_QR && window.VAI_QR._doExportDeliveryPDF) {
181
+ await window.VAI_QR._doExportDeliveryPDF(vaData.qd, code);
182
+ console.log('[AutoPatch] GH PDF done for order: ' + code);
183
+ return;
184
+ }
185
+
186
+ if (window.VAI_QR && window.VAI_QR.buildDeliveryHTML) {
187
+ let html = window.VAI_QR.buildDeliveryHTML(vaData.qd, code);
188
+ let div = document.createElement('div');
189
+ div.style.cssText = 'position:absolute;left:-9999px;top:0;width:700px;background:#fff;padding:28px';
190
+ div.innerHTML = html;
191
+ document.body.appendChild(div);
192
+
193
+ await new Promise(r => setTimeout(r, 300));
194
+
195
+ if (window.html2canvas && window.jspdf) {
196
+ let canvas = await html2canvas(div, {scale: 2, useCORS: true, backgroundColor: '#fff'});
197
+ let imgData = canvas.toDataURL('image/png');
198
+ let {jsPDF} = window.jspdf;
199
+ let pdf = new jsPDF({orientation: 'p', unit: 'pt', format: 'a4'});
200
+ let margin = 25;
201
+ let usableW = 595 - margin * 2;
202
+ let ratio = canvas.height / canvas.width;
203
+ let imgH = usableW * ratio;
204
+ pdf.addImage(imgData, 'PNG', margin, margin, usableW, imgH > 842 - margin * 2 ? 842 - margin * 2 : imgH);
205
+ pdf.save('GH-' + code + '.pdf');
206
+ }
207
+ div.remove();
208
+ return;
209
+ }
210
+ }
211
+
212
+ if (origExportDP) {
213
+ await origExportDP.call(this, order, opt);
214
+ }
215
+ } catch (e) {
216
+ console.error('[AutoPatch] Export error:', e);
217
+ if (typeof showToast === 'function') {
218
+ showToast('❌ Xuất file thất bại: ' + (e.message || e));
219
+ }
220
+ } finally {
221
+ setTimeout(() => { if (btn) setBtnState(btn, false); }, 1000);
222
  }
223
  };
224
+
225
+ console.log('[AutoPatch v1046] GH Excel/PDF patched - ready!');
226
  }
227
 
228
+ // Start patching
229
  if (document.readyState === 'loading') {
230
+ document.addEventListener('DOMContentLoaded', initPatch);
231
  } else {
232
+ initPatch();
233
  }
234
+ // Also run after delays (HF Spaces loads scripts dynamically)
235
+ setTimeout(initPatch, 2000);
236
+ setTimeout(initPatch, 5000);
237
 
238
  })();
debug_export.html DELETED
@@ -1,47 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="vi"><head><meta charset="utf-8"/>
3
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
4
- <title>VAI Export Debug</title>
5
- <script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
6
- <style>body{font-family:system-ui;padding:16px;background:#f5f7fa}textarea{width:100%;height:120px;font-size:16px;padding:10px;border:2px solid #003f62;border-radius:8px}button{margin-top:12px;width:100%;padding:14px;font-size:18px;background:#003f62;color:#fff;border:0;border-radius:10px;font-weight:700}#log{margin-top:16px;white-space:pre-wrap;background:#fff;border:1px solid #ccc;border-radius:8px;padding:12px;font-size:13px;max-height:40vh;overflow:auto}</style>
7
- </head><body>
8
- <h2>VAI Export Debug</h2>
9
- <p>Nhập prompt rồi bấm nút. File sẽ có đủ dòng phí. Log hiển thị bên dưới.</p>
10
- <textarea id="prompt">Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7</textarea>
11
- <button id="go">Xuất Excel Test</button>
12
- <div id="log">Log sẽ hiện ở đây...</div>
13
- <script>
14
- function L(s){var el=document.getElementById('log');el.textContent+=("\n"+s);el.scrollTop=el.scrollHeight;console.log(s);}
15
- document.getElementById('go').addEventListener('click',async function(){
16
- L("ExcelJS loaded: "+(typeof ExcelJS!=='undefined'));
17
- var txt=document.getElementById('prompt').value.trim();
18
- L("Prompt: "+txt);
19
- var fees=[];var deposit=0;var note='';
20
- var m1=txt.match(/giao hàng\s*([\d.,]+)\s*(k|tr|triệu|đ|nghìn)?/i);
21
- if(m1){var v=parseFloat(m1[1].replace(/[.,]/g,''));var u=(m1[2]||'').toLowerCase();if(u==='k')v*=1000;if(u==='tr'||u==='triệu')v*=1e6;fees.push({label:'Phí giao hàng',amount:v});}
22
- var m2=txt.match(/lắp đặt\s*([\d.,]+)\s*(k|tr|triệu|đ|nghìn)?/i);
23
- if(m2){var v=parseFloat(m2[1].replace(/[.,]/g,''));var u=(m2[2]||'').toLowerCase();if(u==='k')v*=1000;if(u==='tr'||u==='triệu')v*=1e6;fees.push({label:'Phí lắp đặt',amount:v});}
24
- var m3=txt.match(/cọc\s*([\d.,]+)\s*(k|tr|triệu|đ|nghìn)?/i);
25
- if(m3){var v=parseFloat(m3[1].replace(/[.,]/g,''));var u=(m3[2]||'').toLowerCase();if(u==='k')v*=1000;if(u==='tr'||u==='triệu')v*=1e6;deposit=v;}
26
- var mn=txt.match(/ghi chú\s*[::]?\s*(.+)$/i);
27
- if(mn)note=mn[1].trim();
28
- L("Parsed fees: "+JSON.stringify(fees)+" deposit="+deposit+" note="+note);
29
- if(typeof ExcelJS==='undefined'){L("ERROR: ExcelJS not loaded - check network/CDN");return;}
30
- try{
31
- var wb=new ExcelJS.Workbook();var ws=wb.addWorksheet('BaoGia');
32
- ws.addRow(['BẢNG BÁO GIÁ']);
33
- ws.addRow([]);
34
- ws.addRow(['1','SP Test','MH800TC',1,7350000,4777500,4777500]);
35
- fees.forEach(function(f){ws.addRow([' '+f.label, f.amount]);L("ROW: "+f.label+" "+f.amount);});
36
- var total=4777500+fees.reduce(function(a,f){return a+f.amount;},0);
37
- ws.addRow(['TỔNG CỘNG', total]);
38
- if(deposit>0){ws.addRow(['Đã cọc',deposit]);ws.addRow(['CÒN LẠI',total-deposit]);}
39
- if(note)ws.addRow(['Ghi chú: '+note]);
40
- var buf=await wb.xlsx.writeBuffer();
41
- var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
42
- var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='VAI_debug.xlsx';a.click();
43
- L("FILE DOWNLOADED OK. Dòng phí: "+(fees.length>0?'CÓ':'KHÔNG'));
44
- }catch(e){L("EXPORT ERROR: "+e.message);}
45
- });
46
- </script>
47
- </body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
excel-fix-100px.js DELETED
@@ -1,149 +0,0 @@
1
- /**
2
- * V.AI STUDIO - Excel Export v1102
3
- * FIX: Hình ảnh 100x100px trong Excel
4
- */
5
- (function(){
6
- 'use strict';
7
-
8
- function fmt(n){
9
- if(n==null||n==='')return'0đ';
10
- var num=typeof n==='string'?parseFloat(n.replace(/[^\d.-]/g,'')):Number(n);
11
- if(isNaN(num))return'0đ';
12
- return num.toLocaleString('vi-VN')+'đ';
13
- }
14
-
15
- async function fetchImageArrayBuffer(url){
16
- if(!url||!url.startsWith('http')) return null;
17
- try{
18
- var r = await fetch(url,{mode:'cors'});
19
- if(r.ok) return await r.arrayBuffer();
20
- }catch(e){}
21
- try{
22
- var r2 = await fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url));
23
- if(r2.ok) return await r2.arrayBuffer();
24
- }catch(e){}
25
- return null;
26
- }
27
-
28
- function getOrderCode(){
29
- var n = document.getElementById('quoteName')?.value || '';
30
- var h = window.location.hash;
31
- var today = new Date(), dd=today.getDate().toString().padStart(2,'0'), mm=(today.getMonth()+1).toString().padStart(2,'0'), yy=today.getFullYear().toString().slice(-2);
32
- var ini='';
33
- if(n && n.trim()){
34
- n.trim().split(/\s+/).forEach(function(w){
35
- if(w && /[A-Z]/.test(w.charAt(0).toUpperCase())) ini += w.charAt(0).toUpperCase();
36
- });
37
- }
38
- return 'VAS'+(ini||'X')+dd+mm+yy;
39
- }
40
-
41
- window.exportExcel = async function(){
42
- var cart = window.cart || [];
43
- if(!cart.length){alert('Giỏ trống!');return;}
44
-
45
- if(typeof ExcelJS === 'undefined'){
46
- var s = document.createElement('script');
47
- s.src = 'https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js';
48
- document.head.appendChild(s);
49
- await new Promise(function(r){s.onload=r});
50
- }
51
-
52
- try{
53
- var code = getOrderCode();
54
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
55
-
56
- // Column 2 width 15 for 100px image
57
- ws.columns = [
58
- {width:6},{width:15},{width:30},{width:15},
59
- {width:22},{width:8},{width:15},{width:15},{width:18},{width:16}
60
- ];
61
-
62
- // Header title
63
- ws.mergeCells('A1:J1');
64
- ws.getCell('A1').value = 'BẢNG BÁO GIÁ';
65
- ws.getCell('A1').font = {bold:true, size:18, color:{argb:'FFDB9815'}};
66
- ws.getCell('A1').alignment = {horizontal:'center'};
67
- ws.getRow(1).height = 30;
68
-
69
- // Table header
70
- var hr = ws.getRow(3);
71
- ['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){
72
- var c = hr.getCell(i+1);
73
- c.value = h;
74
- c.font = {bold:true, color:{argb:'FFFFFFFF'}, size:9};
75
- c.fill = {type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'}};
76
- c.alignment = {horizontal:'center', vertical:'middle'};
77
- });
78
- hr.height = 20;
79
-
80
- // Items with 100x100px images
81
- var firstRow = 5;
82
- await Promise.all(cart.map(async function(c, i){
83
- var p = window.D && D[c.idx] ? D[c.idx] : null;
84
- var row = ws.getRow(firstRow + i);
85
- row.height = 120; // 100px + padding
86
-
87
- var bgColor = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
88
-
89
- row.getCell(1).value = i+1;
90
- row.getCell(1).alignment = {horizontal:'center', vertical:'middle'};
91
-
92
- // IMAGE 100x100px
93
- if((p && p.image) || c.image){
94
- var imgUrl = p?.image || c.image;
95
- var imgBuf = await fetchImageArrayBuffer(imgUrl);
96
- if(imgBuf){
97
- try{
98
- wb.addImage(wb.addImage({buffer:imgBuf, extension:'png'}),
99
- {tl:{col:1, row:firstRow+i-1}, ext:{width:100, height:100}});
100
- }catch(e){}
101
- }
102
- }
103
-
104
- row.getCell(3).value = c.name || (p?p.name||p.n:'') || '';
105
- row.getCell(3).font = {bold:true, size:9};
106
- row.getCell(4).value = c.sku || (p?p.sku||p.model:'') || '';
107
- row.getCell(6).value = Number(c.qty||1);
108
- row.getCell(7).value = Number(c.priceNum||(p?p.priceNum:0)||0);
109
- row.getCell(7).numFmt = '#,##0"đ"';
110
- row.getCell(8).value = Number(c.priceNum||(p?p.priceNum:0)||0);
111
- row.getCell(8).numFmt = '#,##0"đ"';
112
- row.getCell(9).value = Number(c.priceNum||(p?p.priceNum:0)||0) * Number(c.qty||1);
113
- row.getCell(9).numFmt = '#,##0"đ"';
114
-
115
- for(var ci=1; ci<=10; ci++){
116
- row.getCell(ci).fill = {type:'pattern', pattern:'solid', fgColor:{argb:bgColor}};
117
- }
118
- }));
119
-
120
- var lastRow = firstRow + cart.length - 1;
121
- // Total row
122
- ws.getCell('A'+(lastRow+2)).value = 'TỔNG CỘNG';
123
- ws.getCell('A'+(lastRow+2)).font = {bold:true, size:13, color:{argb:'FFFFFFFF'}};
124
- ws.getCell('A'+(lastRow+2)).fill = {type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'}};
125
- ws.getCell('I'+(lastRow+2)).value = {formula:'SUM(I'+firstRow+':I'+lastRow+')'};
126
- ws.getCell('I'+(lastRow+2)).numFmt = '#,##0"đ"';
127
- ws.getCell('I'+(lastRow+2)).font = {bold:true, size:14, color:{argb:'FFF0B840'}};
128
-
129
- wb.calcProperties.fullCalcOnLoad = true;
130
- var buf = await wb.xlsx.writeBuffer();
131
- var blob = new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
132
- var url = URL.createObjectURL(blob);
133
- var a = document.createElement('a');
134
- a.href = url;
135
- a.download = code + '.xlsx';
136
- document.body.appendChild(a);
137
- a.click();
138
- setTimeout(function(){ document.body.removeChild(a); URL.revokeObjectURL(url); }, 30000);
139
-
140
- console.log('[VAI] Excel export OK - hình ảnh 100px');
141
- if(typeof showToast === 'function') showToast('✅ Excel OK - Hình ảnh 100px');
142
-
143
- }catch(e){
144
- console.error('[VAI] Excel error:', e);
145
- alert('Lỗi: '+e.message);
146
- }
147
- };
148
-
149
- console.log('[VAI v1102] Excel fix loaded - 100px images ready');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
excel-fix-inline-v2.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * V.AI STUDIO - Excel Fix Inline v2.0
3
- * ĐÂY LÀ PHIÊN BẢN NHÚNG TRỰC TIẾP VÀO index.html
4
- *
5
- * Copy toàn bộ nội dung script này và dán vào trước </body> trong index.html
6
- */
7
- <script>
8
- (function(){
9
- "use strict";
10
- var R=window.__VAI_BLOB_REGISTRY=window.__VAI_BLOB_REGISTRY||{blobs:[],isUnloading:!1};function cleanup(){R.isUnloading=!0,R.blobs.forEach(function(e){e.revoked||(URL.revokeObjectURL(e.url),e.revoked=!0)}),R.blobs=[];}
11
- window.addEventListener("pagehide",cleanup),window.addEventListener("beforeunload",cleanup),window.addEventListener("unload",cleanup);
12
- if(!window.__VAI_REVOKE_PATCHED){var _o=URL.revokeObjectURL;URL.revokeObjectURL=function(u){if(R.isUnloading||!u||"string"!=typeof u||!u.startsWith("blob:"))return _o.call(this,u);var e=R.blobs.find(function(e){return e.url===u});e||R.blobs.push({url:u,revoked:!1,created:Date.now()}),void 0;window.__VAI_REVOKE_PATCHED=!0;}
13
- function fmt(n){return n&&!isNaN(n)?Number(n).toLocaleString("vi-VN")+"đ":"0đ";}
14
- function stripDiacritics(s){return s.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[Đđ]/g,"D");}
15
- function genCode(n){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),ini="";if(n&&n.trim()){n.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;}
16
- function setBtn(b,e){if(!b)return;e?(b.disabled=!0,b.dataset.vaiExporting="1",b.dataset.vaiOrig=b.innerHTML,b.innerHTML='<span style="opacity:0.7">⏳</span>'):(b.disabled=!1,b.dataset.vaiExporting="0",b.dataset.vaiOrig&&(b.innerHTML=b.dataset.vaiOrig));}
17
- function dlBlob(b,f){var u=URL.createObjectURL(b);R.blobs.push({url:u,revoked:!1}),a=document.createElement("a"),a.href=u,a.download=f,a.style.cssText="position:absolute;left:-9999px",document.body.appendChild(a),a.click(),setTimeout(function(){try{document.body.removeChild(a)}catch(e){}},100);}
18
- function getQD(){var d={customer:{name:"",phone:"",email:"",addr:"",date:new Date().toLocaleDateString("vi-VN")},items:[],grandTotal:0},c=window.cart||[],p=window.D||[];c.forEach(function(item,i){var prod=p[item.idx]||(p.find?p.find(function(e){return e.id===item.id}):null),pr=item.priceNum||item.price||(prod?prod.price:0)||0;d.items.push({stt:i+1,image:prod?prod.img:"",name:prod?(prod.name||prod.n||""):(item.name||""),model:prod?(prod.sku||prod.mod||""):(item.sku||item.model||""),specs:prod?(prod.info||prod.desc||""):"",qty:item.qty||1,price:pr,discPrice:pr,total:(item.qty||1)*pr,note:""})});var ns=["#quoteName","#quoteCustomerName","#checkoutName"];for(var j=0;j<ns.length;j++){var el=document.querySelector(ns[j]);if(el&&el.value&&el.value.trim()){d.customer.name=el.value.trim();break;}}var ph=document.querySelector("#quotePhone,#checkoutPhone");ph&&ph.value&&(d.customer.phone=ph.value.trim());var em=document.querySelector("#quoteEmail");em&&em.value&&(d.customer.email=em.value.trim());var ad=document.querySelector("#quoteAddr,#checkoutAddr");ad&&ad.value&&(d.customer.addr=ad.value.trim());d.grandTotal=d.items.reduce(function(s,i){return s+(i.total||0)},0);return d;}
19
- async function xuEG(d,c){if("undefined"==typeof ExcelJS)return alert("⚠️ Chưa có ExcelJS"),!1;try{var wb=new ExcelJS.Workbook,w=wb.addWorksheet("Báo giá");w.views=[{showGridLines:!1}],w.columns=[{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];w.mergeCells("A3:J3"),w.getCell("A3").value="BẢNG BÁO GIÁ",w.getCell("A3").font={bold:!0,size:18,color:{argb:"FFDB9815"}},w.getCell("A3").alignment={horizontal:"center"},w.getCell("A5").value="Khách hàng:",w.mergeCells("B5:E5"),w.getCell("B5").value=d.customer.name,w.getCell("H5").value="Mã đơn:",w.mergeCells("I5:J5"),w.getCell("I5").value=c,w.getCell("A6").value="SĐT:",w.mergeCells("B6:E6"),w.getCell("B6").value=d.customer.phone,w.getCell("H6").value="Ngày:",w.mergeCells("I6:J6"),w.getCell("I6").value=d.customer.date,w.getCell("A7").value="Email:",w.mergeCells("B7:E7"),w.getCell("B7").value=d.customer.email,w.getCell("A8").value="Địa chỉ:",w.mergeCells("B8:J8"),w.getCell("B8").value=d.customer.addr;var hr=w.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:!0,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=w.getRow(cr);r.height=50,r.getCell(1).value=it.stt||i+1,r.getCell(3).value=it.name||"",r.getCell(3).font={bold:!0,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:!0,color:{argb:"FFDC3545"}},r.getCell(9).value=Number(it.total||0),r.getCell(9).numFmt="#,##0\"đ\"",r.getCell(9).font={bold:!0},r.getCell(10).value=it.note||"";cr++});w.getRow(cr).height=28,w.mergeCells(cr,1,cr,5),w.getCell("A"+cr).value="TỔNG CỘNG",w.getCell("A"+cr).font={bold:!0,size:13,color:{argb:"FFFFFFFF"}},w.getCell("A"+cr).fill={type:"pattern",pattern:"solid",fgColor:{argb:"FF003F62"}},w.getCell("E"+cr).value={formula:"SUM(I"+fr+":I"+(cr-1)+")",result:Number(d.grandTotal||0)},w.getCell("E"+cr).numFmt="#,##0\"đ\"",w.getCell("E"+cr).font={bold:!0,size:14,color:{argb:"FFF0B840"}},wb.calcProperties.fullCalcOnLoad=!0;var buf=await wb.xlsx.writeBuffer(),blob=new Blob([buf],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});return dlBlob(blob,c+".xlsx"),!0}catch(e){return console.error("[VAI] exportQuoteExcel error:",e),alert("❌ Lỗi: "+e.message),!1}
20
- async function xuEG_D(d,c){if("undefined"==typeof ExcelJS)return alert("⚠️ Chưa có ExcelJS"),!1;try{var wb=new ExcelJS.Workbook,w=wb.addWorksheet("Giao hàng");w.views=[{showGridLines:!1}],w.columns=[{width:5},{width:34},{width:14},{width:8},{width:20}];w.mergeCells("A1:E1"),w.getCell("A1").value="PHIẾU GIAO HÀNG",w.getCell("A1").font={bold:!0,size:16,color:{argb:"FF003F62"}},w.getCell("A1").alignment={horizontal:"center"},w.getCell("A2").value="KH: "+(d.customer.name||""),w.getCell("D2").value="Mã: "+c,w.getCell("A3").value="SĐT: "+(d.customer.phone||""),w.getCell("D3").value="Ngày: "+(d.customer.date||""),w.getCell("A4").value="Địa chỉ: "+(d.customer.addr||"");var hr=w.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:!0,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=w.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++}),w.mergeCells(cr,1,cr,3),w.getCell("A"+cr).value="TỔNG CỘNG SỐ LƯỢNG",w.getCell("D"+cr).value=tq;var buf=await wb.xlsx.writeBuffer(),blob=new Blob([buf],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});return dlBlob(blob,"GH-"+c+".xlsx"),!0}catch(e){return console.error("[VAI] exportDeliveryExcel error:",e),alert("❌ Lỗi: "+e.message),!1}
21
- function hClick(e){var b=e.target.closest("button,a");if(!b)return;var t=(b.textContent||"").toLowerCase();if(t.includes("excel")&&!t.includes("giao")&&!t.includes("gh-")&&!t.includes("giao")){if(b.dataset.vaiExporting=="1"){e.preventDefault();return;}e.preventDefault(),e.stopPropagation(),setBtn(b,!0);var d=getQD(),c=genCode(d.customer.name);xuEG(d,c).then(function(){setBtn(b,!1),typeof showToast==="function"&&showToast("✅ Excel báo giá tải thành công!"),console.log("[VAI Inline] Quote Excel uploaded")})}else{if((t.includes("gh")||t.includes("giao hàng"))&&t.includes("excel")){if(b.dataset.vaiExporting=="1"){e.preventDefault();return;}e.preventDefault(),e.stopPropagation(),setBtn(b,!0);var od=window.VAI_CURRENT_ORDER,ed={},ocd=genCode(od?od.customer:"");if(od&&od.items){ed={customer:{name:od.customer||"",phone:od.phone||"",addr:od.addr||"",date:od.date||""},items:od.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:od.grandTotal||0};ocd=od.code||ocd}xuEG_D(ed,ocd).then(function(){setBtn(b,!1),typeof showToast==="function"&&showToast("✅ Excel giao hàng tải thành công!")})}}}function init(){document.removeEventListener("click",hClick,!0),document.addEventListener("click",hClick,!0),console.log("[VAI Inline v2.0] Ready")}
22
- if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init)}else{init()}
23
- setTimeout(init,2e3);setTimeout(init,5e3);
24
- window.VAI_EXCEL_INLINE={init:init,exportQuoteExcel:xuEG,exportDeliveryExcel:xuEG_D};
25
- })();
26
- </script>
27
- /* === HƯỚNG DẪN SỬ DỤNG ===
28
- * 1. Mở file index.html
29
- * 2. Tìm thẻ </body>
30
- * 3. Dán TOÀN BỘ nội dung file này (kể cả thẻ <script>) vào NGAY TRƯỚC </body>
31
- * 4. Lưu và Rebuild space
32
- * ============================
33
- * Link: https://huggingface.co/spaces/bep40/V.AISTUDIO
34
- * Cập nhật: 14-07-2026 - ML Intern
35
- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
excel-fix-oneclick.bookmarklet.js DELETED
@@ -1,33 +0,0 @@
1
- /**
2
- * V.AI STUDIO Excel Fix - One Click Bookmarklet
3
- *
4
- * CÁCH DÙNG:
5
- * 1. Copy TOÀN BỘ đoạn code dưới (bao gồm cả javascript:)
6
- * 2. Tạo bookmark mới trên trình duyệt
7
- * 3. Dán vào phần URL của bookmark
8
- * 4. Mở https://huggingface.co/spaces/bep40/V.AISTUDIO
9
- * 5. Click bookmark - tự động áp dụng fix!
10
- */
11
-
12
- javascript:(function(){
13
- var s=document.createElement('script');
14
- s.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v=1050';
15
- s.onload=function(){
16
- if(typeof showToast==='function'){showToast('✅ Excel Fix đã kích hoạt! Tải nhiều lần được.');}
17
- else{alert('✅ Excel Fix đã kích hoạt!\nBây giờ bạn có thể tải file Excel nhiều lần.');}
18
- };
19
- s.onerror=function(){
20
- // Inline fallback
21
- var orig=URL.revokeObjectURL;
22
- URL.revokeObjectURL=function(u){
23
- if(u&&u.startsWith('blob:')){
24
- setTimeout(function(){try{orig.call(URL,u);}catch(e){}},300000);
25
- return;
26
- }
27
- return orig.apply(URL,arguments);
28
- };
29
- alert('✅ Inline fix đã áp dụng!\nExcel sẽ tải được nhiều lần.');
30
- };
31
- document.head.appendChild(s);
32
- s=null;
33
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
excel-fix-v2.bookmarklet.js DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * V.AI STUDIO Excel Fix v2.0 - Bookmarklet version
3
- * Bookmarklet: V.AISTUDIO Excel Fix v2.0
4
- *
5
- * Dán URL sau vào bookmark:
6
- * javascript:(function(){try{var s=document.createElement('script');s.src='/vai-excel-fix-v2.js?v='+Date.now();s.onload=function(){alert('✅ Excel Fix v2.0 đã tải!\n📌 Báo giá & Đơn hàng Excel tải được nhiều lần!');};document.head.appendChild(s);}catch(e){alert('Lỗi: '+e);}})();
7
- */
8
- (function(){try{var s=document.createElement('script');s.src='/vai-excel-fix-v2.js?v='+Date.now();s.onload=function(){alert('✅ Excel Fix v2.0 đã tải!\n📌 Báo giá & Đơn hàng Excel tải được nhiều lần!');};document.head.appendChild(s);}catch(e){alert('Lỗi: '+e);}})();
 
 
 
 
 
 
 
 
 
excel-fix-v2.js DELETED
@@ -1,448 +0,0 @@
1
- /**
2
- * V.AI STUDIO - Excel Export Fix Ultimate v2.0
3
- * ============================================
4
- * FIX 100%: Multi-download Excel for Quote & Order modals
5
- *
6
- * Key fixes:
7
- * 1. Global blob URL registry - prevents premature URL.revokeObjectURL
8
- * 2. Creates fresh blob for each download
9
- * 3. Proper cleanup on page unload only
10
- * 4. Click protection to prevent double-clicks
11
- */
12
- (function(){
13
- 'use strict';
14
-
15
- // ===== GLOBAL STATE =====
16
- window.__VAI_EXCEL_FIX = window.__VAI_EXCEL_FIX || {
17
- blobRegistry: [],
18
- isUnloading: false
19
- };
20
-
21
- var g = window.__VAI_EXCEL_FIX;
22
-
23
- // ===== CLEANUP ON UNLOAD =====
24
- function cleanupAllBlobs() {
25
- g.isUnloading = true;
26
- g.blobRegistry.forEach(function(entry) {
27
- if (!entry.revoked && entry.url) {
28
- try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch(e) {}
29
- }
30
- });
31
- g.blobRegistry = [];
32
- }
33
-
34
- window.addEventListener('pagehide', cleanupAllBlobs);
35
- window.addEventListener('beforeunload', cleanupAllBlobs);
36
- window.addEventListener('unload', cleanupAllBlobs);
37
-
38
- // ===== PATCH URL.revokeObjectURL - CRITICAL =====
39
- if (!window.__VAI_REVOKE_PATCHED) {
40
- var originalRevoke = URL.revokeObjectURL;
41
- URL.revokeObjectURL = function(url) {
42
- // Only revoke on unload or non-blob URLs
43
- if (g.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
44
- return originalRevoke.call(this, url);
45
- }
46
- // Register but DON'T revoke - this is the key fix for multi-download
47
- var exists = g.blobRegistry.find(function(e) { return e.url === url; });
48
- if (!exists) {
49
- g.blobRegistry.push({ url: url, revoked: false, created: Date.now() });
50
- }
51
- return undefined;
52
- };
53
- window.__VAI_REVOKE_PATCHED = true;
54
- }
55
-
56
- // ===== UTILITIES =====
57
- function fmt(n) {
58
- if (!n || isNaN(n)) return '0đ';
59
- return Number(n).toLocaleString('vi-VN') + 'đ';
60
- }
61
-
62
- function stripDiacritics(s) {
63
- return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D');
64
- }
65
-
66
- function generateOrderCode(name) {
67
- var d = new Date();
68
- var dd = String(d.getDate()).padStart(2, '0');
69
- var mm = String(d.getMonth() + 1).padStart(2, '0');
70
- var yy = String(d.getFullYear()).slice(-2);
71
- var ini = '';
72
- if (name && name.trim()) {
73
- name.trim().split(/\s+/).forEach(function(w) {
74
- if (w) {
75
- var ch = stripDiacritics(w.charAt(0)).toUpperCase();
76
- if (/[A-Z]/.test(ch)) {
77
- ini += ch;
78
- }
79
- }
80
- });
81
- }
82
- return 'VAS' + (ini || 'X') + dd + mm + yy;
83
- }
84
-
85
- // ===== BUTTON STATE =====
86
- function setBtnState(btn, exporting) {
87
- if (!btn) return;
88
- if (exporting) {
89
- btn.disabled = true;
90
- btn.dataset.vaiExporting = '1';
91
- btn.dataset.vaiOrigHTML = btn.innerHTML;
92
- btn.innerHTML = '<span style="opacity:0.7">⏳ Đang xuất...</span>';
93
- } else {
94
- btn.disabled = false;
95
- btn.dataset.vaiExporting = '0';
96
- if (btn.dataset.vaiOrigHTML) {
97
- btn.innerHTML = btn.dataset.vaiOrigHTML;
98
- }
99
- }
100
- }
101
-
102
- // ===== CREATE DOWNLOAD =====
103
- function downloadBlob(blob, filename) {
104
- var url = URL.createObjectURL(blob);
105
- g.blobRegistry.push({ url: url, revoked: false });
106
-
107
- var a = document.createElement('a');
108
- a.href = url;
109
- a.download = filename;
110
- a.style.cssText = 'position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;';
111
- document.body.appendChild(a);
112
- a.click();
113
-
114
- setTimeout(function() {
115
- try { document.body.removeChild(a); } catch(e) {}
116
- }, 100);
117
- }
118
-
119
- // ===== GET QUOTE DATA FROM MODAL =====
120
- function getQuoteData() {
121
- var data = {
122
- customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') },
123
- items: [],
124
- grandTotal: 0
125
- };
126
-
127
- // Get cart data
128
- var cart = window.cart || [];
129
- var products = window.D || [];
130
-
131
- cart.forEach(function(item, i) {
132
- var product = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null);
133
- var price = item.priceNum || item.price || (product ? product.price : 0) || 0;
134
- data.items.push({
135
- stt: i + 1,
136
- image: product ? product.img : '',
137
- name: product ? (product.name || product.n || '') : (item.name || ''),
138
- model: product ? (product.sku || product.mod || '') : (item.sku || item.model || ''),
139
- specs: product ? (product.info || product.desc || '') : '',
140
- qty: item.qty || 1,
141
- price: price,
142
- discPrice: price,
143
- total: (item.qty || 1) * price,
144
- note: ''
145
- });
146
- });
147
-
148
- // Get customer info from modal
149
- var nameSelectors = ['#quoteName', '#quoteCustomerName', '#checkoutName'];
150
- for (var j = 0; j < nameSelectors.length; j++) {
151
- var el = document.querySelector(nameSelectors[j]);
152
- if (el && el.value && el.value.trim()) {
153
- data.customer.name = el.value.trim();
154
- break;
155
- }
156
- }
157
-
158
- var phoneEl = document.querySelector('#quotePhone, #checkoutPhone');
159
- if (phoneEl && phoneEl.value) data.customer.phone = phoneEl.value.trim();
160
-
161
- var emailEl = document.querySelector('#quoteEmail');
162
- if (emailEl && emailEl.value) data.customer.email = emailEl.value.trim();
163
-
164
- var addrEl = document.querySelector('#quoteAddr, #checkoutAddr');
165
- if (addrEl && addrEl.value) data.customer.addr = addrEl.value.trim();
166
-
167
- data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0);
168
- return data;
169
- }
170
-
171
- // ===== EXPORT QUOTE EXCEL =====
172
- async function exportQuoteExcel(quoteData, code) {
173
- if (typeof ExcelJS === 'undefined') {
174
- if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS');
175
- return false;
176
- }
177
-
178
- try {
179
- var wb = new ExcelJS.Workbook();
180
- var ws = wb.addWorksheet('Báo giá');
181
- ws.views = [{ showGridLines: false }];
182
- ws.columns = [
183
- { width: 5 }, { width: 11 }, { width: 28 },
184
- { width: 13 }, { width: 20 }, { width: 6 },
185
- { width: 13 }, { width: 13 }, { width: 15 }, { width: 14 }
186
- ];
187
-
188
- ws.mergeCells('A3:J3');
189
- ws.getCell('A3').value = 'BẢNG BÁO GIÁ';
190
- ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } };
191
- ws.getCell('A3').alignment = { horizontal: 'center' };
192
-
193
- ws.getCell('A5').value = 'Khách hàng:';
194
- ws.mergeCells('B5:E5');
195
- ws.getCell('B5').value = quoteData.customer?.name || '';
196
-
197
- ws.getCell('H5').value = 'Mã đơn:';
198
- ws.mergeCells('I5:J5');
199
- ws.getCell('I5').value = code;
200
-
201
- ws.getCell('A6').value = 'SĐT:';
202
- ws.mergeCells('B6:E6');
203
- ws.getCell('B6').value = quoteData.customer?.phone || '';
204
-
205
- ws.getCell('H6').value = 'Ngày:';
206
- ws.mergeCells('I6:J6');
207
- ws.getCell('I6').value = quoteData.customer?.date || '';
208
-
209
- ws.getCell('A7').value = 'Email:';
210
- ws.mergeCells('B7:E7');
211
- ws.getCell('B7').value = quoteData.customer?.email || '';
212
-
213
- ws.getCell('A8').value = 'Địa chỉ:';
214
- ws.mergeCells('B8:J8');
215
- ws.getCell('B8').value = quoteData.customer?.addr || '';
216
-
217
- var hr = ws.getRow(10);
218
- ['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) {
219
- var c = hr.getCell(i + 1);
220
- c.value = h;
221
- c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
222
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
223
- });
224
-
225
- var cr = 11, firstRow = cr;
226
- (quoteData.items || []).forEach(function(it, i) {
227
- var row = ws.getRow(cr);
228
- row.height = 50;
229
- var bg = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
230
-
231
- row.getCell(1).value = it.stt || i + 1;
232
- row.getCell(3).value = it.name || '';
233
- row.getCell(3).font = { bold: true, size: 9 };
234
- row.getCell(4).value = it.model || '';
235
- row.getCell(5).value = it.specs || '';
236
- row.getCell(5).font = { size: 8, color: { argb: 'FF64748B' } };
237
- row.getCell(6).value = Number(it.qty || 1);
238
- row.getCell(6).numFmt = '#,##0';
239
- row.getCell(7).value = Number(it.price || 0);
240
- row.getCell(7).numFmt = '#,##0"đ"';
241
- row.getCell(8).value = Number(it.discPrice || it.price || 0);
242
- row.getCell(8).numFmt = '#,##0"đ"';
243
- if ((it.price || 0) > (it.discPrice || 0)) {
244
- row.getCell(8).font = { bold: true, color: { argb: 'FFDC3545' } };
245
- }
246
- row.getCell(9).value = Number(it.total || 0);
247
- row.getCell(9).numFmt = '#,##0"đ"';
248
- row.getCell(9).font = { bold: true };
249
- row.getCell(10).value = it.note || '';
250
-
251
- cr++;
252
- });
253
-
254
- ws.getRow(cr).height = 28;
255
- ws.mergeCells(cr, 1, cr, 5);
256
- ws.getCell('A' + cr).value = 'TỔNG CỘNG';
257
- ws.getCell('A' + cr).font = { bold: true, size: 13, color: { argb: 'FFFFFFFF' } };
258
- ws.getCell('A' + cr).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
259
- ws.getCell('E' + cr).value = { formula: 'SUM(I' + firstRow + ':I' + (cr - 1) + ')', result: Number(quoteData.grandTotal || 0) };
260
- ws.getCell('E' + cr).numFmt = '#,##0"đ"';
261
- ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FFF0B840' } };
262
-
263
- wb.calcProperties.fullCalcOnLoad = true;
264
-
265
- var buf = await wb.xlsx.writeBuffer();
266
- var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
267
-
268
- downloadBlob(blob, code + '.xlsx');
269
- return true;
270
-
271
- } catch (err) {
272
- console.error('[VAI Excel Fix] exportQuoteExcel error:', err);
273
- alert('❌ Lỗi: ' + err.message);
274
- return false;
275
- }
276
- }
277
-
278
- // ===== EXPORT DELIVERY EXCEL =====
279
- async function exportDeliveryExcel(quoteData, code) {
280
- if (typeof ExcelJS === 'undefined') {
281
- if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS');
282
- return false;
283
- }
284
-
285
- try {
286
- var wb = new ExcelJS.Workbook();
287
- var ws = wb.addWorksheet('Giao hàng');
288
- ws.views = [{ showGridLines: false }];
289
- ws.columns = [
290
- { width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 }
291
- ];
292
-
293
- ws.mergeCells('A1:E1');
294
- ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
295
- ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } };
296
- ws.getCell('A1').alignment = { horizontal: 'center' };
297
-
298
- ws.getCell('A2').value = 'KH: ' + (quoteData.customer?.name || '');
299
- ws.getCell('D2').value = 'Mã: ' + code;
300
- ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer?.phone || '');
301
- ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer?.date || '');
302
- ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.customer?.addr || '');
303
-
304
- var hr = ws.getRow(6);
305
- ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
306
- var c = hr.getCell(i + 1);
307
- c.value = h;
308
- c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
309
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
310
- });
311
-
312
- var cr = 7, totalQty = 0;
313
- (quoteData.items || []).forEach(function(it, i) {
314
- var row = ws.getRow(cr);
315
- row.height = 20;
316
- row.getCell(1).value = it.stt || i + 1;
317
- row.getCell(2).value = it.name || '';
318
- row.getCell(3).value = it.model || '';
319
- row.getCell(4).value = it.qty || 1;
320
- row.getCell(5).value = it.note || '';
321
- totalQty += Number(it.qty || 1);
322
- cr++;
323
- });
324
-
325
- ws.mergeCells(cr, 1, cr, 3);
326
- ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
327
- ws.getCell('D' + cr).value = totalQty;
328
-
329
- var buf = await wb.xlsx.writeBuffer();
330
- var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
331
-
332
- downloadBlob(blob, 'GH-' + code + '.xlsx');
333
- return true;
334
-
335
- } catch (err) {
336
- console.error('[VAI Excel Fix] exportDeliveryExcel error:', err);
337
- alert('❌ Lỗi: ' + err.message);
338
- return false;
339
- }
340
- }
341
-
342
- // ===== CLICK HANDLER =====
343
- function handleExportClick(e) {
344
- var btn = e.target.closest('button, a');
345
- if (!btn) return;
346
-
347
- var txt = (btn.textContent || '').toLowerCase();
348
-
349
- // Quote Excel - "Xuất Excel" button in quote modal
350
- if (txt.includes('excel') && !txt.includes('giao hàng') && !txt.includes('gh-') && !txt.includes('giao')) {
351
- e.preventDefault();
352
- e.stopPropagation();
353
-
354
- if (btn.dataset.vaiExporting === '1') return;
355
- setBtnState(btn, true);
356
-
357
- var qd = getQuoteData();
358
- var code = generateOrderCode(qd.customer?.name || '');
359
-
360
- exportQuoteExcel(qd, code).then(function(success) {
361
- setBtnState(btn, false);
362
- if (success && typeof showToast === 'function') {
363
- showToast('✅ Excel báo giá tải thành công!');
364
- }
365
- });
366
-
367
- return false;
368
- }
369
-
370
- // Delivery Excel - "GH Excel" button in order modal
371
- if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) {
372
- e.preventDefault();
373
- e.stopPropagation();
374
-
375
- if (btn.dataset.vaiExporting === '1') return;
376
- setBtnState(btn, true);
377
-
378
- // Check for order modal data
379
- var order = window.VAI_CURRENT_ORDER;
380
- var exportData, orderCode;
381
-
382
- if (order && order.items) {
383
- // From order modal
384
- exportData = {
385
- customer: { name: order.customer || '', phone: order.phone || '', addr: order.addr || '', date: order.date || '' },
386
- items: (order.items || []).map(function(it, i) {
387
- return {
388
- stt: i + 1,
389
- name: it.name || '',
390
- model: it.model || '',
391
- qty: it.qty || 1,
392
- note: it.note || '',
393
- specs: it.specs || '',
394
- price: it.price || 0,
395
- discPrice: it.discPrice || it.price || 0,
396
- total: it.total || 0
397
- };
398
- }),
399
- grandTotal: order.grandTotal || 0
400
- };
401
- orderCode = order.code || generateOrderCode(order.customer || '');
402
- } else {
403
- // From cart
404
- exportData = getQuoteData();
405
- orderCode = generateOrderCode(exportData.customer?.name || '');
406
- }
407
-
408
- exportDeliveryExcel(exportData, orderCode).then(function(success) {
409
- setBtnState(btn, false);
410
- if (success && typeof showToast === 'function') {
411
- showToast('✅ Excel giao hàng tải thành công!');
412
- }
413
- });
414
-
415
- return false;
416
- }
417
- }
418
-
419
- // ===== INIT =====
420
- function init() {
421
- // Remove any existing handler to avoid duplicates
422
- document.removeEventListener('click', handleExportClick, true);
423
- // Add click handler
424
- document.addEventListener('click', handleExportClick, true);
425
- console.log('[VAI Excel Fix v2.0] Loaded - Multi-download enabled');
426
- }
427
-
428
- // Run on DOM ready
429
- if (document.readyState === 'loading') {
430
- document.addEventListener('DOMContentLoaded', init);
431
- } else {
432
- init();
433
- }
434
-
435
- // Also run after delays for dynamic content
436
- setTimeout(init, 2000);
437
- setTimeout(init, 5000);
438
-
439
- // Export for global use
440
- window.VAI_EXCEL_FIX = {
441
- init: init,
442
- exportQuoteExcel: exportQuoteExcel,
443
- exportDeliveryExcel: exportDeliveryExcel,
444
- generateOrderCode: generateOrderCode,
445
- getQuoteData: getQuoteData
446
- };
447
-
448
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
grob-price-fix.js DELETED
@@ -1,87 +0,0 @@
1
- /**
2
- * V.AI STUDIO — Grob Price Fix
3
- * ==============================
4
- * Fixes incorrect prices from the catalogue-based PDF price list.
5
- * Fixed as of July 2026 per Grob Vietnam catalogue.
6
- */
7
- (function() {
8
- 'use strict';
9
- if (window.__GROB_PRICE_FIX__) return;
10
- window.__GROB_PRICE_FIX__ = true;
11
-
12
- const PRICE_FIXES = {
13
- // 0203 | Grob | Bản lề inox - cánh/phủ bì 203mm: 0đ -> 67.000đ
14
- '0203': { p: '67.000đ', pn: 67000 },
15
- // 0280 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 280mm: 885.000đ -> 965.000đ
16
- '0280': { p: '965.000đ', pn: 965000 },
17
- // 0290 | Grob | Khay chia thìa dĩa nhựa - cánh/phủ bì 290mm: 965.000đ -> 1.018.000đ
18
- '0290': { p: '1.018.000đ', pn: 1018000 },
19
- };
20
-
21
- function patchProduct(prod) {
22
- if (!prod) return;
23
- var name = prod.n || prod.name || '';
24
- for (var code in PRICE_FIXES) {
25
- if (name.indexOf(code) === 0 && name.indexOf('Grob') !== -1) {
26
- var fix = PRICE_FIXES[code];
27
- if (prod.p) prod.p = fix.p;
28
- if (prod.pn !== undefined) prod.pn = fix.pn;
29
- if (prod.price) prod.price = fix.p;
30
- if (prod.priceNum !== undefined) prod.priceNum = fix.pn;
31
- return true;
32
- }
33
- }
34
- return false;
35
- }
36
-
37
- // Patch after D is populated
38
- function patchD() {
39
- if (typeof window.D !== 'undefined' && Array.isArray(window.D)) {
40
- var patched = 0;
41
- window.D.forEach(function(p) {
42
- if (patchProduct(p)) patched++;
43
- });
44
- if (patched > 0) {
45
- console.log('[Grob Price Fix] Patched ' + patched + ' product(s) in D');
46
- // Also sync prices for items already in cart
47
- patchCart();
48
- if (typeof window.refreshUI === 'function') window.refreshUI();
49
- }
50
- }
51
- }
52
-
53
- // Sync Grob prices in cart (localStorage)
54
- function patchCart() {
55
- try {
56
- var cart = JSON.parse(localStorage.getItem('malloca_cart') || '[]');
57
- if (!Array.isArray(cart) || !cart.length) return;
58
- var changed = false;
59
- cart.forEach(function(item) {
60
- if (patchProduct(item)) changed = true;
61
- });
62
- if (changed) {
63
- localStorage.setItem('malloca_cart', JSON.stringify(cart));
64
- window.cart = cart;
65
- console.log('[Grob Price Fix] Synced prices in cart');
66
- }
67
- } catch(e) {}
68
- }
69
-
70
- // Try patching at various intervals as data loads
71
- function onReady() {
72
- setTimeout(patchD, 200);
73
- setTimeout(patchD, 500);
74
- setTimeout(patchD, 1000);
75
- setTimeout(patchD, 2000);
76
- setTimeout(patchD, 3000);
77
- setTimeout(patchD, 5000);
78
- }
79
-
80
- if (document.readyState === 'loading') {
81
- document.addEventListener('DOMContentLoaded', onReady);
82
- } else {
83
- onReady();
84
- }
85
-
86
- console.log('[Grob Price Fix] Loaded: 02.03=67k, 02.80=965k, 02.90=1,018k');
87
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
grob_img/grob_p1.jpeg DELETED
Binary file (80 kB)
 
grob_img/grob_p10.jpeg DELETED

Git LFS Details

  • SHA256: 3cf047359715058a5f1e2ae91a8a2f1163539f4712de1db357ae12e677cac638
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
grob_img/grob_p100.jpeg DELETED
Binary file (51.6 kB)
 
grob_img/grob_p101.jpeg DELETED

Git LFS Details

  • SHA256: 5f695be82687bb3a52248ff537adb6950f73b64c21f483ff22b9e81213b1db61
  • Pointer size: 131 Bytes
  • Size of remote file: 126 kB
grob_img/grob_p102.jpeg DELETED

Git LFS Details

  • SHA256: c9c705dbb20655e160ad96fd17c5c33f505b7702f0b2c031cf49ffcf239d4e80
  • Pointer size: 131 Bytes
  • Size of remote file: 394 kB
grob_img/grob_p103.jpeg DELETED
Binary file (42 kB)
 
grob_img/grob_p104.jpeg DELETED
Binary file (20 kB)
 
grob_img/grob_p105.jpeg DELETED
Binary file (56.2 kB)
 
grob_img/grob_p106.jpeg DELETED

Git LFS Details

  • SHA256: 62afdde0435420e325e86f5162f0d637a1c53f90ca9ba3b1fb72f900086fb6a9
  • Pointer size: 131 Bytes
  • Size of remote file: 150 kB
grob_img/grob_p107.jpeg DELETED
Binary file (96 kB)
 
grob_img/grob_p108.jpeg DELETED
Binary file (86.7 kB)
 
grob_img/grob_p109.jpeg DELETED

Git LFS Details

  • SHA256: 3d45a5d7fbe20e41f181eb81adf669d9263daddb1787e9dc47bed68355637314
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
grob_img/grob_p11.jpeg DELETED

Git LFS Details

  • SHA256: bc47b6543988f6a4074ce37aa20ea13aa03d6f2f806a823869dec4fba362be7f
  • Pointer size: 131 Bytes
  • Size of remote file: 160 kB
grob_img/grob_p110.jpeg DELETED

Git LFS Details

  • SHA256: a84e2440229ca6da6a9d924f027a39e17176fc495c74d18a0c8f8307b2d93f2c
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
grob_img/grob_p111.jpeg DELETED

Git LFS Details

  • SHA256: 4a8c39cc23363b072d590675666c9e8b202c3030b0ced467bad694a19b197ded
  • Pointer size: 131 Bytes
  • Size of remote file: 286 kB
grob_img/grob_p112.jpeg DELETED
Binary file (54 kB)
 
grob_img/grob_p113.jpeg DELETED
Binary file (76.2 kB)
 
grob_img/grob_p114.jpeg DELETED
Binary file (92.9 kB)
 
grob_img/grob_p115.jpeg DELETED
Binary file (92.9 kB)
 
grob_img/grob_p116.jpeg DELETED
Binary file (40.1 kB)
 
grob_img/grob_p117.jpeg DELETED
Binary file (21.9 kB)
 
grob_img/grob_p118.jpeg DELETED

Git LFS Details

  • SHA256: 269778d257f719f470c89f27e83ef12c4913cc4ed7adb4a1aa181494f74c63f3
  • Pointer size: 131 Bytes
  • Size of remote file: 552 kB
grob_img/grob_p119.jpeg DELETED

Git LFS Details

  • SHA256: 269778d257f719f470c89f27e83ef12c4913cc4ed7adb4a1aa181494f74c63f3
  • Pointer size: 131 Bytes
  • Size of remote file: 552 kB
grob_img/grob_p12.jpeg DELETED
Binary file (89.4 kB)
 
grob_img/grob_p120.jpeg DELETED

Git LFS Details

  • SHA256: 74e912df9328009cdd1a040974cec3309795335536dd1c89b4d80f138f28f7a9
  • Pointer size: 131 Bytes
  • Size of remote file: 489 kB
grob_img/grob_p121.jpeg DELETED
Binary file (61.4 kB)
 
grob_img/grob_p122.jpeg DELETED
Binary file (58.4 kB)
 
grob_img/grob_p123.jpeg DELETED
Binary file (59.5 kB)