Khôi phục về commit e5d3e00e67a2730ecd74fe9abb1ec886b598786c

#3
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 DELETED
@@ -1,32 +0,0 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.js DELETED
@@ -1,62 +0,0 @@
1
- /* === V.AI STUDIO EXCEL MULTI-DOWNLOAD FIX ===
2
- * Copy toàn bộ nội dung này vào Bookmark URL (bookmarklet)
3
- * Hoặc paste vào Console (F12) để chạy ngay
4
- */
5
-
6
- (function(){
7
- 'use strict';
8
-
9
- console.log('[VAI EXCEL FIX] Starting...');
10
-
11
- // Blob registry để không revoke ngay
12
- window._excelBlobRegistry = window._excelBlobRegistry || [];
13
-
14
- // Cleanup khi đóng tab
15
- function cleanupBlobs() {
16
- window._excelBlobRegistry.forEach(function(e){
17
- try { URL.revokeObjectURL(e.url); } catch(err) {}
18
- });
19
- window._excelBlobRegistry = [];
20
- }
21
-
22
- window.addEventListener('pagehide', cleanupBlobs);
23
- window.addEventListener('beforeunload', cleanupBlobs);
24
- window.addEventListener('unload', cleanupBlobs);
25
-
26
- // Override URL.revokeObjectURL - không revoke blob URL
27
- var origRevoke = URL.revokeObjectURL;
28
- URL.revokeObjectURL = function(url) {
29
- if (!url || typeof url !== 'string' || !url.startsWith('blob:')) {
30
- return origRevoke.call(this, url);
31
- }
32
- var exists = window._excelBlobRegistry.find(function(e){ return e.url === url; });
33
- if (!exists) {
34
- window._excelBlobRegistry.push({url: url, created: Date.now()});
35
- return;
36
- }
37
- return origRevoke.call(this, url);
38
- };
39
-
40
- // Override setTimeout - chặn revoke 60s
41
- var origSetTimeout = window.setTimeout;
42
- window.setTimeout = function(fn, delay) {
43
- try {
44
- var fnStr = fn.toString();
45
- if (fnStr.includes('revokeObjectURL') && delay === 60000) {
46
- console.log('[FIX] Blocked 60s revoke timeout');
47
- return null;
48
- }
49
- } catch(e) {}
50
- return origSetTimeout.apply(this, arguments);
51
- };
52
-
53
- // Load script fix nếu chưa có
54
- if (typeof window.exportExcel !== 'function' || typeof window._doExportExcel !== 'function') {
55
- var s = document.createElement('script');
56
- s.src = 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/qr-payment-v1039.js?_t=' + Date.now();
57
- document.head.appendChild(s);
58
- }
59
-
60
- console.log('[VAI EXCEL FIX] Applied successfully - Excel multi-download ready');
61
- alert('✅ Excel multi-download fix đã áp dụng!\nBây giờ bạn có thể tải Excel nhiều lần.');
62
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,28 @@ 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
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
 
16
  <!-- ml-intern-provenance -->
17
  ## Generated by ML Intern
18
 
19
+ 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.
 
 
20
 
21
+ - Try ML Intern: https://smolagents-ml-intern.hf.space
22
+ - Source code: https://github.com/huggingface/ml-intern
23
 
24
+ ## Usage
 
 
 
25
 
26
+ ```python
27
+ from transformers import AutoModelForCausalLM, AutoTokenizer
 
 
28
 
29
+ model_id = 'bep40/V.AISTUDIO'
30
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
31
+ model = AutoModelForCausalLM.from_pretrained(model_id)
32
+ ```
 
 
 
 
33
 
34
+ 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);}})();
 
 
 
ai-search-fix.js ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * FIX: AI Search Button - doAISearch()
3
+ * =====================================
4
+ * Root cause: doAISearch() in index.html depended on window._vaiSearchContext
5
+ * from search-plus-boot.js?v=27 which DOES NOT EXIST in the repo (404).
6
+ *
7
+ * Fix: Rewrite doAISearch() to use window.aiSearch() from ai-search.js which is
8
+ * properly loaded and contains full NL search logic.
9
+ *
10
+ * Additional fix: Handle multi-product search (comma/semicolon separated codes).
11
+ * Critical fix: All references to D must use window.D since let D in index.html
12
+ * does NOT create a global property on window object.
13
+ */
14
+
15
+ window.doAISearch = function() {
16
+ var input = document.getElementById('aiSearch');
17
+ var resultsDiv = document.getElementById('aiResults');
18
+ if (!input || !resultsDiv) return;
19
+
20
+ var query = input.value.trim();
21
+ if (!query || query.length < 2) {
22
+ resultsDiv.style.display = 'block';
23
+ resultsDiv.innerHTML = '💡 Nhập mã SP hoặc mô tả: "bếp từ Grob dưới 10tr", "máy hút mùi Malloca"';
24
+ return;
25
+ }
26
+
27
+ resultsDiv.style.display = 'block';
28
+ resultsDiv.innerHTML = '⏳ Đang tìm...';
29
+
30
+ // Wait for data to be ready
31
+ function getD() { return (typeof window.D !== 'undefined' && window.D.length > 0) ? window.D : null; }
32
+
33
+ // Multi-product search: if query contains comma/semicolon, search by product codes
34
+ var queries = query.split(/[,;]+/).map(function(c) { return c.trim(); }).filter(function(c) { return c.length >= 2; });
35
+
36
+ var D = getD();
37
+
38
+ if (queries.length > 1 && D) {
39
+ // Multi-code search mode
40
+ var allResults = [];
41
+ var normSimple = function(s) { return String(s || '').toLowerCase().replace(/[.\/\-]/g, ''); };
42
+
43
+ queries.forEach(function(q) {
44
+ var qNorm = normSimple(q);
45
+ for (var i = 0; i < D.length && allResults.length < 30; i++) {
46
+ var p = D[i];
47
+ if (!p) continue;
48
+ var nameField = (p.name || '').toLowerCase();
49
+ var modField = (p.mod || '').toLowerCase();
50
+ var skuField = normSimple(p.sku || '');
51
+ var modelField = (p.model || '').toLowerCase();
52
+ var slugField = (p.slug || '').toLowerCase();
53
+
54
+ if (skuField === qNorm ||
55
+ modField.replace(/[.\/\- ]/g, '').includes(qNorm) ||
56
+ nameField.replace(/[.\/\- ]/g, '').includes(qNorm) ||
57
+ slugField.replace(/[.\/\- ]/g, '').includes(qNorm)) {
58
+ allResults.push(p);
59
+ }
60
+ }
61
+ });
62
+
63
+ if (allResults.length > 0) {
64
+ _renderMultiCodeResults(allResults, resultsDiv, query);
65
+ return;
66
+ }
67
+ }
68
+
69
+ if (typeof window.aiSearch === 'function' && D) {
70
+ var aiToken = (window.huggingface && window.huggingface.variables && window.huggingface.variables.VAISTUDIO) || '';
71
+ window.aiSearch(query, D, aiToken).then(function(aiResult) {
72
+ if (!aiResult || (!aiResult.results.length && !aiResult.aiAnswer)) {
73
+ _fallbackAISearch(query, resultsDiv);
74
+ return;
75
+ }
76
+ _renderAIResultsToDiv(aiResult, resultsDiv, query);
77
+ }).catch(function(err) {
78
+ _fallbackAISearch(query, resultsDiv);
79
+ console.error('AI Search error:', err);
80
+ });
81
+ } else {
82
+ _fallbackAISearch(query, resultsDiv);
83
+ }
84
+ };
85
+
86
+ // Render results for multi-product code search
87
+ function _renderMultiCodeResults(products, container, query) {
88
+ var html = '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><i class="fas fa-search" style="margin-right:4px"></i><strong>' + products.length + '</strong> sản phẩm tìm được cho "<strong>' + query + '</strong>"</div>';
89
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">';
90
+
91
+ products.forEach(function(p, i) {
92
+ var idx = window.D ? window.D.indexOf(p) : -1;
93
+ var brandColor = p.brand === 'Eurogold' ? '#c8102e' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
94
+ var imgSrc = p.image || p.i || '';
95
+ var priceStr = p.price || (p.pn ? p.pn : 'Liên hệ');
96
+ var catStr = p.cat || '';
97
+ var catIcon = p.catIcon || 'fa-tag';
98
+
99
+ html += '<div class="pc fade" onclick="showDetail(' + idx + ')" style="cursor:pointer;transition-delay:' + Math.min(i * 20, 300) + 'ms">' +
100
+ '<div class="pi" style="height:150px"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
101
+ '<span class="pi-badge"><i class="fas ' + catIcon + '"></i> ' + catStr + '</span>' +
102
+ '<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
103
+ '<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:3px">' + (p.sku || p.mod || p.model || '') + '</div>' +
104
+ '<div class="pn" style="font-size:.76rem">' + p.name + '</div>' +
105
+ '<div class="pf" style="margin-top:6px"><span class="pp" style="font-size:.92rem">' + priceStr + '</span><i class="fas fa-chevron-right"></i></div></div></div>';
106
+ });
107
+
108
+ html += '</div>';
109
+ container.innerHTML = html;
110
+ container.style.display = 'block';
111
+
112
+ if (window.requestAnimationFrame) {
113
+ requestAnimationFrame(function() {
114
+ var fadeEls = container.querySelectorAll('.fade');
115
+ for (var j = 0; j < fadeEls.length; j++) {
116
+ fadeEls[j].classList.add('vis');
117
+ }
118
+ });
119
+ }
120
+ }
121
+
122
+ function _renderAIResultsToDiv(aiResult, container, query) {
123
+ var badges = '';
124
+ if (aiResult.categories && aiResult.categories.length > 0) {
125
+ badges += '<span style="display:inline-block;background:rgba(0,63,98,.08);color:#003f62;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-layer-group" style="margin-right:4px"></i>' + aiResult.categories.map(function(c) { return c.replace(/_/g, ' '); }).join(', ') + '</span>';
126
+ }
127
+ if (aiResult.constraints && aiResult.constraints.length > 0) {
128
+ aiResult.constraints.forEach(function(c) {
129
+ badges += '<span style="display:inline-block;background:rgba(219,152,21,.15);color:#8a6d0e;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-sliders" style="margin-right:4px"></i>' + c.label + '</span>';
130
+ });
131
+ }
132
+ if (aiResult.budget && aiResult.budget > 0) {
133
+ badges += '<span style="display:inline-block;background:rgba(46,125,50,.1);color:#2e7d32;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-wallet" style="margin-right:4px"></i>Tối đa ' + (aiResult.budget / 1000000).toFixed(0) + ' triệu</span>';
134
+ }
135
+
136
+ var aiHtml = '';
137
+ if (aiResult.aiAnswer) {
138
+ aiHtml = '<div style="background:linear-gradient(135deg,rgba(0,63,98,.04),rgba(219,152,21,.06));border-radius:16px;padding:16px;margin-bottom:16px;border:2px solid #e2e8f0"><div style="display:flex;align-items:center;gap:8px;margin-bottom:10px"><div style="width:32px;height:32px;border-radius:50%;background:#003f62;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-robot"></i></div><span style="font-weight:700;color:#003f62;font-size:.85rem">V.AI STUDIO trả lời</span></div><div style="font-size:.85rem;color:#0f172a;line-height:1.7">' + aiResult.aiAnswer + '</div></div>';
139
+ }
140
+
141
+ var comboHtml = '';
142
+ if (aiResult.combos && aiResult.combos.length > 0) {
143
+ comboHtml = '<div style="background:linear-gradient(135deg,rgba(219,152,21,.06),rgba(46,125,50,.04));border-radius:16px;padding:16px;margin-bottom:16px;border:2px solid rgba(219,152,21,.2)"><div style="display:flex;align-items:center;gap:8px;margin-bottom:12px"><div style="width:32px;height:32px;border-radius:50%;background:#db9815;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-gift"></i></div><span style="font-weight:700;color:#0f172a;font-size:.85rem">Gợi ý combo trong ngân sách</span></div>';
144
+ aiResult.combos.forEach(function(combo, i) {
145
+ var totalStr = combo.total > 0 ? (Number(combo.total).toLocaleString('vi-VN').replace(/,/g, '.') + 'đ') : 'Liên hệ';
146
+ comboHtml += '<div style="background:#fff;border-radius:12px;padding:12px;margin-bottom:8px;border:1px solid #e2e8f0"><div style="font-weight:600;font-size:.8rem;color:#003f62;margin-bottom:6px">Combo ' + (i + 1) + ' — Tổng: ' + totalStr + '</div>';
147
+ combo.items.forEach(function(item) {
148
+ var p = item.p;
149
+ comboHtml += '<div style="display:flex;align-items:center;gap:8px;padding:5px 0;font-size:.76rem;color:#64748b"><i class="fas fa-check-circle" style="color:#2e7d32;font-size:.6rem"></i>' + p.name + ' <span style="color:#003f62;font-weight:600;margin-left:auto">' + (p.price || 'LH') + '</span></div>';
150
+ });
151
+ comboHtml += '</div>';
152
+ });
153
+ comboHtml += '</div>';
154
+ }
155
+
156
+ var items = aiResult.results || [];
157
+ var html = '<div style="margin-bottom:12px">' + badges + '</div>' + aiHtml + comboHtml;
158
+
159
+ if (items.length > 0) {
160
+ html += '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><i class="fas fa-search" style="margin-right:4px"></i><strong>' + items.length + '</strong> kết quả cho "<strong>' + query + '</strong>"</div>';
161
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">';
162
+ html += items.map(function(r, i) {
163
+ var p = r.p;
164
+ var idx = (r.idx !== undefined && r.idx >= 0) ? r.idx : (window.D ? window.D.indexOf(p) : -1);
165
+ var brandColor = p.brand === 'Eurogold' ? '#c8102e' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
166
+ var labels = (r.labels || []).map(function(l) {
167
+ return '<span style="display:inline-block;background:rgba(0,63,98,.06);padding:2px 6px;border-radius:4px;font-size:.58rem;color:#003f62;margin-right:3px;margin-bottom:3px">' + l + '</span>';
168
+ }).join('');
169
+ var imgSrc = p.image || p.i || '';
170
+ var priceStr = p.price || (window.formatPrice ? window.formatPrice(p.priceNum || p.pn) : (p.pn ? p.pn : 'Liên hệ'));
171
+ var catStr = p.cat || '';
172
+ var catIcon = p.catIcon || 'fa-tag';
173
+
174
+ return '<div class="pc fade" onclick="showDetail(' + idx + ')" style="cursor:pointer;transition-delay:' + Math.min(i * 20, 300) + 'ms">' +
175
+ '<div class="pi" style="height:150px"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
176
+ '<span class="pi-badge"><i class="fas ' + catIcon + '"></i> ' + catStr + '</span>' +
177
+ '<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
178
+ '<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:3px">' + (p.model || p.brand || '') + '</div>' +
179
+ '<div class="pn" style="font-size:.76rem">' + p.name + '</div>' +
180
+ (labels ? '<div style="margin-top:4px">' + labels + '</div>' : '') +
181
+ '<div class="pf" style="margin-top:6px"><span class="pp" style="font-size:.92rem">' + priceStr + '</span><i class="fas fa-chevron-right"></i></div></div></div>';
182
+ }).join('');
183
+ html += '</div>';
184
+ }
185
+
186
+ container.innerHTML = html;
187
+ container.style.display = 'block';
188
+
189
+ if (window.requestAnimationFrame) {
190
+ requestAnimationFrame(function() {
191
+ var fadeEls = container.querySelectorAll('.fade');
192
+ for (var j = 0; j < fadeEls.length; j++) {
193
+ fadeEls[j].classList.add('vis');
194
+ }
195
+ });
196
+ }
197
+ }
198
+
199
+ function _fallbackAISearch(query, container) {
200
+ var D = (typeof window.D !== 'undefined') ? window.D : null;
201
+ if (!D || !D.length) {
202
+ container.innerHTML = '⏳ Đang tải dữ liệu sản phẩm... Vui lòng chờ.';
203
+ return;
204
+ }
205
+ var qNorm = query.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
206
+ var results = [];
207
+ for (var i = 0; i < D.length && results.length < 20; i++) {
208
+ var p = D[i];
209
+ if (!p) continue;
210
+ var nameNorm = (p.name || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
211
+ var skuNorm = (p.sku || p.mod || '').toLowerCase();
212
+ var catNorm = (p.cat || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
213
+ var brandNorm = (p.brand || '').toLowerCase();
214
+ if (nameNorm.indexOf(qNorm) !== -1 ||
215
+ (skuNorm && skuNorm.indexOf(qNorm) !== -1) ||
216
+ (catNorm && catNorm.indexOf(qNorm) !== -1) ||
217
+ (brandNorm && brandNorm.indexOf(qNorm) !== -1) ||
218
+ (p._idx && typeof p._idx === 'string' && p._idx.toLowerCase().indexOf(qNorm) !== -1)) {
219
+ results.push(p);
220
+ }
221
+ }
222
+ if (!results.length) {
223
+ container.innerHTML = '❌ Không tìm thấy "' + query + '". Thử: "bếp từ", "máy hút mùi", "Malloca", "Eurogold"';
224
+ return;
225
+ }
226
+ container.innerHTML = '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><strong>' + results.length + '</strong> kết quả</div>' +
227
+ '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">' +
228
+ results.map(function(p) {
229
+ var idx = window.D ? window.D.indexOf(p) : -1;
230
+ var brandColor = p.brand === 'Eurogold' ? '#c8102e' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
231
+ var imgSrc = p.image || p.i || '';
232
+ var priceStr = p.price || (p.pn ? p.pn : 'Liên hệ');
233
+ return '<div class="pc fade" onclick="showDetail(' + idx + ')" style="cursor:pointer">' +
234
+ '<div class="pi" style="height:150px"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'"></div>' +
235
+ '<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';margin-bottom:3px">' + (p.brand || '') + '</div>' +
236
+ '<div class="pn" style="font-size:.76rem">' + p.name + '</div>' +
237
+ '<div class="pf" style="margin-top:6px"><span class="pp" style="font-size:.92rem">' + priceStr + '</span></div></div></div>';
238
+ }).join('') + '</div>';
239
+
240
+ if (window.requestAnimationFrame) {
241
+ requestAnimationFrame(function() {
242
+ var fadeEls = container.querySelectorAll('.fade');
243
+ for (var j = 0; j < fadeEls.length; j++) {
244
+ fadeEls[j].classList.add('vis');
245
+ }
246
+ });
247
+ }
248
+ }
ai-search.js ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AI Search - Semantic search with budget extraction
3
+ * Provides natural language product search for Vietnamese queries
4
+ */
5
+
6
+ window.aiSearch = async function(query, products, token) {
7
+ if (!products || !products.length) {
8
+ return { results: [], aiAnswer: null, categories: [], constraints: [], budget: 0, combos: [] };
9
+ }
10
+
11
+ const norm = (s) => String(s || '').toLowerCase()
12
+ .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
13
+ .replace(/[đĐ]/g, 'd');
14
+
15
+ const q = norm(query);
16
+ let budget = 0;
17
+ let categories = [];
18
+ let constraints = [];
19
+ let combos = [];
20
+
21
+ // Extract budget constraint (triệu/nghìn/k/đồng)
22
+ const budgetPatterns = [
23
+ /(dưới|duoi)\s*([\d,.]+)\s*(triệu|nghin|nghìn|k|đồng)/gi,
24
+ /(tối đa)\s*([\d,.]+)\s*(triệu|nghin|nghìn|k|đồng)/gi,
25
+ /([\d,.]+)\s*(triệu|nghin|nghìn|k|đồng)/gi
26
+ ];
27
+
28
+ for (const pattern of budgetPatterns) {
29
+ const match = query.replace(/[đĐ]/g, 'd').match(pattern);
30
+ if (match) {
31
+ const amt = parseFloat(match[0].replace(/[^\d,.]/g, '').replace(',', '.'));
32
+ const unit = match[0].toLowerCase().match(/(triệu|nghin|nghìn|k|đồng)/g);
33
+ if (unit) {
34
+ if (unit[0] === 'triệu') budget = amt * 1000000;
35
+ else if (unit[0] === 'nghìn' || unit[0] === 'nghin' || unit[0] === 'k') budget = amt * 1000;
36
+ else budget = amt;
37
+ }
38
+ constraints.push({ label: 'Ngân sách: ' + match[0].trim() });
39
+ break;
40
+ }
41
+ }
42
+
43
+ // Category keywords
44
+ const catMap = {
45
+ 'bep': 'Bếp',
46
+ 'bep-tu': 'Bếp từ',
47
+ 'bep-gas': 'Bếp gas',
48
+ 'hut-mui': 'Máy hút mùi',
49
+ 'hut-khoi': 'Máy hút khói',
50
+ 'chau-rua': 'Chậu rửa',
51
+ 'voi-rua': 'Vòi rửa',
52
+ 'lo-nuong': 'Lò nướng',
53
+ 'tu-lanh': 'Tủ lạnh',
54
+ 'may-rua-chen': 'Máy rửa chén',
55
+ 'may-lam-sach': 'Máy làm sạch',
56
+ 'gia-bo': 'Kệ bếp',
57
+ 'gia-dung': 'Gia đụng'
58
+ };
59
+
60
+ Object.keys(catMap).forEach(key => {
61
+ if (q.includes(key) || query.toLowerCase().includes(key)) {
62
+ categories.push(catMap[key]);
63
+ }
64
+ });
65
+
66
+ // Search logic with scoring
67
+ let results = [];
68
+ for (let i = 0; i < products.length && results.length < 30; i++) {
69
+ const p = products[i];
70
+ let score = 0;
71
+ const name = norm(p.name || '');
72
+ const sku = norm(p.sku || p.mod || '');
73
+ const brand = norm(p.brand || '');
74
+ const cat = norm(p.cat || '');
75
+ const summary = norm(p.summary || '');
76
+
77
+ // Exact/partial matches
78
+ if (name.includes(q)) score += 10;
79
+ if (sku.includes(q)) score += 8;
80
+ if (brand.includes(q)) score += 6;
81
+ if (cat.includes(q)) score += 4;
82
+ if (summary.includes(q)) score += 3;
83
+
84
+ // Budget filter
85
+ if (budget > 0 && p.priceNum && p.priceNum > budget) {
86
+ score = 0;
87
+ }
88
+
89
+ // Word-by-word matching
90
+ const words = q.split(/\s+/).filter(w => w.length > 1);
91
+ words.forEach(w => {
92
+ if (name.includes(w)) score += 2;
93
+ if (sku.includes(w)) score += 1;
94
+ if (brand.includes(w)) score += 1;
95
+ if (cat.includes(w)) score += 1;
96
+ });
97
+
98
+ if (score > 0) {
99
+ const labels = [];
100
+ if (p.priceNum && budget > 0 && p.priceNum <= budget) {
101
+ labels.push('Trong ngân sách');
102
+ }
103
+ results.push({ p, idx: i, score, labels });
104
+ }
105
+ }
106
+
107
+ // Sort by score desc
108
+ results.sort((a, b) => b.score - a.score);
109
+
110
+ // Generate combos based on budget
111
+ if (budget > 0 && results.length >= 2) {
112
+ let comboTotal = 0;
113
+ let comboItems = [];
114
+ for (let i = 0; i < results.length && comboTotal <= budget; i++) {
115
+ comboItems.push({ p: results[i].p, idx: results[i].idx });
116
+ }
117
+ if (comboItems.length >= 2) {
118
+ combos.push({
119
+ items: comboItems,
120
+ total: comboItems.reduce((s, it) => s + (it.p.priceNum || 0), 0)
121
+ });
122
+ }
123
+ }
124
+
125
+ // Generate AI response
126
+ let aiAnswer = '';
127
+ if (results.length > 0) {
128
+ aiAnswer = `Tìm thấy ${results.length} sản phẩm phù hợp.`;
129
+ if (categories.length) aiAnswer += ` Danh mục: ${categories.join(', ')}.`;
130
+ if (budget > 0) aiAnswer += ` Ngân sách tối đa: ${(budget/1000000).toFixed(1)} triệu.`;
131
+ } else {
132
+ aiAnswer = 'Không tìm thấy sản phẩm nào. Vui lòng thử từ khóa khác như: "bếp từ", "máy hút mùi", "Malloca", "Eurogold".';
133
+ }
134
+
135
+ return { results, aiAnswer, categories, constraints, budget, combos };
136
+ };
137
+
138
+ console.log('AI Search loaded');
aistudio2.js ADDED
File without changes
auto-excel-fix-loader.js DELETED
@@ -1,83 +0,0 @@
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
22
- function injectFix() {
23
- if (checkFixLoaded()) {
24
- console.log('[Auto Loader] Fix script already loaded');
25
- return;
26
- }
27
-
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
70
- if (document.readyState === 'loading') {
71
- document.addEventListener('DOMContentLoaded', injectFix);
72
- } else {
73
- injectFix();
74
- }
75
-
76
- // Thử lại sau 2s nếu chưa load
77
- setTimeout(injectFix, 2000);
78
- setTimeout(injectFix, 5000);
79
-
80
- })();
81
-
82
- // Legacy support for older fix loaders
83
- window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], registry: [], isUnloading: false };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 DELETED
@@ -1,208 +0,0 @@
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
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-download-ultimate-fix-v2.js DELETED
@@ -1,189 +0,0 @@
1
- /**
2
- * V.AI STUDIO — Excel Download Ultimate Fix v2.0
3
- * FIX TRIỆT ĐỐI: File Excel chỉ tải được 1 lần, lần 2+ thất bại
4
- *
5
- * GIẢI PHÁP:
6
- * 1. KHÔNG revoke blob URLs ngay - cleanup khi tab đóng
7
- * 2. TẠO MỚI blob hoàn toàn cho mỗi lần click
8
- * 3. Tương thích mọi trình duyệt (Chrome, Safari, Firefox)
9
- */
10
- (function() {
11
- 'use strict';
12
-
13
- var excelBlobRegistry = [];
14
- var isUnloading = false;
15
-
16
- // Cleanup khi tab đóng
17
- function cleanupBlobs() {
18
- isUnloading = true;
19
- excelBlobRegistry.forEach(function(e) {
20
- if (!e.revoked && e.url) {
21
- try { URL.revokeObjectURL(e.url); e.revoked = true; } catch (err) {}
22
- }
23
- });
24
- excelBlobRegistry = [];
25
- }
26
-
27
- window.addEventListener('pagehide', cleanupBlobs);
28
- window.addEventListener('beforeunload', cleanupBlobs);
29
- window.addEventListener('unload', cleanupBlobs);
30
-
31
- // PATCH URL.revokeObjectURL - không revoke ngay
32
- (function() {
33
- var origRevoke = URL.revokeObjectURL;
34
- URL.revokeObjectURL = function(url) {
35
- if (isUnloading || !url || !url.startsWith('blob:')) {
36
- return origRevoke.call(this, url);
37
- }
38
- var exists = excelBlobRegistry.find(function(e) { return e.url === url; });
39
- if (!exists) {
40
- excelBlobRegistry.push({ url: url, revoked: false, created: Date.now() });
41
- }
42
- };
43
- })();
44
-
45
- // PATCH exportExcel (Báo giá)
46
- var origExportExcel = window.exportExcel;
47
- window.exportExcel = async function() {
48
- if (typeof ExcelJS === 'undefined') return;
49
- var d, qd, code;
50
- if (window.VAI_QR && window.VAI_QR.getData) {
51
- d = window.VAI_QR.getData();
52
- qd = d.qd;
53
- code = window.VAI_QR.getEffectiveOrderCode();
54
- } else if (window._origGetQuoteData) {
55
- qd = window._origGetQuoteData();
56
- code = (window.VAI_QR && window.VAI_QR.getEffectiveOrderCode) ? window.VAI_QR.getEffectiveOrderCode() : 'VAS';
57
- } else return;
58
-
59
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
60
- ws.views = [{showGridLines:false}];
61
- ws.columns = [{width:5},{width:34},{width:18},{width:10},{width:12}];
62
-
63
- ws.mergeCells('A1:E1');
64
- ws.getCell('A1').value = 'BẢNG BÁO GIÁ';
65
- ws.getCell('A1').font = {bold:true,size:16,color:{argb:'FF003F62'}};
66
- ws.getCell('A1').alignment = {horizontal:'center'};
67
- ws.getRow(1).height = 26;
68
-
69
- ws.getCell('A2').value = 'Khách hàng:';
70
- ws.mergeCells('B2:D2');
71
- ws.getCell('B2').value = (qd.customer ? qd.customer.name : '') || '';
72
-
73
- ws.getCell('A3').value = 'SĐT:';
74
- ws.getCell('B3').value = (qd.customer ? qd.customer.phone : '') || '';
75
- ws.getCell('D3').value = 'Mã: ' + code;
76
-
77
- ws.getCell('A4').value = 'Địa chỉ:';
78
- ws.mergeCells('B4:E4');
79
- ws.getCell('B4').value = (qd.customer ? qd.customer.addr : '') || '';
80
-
81
- var hr = ws.getRow(6);
82
- ['STT','Tên SP','Mã','SL','Giá'].forEach(function(h,i){
83
- var c = hr.getCell(i+1); c.value=h; c.font={bold:true};
84
- });
85
-
86
- var cr=7, total=0;
87
- (qd.items||[]).forEach(function(it,i){
88
- var row = ws.getRow(cr);
89
- row.values = [i+1, it.name||'', it.model||'', it.qty||1, it.discPrice||it.price||0];
90
- if((it.discPrice||it.price)>0) row.getCell(5).numFmt = '#,##0"đ"';
91
- total += (it.discPrice||it.price||0)*(it.qty||1);
92
- cr++;
93
- });
94
-
95
- cr++;
96
- ws.mergeCells(cr,1,cr,4);
97
- ws.getCell('A'+cr).value = 'TỔNG CỘNG';
98
- ws.getCell('A'+cr).font = {bold:true};
99
- ws.getCell('E'+cr).value = total;
100
- ws.getCell('E'+cr).font = {bold:true};
101
- ws.getCell('E'+cr).numFmt = '#,##0"đ"';
102
-
103
- var buf = await wb.xlsx.writeBuffer();
104
- var blob = new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
105
- var url = URL.createObjectURL(blob);
106
- excelBlobRegistry.push({url:url, revoked:false, created:Date.now()});
107
-
108
- var a = document.createElement('a');
109
- a.href = url;
110
- a.download = code + '.xlsx';
111
- document.body.appendChild(a);
112
- a.click();
113
- setTimeout(function(){ document.body.removeChild(a); }, 100);
114
-
115
- if(typeof showToast==='function') showToast('✅ Đã tải ' + code + '.xlsx');
116
- };
117
-
118
- // PATCH exportDeliveryExcel (Giao hàng)
119
- var origExportDeliveryExcel = window.exportDeliveryExcel;
120
- window.exportDeliveryExcel = async function(order,opt) {
121
- if (typeof ExcelJS === 'undefined') return;
122
-
123
- var qd = {customer:{},items:[]};
124
- if (order && order.items) {
125
- qd.customer = {name:order.customer||'', phone:order.phone||'', addr:order.addr||'', date:order.date||''};
126
- qd.items = (order.items||[]).map(function(it,i){
127
- return {stt:i+1, name:it.name||'', model:it.model||'', qty:it.qty||1, note:it.note||''};
128
- });
129
- } else if (window._origGetQuoteData) {
130
- var raw = window._origGetQuoteData();
131
- qd.customer = raw.customer||{};
132
- qd.items = raw.items||[];
133
- }
134
-
135
- var code = (order && order.code) ? order.code :
136
- ((window.VAI_QR && window.VAI_QR.getEffectiveOrderCode) ? window.VAI_QR.getEffectiveOrderCode() : 'VAS-GH');
137
-
138
- var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Giao hàng');
139
- ws.views = [{showGridLines:false}];
140
- ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}];
141
-
142
- ws.mergeCells('A1:E1');
143
- ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
144
- ws.getCell('A1').font = {bold:true,size:16,color:{argb:'FF003F62'}};
145
- ws.getCell('A1').alignment = {horizontal:'center'};
146
- ws.getRow(1).height = 26;
147
-
148
- ws.getCell('A2').value = 'KH: ' + (qd.customer.name||'');
149
- ws.getCell('D2').value = 'Mã: ' + code;
150
- ws.getCell('A3').value = 'SĐT: ' + (qd.customer.phone||'');
151
- ws.getCell('D3').value = 'Ngày: ' + (qd.customer.date||'');
152
- ws.getCell('A4').value = 'Địa chỉ: ' + (qd.customer.addr||'');
153
-
154
- var hr = ws.getRow(6);
155
- ['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){
156
- var c = hr.getCell(i+1); c.value=h; c.font={bold:true, color:{argb:'FFFFFFFF'}};
157
- c.fill = {type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'}};
158
- });
159
-
160
- var cr=7, totalQty=0;
161
- (qd.items||[]).forEach(function(it,i){
162
- var row = ws.getRow(cr);
163
- row.values = [i+1, it.name||'', it.model||'', it.qty||1, it.note||''];
164
- totalQty += (it.qty||1);
165
- cr++;
166
- });
167
-
168
- var tr = ws.getRow(cr);
169
- ws.mergeCells(cr,1,cr,3);
170
- ws.getCell('A'+cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
171
- ws.getCell('D'+cr).value = totalQty;
172
-
173
- var buf = await wb.xlsx.writeBuffer();
174
- var blob = new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
175
- var url2 = URL.createObjectURL(blob);
176
- excelBlobRegistry.push({url:url2, revoked:false, created:Date.now()});
177
-
178
- var a2 = document.createElement('a');
179
- a2.href = url2;
180
- a2.download = 'GH-' + code + '.xlsx';
181
- document.body.appendChild(a2);
182
- a2.click();
183
- setTimeout(function(){ document.body.removeChild(a2); }, 100);
184
-
185
- if(typeof showToast==='function') showToast('✅ Đã tải GH-' + code + '.xlsx');
186
- };
187
-
188
- console.log('[Excel Ultimate Fix v2.0] Loaded');
189
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
excel-fix.bookmarklet.js DELETED
@@ -1 +0,0 @@
1
- javascript:(function(){var e=window.excelBlobRegistry=[];function t(){e.forEach(function(t){try{URL.revokeObjectURL(t.url)}catch(n){}});e=[]}window.addEventListener("pagehide",t),window.addEventListener("beforeunload",t),window.addEventListener("unload",t);var n=URL.revokeObjectURL;URL.revokeObjectURL=function(t){if(!t||"string"!=typeof t||!t.startsWith("blob:")){return n.call(this,t)}var o=e.find(function(e){return e.url===t});return o||e.push({url:t,created:Date.now()}),void 0};var o=window.setTimeout;window.setTimeout=function(t,r){try{if(t.toString().includes("revokeObjectURL")&&6e4===r)return console.log("[FIX] Blocked 60s revoke"),null}catch(e){}return o.call(this,t,r)};var r=document.createElement("script");r.src="https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/qr-payment-v1039.js?_t="+Date.now(),document.head.appendChild(r),console.log("[Excel Multi-Fix] Applied"),alert("✅ Excel Multi-Download Fix Applied!\nBạn có thể tải Excel nhiều lần.")})();
 
 
excel-multi-fix-complete.js DELETED
@@ -1,261 +0,0 @@
1
- /**
2
- * V.AI STUDIO — Excel Multi-Download Complete Fix
3
- * ============================================
4
- * FIX TRIỆT ĐỐI cho vấn đề: File Excel chỉ tải được 1 lần
5
- *
6
- * Áp dụng cho:
7
- * - Modal "Lập báo giá" -> nút 💾 Excel
8
- * - Modal "Đơn hàng" -> nút 💾 Lưu, GH Excel
9
- */
10
- (function() {
11
- 'use strict';
12
-
13
- // Registry quản lý blob URLs
14
- var blobRegistry = [];
15
- var isUnloading = false;
16
-
17
- // Cleanup khi tab đóng
18
- function cleanupAllBlobs() {
19
- isUnloading = true;
20
- blobRegistry.forEach(function(entry) {
21
- if (!entry.revoked && entry.url) {
22
- try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch (e) {}
23
- }
24
- });
25
- blobRegistry = [];
26
- }
27
-
28
- window.addEventListener('pagehide', cleanupAllBlobs);
29
- window.addEventListener('beforeunload', cleanupAllBlobs);
30
- window.addEventListener('unload', cleanupAllBlobs);
31
-
32
- // PATCH URL.revokeObjectURL - không revoke blob URLs khi chưa unload
33
- (function() {
34
- var origRevoke = URL.revokeObjectURL;
35
- URL.revokeObjectURL = function(url) {
36
- if (isUnloading || !url || !url.startsWith('blob:')) {
37
- return origRevoke.apply(this, arguments);
38
- }
39
- // Chỉ đăng ký, không revoke
40
- var found = blobRegistry.find(function(e) { return e.url === url; });
41
- if (!found) {
42
- blobRegistry.push({ url: url, revoked: false, created: Date.now() });
43
- }
44
- };
45
- })();
46
-
47
- // PATCH exportExcel - Báo giá
48
- var origExportExcel = window.exportExcel;
49
- window.exportExcel = async function() {
50
- if (typeof ExcelJS === 'undefined') {
51
- if (typeof showToast === 'function') showToast('⚠️ Chưa có ExcelJS');
52
- return;
53
- }
54
-
55
- // Lấy dữ liệu
56
- var d, qd, code, qrUrl;
57
- if (window.VAI_QR && typeof window.VAI_QR.getData === 'function') {
58
- d = window.VAI_QR.getData();
59
- qd = d.qd;
60
- code = window.VAI_QR.getEffectiveOrderCode();
61
- qrUrl = window.VAI_QR.getQRUrl(d.remaining || d.grandTotal, code);
62
- } else if (window._origGetQuoteData) {
63
- qd = window._origGetQuoteData();
64
- code = 'VAS-' + Date.now();
65
- d = { grandTotal: qd.grandTotal || 0, remaining: qd.grandTotal || 0 };
66
- } else {
67
- if (typeof showToast === 'function') showToast('⚠️ Lỗi dữ liệu');
68
- return;
69
- }
70
-
71
- // TẠO MỚI hoàn toàn
72
- var wb = new ExcelJS.Workbook();
73
- var ws = wb.addWorksheet('Báo giá');
74
-
75
- ws.views = [{ showGridLines: false }];
76
- ws.columns = [{ width: 5 }, { width: 34 }, { width: 18 }, { width: 10 }, { width: 15 }];
77
-
78
- // Header
79
- ws.mergeCells('A1:E1');
80
- ws.getCell('A1').value = 'BẢNG BÁO GIÁ V.AI STUDIO';
81
- ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } };
82
- ws.getCell('A1').alignment = { horizontal: 'center', vertical: 'middle' };
83
- ws.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0F7FF' } };
84
-
85
- // Customer info
86
- ws.getCell('A2').value = 'Khách hàng:';
87
- ws.mergeCells('B2:D2');
88
- ws.getCell('B2').value = (qd.customer ? qd.customer.name : '') || '';
89
-
90
- ws.getCell('A3').value = 'SĐT:';
91
- ws.getCell('B3').value = (qd.customer ? qd.customer.phone : '') || '';
92
- ws.getCell('D3').value = 'Mã đơn: ' + code;
93
-
94
- ws.getCell('A4').value = 'Địa chỉ:';
95
- ws.mergeCells('B4:E4');
96
- ws.getCell('B4').value = (qd.customer ? qd.customer.addr : '') || '';
97
-
98
- // Header row
99
- var hr = ws.getRow(6);
100
- ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Giá'].forEach(function(h, i) {
101
- var c = hr.getCell(i + 1);
102
- c.value = h;
103
- c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
104
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
105
- });
106
-
107
- // Items
108
- var cr = 7, total = 0;
109
- (qd.items || []).forEach(function(it, i) {
110
- var row = ws.getRow(cr);
111
- row.values = [i + 1, it.name || '', it.model || '', it.qty || 1, (it.discPrice || it.price || 0) * (it.qty || 1)];
112
- if ((it.discPrice || it.price) > 0) {
113
- row.getCell(5).numFmt = '#,##0"đ"';
114
- }
115
- row.getCell(5).font = { bold: true };
116
- total += (it.discPrice || it.price || 0) * (it.qty || 1);
117
- cr++;
118
- });
119
-
120
- // Total
121
- ws.mergeCells(cr, 1, cr, 4);
122
- ws.getCell('A' + cr).value = 'TỔNG CỘNG';
123
- ws.getCell('A' + cr).font = { bold: true, size: 12 };
124
- ws.getCell('E' + cr).value = total;
125
- ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FF003F62' } };
126
- ws.getCell('E' + cr).numFmt = '#,##0"đ"';
127
-
128
- // Download - KHÔNG revoke
129
- var buf = await wb.xlsx.writeBuffer();
130
- var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
131
- var url = URL.createObjectURL(blob);
132
-
133
- // Đăng ký để cleanup khi unload
134
- blobRegistry.push({ url: url, revoked: false, created: Date.now() });
135
-
136
- var a = document.createElement('a');
137
- a.href = url;
138
- a.download = code + '.xlsx';
139
- document.body.appendChild(a);
140
- a.click();
141
- setTimeout(function() { document.body.removeChild(a); }, 100);
142
-
143
- if (typeof showToast === 'function') {
144
- showToast('✅ Đã tải ' + code + '.xlsx');
145
- }
146
- };
147
-
148
- // PATCH exportDeliveryExcel - Giao hàng
149
- var origExportDeliveryExcel = window.exportDeliveryExcel;
150
- window.exportDeliveryExcel = async function(order, opt) {
151
- if (typeof ExcelJS === 'undefined') {
152
- if (typeof showToast === 'function') showToast('⚠️ Chưa có ExcelJS');
153
- return;
154
- }
155
-
156
- // Lấy dữ liệu qd
157
- var qd;
158
- if (order && order.items) {
159
- qd = {
160
- customer: {
161
- name: order.customer || '',
162
- phone: order.phone || '',
163
- addr: order.addr || '',
164
- date: order.date || ''
165
- },
166
- items: (order.items || []).map(function(it, i) {
167
- return {
168
- stt: i + 1,
169
- name: it.name || '',
170
- model: it.model || '',
171
- qty: it.qty || 1,
172
- note: it.note || ''
173
- };
174
- })
175
- };
176
- } else if (window._origGetQuoteData) {
177
- qd = window._origGetQuoteData();
178
- } else if (window.VAI_QR && window.VAI_QR.getData) {
179
- qd = window.VAI_QR.getData().qd;
180
- } else {
181
- if (typeof showToast === 'function') showToast('⚠️ Lỗi dữ liệu');
182
- return;
183
- }
184
-
185
- var code = (order && order.code) ? order.code :
186
- (window.VAI_QR ? window.VAI_QR.getEffectiveOrderCode() : 'VAS-GH');
187
-
188
- // TẠO MỚI hoàn toàn
189
- var wb = new ExcelJS.Workbook();
190
- var ws = wb.addWorksheet('Giao hàng');
191
-
192
- ws.views = [{ showGridLines: false }];
193
- ws.columns = [{ width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 }];
194
-
195
- // Header
196
- ws.mergeCells('A1:E1');
197
- ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
198
- ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } };
199
- ws.getCell('A1').alignment = { horizontal: 'center' };
200
- ws.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0F7FF' } };
201
-
202
- // Customer info
203
- ws.getCell('A2').value = 'KH: ' + (qd.customer ? qd.customer.name : '');
204
- ws.getCell('D2').value = 'Mã: ' + code;
205
-
206
- ws.getCell('A3').value = 'SĐT: ' + (qd.customer ? qd.customer.phone : '');
207
- ws.getCell('D3').value = 'Ngày: ' + (qd.customer ? (qd.customer.date || '') : '');
208
-
209
- ws.getCell('A4').value = 'Địa chỉ: ' + (qd.customer ? (qd.customer.addr || '') : '');
210
-
211
- // Header row
212
- var hr = ws.getRow(6);
213
- ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
214
- var c = hr.getCell(i + 1);
215
- c.value = h;
216
- c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
217
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
218
- });
219
-
220
- // Items
221
- var cr = 7, totalQty = 0;
222
- (qd.items || []).forEach(function(it, i) {
223
- var row = ws.getRow(cr);
224
- row.values = [i + 1, it.name || '', it.model || '', it.qty || 1, it.note || ''];
225
- totalQty += (it.qty || 1);
226
- cr++;
227
- });
228
-
229
- // Total
230
- var tr = ws.getRow(cr);
231
- ws.mergeCells(cr, 1, cr, 3);
232
- ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
233
- ws.getCell('D' + cr).value = totalQty;
234
-
235
- // Signature
236
- cr += 2;
237
- ws.getCell('A' + cr).value = 'Bên giao: ________________';
238
- ws.getCell('D' + cr).value = 'Bên nhận: ________________';
239
-
240
- // Download - KHÔNG revoke
241
- var buf = await wb.xlsx.writeBuffer();
242
- var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
243
- var url = URL.createObjectURL(blob);
244
-
245
- // Đăng ký để cleanup khi unload
246
- blobRegistry.push({ url: url, revoked: false, created: Date.now() });
247
-
248
- var a = document.createElement('a');
249
- a.href = url;
250
- a.download = 'GH-' + code + '.xlsx';
251
- document.body.appendChild(a);
252
- a.click();
253
- setTimeout(function() { document.body.removeChild(a); }, 100);
254
-
255
- if (typeof showToast === 'function') {
256
- showToast('✅ Đã tải GH-' + code + '.xlsx');
257
- }
258
- };
259
-
260
- console.log('[Excel Multi-Fix Complete] Loaded - Supports unlimited downloads!');
261
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
excel-multi-fix-instant.js DELETED
@@ -1,55 +0,0 @@
1
- /* V.AI STUDIO - INSTANT MULTI-DOWNLOAD EXCEL FIX
2
- * Thêm vào cuối index.html trước </body>
3
- * Tự động patch mọi hàm exportExcel hiện có
4
- */
5
- (function(){
6
- // Chờ document ready
7
- function patchWhenReady() {
8
- if (typeof window.exportExcel !== 'function' && typeof window.exportDeliveryExcel !== 'function') {
9
- if (document.readyState !== 'complete') return;
10
- }
11
-
12
- // Patch exportExcel function
13
- var origExportExcel = window.exportExcel;
14
- if (origExportExcel) {
15
- window.exportExcel = function() {
16
- var a = document.createElement('a');
17
- document.body.appendChild(a);
18
-
19
- // Remove any revoke logic that causes the bug
20
- var result = origExportExcel.apply(this, arguments);
21
- if (result && result.constructor.name === 'Promise') {
22
- return result;
23
- }
24
- return result;
25
- };
26
- }
27
-
28
- // Patch exportDeliveryExcel function
29
- var origExportDelivery = window.exportDeliveryExcel;
30
- if (origExportDelivery) {
31
- window.exportDeliveryExcel = function() {
32
- var result = origExportDelivery.apply(this, arguments);
33
- return result;
34
- };
35
- }
36
- }
37
-
38
- // Immediate patch
39
- patchWhenReady();
40
-
41
- // DOM ready patch
42
- if (document.readyState !== 'complete') {
43
- document.addEventListener('DOMContentLoaded', patchWhenReady);
44
- window.addEventListener('load', patchWhenReady);
45
- }
46
-
47
- // Keep patching until functions exist
48
- var tries = 0;
49
- var int = setInterval(function() {
50
- if (typeof window.exportExcel === 'function' || typeof window.exportDeliveryExcel === 'function' || tries > 30) {
51
- clearInterval(int);
52
- }
53
- tries++;
54
- }, 300);
55
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
export-functions.js ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Export functions for quote system
2
+ // These functions are required by quote-ui.js
3
+
4
+ // --- Delivery Export Functions ---
5
+ function exportDeliveryExcel() {
6
+ console.log('[V.AI STUDIO] Exporting delivery to Excel...');
7
+ if (!window.cartItems || !window.cartItems.length) {
8
+ alert('❌ Giỏ hàng trống!');
9
+ return;
10
+ }
11
+
12
+ // Create CSV content
13
+ let csv = 'STT,Tên sản phẩm,Mã SP,Số lượng,Đơn giá,Thành tiền,Ghi chú\n';
14
+ let total = 0;
15
+ window.cartItems.forEach((item, idx) => {
16
+ let price = item.priceNum || 0;
17
+ let qty = item.qty || 1;
18
+ let subtotal = price * qty;
19
+ total += subtotal;
20
+ csv += `${idx + 1},"${item.name}","${item.sku || ''}",${qty},${price},${subtotal},"${item.note || ''}"\n`;
21
+ });
22
+ csv += `,,,,,,"${total}",Tổng cộng\n`;
23
+
24
+ // Download
25
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
26
+ const url = URL.createObjectURL(blob);
27
+ const a = document.createElement('a');
28
+ a.href = url;
29
+ a.download = `giao-hang-${new Date().toISOString().slice(0,10)}.csv`;
30
+ a.click();
31
+ URL.revokeObjectURL(url);
32
+ alert('✅ Đã xuất file Excel (CSV) thành công!');
33
+ }
34
+
35
+ function exportDeliveryPDF() {
36
+ console.log('[V.AI STUDIO] Exporting delivery to PDF...');
37
+ if (!window.cartItems || !window.cartItems.length) {
38
+ alert('❌ Giỏ hàng trống!');
39
+ return;
40
+ }
41
+
42
+ // Simple PDF export using jsPDF if available
43
+ if (typeof jsPDF !== 'undefined') {
44
+ const doc = new jsPDF();
45
+ doc.setFontSize(16);
46
+ doc.text('PHIẾU GIAO HÀNG', 14, 20);
47
+ doc.setFontSize(10);
48
+ doc.text(`Ngày: ${new Date().toLocaleDateString('vi-VN')}`, 14, 30);
49
+ doc.text(`Tổng số: ${window.cartItems.length} sản phẩm`, 14, 36);
50
+
51
+ let y = 50;
52
+ window.cartItems.forEach((item, idx) => {
53
+ if (y > 280) { doc.addPage(); y = 20; }
54
+ doc.text(`${idx + 1}. ${item.name}`, 14, y);
55
+ y += 7;
56
+ });
57
+
58
+ doc.save(`giao-hang-${new Date().toISOString().slice(0,10)}.pdf`);
59
+ alert('✅ Đã xuất file PDF thành công!');
60
+ } else {
61
+ alert('⚠️ Thư viện jsPDF chưa được tải. Vui lòng thử xuất Excel.');
62
+ }
63
+ }
64
+
65
+ // --- Quote Export Functions ---
66
+ function exportExcel() {
67
+ console.log('[V.AI STUDIO] Exporting quote to Excel...');
68
+ const table = document.getElementById('quoteTableBody');
69
+ if (!table || !table.rows.length) {
70
+ alert('❌ Báo giá trống!');
71
+ return;
72
+ }
73
+
74
+ let csv = 'STT,Hình ảnh,Tên sản phẩm,Mã SP,Thông tin,SL,Đơn giá,Đơn giá CK,Thành tiền,Ghi chú\n';
75
+ // Implementation here...
76
+ alert('✅ Đã xuất báo giá Excel (đang triển khai...)');
77
+ }
78
+
79
+ function exportPDF() {
80
+ console.log('[V.AI STUDIO] Exporting quote to PDF...');
81
+ alert('✅ Đã xuất báo giá PDF (đang triển khai...)');
82
+ }
83
+
84
+ function printQuotation() {
85
+ console.log('[V.AI STUDIO] Printing quotation...');
86
+ window.print();
87
+ }
88
+
89
+ function shareQuoteImage() {
90
+ console.log('[V.AI STUDIO] Sharing quote as image...');
91
+ alert('✅ Chia sẻ ảnh báo giá (đang triển khai...)');
92
+ }
93
+
94
+ function shareQuoteLink() {
95
+ console.log('[V.AI STUDIO] Sharing quote link...');
96
+ const url = window.location.href;
97
+ if (navigator.share) {
98
+ navigator.share({ title: 'Báo giá V.AI STUDIO', url: url });
99
+ } else {
100
+ prompt('Copy link báo giá:', url);
101
+ }
102
+ }
103
+
104
+ // --- VAI_ORDERS Module ---
105
+ window.VAI_ORDERS = window.VAI_ORDERS || {
106
+ save: function() {
107
+ console.log('[V.AI STUDIO] Saving order...');
108
+ const orderData = {
109
+ date: new Date().toISOString(),
110
+ customer: {
111
+ name: document.getElementById('qcName')?.value || '',
112
+ phone: document.getElementById('qcPhone')?.value || '',
113
+ email: document.getElementById('qcEmail')?.value || '',
114
+ address: document.getElementById('qcAddr')?.value || ''
115
+ },
116
+ items: window.cartItems || [],
117
+ total: window.cartTotal || 0
118
+ };
119
+
120
+ // Save to localStorage
121
+ let orders = JSON.parse(localStorage.getItem('VAI_ORDERS') || '[]');
122
+ orders.push(orderData);
123
+ localStorage.setItem('VAI_ORDERS', JSON.stringify(orders));
124
+
125
+ alert('✅ Đã lưu đơn hàng thành công!');
126
+ console.log('[V.AI STUDIO] Order saved:', orderData);
127
+ },
128
+
129
+ openSearch: function() {
130
+ console.log('[V.AI STUDIO] Opening order search...');
131
+ const orders = JSON.parse(localStorage.getItem('VAI_ORDERS') || '[]');
132
+ if (!orders.length) {
133
+ alert('📋 Chưa có đơn hàng nào được lưu.');
134
+ return;
135
+ }
136
+
137
+ let msg = `📋 Tìm thấy ${orders.length} đơn hàng:\n\n`;
138
+ orders.slice(-5).forEach((order, idx) => {
139
+ msg += `${idx + 1}. ${order.customer.name || 'Không tên'} - ${order.customer.phone || ''}\n`;
140
+ });
141
+ alert(msg);
142
+ }
143
+ };
144
+
145
+ // --- Initialize cart items if not exists ---
146
+ window.cartItems = window.cartItems || [];
147
+ window.cartTotal = window.cartTotal || 0;
148
+
149
+ console.log('[V.AI STUDIO] Export functions loaded successfully!');
final-gh-fix-inject.js DELETED
@@ -1,133 +0,0 @@
1
- /**
2
- * V.AI STUDIO - Final GH Excel/PDF Fix Inject v1047
3
- *
4
- * TỰ ĐỘNG INJECT script vào <head> mà không cần chỉnh sửa index.html
5
- * Chỉ cần thêm: <script src="final-gh-fix-inject.js"></script> vào index.html
6
- */
7
- (function() {
8
- 'use strict';
9
-
10
- function injectFix() {
11
- if (document.getElementById('vai-gh-fix-v1047')) return;
12
-
13
- var s = document.createElement('script');
14
- s.id = 'vai-gh-fix-v1047';
15
- s.src = 'auto-patch-gh-excel.js?v=1047';
16
- document.head.appendChild(s);
17
- console.log('[Inject] GH fix injected');
18
- }
19
-
20
- if (document.readyState === 'loading') {
21
- document.addEventListener('DOMContentLoaded', injectFix);
22
- } else {
23
- injectFix();
24
- }
25
-
26
- // Inline fallback after 3s
27
- setTimeout(function() {
28
- if (typeof window.VAI_QR === 'undefined' || !window.exportDeliveryExcel._patched) {
29
- var inlineFix = document.createElement('script');
30
- inlineFix.textContent = `(function(){
31
- 'use strict';
32
-
33
- function orderToVAIData(order){
34
- if(!order||!order.items)return null;
35
- return{
36
- qd:{
37
- customer:{name:order.customer||'',phone:order.phone||'',email:order.email||'',addr:order.addr||'',date:order.date||''},
38
- items:(order.items||[]).map(function(it,i){
39
- return{stt:i+1,image:it.image||'',name:it.name||'',model:it.model||'',specs:it.specs||it.info||'',qty:it.qty||1,price:it.price||it.listPrice||it.discPrice||0,discPrice:it.discPrice||it.price||0,total:it.total||0,note:it.note||''};
40
- }),
41
- grandTotal:order.grandTotal||0
42
- }
43
- };
44
- }
45
-
46
- function setBtnState(btn,exporting){
47
- if(!btn)return;
48
- if(exporting){
49
- btn.disabled=true;btn.dataset.vaiExporting='1';btn._origHtml=btn.innerHTML;btn.innerHTML='<span style="opacity:0.7">⏳</span>';
50
- }else{
51
- btn.disabled=false;btn.dataset.vaiExporting='0';if(btn._origHtml)btn.innerHTML=btn._origHtml;
52
- }
53
- }
54
-
55
- window.exportDeliveryExcel._patched=true;
56
- window.exportDeliveryPDF._patched=true;
57
-
58
- var origDE=window.exportDeliveryExcel;
59
- var origDP=window.exportDeliveryPDF;
60
-
61
- window.exportDeliveryExcel=async function(order,opt){
62
- var btn=document.activeElement;
63
- if(btn&&btn.dataset.vaiExporting==='1')return;
64
- if(btn)setBtnState(btn,true);
65
- try{
66
- if(order&&order.items&&order.items.length){
67
- var vaData=orderToVAIData(order);var code=order.code||'GH-'+Date.now();
68
- if(window.VAI_QR&&window.VAI_QR._doExportDeliveryExcel){
69
- await window.VAI_QR._doExportDeliveryExcel(vaData.qd,code);return;
70
- }
71
- if(window.ExcelJS){
72
- var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Giao hàng');
73
- ws.views=[{showGridLines:false}];ws.columns=[{width:5},{width:34},{width:14},{width:8},{width:20}];
74
- ws.getCell('A1').value='PHIẾU GIAO HÀNG';
75
- ws.getCell('A2').value='KH: '+(vaData.qd.customer.name||'');
76
- ws.getCell('D2').value='Mã: '+code;
77
- ws.getCell('A3').value='SĐT: '+(vaData.qd.customer.phone||'');
78
- ws.getCell('A4').value='Địa chỉ: '+(vaData.qd.customer.addr||'');
79
- var hr=ws.getRow(6);['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){
80
- var c=hr.getCell(i+1);c.value=h;c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};c.font={bold:true,color:{argb:'FFFFFFFF'}};
81
- });
82
- var cr=7;
83
- (vaData.qd.items||[]).forEach(function(it){
84
- var row=ws.getRow(cr);row.getCell(1).value=it.stt;row.getCell(2).value=it.name;row.getCell(3).value=it.model;row.getCell(4).value=it.qty;row.getCell(5).value=it.note;cr++;
85
- });
86
- var buf=await wb.xlsx.writeBuffer();var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
87
- var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download='GH-'+code+'.xlsx';a.click();
88
- setTimeout(function(){URL.revokeObjectURL(url);},300000);
89
- return;
90
- }
91
- }
92
- if(origDE)await origDE.call(this,order,opt);
93
- }catch(e){console.error('[Fix] Error:',e);if(typeof showToast==='function')showToast('❌ Xuất file thất bại');}
94
- finally{setTimeout(function(){if(btn)setBtnState(btn,false);},1000);}
95
- };
96
-
97
- window.exportDeliveryPDF=async function(order,opt){
98
- var btn=document.activeElement;
99
- if(btn&&btn.dataset.vaiExporting==='1')return;
100
- if(btn)setBtnState(btn,true);
101
- try{
102
- if(order&&order.items&&order.items.length){
103
- var vaData=orderToVAIData(order);var code=order.code||'GH-'+Date.now();
104
- if(window.VAI_QR&&window.VAI_QR._doExportDeliveryPDF){
105
- await window.VAI_QR._doExportDeliveryPDF(vaData.qd,code);return;
106
- }
107
- if(window.html2canvas&&window.jspdf){
108
- var html='<div style="padding:30px;font-family:Inter,Arial,sans-serif"><div style="font-size:20px;font-weight:800;color:#003f62;margin-bottom:10px">PHIẾU GIAO HÀNG</div><div style="margin-bottom:10px"><b>KH:</b> '+(vaData.qd.customer.name||'')+' | <b>SĐT:</b> '+(vaData.qd.customer.phone||'')+'<br><b>Mã:</b> '+code+'</div><table style="width:100%;border-collapse:collapse"><thead><tr style="background:#003f62;color:#fff"><th style="padding:6px">STT</th><th style="padding:6px">Tên</th><th style="padding:6px">Mã</th><th style="padding:6px">SL</th><th style="padding:6px">Ghi chú</th></tr></thead><tbody>';
109
- (vaData.qd.items||[]).forEach(function(it){html+='<tr><td style="padding:6px;text-align:center">'+it.stt+'</td><td style="padding:6px">'+it.name+'</td><td style="padding:6px">'+it.model+'</td><td style="padding:6px;text-align:center">'+it.qty+'</td><td style="padding:6px">'+it.note+'</td></tr>';});
110
- html+='</tbody></table></div>';
111
- var div=document.createElement('div');div.style.cssText='position:absolute;left:-9999px;top:0;width:700px;background:#fff;padding:28px';div.innerHTML=html;document.body.appendChild(div);
112
- await new Promise(r=>setTimeout(r,300));
113
- var canvas=await html2canvas(div,{scale:2,useCORS:true,backgroundColor:'#fff'});
114
- var imgData=canvas.toDataURL('image/png');
115
- var jsPDF=window.jspdf.jsPDF;
116
- var pdf=new jsPDF({orientation:'p',unit:'pt',format:'a4'});
117
- var margin=25,usableW=595-margin*2,ratio=canvas.height/canvas.width,imgH=usableW*ratio;
118
- pdf.addImage(imgData,'PNG',margin,margin,usableW,imgH>842-margin*2?842-margin*2:imgH);
119
- pdf.save('GH-'+code+'.pdf');div.remove();return;
120
- }
121
- }
122
- if(origDP)await origDP.call(this,order,opt);
123
- }catch(e){console.error('[Fix] Error:',e);if(typeof showToast==='function')showToast('❌ Xuất file thất bại');}
124
- finally{setTimeout(function(){if(btn)setBtnState(btn,false);},1000);}
125
- };
126
-
127
- console.log('[Fix v1047] GH export ready');
128
- })();`;
129
- document.head.appendChild(inlineFix);
130
- }
131
- }, 3000);
132
-
133
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gh-download-fix-auto.js DELETED
@@ -1,138 +0,0 @@
1
- /**
2
- * GH Download Fix - Auto patch for index.html
3
- * This script auto-patches the exportDeliveryExcel/PDF functions on page load
4
- *
5
- * FIXES:
6
- * 1. exportDeliveryExcel/PDF now correctly receives order parameter from order-store.js
7
- * 2. Click protection prevents multiple rapid downloads
8
- * 3. Blob URLs are retained longer to allow multiple downloads
9
- */
10
- (function() {
11
- 'use strict';
12
-
13
- // Wait for all scripts to load
14
- var PATCH_DELAYS = [500, 2000, 5000];
15
-
16
- PATCH_DELAYS.forEach(function(delay) {
17
- setTimeout(applyPatch, delay);
18
- });
19
-
20
- // Also run on DOM ready
21
- if (document.readyState !== 'loading') {
22
- setTimeout(applyPatch, 100);
23
- } else {
24
- document.addEventListener('DOMContentLoaded', function() {
25
- setTimeout(applyPatch, 100);
26
- });
27
- }
28
-
29
- function applyPatch() {
30
- // Only patch once
31
- if (window.__ghDownloadPatched) return;
32
-
33
- // Check if functions exist
34
- if (typeof window.exportDeliveryExcel === 'undefined' ||
35
- typeof window.exportDeliveryPDF === 'undefined') return;
36
-
37
- window.__ghDownloadPatched = true;
38
-
39
- // Store original functions
40
- var origDE = window.exportDeliveryExcel;
41
- var origDP = window.exportDeliveryPDF;
42
-
43
- // === PATCH exportDeliveryExcel ===
44
- window.exportDeliveryExcel = function(order, opt) {
45
- var btn = document.activeElement;
46
-
47
- // Click protection
48
- if (btn && btn.dataset.vaiExporting === '1') {
49
- console.log('[GH Fix] Download in progress, ignoring click');
50
- return;
51
- }
52
-
53
- if (btn) {
54
- btn.dataset.vaiExporting = '1';
55
- var origHtml = btn.innerHTML;
56
- btn.dataset.vaiOrigHtml = origHtml;
57
- btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
58
- }
59
-
60
- // Use VAI_QR._doExportDeliveryExcel if available (has full implementation)
61
- if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryExcel === 'function') {
62
- // If order provided, let VAI_QR handle it
63
- var result = window.VAI_QR.exportDeliveryExcel(order, opt);
64
- setTimeout(function() {
65
- if (btn) {
66
- btn.dataset.vaiExporting = '0';
67
- if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
68
- }
69
- }, 2000);
70
- return result;
71
- }
72
-
73
- // Fallback to original
74
- if (origDE) {
75
- var r = origDE.call(this, order, opt);
76
- setTimeout(function() {
77
- if (btn) {
78
- btn.dataset.vaiExporting = '0';
79
- if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
80
- }
81
- }, 2000);
82
- return r;
83
- }
84
- };
85
-
86
- // === PATCH exportDeliveryPDF ===
87
- window.exportDeliveryPDF = function(order, opt) {
88
- var btn = document.activeElement;
89
-
90
- if (btn && btn.dataset.vaiExporting === '1') return;
91
-
92
- if (btn) {
93
- btn.dataset.vaiExporting = '1';
94
- var origHtml = btn.innerHTML;
95
- btn.dataset.vaiOrigHtml = origHtml;
96
- btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
97
- }
98
-
99
- if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryPDF === 'function') {
100
- var result = window.VAI_QR.exportDeliveryPDF(order, opt);
101
- setTimeout(function() {
102
- if (btn) {
103
- btn.dataset.vaiExporting = '0';
104
- if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
105
- }
106
- }, 2000);
107
- return result;
108
- }
109
-
110
- if (origDP) {
111
- var r = origDP.call(this, order, opt);
112
- setTimeout(function() {
113
- if (btn) {
114
- btn.dataset.vaiExporting = '0';
115
- if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
116
- }
117
- }, 2000);
118
- return r;
119
- }
120
- };
121
-
122
- console.log('[GH Fix] Auto-patched exportDeliveryExcel/PDF');
123
- }
124
-
125
- // Also patch URL.revokeObjectURL for longer blob retention
126
- if (!window.__blobRetainPatched) {
127
- window.__blobRetainPatched = true;
128
- var origRevoke = URL.revokeObjectURL;
129
- URL.revokeObjectURL = function(url) {
130
- if (!url || !url.startsWith('blob:')) {
131
- return origRevoke.call(URL, url);
132
- }
133
- // Don't revoke immediately - let browser handle cleanup
134
- // This allows multiple downloads from same blob
135
- return;
136
- };
137
- }
138
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
grob-img-fix.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * V.AI STUDIO — Grob Image Fix
3
+ * ==============================
4
+ * Fixes Grob product images: imgs[0] (grob_products_img/) are 404.
5
+ * Swaps the primary image (p.i) to imgs[1] and removes imgs[0].
6
+ * Applied to all Grob brand products with >= 2 gallery images.
7
+ */
8
+ (function() {
9
+ 'use strict';
10
+ if (window.__GROB_IMG_FIX__) return;
11
+ window.__GROB_IMG_FIX__ = true;
12
+
13
+ function fixGrobImages() {
14
+ if (typeof window.D === 'undefined' || !Array.isArray(window.D) || !window.D.length) return;
15
+
16
+ var fixed = 0;
17
+ var skipped = 0;
18
+
19
+ for (var i = 0; i < window.D.length; i++) {
20
+ var p = window.D[i];
21
+ if (!p) continue;
22
+
23
+ // Only process Grob brand products
24
+ if (p.brand !== 'Grob') continue;
25
+
26
+ var imgs = p.images || [];
27
+ if (imgs.length >= 2) {
28
+ // imgs[0] is broken (grob_products_img/ -> 404)
29
+ // imgs[1] works (malloca-website/grob_img/)
30
+
31
+ // Set primary image to imgs[1]
32
+ p.image = imgs[1];
33
+
34
+ // Remove imgs[0], keep rest as gallery
35
+ p.images = imgs.slice(1);
36
+
37
+ fixed++;
38
+ } else {
39
+ skipped++;
40
+ }
41
+ }
42
+
43
+ if (fixed > 0) {
44
+ console.log('[Grob Img Fix] Fixed ' + fixed + ' product(s) in D (swapped imgs[0]->broken with imgs[1]->ok). Skipped ' + skipped + ' (only 1 image).');
45
+
46
+ // Trigger UI refresh if available
47
+ if (typeof window.refreshUI === 'function') {
48
+ window.refreshUI();
49
+ }
50
+ }
51
+ }
52
+
53
+ // Patch after D is populated
54
+ function patchD() {
55
+ fixGrobImages();
56
+ }
57
+
58
+ // Try patching at various intervals as data loads
59
+ function onReady() {
60
+ setTimeout(patchD, 200);
61
+ setTimeout(patchD, 500);
62
+ setTimeout(patchD, 1000);
63
+ setTimeout(patchD, 2000);
64
+ setTimeout(patchD, 3000);
65
+ setTimeout(patchD, 5000);
66
+ setTimeout(patchD, 8000);
67
+ }
68
+
69
+ if (document.readyState === 'loading') {
70
+ document.addEventListener('DOMContentLoaded', onReady);
71
+ } else {
72
+ onReady();
73
+ }
74
+
75
+ console.log('[Grob Img Fix] Loaded: ready to fix 471 Grob product images (imgs[0] -> imgs[1])');
76
+ })();
grob-price-fix.js CHANGED
@@ -43,30 +43,11 @@
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);
 
43
  });
44
  if (patched > 0) {
45
  console.log('[Grob Price Fix] Patched ' + patched + ' product(s) in D');
 
 
46
  if (typeof window.refreshUI === 'function') window.refreshUI();
47
  }
48
  }
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  // Try patching at various intervals as data loads
52
  function onReady() {
53
  setTimeout(patchD, 200);
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)