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