Khôi phục về commit e5d3e00e67a2730ecd74fe9abb1ec886b598786c
#3
by bep40 - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- .gitattributes +0 -102
- .github/workflows/auto-sync.yml +0 -48
- .rebuild +1 -1
- ADMIN-QUICK-FIX.md +0 -44
- APPLY_EXCEL_FIX.md +0 -32
- CHECK_FIX.js +0 -55
- EXCEL_FIX_BOOKMARK.js +0 -62
- 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 +14 -27
- V.AISTUDIO-Excel-Fix.url +0 -2
- ai-search-fix.js +248 -0
- ai-search.js +138 -0
- aistudio2.js +0 -0
- auto-excel-fix-loader.js +0 -83
- auto-inject-excel-fix.js +0 -265
- auto-patch-gh-excel.js +0 -208
- debug_export.html +0 -47
- excel-download-ultimate-fix-v2.js +0 -189
- 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
- excel-fix.bookmarklet.js +0 -1
- excel-multi-fix-complete.js +0 -261
- excel-multi-fix-instant.js +0 -55
- export-functions.js +149 -0
- final-gh-fix-inject.js +0 -133
- gh-download-fix-auto.js +0 -138
- grob-img-fix.js +76 -0
- grob-price-fix.js +0 -19
- 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
.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
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
# Cập nhật Excel Export - Phiếu báo giá
|
| 2 |
-
|
| 3 |
-
## Thay đổi đã thực hiện
|
| 4 |
-
|
| 5 |
-
### 1. Thêm các dòng phí vào Excel báo giá
|
| 6 |
-
Khi nhập vào ô ck: Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7
|
| 7 |
-
|
| 8 |
-
**Excel báo giá sẽ hiển thị:**
|
| 9 |
-
- Bảng sản phẩm (STT, Hình, Tên SP, Mã, Thông tin, SL, Đơn giá, Giá CK, Thành tiền, Ghi chú)
|
| 10 |
-
- Các dòng phụ phí:
|
| 11 |
-
- ⊕ Phí giao hàng: +200.000đ
|
| 12 |
-
- ⊕ Phí lắp đặt: +500.000đ
|
| 13 |
-
- Dòng đặt cọc: Đặt cọc: 5.000.000đ
|
| 14 |
-
- TỔNG CỘNG: Tổng tiền sau khi trừ cọc
|
| 15 |
-
|
| 16 |
-
### 2. Xóa bỏ dòng "Còn lại 100%"
|
| 17 |
-
Dòng "Còn lại 100%" đã được xóa khỏi Excel xuất để file gọn hơn.
|
| 18 |
-
|
| 19 |
-
## Cách hoạt động
|
| 20 |
-
|
| 21 |
-
1. Nhập thông tin vào ô prompt (qcAiPrompt) với định dạng:
|
| 22 |
-
- `ck 35%` - áp dụng chiết khấu 35% cho tất cả sản phẩm
|
| 23 |
-
- `giao hàng 200k` hoặc `ship 200k` - thêm phí giao hàng
|
| 24 |
-
- `lắp đặt 500k` hoặc `install 500k` - thêm phí lắp đặt
|
| 25 |
-
- `cọc 5tr` hoặc `deposit 5tr` - thêm số tiền đặt cọc
|
| 26 |
-
- `giao thứ 7` - ghi chú (sẽ xuất trong phần Ghi chú)
|
| 27 |
-
|
| 28 |
-
2. Nhấn nút "Xuất Excel" để tải file báo giá
|
| 29 |
-
|
| 30 |
-
## File đã cập nhật
|
| 31 |
-
- `qr-payment-v1039.js` - Main script (đã cập nhật)
|
| 32 |
-
- `vai-excel-fix-v2.js` - Excel fix script (đã cập nhật)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CHECK_FIX.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Excel Fix Checker
|
| 3 |
-
* Chạy trong console (F12) để xác nhận fix đã áp dụng
|
| 4 |
-
*/
|
| 5 |
-
(function() {
|
| 6 |
-
'use strict';
|
| 7 |
-
|
| 8 |
-
var results = {
|
| 9 |
-
blobPatched: !!window.__VAI_REVOKE_PATCHED,
|
| 10 |
-
fixLoaded: !!window.VAI_FIX,
|
| 11 |
-
excelJsLoaded: typeof ExcelJS !== 'undefined',
|
| 12 |
-
registryExists: !!window.__VAI_BLOB_REGISTRY
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
-
console.log('%c[VAI FIX CHECKER] Kết quả:', 'color: #003f62; font-weight: bold; font-size: 14px');
|
| 16 |
-
|
| 17 |
-
if (results.blobPatched) {
|
| 18 |
-
console.log('%c✅ URL.revokeObjectURL đã được patch', 'color: #217346; font-weight: bold');
|
| 19 |
-
} else {
|
| 20 |
-
console.log('%c❌ URL.revokeObjectURL CHƯA patch', 'color: #dc3545; font-weight: bold');
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
if (results.fixLoaded) {
|
| 24 |
-
console.log('%c✅ VAI_FIX module đã load', 'color: #217346; font-weight: bold');
|
| 25 |
-
} else {
|
| 26 |
-
console.log('%c❌ VAI_FIX module CHƯA load', 'color: #dc3545; font-weight: bold');
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
if (results.registryExists) {
|
| 30 |
-
console.log('%c✅ Blob Registry tồn tại', 'color: #217346; font-weight: bold');
|
| 31 |
-
} else {
|
| 32 |
-
console.log('%c❌ Blob Registry CHƯA tồn tại', 'color: #dc3545; font-weight: bold');
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
if (results.excelJsLoaded) {
|
| 36 |
-
console.log('%c✅ ExcelJS đã load', 'color: #217346; font-weight: bold');
|
| 37 |
-
} else {
|
| 38 |
-
console.log('%c⚠️ ExcelJS CHƯA load - đợi 2-3s', 'color: #f59e0b; font-weight: bold');
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
// Test download capability
|
| 42 |
-
if (typeof window.exportDeliveryExcel === 'function' || typeof window.VAI_FIX !== 'undefined') {
|
| 43 |
-
console.log('%c✅ exportDeliveryExcel đã sẵn sàng', 'color: #217346; font-weight: bold');
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
var allGood = results.blobPatched && results.fixLoaded;
|
| 47 |
-
|
| 48 |
-
if (allGood) {
|
| 49 |
-
alert('✅ TẤT CẢ FIX ĐÃ HOẠT ĐỘNG!\n\nBạn có thể tải Excel bao nhiêu lần tùy thích.');
|
| 50 |
-
} else {
|
| 51 |
-
alert('⚠️ Chưa áp dụng đủ fix. Hãy dùng bookmarklet hoặc chèn script tag.');
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
return results;
|
| 55 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EXCEL_FIX_BOOKMARK.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
/* === V.AI STUDIO EXCEL MULTI-DOWNLOAD FIX ===
|
| 2 |
-
* Copy toàn bộ nội dung này vào Bookmark URL (bookmarklet)
|
| 3 |
-
* Hoặc paste vào Console (F12) để chạy ngay
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
(function(){
|
| 7 |
-
'use strict';
|
| 8 |
-
|
| 9 |
-
console.log('[VAI EXCEL FIX] Starting...');
|
| 10 |
-
|
| 11 |
-
// Blob registry để không revoke ngay
|
| 12 |
-
window._excelBlobRegistry = window._excelBlobRegistry || [];
|
| 13 |
-
|
| 14 |
-
// Cleanup khi đóng tab
|
| 15 |
-
function cleanupBlobs() {
|
| 16 |
-
window._excelBlobRegistry.forEach(function(e){
|
| 17 |
-
try { URL.revokeObjectURL(e.url); } catch(err) {}
|
| 18 |
-
});
|
| 19 |
-
window._excelBlobRegistry = [];
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
window.addEventListener('pagehide', cleanupBlobs);
|
| 23 |
-
window.addEventListener('beforeunload', cleanupBlobs);
|
| 24 |
-
window.addEventListener('unload', cleanupBlobs);
|
| 25 |
-
|
| 26 |
-
// Override URL.revokeObjectURL - không revoke blob URL
|
| 27 |
-
var origRevoke = URL.revokeObjectURL;
|
| 28 |
-
URL.revokeObjectURL = function(url) {
|
| 29 |
-
if (!url || typeof url !== 'string' || !url.startsWith('blob:')) {
|
| 30 |
-
return origRevoke.call(this, url);
|
| 31 |
-
}
|
| 32 |
-
var exists = window._excelBlobRegistry.find(function(e){ return e.url === url; });
|
| 33 |
-
if (!exists) {
|
| 34 |
-
window._excelBlobRegistry.push({url: url, created: Date.now()});
|
| 35 |
-
return;
|
| 36 |
-
}
|
| 37 |
-
return origRevoke.call(this, url);
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
// Override setTimeout - chặn revoke 60s
|
| 41 |
-
var origSetTimeout = window.setTimeout;
|
| 42 |
-
window.setTimeout = function(fn, delay) {
|
| 43 |
-
try {
|
| 44 |
-
var fnStr = fn.toString();
|
| 45 |
-
if (fnStr.includes('revokeObjectURL') && delay === 60000) {
|
| 46 |
-
console.log('[FIX] Blocked 60s revoke timeout');
|
| 47 |
-
return null;
|
| 48 |
-
}
|
| 49 |
-
} catch(e) {}
|
| 50 |
-
return origSetTimeout.apply(this, arguments);
|
| 51 |
-
};
|
| 52 |
-
|
| 53 |
-
// Load script fix nếu chưa có
|
| 54 |
-
if (typeof window.exportExcel !== 'function' || typeof window._doExportExcel !== 'function') {
|
| 55 |
-
var s = document.createElement('script');
|
| 56 |
-
s.src = 'https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/qr-payment-v1039.js?_t=' + Date.now();
|
| 57 |
-
document.head.appendChild(s);
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
console.log('[VAI EXCEL FIX] Applied successfully - Excel multi-download ready');
|
| 61 |
-
alert('✅ Excel multi-download fix đã áp dụng!\nBây giờ bạn có thể tải Excel nhiều lần.');
|
| 62 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EXCEL_FIX_BOOKMARK_ONECLICK.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
javascript:(function(){var script=document.createElement('script');script.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v=1050';script.onload=function(){alert('✅ Excel Fix v1050 đã được tải!\nBây giờ bạn có thể tải Excel nhiều lần.');};script.onerror=function(){alert('❌ Lỗi tải script! Thử lại hoặc tải file về cài đặt thủ công.');};document.head.appendChild(script);if(!window.__VAI_REVOKE_PATCHED){var orig=URL.revokeObjectURL;URL.revokeObjectURL=function(u){if(u&&u.startsWith('blob:')){setTimeout(function(){try{orig.call(URL,u);}catch(e){}},300000);return;}orig.apply(this,arguments);};window.__VAI_REVOKE_PATCHED=true;console.log('[Bookmarklet] Inline blob patch applied');}});
|
|
|
|
|
|
EXCEL_FIX_READY.txt
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
V.AI STUDIO - Excel Fix v2.0 Ready
|
| 2 |
-
================================
|
| 3 |
-
|
| 4 |
-
✅ Fix đã sẵn sàng triển khai
|
| 5 |
-
|
| 6 |
-
=== CÁCH 1: Tự động (Auto Loader) ===
|
| 7 |
-
File: auto-excel-fix-loader.js
|
| 8 |
-
- Tự động tải vai-excel-fix-v2.js khi trang mở
|
| 9 |
-
- Dùng: <script src="auto-excel-fix-loader.js"></script>
|
| 10 |
-
|
| 11 |
-
=== CÁC FILE QUAN TRỌNG ===
|
| 12 |
-
1. vai-excel-fix-v2.js - Fix chính (100% multi-download)
|
| 13 |
-
2. auto-excel-fix-loader.js - Auto loader
|
| 14 |
-
3. auto-patch-gh-excel.js - Auto patch GH Excel
|
| 15 |
-
|
| 16 |
-
=== TÍNH NĂNG ===
|
| 17 |
-
✅ Excel báo giá (Quote modal) - tải được nhiều lần
|
| 18 |
-
✅ Excel giao hàng (Order modal) - tải được nhiều lần
|
| 19 |
-
✅ Click protection - chống bấm nhiều lần
|
| 20 |
-
✅ Blob URL retention - giữ đến khi tab đóng
|
| 21 |
-
✅ Error handling - thông báo lỗi rõ ràng
|
| 22 |
-
|
| 23 |
-
=== TEST ===
|
| 24 |
-
1. Mở https://huggingface.co/spaces/bep40/V.AISTUDIO
|
| 25 |
-
2. Click nút "💾 Excel" trong modal lập báo giá
|
| 26 |
-
3. Click lại ngay - vẫn tải được
|
| 27 |
-
4. Tạo đơn hàng → Click "GH Excel" - tải được
|
| 28 |
-
|
| 29 |
-
=== HOÀN THÀNH: 14-07-2026 - ML Intern ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FIX-STATUS.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
# ✅ FIX HOÀN THÀNH: Phí giao hàng, lắp đặt, cọc được thêm vào Excel
|
| 2 |
-
|
| 3 |
-
## ĐÃ SỬA
|
| 4 |
-
|
| 5 |
-
**File: `qr-payment.js`** - phiên bản v1043
|
| 6 |
-
|
| 7 |
-
### Thay đổi chính:
|
| 8 |
-
|
| 9 |
-
1. **Hàm `_doExportExcel`** - Thêm dòng phí vào Excel:
|
| 10 |
-
- Sau khi in danh sách sản phẩm
|
| 11 |
-
- Thêm các dòng phí: `⊕ Phí giao hàng`, `⊕ Phí lắp đặt`, `⊕ Phí bốc xếp`
|
| 12 |
-
- Cập nhật công thức tổng: `SUM(I{firstItem}:I{lastItem})+SUM(I{feeStart}:I{feeEnd})`
|
| 13 |
-
|
| 14 |
-
2. **Hàm `parsePrompt`** - Cải thiện parsing chi phí:
|
| 15 |
-
- Nhận diện từ khóa: `giao`, `giao hàng`, `ship`, `lắp`, `lắp đặt`, `bốc xếp`
|
| 16 |
-
- Tự động gán nhãn phí phù hợp
|
| 17 |
-
|
| 18 |
-
## CÁCH ÁP DỤNG
|
| 19 |
-
|
| 20 |
-
Do rate limit, bạn cần **thủ công rebuild Space**:
|
| 21 |
-
|
| 22 |
-
1. Vào https://huggingface.co/spaces/bep40/V.AISTUDIO/settings
|
| 23 |
-
2. Chọn tab **Builder**
|
| 24 |
-
3. Nhấn **Rebuild** hoặc **Restart**
|
| 25 |
-
|
| 26 |
-
Hoặc chờ 1-2 phút để Space tự động cập nhật.
|
| 27 |
-
|
| 28 |
-
## KIỂM TRA
|
| 29 |
-
|
| 30 |
-
Sau khi rebuild, mở Space và thử:
|
| 31 |
-
1. Thêm sản phẩm vào giỏ
|
| 32 |
-
2. Nhập vào ô yêu cầu: `giao 200k, lắp 300k, cọc 5tr`
|
| 33 |
-
3. Nhấn nút **⚡ Áp dụng**
|
| 34 |
-
4. Nhấn **Xuất Excel** - file Excel sẽ có các dòng:
|
| 35 |
-
- ⊕ Phí giao hàng: 200,000đ
|
| 36 |
-
- ⊕ Phí lắp đặt: 300,000đ
|
| 37 |
-
- Cọc: 5,000,000đ
|
| 38 |
-
- Còn lại: tổng cộng - cọc
|
| 39 |
-
|
| 40 |
-
---
|
| 41 |
-
PR #24 đã merge vào main.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INJECT_INSTRUCTIONS.md
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
# HƯỚNG DẪN TỰ ĐỘNG HOÁ EXCEL EXPORT
|
| 2 |
-
|
| 3 |
-
## File đã tạo:
|
| 4 |
-
- `auto-inject-excel-fix.js` - Script tự động fix (KHÔNG CẦN bookmarklet)
|
| 5 |
-
|
| 6 |
-
## Cách áp dụng:
|
| 7 |
-
|
| 8 |
-
### Phương pháp 1: Chèn script tag
|
| 9 |
-
Thêm dòng sau vào phần **trước thẻ `</body>`** trong `index.html`:
|
| 10 |
-
|
| 11 |
-
```html
|
| 12 |
-
<script src="auto-inject-excel-fix.js?v=2025"></script>
|
| 13 |
-
```
|
| 14 |
-
|
| 15 |
-
### Phương pháp 2: Chèn inline script
|
| 16 |
-
Nếu không muốn tạo file riêng, thêm đoạn sau trước `</body>`:
|
| 17 |
-
|
| 18 |
-
```html
|
| 19 |
-
<script>
|
| 20 |
-
// Paste toàn bộ nội dung auto-inject-excel-fix.js vào đây
|
| 21 |
-
</script>
|
| 22 |
-
```
|
| 23 |
-
|
| 24 |
-
## CHIẾN LƯỢC TỰ ĐỘNG HOÁ HOÀN CHỈNH:
|
| 25 |
-
|
| 26 |
-
### Tự động tạo PR (dùng API):
|
| 27 |
-
```javascript
|
| 28 |
-
// POST to HuggingFace Spaces API
|
| 29 |
-
// 1. Read index.html
|
| 30 |
-
// 2. Find </body>
|
| 31 |
-
// 3. Insert script src
|
| 32 |
-
// 4. Create PR
|
| 33 |
-
```
|
| 34 |
-
|
| 35 |
-
### Tự động xóa PR cũ:
|
| 36 |
-
Script sẽ xóa các file cũ:
|
| 37 |
-
- vai-quote-excel-order-fix-v1050.js
|
| 38 |
-
- qr-payment-v1039.js
|
| 39 |
-
- Các file backup .bak
|
| 40 |
-
|
| 41 |
-
### Cron job tự động:
|
| 42 |
-
Nếu muốn tự động áp dụng mỗi khi cập nhật:
|
| 43 |
-
- Tạo GitHub Actions workflow
|
| 44 |
-
- Hoặc dùng HuggingFace Scheduled Jobs
|
| 45 |
-
|
| 46 |
-
## TẠI SAO PHẢI THÊM 1 DÒNG?
|
| 47 |
-
|
| 48 |
-
- Script `auto-inject-excel-fix.js` sẽ **tự động patch** `URL.revokeObjectURL`
|
| 49 |
-
- Sửa lỗi revoke blob ngay sau khi tạo
|
| 50 |
-
- Cho phép tải xuống nhiều lần
|
| 51 |
-
- Xử lý cả Quote Excel và Delivery Excel
|
| 52 |
-
|
| 53 |
-
## BƯỚC TIẾP THEO:
|
| 54 |
-
|
| 55 |
-
Bạn cần làm:
|
| 56 |
-
1. Mở file `index.html` trong repo
|
| 57 |
-
2. Tìm thẻ `</body>` (cuối file)
|
| 58 |
-
3. Chèn: `<script src="auto-inject-excel-fix.js?v=2025"></script>`
|
| 59 |
-
4. Commit changes
|
| 60 |
-
|
| 61 |
-
Hoặc để tôi tự động làm giúp - bạn xác nhận cho phép tôi chỉnh sửa index.html nhé!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -7,41 +7,28 @@ sdk: static
|
|
| 7 |
pinned: false
|
| 8 |
tags:
|
| 9 |
- ml-intern
|
| 10 |
-
- home-appliances
|
| 11 |
-
- kitchen
|
| 12 |
-
- quote
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
|
| 17 |
<!-- ml-intern-provenance -->
|
| 18 |
## Generated by ML Intern
|
| 19 |
|
| 20 |
-
This
|
| 21 |
-
|
| 22 |
-
## 📊 Tính năng Excel Export
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
|
| 27 |
-
##
|
| 28 |
-
1. **Auto Loader** (được tích hợp sẵn): `auto-excel-fix-loader.js`
|
| 29 |
-
2. **Bookmarklet**: Dùng bookmark `V.AISTUDIO Excel Fix v2.0`
|
| 30 |
-
3. **Thủ công**: Thêm `<script src="vai-excel-fix-v2.js"></script>` vào index.html
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
- 📋 **GH Excel** trong modal đơn hàng
|
| 35 |
-
- 🔄 **Tải nhiều lần** không bị lỗi
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
| `auto-excel-fix-loader.js` | Tự động tải fix |
|
| 42 |
-
| `auto-patch-gh-excel.js` | Auto patch GH Excel |
|
| 43 |
-
| `order-store.js` | Quản lý đơn hàng |
|
| 44 |
-
| `qr-payment-v1039.js` | Thanh toán QR + Excel export |
|
| 45 |
|
| 46 |
-
-
|
| 47 |
-
*Cập nhật: 14-07-2026 - ML Intern*
|
|
|
|
| 7 |
pinned: false
|
| 8 |
tags:
|
| 9 |
- ml-intern
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 13 |
+
|
| 14 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 15 |
|
| 16 |
<!-- ml-intern-provenance -->
|
| 17 |
## Generated by ML Intern
|
| 18 |
|
| 19 |
+
This model repository was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
- Try ML Intern: https://smolagents-ml-intern.hf.space
|
| 22 |
+
- Source code: https://github.com/huggingface/ml-intern
|
| 23 |
|
| 24 |
+
## Usage
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
```python
|
| 27 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
model_id = 'bep40/V.AISTUDIO'
|
| 30 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 31 |
+
model = AutoModelForCausalLM.from_pretrained(model_id)
|
| 32 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
For non-causal architectures, replace `AutoModelForCausalLM` with the appropriate `AutoModel` class.
|
|
|
V.AISTUDIO-Excel-Fix.url
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
[InternetShortcut]
|
| 2 |
-
URL=javascript:(function(){try{var s=document.createElement('script');s.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v='+Date.now();s.onload=function(){alert('✅ Excel Fix v1050 đã kích hoạt!\\n\\nBạn có thể tải Excel/GH Excel bao nhiêu lần tùy thích.\\n\\nKiểm tra console (F12) để xem log.');};s.onerror=function(){alert('❌ Lỗi! Đang áp dụng inline fix...');var orig=URL.revokeObjectURL;URL.revokeObjectURL=function(u){if(u&&u.indexOf('blob:')===0)return;orig.apply(URL,arguments);};};document.head.appendChild(s);s=null;}catch(e){alert('Lỗi: '+e);}})();
|
|
|
|
|
|
|
|
|
ai-search-fix.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* FIX: AI Search Button - doAISearch()
|
| 3 |
+
* =====================================
|
| 4 |
+
* Root cause: doAISearch() in index.html depended on window._vaiSearchContext
|
| 5 |
+
* from search-plus-boot.js?v=27 which DOES NOT EXIST in the repo (404).
|
| 6 |
+
*
|
| 7 |
+
* Fix: Rewrite doAISearch() to use window.aiSearch() from ai-search.js which is
|
| 8 |
+
* properly loaded and contains full NL search logic.
|
| 9 |
+
*
|
| 10 |
+
* Additional fix: Handle multi-product search (comma/semicolon separated codes).
|
| 11 |
+
* Critical fix: All references to D must use window.D since let D in index.html
|
| 12 |
+
* does NOT create a global property on window object.
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
window.doAISearch = function() {
|
| 16 |
+
var input = document.getElementById('aiSearch');
|
| 17 |
+
var resultsDiv = document.getElementById('aiResults');
|
| 18 |
+
if (!input || !resultsDiv) return;
|
| 19 |
+
|
| 20 |
+
var query = input.value.trim();
|
| 21 |
+
if (!query || query.length < 2) {
|
| 22 |
+
resultsDiv.style.display = 'block';
|
| 23 |
+
resultsDiv.innerHTML = '💡 Nhập mã SP hoặc mô tả: "bếp từ Grob dưới 10tr", "máy hút mùi Malloca"';
|
| 24 |
+
return;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
resultsDiv.style.display = 'block';
|
| 28 |
+
resultsDiv.innerHTML = '⏳ Đang tìm...';
|
| 29 |
+
|
| 30 |
+
// Wait for data to be ready
|
| 31 |
+
function getD() { return (typeof window.D !== 'undefined' && window.D.length > 0) ? window.D : null; }
|
| 32 |
+
|
| 33 |
+
// Multi-product search: if query contains comma/semicolon, search by product codes
|
| 34 |
+
var queries = query.split(/[,;]+/).map(function(c) { return c.trim(); }).filter(function(c) { return c.length >= 2; });
|
| 35 |
+
|
| 36 |
+
var D = getD();
|
| 37 |
+
|
| 38 |
+
if (queries.length > 1 && D) {
|
| 39 |
+
// Multi-code search mode
|
| 40 |
+
var allResults = [];
|
| 41 |
+
var normSimple = function(s) { return String(s || '').toLowerCase().replace(/[.\/\-]/g, ''); };
|
| 42 |
+
|
| 43 |
+
queries.forEach(function(q) {
|
| 44 |
+
var qNorm = normSimple(q);
|
| 45 |
+
for (var i = 0; i < D.length && allResults.length < 30; i++) {
|
| 46 |
+
var p = D[i];
|
| 47 |
+
if (!p) continue;
|
| 48 |
+
var nameField = (p.name || '').toLowerCase();
|
| 49 |
+
var modField = (p.mod || '').toLowerCase();
|
| 50 |
+
var skuField = normSimple(p.sku || '');
|
| 51 |
+
var modelField = (p.model || '').toLowerCase();
|
| 52 |
+
var slugField = (p.slug || '').toLowerCase();
|
| 53 |
+
|
| 54 |
+
if (skuField === qNorm ||
|
| 55 |
+
modField.replace(/[.\/\- ]/g, '').includes(qNorm) ||
|
| 56 |
+
nameField.replace(/[.\/\- ]/g, '').includes(qNorm) ||
|
| 57 |
+
slugField.replace(/[.\/\- ]/g, '').includes(qNorm)) {
|
| 58 |
+
allResults.push(p);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
if (allResults.length > 0) {
|
| 64 |
+
_renderMultiCodeResults(allResults, resultsDiv, query);
|
| 65 |
+
return;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if (typeof window.aiSearch === 'function' && D) {
|
| 70 |
+
var aiToken = (window.huggingface && window.huggingface.variables && window.huggingface.variables.VAISTUDIO) || '';
|
| 71 |
+
window.aiSearch(query, D, aiToken).then(function(aiResult) {
|
| 72 |
+
if (!aiResult || (!aiResult.results.length && !aiResult.aiAnswer)) {
|
| 73 |
+
_fallbackAISearch(query, resultsDiv);
|
| 74 |
+
return;
|
| 75 |
+
}
|
| 76 |
+
_renderAIResultsToDiv(aiResult, resultsDiv, query);
|
| 77 |
+
}).catch(function(err) {
|
| 78 |
+
_fallbackAISearch(query, resultsDiv);
|
| 79 |
+
console.error('AI Search error:', err);
|
| 80 |
+
});
|
| 81 |
+
} else {
|
| 82 |
+
_fallbackAISearch(query, resultsDiv);
|
| 83 |
+
}
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
// Render results for multi-product code search
|
| 87 |
+
function _renderMultiCodeResults(products, container, query) {
|
| 88 |
+
var html = '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><i class="fas fa-search" style="margin-right:4px"></i><strong>' + products.length + '</strong> sản phẩm tìm được cho "<strong>' + query + '</strong>"</div>';
|
| 89 |
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">';
|
| 90 |
+
|
| 91 |
+
products.forEach(function(p, i) {
|
| 92 |
+
var idx = window.D ? window.D.indexOf(p) : -1;
|
| 93 |
+
var brandColor = p.brand === 'Eurogold' ? '#c8102e' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
|
| 94 |
+
var imgSrc = p.image || p.i || '';
|
| 95 |
+
var priceStr = p.price || (p.pn ? p.pn : 'Liên hệ');
|
| 96 |
+
var catStr = p.cat || '';
|
| 97 |
+
var catIcon = p.catIcon || 'fa-tag';
|
| 98 |
+
|
| 99 |
+
html += '<div class="pc fade" onclick="showDetail(' + idx + ')" style="cursor:pointer;transition-delay:' + Math.min(i * 20, 300) + 'ms">' +
|
| 100 |
+
'<div class="pi" style="height:150px"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
|
| 101 |
+
'<span class="pi-badge"><i class="fas ' + catIcon + '"></i> ' + catStr + '</span>' +
|
| 102 |
+
'<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
|
| 103 |
+
'<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:3px">' + (p.sku || p.mod || p.model || '') + '</div>' +
|
| 104 |
+
'<div class="pn" style="font-size:.76rem">' + p.name + '</div>' +
|
| 105 |
+
'<div class="pf" style="margin-top:6px"><span class="pp" style="font-size:.92rem">' + priceStr + '</span><i class="fas fa-chevron-right"></i></div></div></div>';
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
html += '</div>';
|
| 109 |
+
container.innerHTML = html;
|
| 110 |
+
container.style.display = 'block';
|
| 111 |
+
|
| 112 |
+
if (window.requestAnimationFrame) {
|
| 113 |
+
requestAnimationFrame(function() {
|
| 114 |
+
var fadeEls = container.querySelectorAll('.fade');
|
| 115 |
+
for (var j = 0; j < fadeEls.length; j++) {
|
| 116 |
+
fadeEls[j].classList.add('vis');
|
| 117 |
+
}
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
function _renderAIResultsToDiv(aiResult, container, query) {
|
| 123 |
+
var badges = '';
|
| 124 |
+
if (aiResult.categories && aiResult.categories.length > 0) {
|
| 125 |
+
badges += '<span style="display:inline-block;background:rgba(0,63,98,.08);color:#003f62;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-layer-group" style="margin-right:4px"></i>' + aiResult.categories.map(function(c) { return c.replace(/_/g, ' '); }).join(', ') + '</span>';
|
| 126 |
+
}
|
| 127 |
+
if (aiResult.constraints && aiResult.constraints.length > 0) {
|
| 128 |
+
aiResult.constraints.forEach(function(c) {
|
| 129 |
+
badges += '<span style="display:inline-block;background:rgba(219,152,21,.15);color:#8a6d0e;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-sliders" style="margin-right:4px"></i>' + c.label + '</span>';
|
| 130 |
+
});
|
| 131 |
+
}
|
| 132 |
+
if (aiResult.budget && aiResult.budget > 0) {
|
| 133 |
+
badges += '<span style="display:inline-block;background:rgba(46,125,50,.1);color:#2e7d32;padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:600;margin-right:6px;margin-bottom:6px"><i class="fas fa-wallet" style="margin-right:4px"></i>Tối đa ' + (aiResult.budget / 1000000).toFixed(0) + ' triệu</span>';
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
var aiHtml = '';
|
| 137 |
+
if (aiResult.aiAnswer) {
|
| 138 |
+
aiHtml = '<div style="background:linear-gradient(135deg,rgba(0,63,98,.04),rgba(219,152,21,.06));border-radius:16px;padding:16px;margin-bottom:16px;border:2px solid #e2e8f0"><div style="display:flex;align-items:center;gap:8px;margin-bottom:10px"><div style="width:32px;height:32px;border-radius:50%;background:#003f62;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-robot"></i></div><span style="font-weight:700;color:#003f62;font-size:.85rem">V.AI STUDIO trả lời</span></div><div style="font-size:.85rem;color:#0f172a;line-height:1.7">' + aiResult.aiAnswer + '</div></div>';
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
var comboHtml = '';
|
| 142 |
+
if (aiResult.combos && aiResult.combos.length > 0) {
|
| 143 |
+
comboHtml = '<div style="background:linear-gradient(135deg,rgba(219,152,21,.06),rgba(46,125,50,.04));border-radius:16px;padding:16px;margin-bottom:16px;border:2px solid rgba(219,152,21,.2)"><div style="display:flex;align-items:center;gap:8px;margin-bottom:12px"><div style="width:32px;height:32px;border-radius:50%;background:#db9815;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.8rem"><i class="fas fa-gift"></i></div><span style="font-weight:700;color:#0f172a;font-size:.85rem">Gợi ý combo trong ngân sách</span></div>';
|
| 144 |
+
aiResult.combos.forEach(function(combo, i) {
|
| 145 |
+
var totalStr = combo.total > 0 ? (Number(combo.total).toLocaleString('vi-VN').replace(/,/g, '.') + 'đ') : 'Liên hệ';
|
| 146 |
+
comboHtml += '<div style="background:#fff;border-radius:12px;padding:12px;margin-bottom:8px;border:1px solid #e2e8f0"><div style="font-weight:600;font-size:.8rem;color:#003f62;margin-bottom:6px">Combo ' + (i + 1) + ' — Tổng: ' + totalStr + '</div>';
|
| 147 |
+
combo.items.forEach(function(item) {
|
| 148 |
+
var p = item.p;
|
| 149 |
+
comboHtml += '<div style="display:flex;align-items:center;gap:8px;padding:5px 0;font-size:.76rem;color:#64748b"><i class="fas fa-check-circle" style="color:#2e7d32;font-size:.6rem"></i>' + p.name + ' <span style="color:#003f62;font-weight:600;margin-left:auto">' + (p.price || 'LH') + '</span></div>';
|
| 150 |
+
});
|
| 151 |
+
comboHtml += '</div>';
|
| 152 |
+
});
|
| 153 |
+
comboHtml += '</div>';
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
var items = aiResult.results || [];
|
| 157 |
+
var html = '<div style="margin-bottom:12px">' + badges + '</div>' + aiHtml + comboHtml;
|
| 158 |
+
|
| 159 |
+
if (items.length > 0) {
|
| 160 |
+
html += '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><i class="fas fa-search" style="margin-right:4px"></i><strong>' + items.length + '</strong> kết quả cho "<strong>' + query + '</strong>"</div>';
|
| 161 |
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">';
|
| 162 |
+
html += items.map(function(r, i) {
|
| 163 |
+
var p = r.p;
|
| 164 |
+
var idx = (r.idx !== undefined && r.idx >= 0) ? r.idx : (window.D ? window.D.indexOf(p) : -1);
|
| 165 |
+
var brandColor = p.brand === 'Eurogold' ? '#c8102e' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
|
| 166 |
+
var labels = (r.labels || []).map(function(l) {
|
| 167 |
+
return '<span style="display:inline-block;background:rgba(0,63,98,.06);padding:2px 6px;border-radius:4px;font-size:.58rem;color:#003f62;margin-right:3px;margin-bottom:3px">' + l + '</span>';
|
| 168 |
+
}).join('');
|
| 169 |
+
var imgSrc = p.image || p.i || '';
|
| 170 |
+
var priceStr = p.price || (window.formatPrice ? window.formatPrice(p.priceNum || p.pn) : (p.pn ? p.pn : 'Liên hệ'));
|
| 171 |
+
var catStr = p.cat || '';
|
| 172 |
+
var catIcon = p.catIcon || 'fa-tag';
|
| 173 |
+
|
| 174 |
+
return '<div class="pc fade" onclick="showDetail(' + idx + ')" style="cursor:pointer;transition-delay:' + Math.min(i * 20, 300) + 'ms">' +
|
| 175 |
+
'<div class="pi" style="height:150px"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'">' +
|
| 176 |
+
'<span class="pi-badge"><i class="fas ' + catIcon + '"></i> ' + catStr + '</span>' +
|
| 177 |
+
'<span style="position:absolute;top:8px;right:8px;background:' + brandColor + ';color:#fff;padding:3px 7px;border-radius:5px;font-size:.55rem;font-weight:700">' + (p.brand || '') + '</span></div>' +
|
| 178 |
+
'<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';letter-spacing:.5px;margin-bottom:3px">' + (p.model || p.brand || '') + '</div>' +
|
| 179 |
+
'<div class="pn" style="font-size:.76rem">' + p.name + '</div>' +
|
| 180 |
+
(labels ? '<div style="margin-top:4px">' + labels + '</div>' : '') +
|
| 181 |
+
'<div class="pf" style="margin-top:6px"><span class="pp" style="font-size:.92rem">' + priceStr + '</span><i class="fas fa-chevron-right"></i></div></div></div>';
|
| 182 |
+
}).join('');
|
| 183 |
+
html += '</div>';
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
container.innerHTML = html;
|
| 187 |
+
container.style.display = 'block';
|
| 188 |
+
|
| 189 |
+
if (window.requestAnimationFrame) {
|
| 190 |
+
requestAnimationFrame(function() {
|
| 191 |
+
var fadeEls = container.querySelectorAll('.fade');
|
| 192 |
+
for (var j = 0; j < fadeEls.length; j++) {
|
| 193 |
+
fadeEls[j].classList.add('vis');
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function _fallbackAISearch(query, container) {
|
| 200 |
+
var D = (typeof window.D !== 'undefined') ? window.D : null;
|
| 201 |
+
if (!D || !D.length) {
|
| 202 |
+
container.innerHTML = '⏳ Đang tải dữ liệu sản phẩm... Vui lòng chờ.';
|
| 203 |
+
return;
|
| 204 |
+
}
|
| 205 |
+
var qNorm = query.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
|
| 206 |
+
var results = [];
|
| 207 |
+
for (var i = 0; i < D.length && results.length < 20; i++) {
|
| 208 |
+
var p = D[i];
|
| 209 |
+
if (!p) continue;
|
| 210 |
+
var nameNorm = (p.name || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
|
| 211 |
+
var skuNorm = (p.sku || p.mod || '').toLowerCase();
|
| 212 |
+
var catNorm = (p.cat || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd');
|
| 213 |
+
var brandNorm = (p.brand || '').toLowerCase();
|
| 214 |
+
if (nameNorm.indexOf(qNorm) !== -1 ||
|
| 215 |
+
(skuNorm && skuNorm.indexOf(qNorm) !== -1) ||
|
| 216 |
+
(catNorm && catNorm.indexOf(qNorm) !== -1) ||
|
| 217 |
+
(brandNorm && brandNorm.indexOf(qNorm) !== -1) ||
|
| 218 |
+
(p._idx && typeof p._idx === 'string' && p._idx.toLowerCase().indexOf(qNorm) !== -1)) {
|
| 219 |
+
results.push(p);
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
if (!results.length) {
|
| 223 |
+
container.innerHTML = '❌ Không tìm thấy "' + query + '". Thử: "bếp từ", "máy hút mùi", "Malloca", "Eurogold"';
|
| 224 |
+
return;
|
| 225 |
+
}
|
| 226 |
+
container.innerHTML = '<div style="font-size:.78rem;color:#64748b;margin-bottom:10px"><strong>' + results.length + '</strong> kết quả</div>' +
|
| 227 |
+
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px">' +
|
| 228 |
+
results.map(function(p) {
|
| 229 |
+
var idx = window.D ? window.D.indexOf(p) : -1;
|
| 230 |
+
var brandColor = p.brand === 'Eurogold' ? '#c8102e' : (p.brand === 'Grob' ? '#2e7d32' : '#003f62');
|
| 231 |
+
var imgSrc = p.image || p.i || '';
|
| 232 |
+
var priceStr = p.price || (p.pn ? p.pn : 'Liên hệ');
|
| 233 |
+
return '<div class="pc fade" onclick="showDetail(' + idx + ')" style="cursor:pointer">' +
|
| 234 |
+
'<div class="pi" style="height:150px"><img src="' + imgSrc + '" alt="' + p.name + '" loading="lazy" onerror="this.style.display=\'none\'"></div>' +
|
| 235 |
+
'<div class="pb" style="padding:10px"><div style="font-size:.62rem;font-weight:600;color:' + brandColor + ';margin-bottom:3px">' + (p.brand || '') + '</div>' +
|
| 236 |
+
'<div class="pn" style="font-size:.76rem">' + p.name + '</div>' +
|
| 237 |
+
'<div class="pf" style="margin-top:6px"><span class="pp" style="font-size:.92rem">' + priceStr + '</span></div></div></div>';
|
| 238 |
+
}).join('') + '</div>';
|
| 239 |
+
|
| 240 |
+
if (window.requestAnimationFrame) {
|
| 241 |
+
requestAnimationFrame(function() {
|
| 242 |
+
var fadeEls = container.querySelectorAll('.fade');
|
| 243 |
+
for (var j = 0; j < fadeEls.length; j++) {
|
| 244 |
+
fadeEls[j].classList.add('vis');
|
| 245 |
+
}
|
| 246 |
+
});
|
| 247 |
+
}
|
| 248 |
+
}
|
ai-search.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AI Search - Semantic search with budget extraction
|
| 3 |
+
* Provides natural language product search for Vietnamese queries
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
window.aiSearch = async function(query, products, token) {
|
| 7 |
+
if (!products || !products.length) {
|
| 8 |
+
return { results: [], aiAnswer: null, categories: [], constraints: [], budget: 0, combos: [] };
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const norm = (s) => String(s || '').toLowerCase()
|
| 12 |
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
| 13 |
+
.replace(/[đĐ]/g, 'd');
|
| 14 |
+
|
| 15 |
+
const q = norm(query);
|
| 16 |
+
let budget = 0;
|
| 17 |
+
let categories = [];
|
| 18 |
+
let constraints = [];
|
| 19 |
+
let combos = [];
|
| 20 |
+
|
| 21 |
+
// Extract budget constraint (triệu/nghìn/k/đồng)
|
| 22 |
+
const budgetPatterns = [
|
| 23 |
+
/(dưới|duoi)\s*([\d,.]+)\s*(triệu|nghin|nghìn|k|đồng)/gi,
|
| 24 |
+
/(tối đa)\s*([\d,.]+)\s*(triệu|nghin|nghìn|k|đồng)/gi,
|
| 25 |
+
/([\d,.]+)\s*(triệu|nghin|nghìn|k|đồng)/gi
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
for (const pattern of budgetPatterns) {
|
| 29 |
+
const match = query.replace(/[đĐ]/g, 'd').match(pattern);
|
| 30 |
+
if (match) {
|
| 31 |
+
const amt = parseFloat(match[0].replace(/[^\d,.]/g, '').replace(',', '.'));
|
| 32 |
+
const unit = match[0].toLowerCase().match(/(triệu|nghin|nghìn|k|đồng)/g);
|
| 33 |
+
if (unit) {
|
| 34 |
+
if (unit[0] === 'triệu') budget = amt * 1000000;
|
| 35 |
+
else if (unit[0] === 'nghìn' || unit[0] === 'nghin' || unit[0] === 'k') budget = amt * 1000;
|
| 36 |
+
else budget = amt;
|
| 37 |
+
}
|
| 38 |
+
constraints.push({ label: 'Ngân sách: ' + match[0].trim() });
|
| 39 |
+
break;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Category keywords
|
| 44 |
+
const catMap = {
|
| 45 |
+
'bep': 'Bếp',
|
| 46 |
+
'bep-tu': 'Bếp từ',
|
| 47 |
+
'bep-gas': 'Bếp gas',
|
| 48 |
+
'hut-mui': 'Máy hút mùi',
|
| 49 |
+
'hut-khoi': 'Máy hút khói',
|
| 50 |
+
'chau-rua': 'Chậu rửa',
|
| 51 |
+
'voi-rua': 'Vòi rửa',
|
| 52 |
+
'lo-nuong': 'Lò nướng',
|
| 53 |
+
'tu-lanh': 'Tủ lạnh',
|
| 54 |
+
'may-rua-chen': 'Máy rửa chén',
|
| 55 |
+
'may-lam-sach': 'Máy làm sạch',
|
| 56 |
+
'gia-bo': 'Kệ bếp',
|
| 57 |
+
'gia-dung': 'Gia đụng'
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
Object.keys(catMap).forEach(key => {
|
| 61 |
+
if (q.includes(key) || query.toLowerCase().includes(key)) {
|
| 62 |
+
categories.push(catMap[key]);
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
// Search logic with scoring
|
| 67 |
+
let results = [];
|
| 68 |
+
for (let i = 0; i < products.length && results.length < 30; i++) {
|
| 69 |
+
const p = products[i];
|
| 70 |
+
let score = 0;
|
| 71 |
+
const name = norm(p.name || '');
|
| 72 |
+
const sku = norm(p.sku || p.mod || '');
|
| 73 |
+
const brand = norm(p.brand || '');
|
| 74 |
+
const cat = norm(p.cat || '');
|
| 75 |
+
const summary = norm(p.summary || '');
|
| 76 |
+
|
| 77 |
+
// Exact/partial matches
|
| 78 |
+
if (name.includes(q)) score += 10;
|
| 79 |
+
if (sku.includes(q)) score += 8;
|
| 80 |
+
if (brand.includes(q)) score += 6;
|
| 81 |
+
if (cat.includes(q)) score += 4;
|
| 82 |
+
if (summary.includes(q)) score += 3;
|
| 83 |
+
|
| 84 |
+
// Budget filter
|
| 85 |
+
if (budget > 0 && p.priceNum && p.priceNum > budget) {
|
| 86 |
+
score = 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Word-by-word matching
|
| 90 |
+
const words = q.split(/\s+/).filter(w => w.length > 1);
|
| 91 |
+
words.forEach(w => {
|
| 92 |
+
if (name.includes(w)) score += 2;
|
| 93 |
+
if (sku.includes(w)) score += 1;
|
| 94 |
+
if (brand.includes(w)) score += 1;
|
| 95 |
+
if (cat.includes(w)) score += 1;
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
if (score > 0) {
|
| 99 |
+
const labels = [];
|
| 100 |
+
if (p.priceNum && budget > 0 && p.priceNum <= budget) {
|
| 101 |
+
labels.push('Trong ngân sách');
|
| 102 |
+
}
|
| 103 |
+
results.push({ p, idx: i, score, labels });
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Sort by score desc
|
| 108 |
+
results.sort((a, b) => b.score - a.score);
|
| 109 |
+
|
| 110 |
+
// Generate combos based on budget
|
| 111 |
+
if (budget > 0 && results.length >= 2) {
|
| 112 |
+
let comboTotal = 0;
|
| 113 |
+
let comboItems = [];
|
| 114 |
+
for (let i = 0; i < results.length && comboTotal <= budget; i++) {
|
| 115 |
+
comboItems.push({ p: results[i].p, idx: results[i].idx });
|
| 116 |
+
}
|
| 117 |
+
if (comboItems.length >= 2) {
|
| 118 |
+
combos.push({
|
| 119 |
+
items: comboItems,
|
| 120 |
+
total: comboItems.reduce((s, it) => s + (it.p.priceNum || 0), 0)
|
| 121 |
+
});
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Generate AI response
|
| 126 |
+
let aiAnswer = '';
|
| 127 |
+
if (results.length > 0) {
|
| 128 |
+
aiAnswer = `Tìm thấy ${results.length} sản phẩm phù hợp.`;
|
| 129 |
+
if (categories.length) aiAnswer += ` Danh mục: ${categories.join(', ')}.`;
|
| 130 |
+
if (budget > 0) aiAnswer += ` Ngân sách tối đa: ${(budget/1000000).toFixed(1)} triệu.`;
|
| 131 |
+
} else {
|
| 132 |
+
aiAnswer = 'Không tìm thấy sản phẩm nào. Vui lòng thử từ khóa khác như: "bếp từ", "máy hút mùi", "Malloca", "Eurogold".';
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return { results, aiAnswer, categories, constraints, budget, combos };
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
console.log('AI Search loaded');
|
aistudio2.js
ADDED
|
File without changes
|
auto-excel-fix-loader.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Auto Excel Fix Loader v2.0
|
| 3 |
-
* Tự động tải fix script nếu chưa có, inject vào cuối body
|
| 4 |
-
*
|
| 5 |
-
* CẬP NHẬT: Hỗ trợ cả fix v2.0 - multi-download hoàn hảo
|
| 6 |
-
*/
|
| 7 |
-
(function() {
|
| 8 |
-
'use strict';
|
| 9 |
-
|
| 10 |
-
// Kiểm tra xem file fix đã tồn tại chưa - ưu tiên v2
|
| 11 |
-
var fixFile = 'vai-excel-fix-v2.js';
|
| 12 |
-
var fixFileLegacy = 'vai-quote-excel-order-fix-v1050.js';
|
| 13 |
-
|
| 14 |
-
// Kiểm tra script đã load chưa
|
| 15 |
-
function checkFixLoaded() {
|
| 16 |
-
return (typeof window.VAI_EXCEL_FIX !== 'undefined') ||
|
| 17 |
-
(typeof window.VAI_FIX !== 'undefined') ||
|
| 18 |
-
document.querySelector('script[src*="vai-excel-fix"]');
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
// Inject script tag
|
| 22 |
-
function injectFix() {
|
| 23 |
-
if (checkFixLoaded()) {
|
| 24 |
-
console.log('[Auto Loader] Fix script already loaded');
|
| 25 |
-
return;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
var script = document.createElement('script');
|
| 29 |
-
script.src = '/' + fixFile + '?v=' + Date.now();
|
| 30 |
-
script.onload = function() {
|
| 31 |
-
console.log('[Auto Loader] Excel fix v2 loaded successfully - multi-download enabled');
|
| 32 |
-
};
|
| 33 |
-
script.onerror = function() {
|
| 34 |
-
console.warn('[Auto Loader] v2 failed, trying legacy fix');
|
| 35 |
-
var legacy = document.createElement('script');
|
| 36 |
-
legacy.src = '/' + fixFileLegacy + '?v=' + Date.now();
|
| 37 |
-
legacy.onload = function() {
|
| 38 |
-
console.log('[Auto Loader] Legacy fix loaded');
|
| 39 |
-
};
|
| 40 |
-
document.head.appendChild(legacy);
|
| 41 |
-
};
|
| 42 |
-
|
| 43 |
-
document.head.appendChild(script);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
// Inline fix fallback (backup)
|
| 47 |
-
function applyInlineFix() {
|
| 48 |
-
var originalRevoke = URL.revokeObjectURL;
|
| 49 |
-
URL.revokeObjectURL = function(url) {
|
| 50 |
-
if (!url || typeof url !== 'string') return originalRevoke.apply(this, arguments);
|
| 51 |
-
if (url.startsWith('blob:')) {
|
| 52 |
-
// Register but don't revoke immediately - blob persists until unload
|
| 53 |
-
window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], isUnloading: false };
|
| 54 |
-
window.__VAI_BLOB_REGISTRY.blobs.push({ url: url, revoked: false });
|
| 55 |
-
window.addEventListener('beforeunload', function() {
|
| 56 |
-
window.__VAI_BLOB_REGISTRY.isUnloading = true;
|
| 57 |
-
window.__VAI_BLOB_REGISTRY.blobs.forEach(function(b) {
|
| 58 |
-
if (!b.revoked) try { URL.revokeObjectURL(b.url); b.revoked = true; } catch(e) {}
|
| 59 |
-
});
|
| 60 |
-
});
|
| 61 |
-
return;
|
| 62 |
-
}
|
| 63 |
-
return originalRevoke.apply(this, arguments);
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
console.log('[Auto Loader] Inline fallback applied');
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
// Chờ DOM ready
|
| 70 |
-
if (document.readyState === 'loading') {
|
| 71 |
-
document.addEventListener('DOMContentLoaded', injectFix);
|
| 72 |
-
} else {
|
| 73 |
-
injectFix();
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
// Thử lại sau 2s nếu chưa load
|
| 77 |
-
setTimeout(injectFix, 2000);
|
| 78 |
-
setTimeout(injectFix, 5000);
|
| 79 |
-
|
| 80 |
-
})();
|
| 81 |
-
|
| 82 |
-
// Legacy support for older fix loaders
|
| 83 |
-
window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], registry: [], isUnloading: false };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto-inject-excel-fix.js
DELETED
|
@@ -1,265 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Auto Inject Excel Fix
|
| 3 |
-
* Script này tự động chạy khi trang load
|
| 4 |
-
* KHÔNG CẦN BOOKMARKLET - TỰ ĐỘNG HOẠT ĐỘNG
|
| 5 |
-
*/
|
| 6 |
-
(function(){
|
| 7 |
-
'use strict';
|
| 8 |
-
|
| 9 |
-
// ===== AUTO INJECT CONFIG =====
|
| 10 |
-
var AUTO_FIX_VERSION = 'v2.0';
|
| 11 |
-
var SCRIPT_INJECTED = false;
|
| 12 |
-
|
| 13 |
-
// ===== GLOBAL BLOB REGISTRY =====
|
| 14 |
-
window.__VAI_BLOB = window.__VAI_BLOB || { blobs: [], isUnloading: false };
|
| 15 |
-
var REG = window.__VAI_BLOB;
|
| 16 |
-
|
| 17 |
-
// ===== CLEANUP ON UNLOAD =====
|
| 18 |
-
function cleanupBlobs() {
|
| 19 |
-
REG.isUnloading = true;
|
| 20 |
-
REG.blobs.forEach(function(e) {
|
| 21 |
-
e.revoked || (URL.revokeObjectURL(e.url), e.revoked = true);
|
| 22 |
-
});
|
| 23 |
-
REG.blobs = [];
|
| 24 |
-
}
|
| 25 |
-
window.addEventListener('pagehide', cleanupBlobs);
|
| 26 |
-
window.addEventListener('beforeunload', cleanupBlobs);
|
| 27 |
-
window.addEventListener('unload', cleanupBlobs);
|
| 28 |
-
|
| 29 |
-
// ===== PATCH URL.revokeObjectURL - CRITICAL FIX =====
|
| 30 |
-
if (!window.__VAI_REVOKE_PATCHED) {
|
| 31 |
-
var _orig = URL.revokeObjectURL;
|
| 32 |
-
URL.revokeObjectURL = function(url) {
|
| 33 |
-
if (REG.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
|
| 34 |
-
return _orig.call(this, url);
|
| 35 |
-
}
|
| 36 |
-
// Register but DON'T revoke - fix for multi-download
|
| 37 |
-
var exists = REG.blobs.find(function(e) { return e.url === url; });
|
| 38 |
-
exists || REG.blobs.push({ url: url, revoked: false });
|
| 39 |
-
console.log('[Auto Inject] Blob preserved for multi-download');
|
| 40 |
-
return undefined;
|
| 41 |
-
};
|
| 42 |
-
window.__VAI_REVOKE_PATCHED = true;
|
| 43 |
-
console.log('[Auto Inject v2.0] URL.revokeObjectURL patched');
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
// ===== UTILITIES =====
|
| 47 |
-
function fmt(n) { return n && !isNaN(n) ? Number(n).toLocaleString('vi-VN') + 'đ' : '0đ'; }
|
| 48 |
-
function stripDiacritics(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D'); }
|
| 49 |
-
function genOrderCode(name) {
|
| 50 |
-
var d = new Date();
|
| 51 |
-
var dd = String(d.getDate()).padStart(2, '0');
|
| 52 |
-
var mm = String(d.getMonth() + 1).padStart(2, '0');
|
| 53 |
-
var yy = String(d.getFullYear()).slice(-2);
|
| 54 |
-
var ini = '';
|
| 55 |
-
if (name && name.trim()) {
|
| 56 |
-
name.trim().split(/\s+/).forEach(function(w) {
|
| 57 |
-
w && /[A-Z]/.test(stripDiacritics(w.charAt(0))) && (ini += stripDiacritics(w.charAt(0)).toUpperCase());
|
| 58 |
-
});
|
| 59 |
-
}
|
| 60 |
-
return 'VAS' + (ini || 'X') + dd + mm + yy;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
// ===== BUTTON STATE =====
|
| 64 |
-
function setBtnState(btn, exporting) {
|
| 65 |
-
if (!btn) return;
|
| 66 |
-
if (exporting) {
|
| 67 |
-
btn.disabled = true;
|
| 68 |
-
btn.dataset.vaiExporting = '1';
|
| 69 |
-
btn._orig = btn.innerHTML;
|
| 70 |
-
btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
|
| 71 |
-
} else {
|
| 72 |
-
btn.disabled = false;
|
| 73 |
-
btn.dataset.vaiExporting = '0';
|
| 74 |
-
btn._orig && (btn.innerHTML = btn._orig);
|
| 75 |
-
}
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
// ===== DOWNLOAD BLOB =====
|
| 79 |
-
function downloadBlob(blob, filename) {
|
| 80 |
-
var url = URL.createObjectURL(blob);
|
| 81 |
-
REG.blobs.push({ url: url, revoked: false });
|
| 82 |
-
var a = document.createElement('a');
|
| 83 |
-
a.href = url;
|
| 84 |
-
a.download = filename;
|
| 85 |
-
a.style.cssText = 'position:absolute;left:-9999px;top:-9999px';
|
| 86 |
-
document.body.appendChild(a);
|
| 87 |
-
a.click();
|
| 88 |
-
setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 100);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
// ===== GET QUOTE DATA =====
|
| 92 |
-
function getQuoteData() {
|
| 93 |
-
var data = {
|
| 94 |
-
customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') },
|
| 95 |
-
items: [],
|
| 96 |
-
grandTotal: 0
|
| 97 |
-
};
|
| 98 |
-
var cart = window.cart || [];
|
| 99 |
-
var products = window.D || [];
|
| 100 |
-
cart.forEach(function(item, i) {
|
| 101 |
-
var prod = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null);
|
| 102 |
-
var price = item.priceNum || item.price || (prod ? prod.price : 0) || 0;
|
| 103 |
-
data.items.push({
|
| 104 |
-
stt: i + 1,
|
| 105 |
-
image: prod ? prod.img : '',
|
| 106 |
-
name: prod ? (prod.name || prod.n || '') : (item.name || ''),
|
| 107 |
-
model: prod ? (prod.sku || prod.mod || '') : (item.sku || item.model || ''),
|
| 108 |
-
specs: prod ? (prod.info || prod.desc || '') : '',
|
| 109 |
-
qty: item.qty || 1,
|
| 110 |
-
price: price,
|
| 111 |
-
discPrice: price,
|
| 112 |
-
total: (item.qty || 1) * price,
|
| 113 |
-
note: ''
|
| 114 |
-
});
|
| 115 |
-
});
|
| 116 |
-
// Get customer info
|
| 117 |
-
var nameInputs = ['#quoteName', '#quoteCustomerName', '#checkoutName'];
|
| 118 |
-
for (var j = 0; j < nameInputs.length; j++) {
|
| 119 |
-
var el = document.querySelector(nameInputs[j]);
|
| 120 |
-
if (el && el.value && el.value.trim()) { data.customer.name = el.value.trim(); break; }
|
| 121 |
-
}
|
| 122 |
-
var ph = document.querySelector('#quotePhone, #checkoutPhone'); ph && ph.value && (data.customer.phone = ph.value.trim());
|
| 123 |
-
var em = document.querySelector('#quoteEmail'); em && em.value && (data.customer.email = em.value.trim());
|
| 124 |
-
var ad = document.querySelector('#quoteAddr, #checkoutAddr'); ad && ad.value && (data.customer.addr = ad.value.trim());
|
| 125 |
-
data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0);
|
| 126 |
-
return data;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
// ===== EXPORT FUNCTIONS =====
|
| 130 |
-
async function exportQuoteExcel(quoteData, code) {
|
| 131 |
-
if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
|
| 132 |
-
try {
|
| 133 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
|
| 134 |
-
ws.views = [{ showGridLines: false }];
|
| 135 |
-
ws.columns = [{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];
|
| 136 |
-
ws.mergeCells('A3:J3'); ws.getCell('A3').value = 'BẢNG BÁO GIÁ';
|
| 137 |
-
ws.getCell('A3').font = { bold:true, size:18, color:{argb:'FFDB9815'} }; ws.getCell('A3').alignment = { horizontal:'center' };
|
| 138 |
-
ws.getCell('A5').value = 'Khách hàng:'; ws.mergeCells('B5:E5'); ws.getCell('B5').value = quoteData.customer.name;
|
| 139 |
-
ws.getCell('H5').value = 'Mã đơn:'; ws.mergeCells('I5:J5'); ws.getCell('I5').value = code;
|
| 140 |
-
ws.getCell('A6').value = 'SĐT:'; ws.mergeCells('B6:E6'); ws.getCell('B6').value = quoteData.customer.phone;
|
| 141 |
-
ws.getCell('H6').value = 'Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value = quoteData.customer.date;
|
| 142 |
-
ws.getCell('A7').value = 'Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value = quoteData.customer.email;
|
| 143 |
-
ws.getCell('A8').value = 'Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value = quoteData.customer.addr;
|
| 144 |
-
var hr = ws.getRow(10);
|
| 145 |
-
['STT','Hình','Tên sản phẩm','Mã SP','Thông tin','SL','Đơn giá','Giá CK','Thành tiền','Ghi chú'].forEach(function(h, i) {
|
| 146 |
-
var c = hr.getCell(i+1); c.value = h; c.font = { bold:true, color:{argb:'FFFFFFFF'} }; c.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'} };
|
| 147 |
-
});
|
| 148 |
-
var cr = 11, firstRow = cr;
|
| 149 |
-
quoteData.items.forEach(function(it, i) {
|
| 150 |
-
var row = ws.getRow(cr); row.height = 50;
|
| 151 |
-
row.getCell(1).value = it.stt || i+1;
|
| 152 |
-
row.getCell(3).value = it.name || ''; row.getCell(3).font = { bold:true, size:9 };
|
| 153 |
-
row.getCell(4).value = it.model || '';
|
| 154 |
-
row.getCell(5).value = it.specs || ''; row.getCell(5).font = { size:8, color:{argb:'FF64748B'} };
|
| 155 |
-
row.getCell(6).value = Number(it.qty||1); row.getCell(6).numFmt = '#,##0';
|
| 156 |
-
row.getCell(7).value = Number(it.price||0); row.getCell(7).numFmt = '#,##0"đ"';
|
| 157 |
-
row.getCell(8).value = Number(it.discPrice||it.price||0); row.getCell(8).numFmt = '#,##0"đ"';
|
| 158 |
-
row.getCell(8).font = (it.price||0) > (it.discPrice||0) ? { bold:true, color:{argb:'FFDC3545'} } : {};
|
| 159 |
-
row.getCell(9).value = Number(it.total||0); row.getCell(9).numFmt = '#,##0"đ"'; row.getCell(9).font = { bold:true };
|
| 160 |
-
row.getCell(10).value = it.note || '';
|
| 161 |
-
cr++;
|
| 162 |
-
});
|
| 163 |
-
ws.getRow(cr).height = 28; ws.mergeCells(cr,1,cr,5); ws.getCell('A'+cr).value = 'TỔNG CỘNG';
|
| 164 |
-
ws.getCell('A'+cr).font = { bold:true, size:13, color:{argb:'FFFFFFFF'} }; ws.getCell('A'+cr).fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'} };
|
| 165 |
-
ws.getCell('E'+cr).value = { formula:'SUM(I'+firstRow+':I'+(cr-1)+')', result:Number(quoteData.grandTotal||0) };
|
| 166 |
-
ws.getCell('E'+cr).numFmt = '#,##0"đ"'; ws.getCell('E'+cr).font = { bold:true, size:14, color:{argb:'FFF0B840'} };
|
| 167 |
-
wb.calcProperties.fullCalcOnLoad = true;
|
| 168 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 169 |
-
downloadBlob(new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), code + '.xlsx');
|
| 170 |
-
return true;
|
| 171 |
-
} catch (e) {
|
| 172 |
-
alert('❌ Lỗi: ' + e.message);
|
| 173 |
-
console.error('[Auto Inject] exportQuoteExcel error:', e);
|
| 174 |
-
return false;
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
async function exportDeliveryExcel(quoteData, code) {
|
| 179 |
-
if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
|
| 180 |
-
try {
|
| 181 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Giao hàng');
|
| 182 |
-
ws.views = [{ showGridLines: false }]; ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}];
|
| 183 |
-
ws.mergeCells('A1:E1'); ws.getCell('A1').value = 'PHIẾU GIAO HÀNG'; ws.getCell('A1').font = { bold:true, size:16, color:{argb:'FF003F62'} }; ws.getCell('A1').alignment = { horizontal:'center' };
|
| 184 |
-
ws.getCell('A2').value = 'KH: ' + (quoteData.customer.name||''); ws.getCell('D2').value = 'Mã: ' + code;
|
| 185 |
-
ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer.phone||''); ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer.date||'');
|
| 186 |
-
ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.customer.addr||'');
|
| 187 |
-
var hr = ws.getRow(6); ['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h, i) {
|
| 188 |
-
var c = hr.getCell(i+1); c.value = h; c.font = { bold:true, color:{argb:'FFFFFFFF'} }; c.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'} };
|
| 189 |
-
});
|
| 190 |
-
var cr = 7, totalQty = 0;
|
| 191 |
-
quoteData.items.forEach(function(it, i) {
|
| 192 |
-
var row = ws.getRow(cr); row.height = 20;
|
| 193 |
-
row.getCell(1).value = it.stt || i+1; row.getCell(2).value = it.name || ''; row.getCell(3).value = it.model || ''; row.getCell(4).value = it.qty || 1; row.getCell(5).value = it.note || '';
|
| 194 |
-
totalQty += Number(it.qty||1); cr++;
|
| 195 |
-
});
|
| 196 |
-
ws.mergeCells(cr,1,cr,3); ws.getCell('A'+cr).value = 'TỔNG CỘNG SỐ LƯỢNG'; ws.getCell('D'+cr).value = totalQty;
|
| 197 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 198 |
-
downloadBlob(new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), 'GH-' + code + '.xlsx');
|
| 199 |
-
return true;
|
| 200 |
-
} catch (e) {
|
| 201 |
-
alert('❌ Lỗi: ' + e.message);
|
| 202 |
-
console.error('[Auto Inject] exportDeliveryExcel error:', e);
|
| 203 |
-
return false;
|
| 204 |
-
}
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
// ===== CLICK HANDLER =====
|
| 208 |
-
function handleClick(e) {
|
| 209 |
-
var btn = e.target.closest('button, a');
|
| 210 |
-
if (!btn) return;
|
| 211 |
-
var txt = (btn.textContent || '').toLowerCase();
|
| 212 |
-
// Quote Excel
|
| 213 |
-
if (txt.includes('excel') && !txt.includes('giao') && !txt.includes('gh-')) {
|
| 214 |
-
if (btn.dataset.vaiExporting === '1') return;
|
| 215 |
-
e.preventDefault(); e.stopPropagation();
|
| 216 |
-
setBtnState(btn, true);
|
| 217 |
-
var qd = getQuoteData(); var code = genOrderCode(qd.customer.name);
|
| 218 |
-
exportQuoteExcel(qd, code).then(function() {
|
| 219 |
-
setBtnState(btn, false);
|
| 220 |
-
showToast && showToast('✅ Excel báo giá tải thành công!');
|
| 221 |
-
});
|
| 222 |
-
return false;
|
| 223 |
-
}
|
| 224 |
-
// Delivery Excel
|
| 225 |
-
if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) {
|
| 226 |
-
if (btn.dataset.vaiExporting === '1') return;
|
| 227 |
-
e.preventDefault(); e.stopPropagation();
|
| 228 |
-
setBtnState(btn, true);
|
| 229 |
-
var order = window.VAI_CURRENT_ORDER || {};
|
| 230 |
-
var ed = {
|
| 231 |
-
customer: { name: order.customer||'', phone: order.phone||'', addr: order.addr||'', date: order.date||'' },
|
| 232 |
-
items: (order.items||[]).map(function(it, i) { return { stt:i+1, name:it.name||'', model:it.model||'', qty:it.qty||1, note:it.note||'', specs:it.specs||'', price:it.price||0, discPrice:it.discPrice||it.price||0, total:it.total||0 }; }),
|
| 233 |
-
grandTotal: order.grandTotal||0
|
| 234 |
-
};
|
| 235 |
-
var ocode = order.code || genOrderCode(order.customer||'');
|
| 236 |
-
exportDeliveryExcel(ed, ocode).then(function() {
|
| 237 |
-
setBtnState(btn, false);
|
| 238 |
-
showToast && showToast('✅ Excel giao hàng tải thành công!');
|
| 239 |
-
});
|
| 240 |
-
return false;
|
| 241 |
-
}
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
// ===== INIT =====
|
| 245 |
-
function init() {
|
| 246 |
-
if (SCRIPT_INJECTED) return;
|
| 247 |
-
document.removeEventListener('click', handleClick, true);
|
| 248 |
-
document.addEventListener('click', handleClick, true);
|
| 249 |
-
SCRIPT_INJECTED = true;
|
| 250 |
-
console.log('[Auto Inject v2.0] Excel fix active - multi-download enabled');
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
// Run immediately
|
| 254 |
-
if (document.readyState === 'loading') {
|
| 255 |
-
document.addEventListener('DOMContentLoaded', init);
|
| 256 |
-
} else {
|
| 257 |
-
init();
|
| 258 |
-
}
|
| 259 |
-
setTimeout(init, 2000);
|
| 260 |
-
setTimeout(init, 5000);
|
| 261 |
-
|
| 262 |
-
window.VAI_AUTO_FIX = { init: init, exportQuoteExcel: exportQuoteExcel, exportDeliveryExcel: exportDeliveryExcel };
|
| 263 |
-
})();
|
| 264 |
-
|
| 265 |
-
console.log('[VAI] Auto Excel Fix v2.0 loaded!');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto-patch-gh-excel.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Auto Patch GH Excel - v2.0
|
| 3 |
-
*
|
| 4 |
-
* TỰ ĐỘNG PATCH mà không cần sửa index.html
|
| 5 |
-
* Chỉ cần thêm dòng: <script src="auto-patch-gh-excel.js"></script> vào index.html
|
| 6 |
-
*
|
| 7 |
-
* FIX HOÀN HẢO:
|
| 8 |
-
* - exportDeliveryExcel nhận order parameter đúng
|
| 9 |
-
* - Click protection (chống bấm nhiều lần)
|
| 10 |
-
* - Blob retention đến khi unload (tải nhiều lần được)
|
| 11 |
-
* - Hỗ trợ cả Quote & Order modal
|
| 12 |
-
*/
|
| 13 |
-
(function() {
|
| 14 |
-
'use strict';
|
| 15 |
-
|
| 16 |
-
// ===== GLOBAL BLOB REGISTRY =====
|
| 17 |
-
window.__VAI_BLOB_REGISTRY = window.__VAI_BLOB_REGISTRY || { blobs: [], isUnloading: false };
|
| 18 |
-
var reg = window.__VAI_BLOB_REGISTRY;
|
| 19 |
-
|
| 20 |
-
// Cleanup on unload
|
| 21 |
-
function cleanupBlobs() {
|
| 22 |
-
reg.isUnloading = true;
|
| 23 |
-
reg.blobs.forEach(function(b) {
|
| 24 |
-
if (!b.revoked) {
|
| 25 |
-
try { URL.revokeObjectURL(b.url); b.revoked = true; } catch(e) {}
|
| 26 |
-
}
|
| 27 |
-
});
|
| 28 |
-
reg.blobs = [];
|
| 29 |
-
}
|
| 30 |
-
window.addEventListener('pagehide', cleanupBlobs);
|
| 31 |
-
window.addEventListener('beforeunload', cleanupBlobs);
|
| 32 |
-
window.addEventListener('unload', cleanupBlobs);
|
| 33 |
-
|
| 34 |
-
// ===== PATCH URL.revokeObjectURL - CRITICAL =====
|
| 35 |
-
if (!window.__VAI_REVOKE_PATCHED_AUTO) {
|
| 36 |
-
var origRevoke = URL.revokeObjectURL;
|
| 37 |
-
URL.revokeObjectURL = function(url) {
|
| 38 |
-
if (reg.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
|
| 39 |
-
return origRevoke.call(this, url);
|
| 40 |
-
}
|
| 41 |
-
var exists = reg.blobs.find(function(b) { return b.url === url; });
|
| 42 |
-
if (!exists) reg.blobs.push({ url: url, revoked: false, created: Date.now() });
|
| 43 |
-
return undefined;
|
| 44 |
-
};
|
| 45 |
-
window.__VAI_REVOKE_PATCHED_AUTO = true;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
// ===== UTILITIES =====
|
| 49 |
-
function fmt(n) { return n && !isNaN(n) ? Number(n).toLocaleString('vi-VN') + 'đ' : '0đ'; }
|
| 50 |
-
function stripDiacritics(s) { return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'D'); }
|
| 51 |
-
function genCode(name) {
|
| 52 |
-
var d = new Date(), dd = String(d.getDate()).padStart(2,'0'), mm = String(d.getMonth()+1).padStart(2,'0'), yy = String(d.getFullYear()).slice(-2);
|
| 53 |
-
var ini = '';
|
| 54 |
-
if (name && name.trim()) {
|
| 55 |
-
name.trim().split(/\s+/).forEach(function(w) {
|
| 56 |
-
if (w) { var c = stripDiacritics(w.charAt(0)).toUpperCase(); if (/[A-Z]/.test(c)) ini += c; }
|
| 57 |
-
});
|
| 58 |
-
}
|
| 59 |
-
return 'VAS' + (ini || 'X') + dd + mm + yy;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
// ===== BUTTON STATE =====
|
| 63 |
-
function setBtn(b, exporting) {
|
| 64 |
-
if (!b) return;
|
| 65 |
-
if (exporting) {
|
| 66 |
-
b.disabled = true;
|
| 67 |
-
b.dataset.vaiExporting = '1';
|
| 68 |
-
b._orig = b.innerHTML;
|
| 69 |
-
b.innerHTML = '<span style="opacity:0.7">⏳ Đang xuất...</span>';
|
| 70 |
-
} else {
|
| 71 |
-
b.disabled = false;
|
| 72 |
-
b.dataset.vaiExporting = '0';
|
| 73 |
-
if (b._orig) b.innerHTML = b._orig;
|
| 74 |
-
}
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
// ===== DOWNLOAD BLOB =====
|
| 78 |
-
function dlBlob(blob, filename) {
|
| 79 |
-
var url = URL.createObjectURL(blob);
|
| 80 |
-
reg.blobs.push({ url: url, revoked: false });
|
| 81 |
-
var a = document.createElement('a');
|
| 82 |
-
a.href = url; a.download = filename; a.style.cssText = 'position:absolute;left:-9999px';
|
| 83 |
-
document.body.appendChild(a); a.click();
|
| 84 |
-
setTimeout(function() { try { document.body.removeChild(a); } catch(e) {} }, 100);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
// ===== EXPORT QUOTE EXCEL =====
|
| 88 |
-
async function exportQuoteExcel(d, c) {
|
| 89 |
-
if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
|
| 90 |
-
try {
|
| 91 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
|
| 92 |
-
ws.views = [{ showGridLines: false }];
|
| 93 |
-
ws.columns = [{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];
|
| 94 |
-
ws.mergeCells('A3:J3'); ws.getCell('A3').value='BẢNG BÁO GIÁ';
|
| 95 |
-
ws.getCell('A3').font = { bold:true, size:18, color:{argb:'FFDB9815'} };
|
| 96 |
-
ws.getCell('A3').alignment = { horizontal:'center' };
|
| 97 |
-
ws.getCell('A5').value='Khách hàng:'; ws.mergeCells('B5:E5'); ws.getCell('B5').value=d.customer?.name||'';
|
| 98 |
-
ws.getCell('H5').value='Mã đơn:'; ws.mergeCells('I5:J5'); ws.getCell('I5').value=c;
|
| 99 |
-
ws.getCell('A6').value='SĐT:'; ws.mergeCells('B6:E6'); ws.getCell('B6').value=d.customer?.phone||'';
|
| 100 |
-
ws.getCell('H6').value='Ngày:'; ws.mergeCells('I6:J6'); ws.getCell('I6').value=d.customer?.date||'';
|
| 101 |
-
ws.getCell('A7').value='Email:'; ws.mergeCells('B7:E7'); ws.getCell('B7').value=d.customer?.email||'';
|
| 102 |
-
ws.getCell('A8').value='Địa chỉ:'; ws.mergeCells('B8:J8'); ws.getCell('B8').value=d.customer?.addr||'';
|
| 103 |
-
|
| 104 |
-
var hr = ws.getRow(10);
|
| 105 |
-
['STT','Hình','Tên sản phẩm','Mã SP','Thông tin','SL','Đơn giá','Giá CK','Thành tiền','Ghi chú'].forEach(function(h,i){
|
| 106 |
-
var c = hr.getCell(i+1); c.value=h; c.font={bold:true,color:{argb:'FFFFFFFF'}}; c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};
|
| 107 |
-
});
|
| 108 |
-
|
| 109 |
-
var cr = 11, fr = cr;
|
| 110 |
-
(d.items||[]).forEach(function(it,i){
|
| 111 |
-
var r = ws.getRow(cr), bg = i%2===0?'FFF8FAFC':'FFFFFFFF'; r.height=50;
|
| 112 |
-
r.getCell(1).value=it.stt||i+1; r.getCell(3).value=it.name||''; r.getCell(3).font={bold:true,size:9};
|
| 113 |
-
r.getCell(4).value=it.model||''; r.getCell(5).value=it.specs||''; r.getCell(5).font={size:8,color:{argb:'FF64748B'}};
|
| 114 |
-
r.getCell(6).value=Number(it.qty||1); r.getCell(6).numFmt='#,##0';
|
| 115 |
-
r.getCell(7).value=Number(it.price||0); r.getCell(7).numFmt='#,##0"đ"';
|
| 116 |
-
r.getCell(8).value=Number(it.discPrice||it.price||0); r.getCell(8).numFmt='#,##0"đ"';
|
| 117 |
-
r.getCell(8).font=(it.price||0)>(it.discPrice||0)?{bold:true,color:{argb:'FFDC3545'}}:{};
|
| 118 |
-
r.getCell(9).value=Number(it.total||0); r.getCell(9).numFmt='#,##0"đ"'; r.getCell(9).font={bold:true};
|
| 119 |
-
r.getCell(10).value=it.note||''; cr++;
|
| 120 |
-
});
|
| 121 |
-
|
| 122 |
-
ws.getRow(cr).height=28; ws.mergeCells(cr,1,cr,5); ws.getCell('A'+cr).value='TỔNG CỘNG';
|
| 123 |
-
ws.getCell('A'+cr).font={bold:true,size:13,color:{argb:'FFFFFFFF'}}; ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};
|
| 124 |
-
ws.getCell('E'+cr).value={formula:'SUM(I'+fr+':I'+(cr-1)+')',result:Number(d.grandTotal||0)}; ws.getCell('E'+cr).numFmt='#,##0"đ"';
|
| 125 |
-
ws.getCell('E'+cr).font={bold:true,size:14,color:{argb:'FFF0B840'}};
|
| 126 |
-
wb.calcProperties.fullCalcOnLoad=true;
|
| 127 |
-
|
| 128 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 129 |
-
dlBlob(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), c+'.xlsx');
|
| 130 |
-
return true;
|
| 131 |
-
} catch(e) { console.error('[AutoPatch] exportQuoteExcel error:',e); alert('❌ Lỗi: '+e.message); return false; }
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
// ===== EXPORT DELIVERY EXCEL =====
|
| 135 |
-
async function exportDeliveryExcel(d, c) {
|
| 136 |
-
if (typeof ExcelJS === 'undefined') return alert('⚠️ Chưa có ExcelJS'), false;
|
| 137 |
-
try {
|
| 138 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Giao hàng');
|
| 139 |
-
ws.views = [{ showGridLines: false }]; ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}];
|
| 140 |
-
ws.mergeCells('A1:E1'); ws.getCell('A1').value='PHIẾU GIAO HÀNG'; ws.getCell('A1').font={bold:true,size:16,color:{argb:'FF003F62'}}; ws.getCell('A1').alignment={horizontal:'center'};
|
| 141 |
-
ws.getCell('A2').value='KH: '+(d.customer?.name||''); ws.getCell('D2').value='Mã: '+c;
|
| 142 |
-
ws.getCell('A3').value='SĐT: '+(d.customer?.phone||''); ws.getCell('D3').value='Ngày: '+(d.customer?.date||'');
|
| 143 |
-
ws.getCell('A4').value='Địa chỉ: '+(d.customer?.addr||'');
|
| 144 |
-
|
| 145 |
-
var hr = ws.getRow(6); ['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){
|
| 146 |
-
var c = hr.getCell(i+1); c.value=h; c.font={bold:true,color:{argb:'FFFFFFFF'}}; c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};
|
| 147 |
-
});
|
| 148 |
-
|
| 149 |
-
var cr = 7, tq = 0;
|
| 150 |
-
(d.items||[]).forEach(function(it,i){
|
| 151 |
-
var r = ws.getRow(cr); r.height=20;
|
| 152 |
-
r.getCell(1).value=it.stt||i+1; r.getCell(2).value=it.name||''; r.getCell(3).value=it.model||''; r.getCell(4).value=it.qty||1; r.getCell(5).value=it.note||'';
|
| 153 |
-
tq += Number(it.qty||1); cr++;
|
| 154 |
-
});
|
| 155 |
-
|
| 156 |
-
ws.mergeCells(cr,1,cr,3); ws.getCell('A'+cr).value='TỔNG CỘNG SỐ LƯỢNG'; ws.getCell('D'+cr).value=tq;
|
| 157 |
-
|
| 158 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 159 |
-
dlBlob(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}), 'GH-'+c+'.xlsx');
|
| 160 |
-
return true;
|
| 161 |
-
} catch(e) { console.error('[AutoPatch] exportDeliveryExcel error:',e); alert('❌ Lỗi: '+e.message); return false; }
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
// ===== INIT =====
|
| 165 |
-
function init() {
|
| 166 |
-
// Check if VAI_EXCEL_FIX v2 already loaded
|
| 167 |
-
if (typeof window.VAI_EXCEL_FIX !== 'undefined' && typeof window.VAI_EXCEL_FIX.exportQuoteExcel === 'function') {
|
| 168 |
-
console.log('[AutoPatch v2.0] VAI_EXCEL_FIX v2 already loaded - done');
|
| 169 |
-
return;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
// Load vai-excel-fix-v2.js if not exists
|
| 173 |
-
var s = document.createElement('script');
|
| 174 |
-
s.src = '/vai-excel-fix-v2.js?v=' + Date.now();
|
| 175 |
-
s.onload = function() {
|
| 176 |
-
console.log('[AutoPatch v2.0] Excel fix v2 loaded');
|
| 177 |
-
};
|
| 178 |
-
s.onerror = function() {
|
| 179 |
-
console.warn('[AutoPatch v2.0] Failed to load v2, applying inline patch');
|
| 180 |
-
// Inline fallback patch
|
| 181 |
-
if (typeof window.exportDeliveryExcel === 'function') {
|
| 182 |
-
var origDE = window.exportDeliveryExcel;
|
| 183 |
-
window.exportDeliveryExcel = async function(order, opt) {
|
| 184 |
-
var btn = document.activeElement;
|
| 185 |
-
if (btn && btn.dataset.vaiExporting === '1') return;
|
| 186 |
-
if (btn) setBtn(btn, true);
|
| 187 |
-
try {
|
| 188 |
-
var d = { customer: { name: order?.customer||'', phone: order?.phone||'', addr: order?.addr||'', date: order?.date||'' },
|
| 189 |
-
items: (order?.items||[]).map(function(it,i){return{stt:i+1,name:it.name||'',model:it.model||'',qty:it.qty||1,note:it.note||'',specs:it.specs||'',price:it.price||0,discPrice:it.discPrice||it.price||0,total:it.total||0};}),
|
| 190 |
-
grandTotal: order?.grandTotal||0 };
|
| 191 |
-
var c = order?.code || genCode(order?.customer||'');
|
| 192 |
-
await exportDeliveryExcel(d, c);
|
| 193 |
-
} finally { if (btn) setBtn(btn, false); }
|
| 194 |
-
};
|
| 195 |
-
}
|
| 196 |
-
};
|
| 197 |
-
document.head.appendChild(s);
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
if (document.readyState === 'loading') {
|
| 201 |
-
document.addEventListener('DOMContentLoaded', init);
|
| 202 |
-
} else {
|
| 203 |
-
init();
|
| 204 |
-
}
|
| 205 |
-
setTimeout(init, 2000);
|
| 206 |
-
setTimeout(init, 5000);
|
| 207 |
-
|
| 208 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug_export.html
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="vi"><head><meta charset="utf-8"/>
|
| 3 |
-
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
| 4 |
-
<title>VAI Export Debug</title>
|
| 5 |
-
<script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
|
| 6 |
-
<style>body{font-family:system-ui;padding:16px;background:#f5f7fa}textarea{width:100%;height:120px;font-size:16px;padding:10px;border:2px solid #003f62;border-radius:8px}button{margin-top:12px;width:100%;padding:14px;font-size:18px;background:#003f62;color:#fff;border:0;border-radius:10px;font-weight:700}#log{margin-top:16px;white-space:pre-wrap;background:#fff;border:1px solid #ccc;border-radius:8px;padding:12px;font-size:13px;max-height:40vh;overflow:auto}</style>
|
| 7 |
-
</head><body>
|
| 8 |
-
<h2>VAI Export Debug</h2>
|
| 9 |
-
<p>Nhập prompt rồi bấm nút. File sẽ có đủ dòng phí. Log hiển thị bên dưới.</p>
|
| 10 |
-
<textarea id="prompt">Malloca ck 35%, giao hàng 200k, lắp đặt 500k, cọc 5tr, ghi chú: giao thứ 7</textarea>
|
| 11 |
-
<button id="go">Xuất Excel Test</button>
|
| 12 |
-
<div id="log">Log sẽ hiện ở đây...</div>
|
| 13 |
-
<script>
|
| 14 |
-
function L(s){var el=document.getElementById('log');el.textContent+=("\n"+s);el.scrollTop=el.scrollHeight;console.log(s);}
|
| 15 |
-
document.getElementById('go').addEventListener('click',async function(){
|
| 16 |
-
L("ExcelJS loaded: "+(typeof ExcelJS!=='undefined'));
|
| 17 |
-
var txt=document.getElementById('prompt').value.trim();
|
| 18 |
-
L("Prompt: "+txt);
|
| 19 |
-
var fees=[];var deposit=0;var note='';
|
| 20 |
-
var m1=txt.match(/giao hàng\s*([\d.,]+)\s*(k|tr|triệu|đ|nghìn)?/i);
|
| 21 |
-
if(m1){var v=parseFloat(m1[1].replace(/[.,]/g,''));var u=(m1[2]||'').toLowerCase();if(u==='k')v*=1000;if(u==='tr'||u==='triệu')v*=1e6;fees.push({label:'Phí giao hàng',amount:v});}
|
| 22 |
-
var m2=txt.match(/lắp đặt\s*([\d.,]+)\s*(k|tr|triệu|đ|nghìn)?/i);
|
| 23 |
-
if(m2){var v=parseFloat(m2[1].replace(/[.,]/g,''));var u=(m2[2]||'').toLowerCase();if(u==='k')v*=1000;if(u==='tr'||u==='triệu')v*=1e6;fees.push({label:'Phí lắp đặt',amount:v});}
|
| 24 |
-
var m3=txt.match(/cọc\s*([\d.,]+)\s*(k|tr|triệu|đ|nghìn)?/i);
|
| 25 |
-
if(m3){var v=parseFloat(m3[1].replace(/[.,]/g,''));var u=(m3[2]||'').toLowerCase();if(u==='k')v*=1000;if(u==='tr'||u==='triệu')v*=1e6;deposit=v;}
|
| 26 |
-
var mn=txt.match(/ghi chú\s*[::]?\s*(.+)$/i);
|
| 27 |
-
if(mn)note=mn[1].trim();
|
| 28 |
-
L("Parsed fees: "+JSON.stringify(fees)+" deposit="+deposit+" note="+note);
|
| 29 |
-
if(typeof ExcelJS==='undefined'){L("ERROR: ExcelJS not loaded - check network/CDN");return;}
|
| 30 |
-
try{
|
| 31 |
-
var wb=new ExcelJS.Workbook();var ws=wb.addWorksheet('BaoGia');
|
| 32 |
-
ws.addRow(['BẢNG BÁO GIÁ']);
|
| 33 |
-
ws.addRow([]);
|
| 34 |
-
ws.addRow(['1','SP Test','MH800TC',1,7350000,4777500,4777500]);
|
| 35 |
-
fees.forEach(function(f){ws.addRow([' '+f.label, f.amount]);L("ROW: "+f.label+" "+f.amount);});
|
| 36 |
-
var total=4777500+fees.reduce(function(a,f){return a+f.amount;},0);
|
| 37 |
-
ws.addRow(['TỔNG CỘNG', total]);
|
| 38 |
-
if(deposit>0){ws.addRow(['Đã cọc',deposit]);ws.addRow(['CÒN LẠI',total-deposit]);}
|
| 39 |
-
if(note)ws.addRow(['Ghi chú: '+note]);
|
| 40 |
-
var buf=await wb.xlsx.writeBuffer();
|
| 41 |
-
var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
| 42 |
-
var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='VAI_debug.xlsx';a.click();
|
| 43 |
-
L("FILE DOWNLOADED OK. Dòng phí: "+(fees.length>0?'CÓ':'KHÔNG'));
|
| 44 |
-
}catch(e){L("EXPORT ERROR: "+e.message);}
|
| 45 |
-
});
|
| 46 |
-
</script>
|
| 47 |
-
</body></html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-download-ultimate-fix-v2.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO — Excel Download Ultimate Fix v2.0
|
| 3 |
-
* FIX TRIỆT ĐỐI: File Excel chỉ tải được 1 lần, lần 2+ thất bại
|
| 4 |
-
*
|
| 5 |
-
* GIẢI PHÁP:
|
| 6 |
-
* 1. KHÔNG revoke blob URLs ngay - cleanup khi tab đóng
|
| 7 |
-
* 2. TẠO MỚI blob hoàn toàn cho mỗi lần click
|
| 8 |
-
* 3. Tương thích mọi trình duyệt (Chrome, Safari, Firefox)
|
| 9 |
-
*/
|
| 10 |
-
(function() {
|
| 11 |
-
'use strict';
|
| 12 |
-
|
| 13 |
-
var excelBlobRegistry = [];
|
| 14 |
-
var isUnloading = false;
|
| 15 |
-
|
| 16 |
-
// Cleanup khi tab đóng
|
| 17 |
-
function cleanupBlobs() {
|
| 18 |
-
isUnloading = true;
|
| 19 |
-
excelBlobRegistry.forEach(function(e) {
|
| 20 |
-
if (!e.revoked && e.url) {
|
| 21 |
-
try { URL.revokeObjectURL(e.url); e.revoked = true; } catch (err) {}
|
| 22 |
-
}
|
| 23 |
-
});
|
| 24 |
-
excelBlobRegistry = [];
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
window.addEventListener('pagehide', cleanupBlobs);
|
| 28 |
-
window.addEventListener('beforeunload', cleanupBlobs);
|
| 29 |
-
window.addEventListener('unload', cleanupBlobs);
|
| 30 |
-
|
| 31 |
-
// PATCH URL.revokeObjectURL - không revoke ngay
|
| 32 |
-
(function() {
|
| 33 |
-
var origRevoke = URL.revokeObjectURL;
|
| 34 |
-
URL.revokeObjectURL = function(url) {
|
| 35 |
-
if (isUnloading || !url || !url.startsWith('blob:')) {
|
| 36 |
-
return origRevoke.call(this, url);
|
| 37 |
-
}
|
| 38 |
-
var exists = excelBlobRegistry.find(function(e) { return e.url === url; });
|
| 39 |
-
if (!exists) {
|
| 40 |
-
excelBlobRegistry.push({ url: url, revoked: false, created: Date.now() });
|
| 41 |
-
}
|
| 42 |
-
};
|
| 43 |
-
})();
|
| 44 |
-
|
| 45 |
-
// PATCH exportExcel (Báo giá)
|
| 46 |
-
var origExportExcel = window.exportExcel;
|
| 47 |
-
window.exportExcel = async function() {
|
| 48 |
-
if (typeof ExcelJS === 'undefined') return;
|
| 49 |
-
var d, qd, code;
|
| 50 |
-
if (window.VAI_QR && window.VAI_QR.getData) {
|
| 51 |
-
d = window.VAI_QR.getData();
|
| 52 |
-
qd = d.qd;
|
| 53 |
-
code = window.VAI_QR.getEffectiveOrderCode();
|
| 54 |
-
} else if (window._origGetQuoteData) {
|
| 55 |
-
qd = window._origGetQuoteData();
|
| 56 |
-
code = (window.VAI_QR && window.VAI_QR.getEffectiveOrderCode) ? window.VAI_QR.getEffectiveOrderCode() : 'VAS';
|
| 57 |
-
} else return;
|
| 58 |
-
|
| 59 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
|
| 60 |
-
ws.views = [{showGridLines:false}];
|
| 61 |
-
ws.columns = [{width:5},{width:34},{width:18},{width:10},{width:12}];
|
| 62 |
-
|
| 63 |
-
ws.mergeCells('A1:E1');
|
| 64 |
-
ws.getCell('A1').value = 'BẢNG BÁO GIÁ';
|
| 65 |
-
ws.getCell('A1').font = {bold:true,size:16,color:{argb:'FF003F62'}};
|
| 66 |
-
ws.getCell('A1').alignment = {horizontal:'center'};
|
| 67 |
-
ws.getRow(1).height = 26;
|
| 68 |
-
|
| 69 |
-
ws.getCell('A2').value = 'Khách hàng:';
|
| 70 |
-
ws.mergeCells('B2:D2');
|
| 71 |
-
ws.getCell('B2').value = (qd.customer ? qd.customer.name : '') || '';
|
| 72 |
-
|
| 73 |
-
ws.getCell('A3').value = 'SĐT:';
|
| 74 |
-
ws.getCell('B3').value = (qd.customer ? qd.customer.phone : '') || '';
|
| 75 |
-
ws.getCell('D3').value = 'Mã: ' + code;
|
| 76 |
-
|
| 77 |
-
ws.getCell('A4').value = 'Địa chỉ:';
|
| 78 |
-
ws.mergeCells('B4:E4');
|
| 79 |
-
ws.getCell('B4').value = (qd.customer ? qd.customer.addr : '') || '';
|
| 80 |
-
|
| 81 |
-
var hr = ws.getRow(6);
|
| 82 |
-
['STT','Tên SP','Mã','SL','Giá'].forEach(function(h,i){
|
| 83 |
-
var c = hr.getCell(i+1); c.value=h; c.font={bold:true};
|
| 84 |
-
});
|
| 85 |
-
|
| 86 |
-
var cr=7, total=0;
|
| 87 |
-
(qd.items||[]).forEach(function(it,i){
|
| 88 |
-
var row = ws.getRow(cr);
|
| 89 |
-
row.values = [i+1, it.name||'', it.model||'', it.qty||1, it.discPrice||it.price||0];
|
| 90 |
-
if((it.discPrice||it.price)>0) row.getCell(5).numFmt = '#,##0"đ"';
|
| 91 |
-
total += (it.discPrice||it.price||0)*(it.qty||1);
|
| 92 |
-
cr++;
|
| 93 |
-
});
|
| 94 |
-
|
| 95 |
-
cr++;
|
| 96 |
-
ws.mergeCells(cr,1,cr,4);
|
| 97 |
-
ws.getCell('A'+cr).value = 'TỔNG CỘNG';
|
| 98 |
-
ws.getCell('A'+cr).font = {bold:true};
|
| 99 |
-
ws.getCell('E'+cr).value = total;
|
| 100 |
-
ws.getCell('E'+cr).font = {bold:true};
|
| 101 |
-
ws.getCell('E'+cr).numFmt = '#,##0"đ"';
|
| 102 |
-
|
| 103 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 104 |
-
var blob = new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
| 105 |
-
var url = URL.createObjectURL(blob);
|
| 106 |
-
excelBlobRegistry.push({url:url, revoked:false, created:Date.now()});
|
| 107 |
-
|
| 108 |
-
var a = document.createElement('a');
|
| 109 |
-
a.href = url;
|
| 110 |
-
a.download = code + '.xlsx';
|
| 111 |
-
document.body.appendChild(a);
|
| 112 |
-
a.click();
|
| 113 |
-
setTimeout(function(){ document.body.removeChild(a); }, 100);
|
| 114 |
-
|
| 115 |
-
if(typeof showToast==='function') showToast('✅ Đã tải ' + code + '.xlsx');
|
| 116 |
-
};
|
| 117 |
-
|
| 118 |
-
// PATCH exportDeliveryExcel (Giao hàng)
|
| 119 |
-
var origExportDeliveryExcel = window.exportDeliveryExcel;
|
| 120 |
-
window.exportDeliveryExcel = async function(order,opt) {
|
| 121 |
-
if (typeof ExcelJS === 'undefined') return;
|
| 122 |
-
|
| 123 |
-
var qd = {customer:{},items:[]};
|
| 124 |
-
if (order && order.items) {
|
| 125 |
-
qd.customer = {name:order.customer||'', phone:order.phone||'', addr:order.addr||'', date:order.date||''};
|
| 126 |
-
qd.items = (order.items||[]).map(function(it,i){
|
| 127 |
-
return {stt:i+1, name:it.name||'', model:it.model||'', qty:it.qty||1, note:it.note||''};
|
| 128 |
-
});
|
| 129 |
-
} else if (window._origGetQuoteData) {
|
| 130 |
-
var raw = window._origGetQuoteData();
|
| 131 |
-
qd.customer = raw.customer||{};
|
| 132 |
-
qd.items = raw.items||[];
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
var code = (order && order.code) ? order.code :
|
| 136 |
-
((window.VAI_QR && window.VAI_QR.getEffectiveOrderCode) ? window.VAI_QR.getEffectiveOrderCode() : 'VAS-GH');
|
| 137 |
-
|
| 138 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Giao hàng');
|
| 139 |
-
ws.views = [{showGridLines:false}];
|
| 140 |
-
ws.columns = [{width:5},{width:34},{width:14},{width:8},{width:20}];
|
| 141 |
-
|
| 142 |
-
ws.mergeCells('A1:E1');
|
| 143 |
-
ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
|
| 144 |
-
ws.getCell('A1').font = {bold:true,size:16,color:{argb:'FF003F62'}};
|
| 145 |
-
ws.getCell('A1').alignment = {horizontal:'center'};
|
| 146 |
-
ws.getRow(1).height = 26;
|
| 147 |
-
|
| 148 |
-
ws.getCell('A2').value = 'KH: ' + (qd.customer.name||'');
|
| 149 |
-
ws.getCell('D2').value = 'Mã: ' + code;
|
| 150 |
-
ws.getCell('A3').value = 'SĐT: ' + (qd.customer.phone||'');
|
| 151 |
-
ws.getCell('D3').value = 'Ngày: ' + (qd.customer.date||'');
|
| 152 |
-
ws.getCell('A4').value = 'Địa chỉ: ' + (qd.customer.addr||'');
|
| 153 |
-
|
| 154 |
-
var hr = ws.getRow(6);
|
| 155 |
-
['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){
|
| 156 |
-
var c = hr.getCell(i+1); c.value=h; c.font={bold:true, color:{argb:'FFFFFFFF'}};
|
| 157 |
-
c.fill = {type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'}};
|
| 158 |
-
});
|
| 159 |
-
|
| 160 |
-
var cr=7, totalQty=0;
|
| 161 |
-
(qd.items||[]).forEach(function(it,i){
|
| 162 |
-
var row = ws.getRow(cr);
|
| 163 |
-
row.values = [i+1, it.name||'', it.model||'', it.qty||1, it.note||''];
|
| 164 |
-
totalQty += (it.qty||1);
|
| 165 |
-
cr++;
|
| 166 |
-
});
|
| 167 |
-
|
| 168 |
-
var tr = ws.getRow(cr);
|
| 169 |
-
ws.mergeCells(cr,1,cr,3);
|
| 170 |
-
ws.getCell('A'+cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
|
| 171 |
-
ws.getCell('D'+cr).value = totalQty;
|
| 172 |
-
|
| 173 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 174 |
-
var blob = new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
| 175 |
-
var url2 = URL.createObjectURL(blob);
|
| 176 |
-
excelBlobRegistry.push({url:url2, revoked:false, created:Date.now()});
|
| 177 |
-
|
| 178 |
-
var a2 = document.createElement('a');
|
| 179 |
-
a2.href = url2;
|
| 180 |
-
a2.download = 'GH-' + code + '.xlsx';
|
| 181 |
-
document.body.appendChild(a2);
|
| 182 |
-
a2.click();
|
| 183 |
-
setTimeout(function(){ document.body.removeChild(a2); }, 100);
|
| 184 |
-
|
| 185 |
-
if(typeof showToast==='function') showToast('✅ Đã tải GH-' + code + '.xlsx');
|
| 186 |
-
};
|
| 187 |
-
|
| 188 |
-
console.log('[Excel Ultimate Fix v2.0] Loaded');
|
| 189 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-fix-100px.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Excel Export v1102
|
| 3 |
-
* FIX: Hình ảnh 100x100px trong Excel
|
| 4 |
-
*/
|
| 5 |
-
(function(){
|
| 6 |
-
'use strict';
|
| 7 |
-
|
| 8 |
-
function fmt(n){
|
| 9 |
-
if(n==null||n==='')return'0đ';
|
| 10 |
-
var num=typeof n==='string'?parseFloat(n.replace(/[^\d.-]/g,'')):Number(n);
|
| 11 |
-
if(isNaN(num))return'0đ';
|
| 12 |
-
return num.toLocaleString('vi-VN')+'đ';
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
async function fetchImageArrayBuffer(url){
|
| 16 |
-
if(!url||!url.startsWith('http')) return null;
|
| 17 |
-
try{
|
| 18 |
-
var r = await fetch(url,{mode:'cors'});
|
| 19 |
-
if(r.ok) return await r.arrayBuffer();
|
| 20 |
-
}catch(e){}
|
| 21 |
-
try{
|
| 22 |
-
var r2 = await fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(url));
|
| 23 |
-
if(r2.ok) return await r2.arrayBuffer();
|
| 24 |
-
}catch(e){}
|
| 25 |
-
return null;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
function getOrderCode(){
|
| 29 |
-
var n = document.getElementById('quoteName')?.value || '';
|
| 30 |
-
var h = window.location.hash;
|
| 31 |
-
var today = new Date(), dd=today.getDate().toString().padStart(2,'0'), mm=(today.getMonth()+1).toString().padStart(2,'0'), yy=today.getFullYear().toString().slice(-2);
|
| 32 |
-
var ini='';
|
| 33 |
-
if(n && n.trim()){
|
| 34 |
-
n.trim().split(/\s+/).forEach(function(w){
|
| 35 |
-
if(w && /[A-Z]/.test(w.charAt(0).toUpperCase())) ini += w.charAt(0).toUpperCase();
|
| 36 |
-
});
|
| 37 |
-
}
|
| 38 |
-
return 'VAS'+(ini||'X')+dd+mm+yy;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
window.exportExcel = async function(){
|
| 42 |
-
var cart = window.cart || [];
|
| 43 |
-
if(!cart.length){alert('Giỏ trống!');return;}
|
| 44 |
-
|
| 45 |
-
if(typeof ExcelJS === 'undefined'){
|
| 46 |
-
var s = document.createElement('script');
|
| 47 |
-
s.src = 'https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js';
|
| 48 |
-
document.head.appendChild(s);
|
| 49 |
-
await new Promise(function(r){s.onload=r});
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
try{
|
| 53 |
-
var code = getOrderCode();
|
| 54 |
-
var wb = new ExcelJS.Workbook(), ws = wb.addWorksheet('Báo giá');
|
| 55 |
-
|
| 56 |
-
// Column 2 width 15 for 100px image
|
| 57 |
-
ws.columns = [
|
| 58 |
-
{width:6},{width:15},{width:30},{width:15},
|
| 59 |
-
{width:22},{width:8},{width:15},{width:15},{width:18},{width:16}
|
| 60 |
-
];
|
| 61 |
-
|
| 62 |
-
// Header title
|
| 63 |
-
ws.mergeCells('A1:J1');
|
| 64 |
-
ws.getCell('A1').value = 'BẢNG BÁO GIÁ';
|
| 65 |
-
ws.getCell('A1').font = {bold:true, size:18, color:{argb:'FFDB9815'}};
|
| 66 |
-
ws.getCell('A1').alignment = {horizontal:'center'};
|
| 67 |
-
ws.getRow(1).height = 30;
|
| 68 |
-
|
| 69 |
-
// Table header
|
| 70 |
-
var hr = ws.getRow(3);
|
| 71 |
-
['STT','Hình','Tên sản phẩm','Mã SP','Thông tin','SL','Đơn giá','Giá CK','Thành tiền','Ghi chú'].forEach(function(h,i){
|
| 72 |
-
var c = hr.getCell(i+1);
|
| 73 |
-
c.value = h;
|
| 74 |
-
c.font = {bold:true, color:{argb:'FFFFFFFF'}, size:9};
|
| 75 |
-
c.fill = {type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'}};
|
| 76 |
-
c.alignment = {horizontal:'center', vertical:'middle'};
|
| 77 |
-
});
|
| 78 |
-
hr.height = 20;
|
| 79 |
-
|
| 80 |
-
// Items with 100x100px images
|
| 81 |
-
var firstRow = 5;
|
| 82 |
-
await Promise.all(cart.map(async function(c, i){
|
| 83 |
-
var p = window.D && D[c.idx] ? D[c.idx] : null;
|
| 84 |
-
var row = ws.getRow(firstRow + i);
|
| 85 |
-
row.height = 120; // 100px + padding
|
| 86 |
-
|
| 87 |
-
var bgColor = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
|
| 88 |
-
|
| 89 |
-
row.getCell(1).value = i+1;
|
| 90 |
-
row.getCell(1).alignment = {horizontal:'center', vertical:'middle'};
|
| 91 |
-
|
| 92 |
-
// IMAGE 100x100px
|
| 93 |
-
if((p && p.image) || c.image){
|
| 94 |
-
var imgUrl = p?.image || c.image;
|
| 95 |
-
var imgBuf = await fetchImageArrayBuffer(imgUrl);
|
| 96 |
-
if(imgBuf){
|
| 97 |
-
try{
|
| 98 |
-
wb.addImage(wb.addImage({buffer:imgBuf, extension:'png'}),
|
| 99 |
-
{tl:{col:1, row:firstRow+i-1}, ext:{width:100, height:100}});
|
| 100 |
-
}catch(e){}
|
| 101 |
-
}
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
row.getCell(3).value = c.name || (p?p.name||p.n:'') || '';
|
| 105 |
-
row.getCell(3).font = {bold:true, size:9};
|
| 106 |
-
row.getCell(4).value = c.sku || (p?p.sku||p.model:'') || '';
|
| 107 |
-
row.getCell(6).value = Number(c.qty||1);
|
| 108 |
-
row.getCell(7).value = Number(c.priceNum||(p?p.priceNum:0)||0);
|
| 109 |
-
row.getCell(7).numFmt = '#,##0"đ"';
|
| 110 |
-
row.getCell(8).value = Number(c.priceNum||(p?p.priceNum:0)||0);
|
| 111 |
-
row.getCell(8).numFmt = '#,##0"đ"';
|
| 112 |
-
row.getCell(9).value = Number(c.priceNum||(p?p.priceNum:0)||0) * Number(c.qty||1);
|
| 113 |
-
row.getCell(9).numFmt = '#,##0"đ"';
|
| 114 |
-
|
| 115 |
-
for(var ci=1; ci<=10; ci++){
|
| 116 |
-
row.getCell(ci).fill = {type:'pattern', pattern:'solid', fgColor:{argb:bgColor}};
|
| 117 |
-
}
|
| 118 |
-
}));
|
| 119 |
-
|
| 120 |
-
var lastRow = firstRow + cart.length - 1;
|
| 121 |
-
// Total row
|
| 122 |
-
ws.getCell('A'+(lastRow+2)).value = 'TỔNG CỘNG';
|
| 123 |
-
ws.getCell('A'+(lastRow+2)).font = {bold:true, size:13, color:{argb:'FFFFFFFF'}};
|
| 124 |
-
ws.getCell('A'+(lastRow+2)).fill = {type:'pattern', pattern:'solid', fgColor:{argb:'FF003F62'}};
|
| 125 |
-
ws.getCell('I'+(lastRow+2)).value = {formula:'SUM(I'+firstRow+':I'+lastRow+')'};
|
| 126 |
-
ws.getCell('I'+(lastRow+2)).numFmt = '#,##0"đ"';
|
| 127 |
-
ws.getCell('I'+(lastRow+2)).font = {bold:true, size:14, color:{argb:'FFF0B840'}};
|
| 128 |
-
|
| 129 |
-
wb.calcProperties.fullCalcOnLoad = true;
|
| 130 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 131 |
-
var blob = new Blob([buf], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
| 132 |
-
var url = URL.createObjectURL(blob);
|
| 133 |
-
var a = document.createElement('a');
|
| 134 |
-
a.href = url;
|
| 135 |
-
a.download = code + '.xlsx';
|
| 136 |
-
document.body.appendChild(a);
|
| 137 |
-
a.click();
|
| 138 |
-
setTimeout(function(){ document.body.removeChild(a); URL.revokeObjectURL(url); }, 30000);
|
| 139 |
-
|
| 140 |
-
console.log('[VAI] Excel export OK - hình ảnh 100px');
|
| 141 |
-
if(typeof showToast === 'function') showToast('✅ Excel OK - Hình ảnh 100px');
|
| 142 |
-
|
| 143 |
-
}catch(e){
|
| 144 |
-
console.error('[VAI] Excel error:', e);
|
| 145 |
-
alert('Lỗi: '+e.message);
|
| 146 |
-
}
|
| 147 |
-
};
|
| 148 |
-
|
| 149 |
-
console.log('[VAI v1102] Excel fix loaded - 100px images ready');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-fix-inline-v2.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Excel Fix Inline v2.0
|
| 3 |
-
* ĐÂY LÀ PHIÊN BẢN NHÚNG TRỰC TIẾP VÀO index.html
|
| 4 |
-
*
|
| 5 |
-
* Copy toàn bộ nội dung script này và dán vào trước </body> trong index.html
|
| 6 |
-
*/
|
| 7 |
-
<script>
|
| 8 |
-
(function(){
|
| 9 |
-
"use strict";
|
| 10 |
-
var R=window.__VAI_BLOB_REGISTRY=window.__VAI_BLOB_REGISTRY||{blobs:[],isUnloading:!1};function cleanup(){R.isUnloading=!0,R.blobs.forEach(function(e){e.revoked||(URL.revokeObjectURL(e.url),e.revoked=!0)}),R.blobs=[];}
|
| 11 |
-
window.addEventListener("pagehide",cleanup),window.addEventListener("beforeunload",cleanup),window.addEventListener("unload",cleanup);
|
| 12 |
-
if(!window.__VAI_REVOKE_PATCHED){var _o=URL.revokeObjectURL;URL.revokeObjectURL=function(u){if(R.isUnloading||!u||"string"!=typeof u||!u.startsWith("blob:"))return _o.call(this,u);var e=R.blobs.find(function(e){return e.url===u});e||R.blobs.push({url:u,revoked:!1,created:Date.now()}),void 0;window.__VAI_REVOKE_PATCHED=!0;}
|
| 13 |
-
function fmt(n){return n&&!isNaN(n)?Number(n).toLocaleString("vi-VN")+"đ":"0đ";}
|
| 14 |
-
function stripDiacritics(s){return s.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[Đđ]/g,"D");}
|
| 15 |
-
function genCode(n){var d=new Date,dd=String(d.getDate()).padStart(2,"0"),mm=String(d.getMonth()+1).padStart(2,"0"),yy=String(d.getFullYear()).slice(-2),ini="";if(n&&n.trim()){n.trim().split(/\s+/).forEach(function(w){if(w){var c=stripDiacritics(w.charAt(0)).toUpperCase();if(/[A-Z]/.test(c))ini+=c;}})}return"VAS"+(ini||"X")+dd+mm+yy;}
|
| 16 |
-
function setBtn(b,e){if(!b)return;e?(b.disabled=!0,b.dataset.vaiExporting="1",b.dataset.vaiOrig=b.innerHTML,b.innerHTML='<span style="opacity:0.7">⏳</span>'):(b.disabled=!1,b.dataset.vaiExporting="0",b.dataset.vaiOrig&&(b.innerHTML=b.dataset.vaiOrig));}
|
| 17 |
-
function dlBlob(b,f){var u=URL.createObjectURL(b);R.blobs.push({url:u,revoked:!1}),a=document.createElement("a"),a.href=u,a.download=f,a.style.cssText="position:absolute;left:-9999px",document.body.appendChild(a),a.click(),setTimeout(function(){try{document.body.removeChild(a)}catch(e){}},100);}
|
| 18 |
-
function getQD(){var d={customer:{name:"",phone:"",email:"",addr:"",date:new Date().toLocaleDateString("vi-VN")},items:[],grandTotal:0},c=window.cart||[],p=window.D||[];c.forEach(function(item,i){var prod=p[item.idx]||(p.find?p.find(function(e){return e.id===item.id}):null),pr=item.priceNum||item.price||(prod?prod.price:0)||0;d.items.push({stt:i+1,image:prod?prod.img:"",name:prod?(prod.name||prod.n||""):(item.name||""),model:prod?(prod.sku||prod.mod||""):(item.sku||item.model||""),specs:prod?(prod.info||prod.desc||""):"",qty:item.qty||1,price:pr,discPrice:pr,total:(item.qty||1)*pr,note:""})});var ns=["#quoteName","#quoteCustomerName","#checkoutName"];for(var j=0;j<ns.length;j++){var el=document.querySelector(ns[j]);if(el&&el.value&&el.value.trim()){d.customer.name=el.value.trim();break;}}var ph=document.querySelector("#quotePhone,#checkoutPhone");ph&&ph.value&&(d.customer.phone=ph.value.trim());var em=document.querySelector("#quoteEmail");em&&em.value&&(d.customer.email=em.value.trim());var ad=document.querySelector("#quoteAddr,#checkoutAddr");ad&&ad.value&&(d.customer.addr=ad.value.trim());d.grandTotal=d.items.reduce(function(s,i){return s+(i.total||0)},0);return d;}
|
| 19 |
-
async function xuEG(d,c){if("undefined"==typeof ExcelJS)return alert("⚠️ Chưa có ExcelJS"),!1;try{var wb=new ExcelJS.Workbook,w=wb.addWorksheet("Báo giá");w.views=[{showGridLines:!1}],w.columns=[{width:5},{width:11},{width:28},{width:13},{width:20},{width:6},{width:13},{width:13},{width:15},{width:14}];w.mergeCells("A3:J3"),w.getCell("A3").value="BẢNG BÁO GIÁ",w.getCell("A3").font={bold:!0,size:18,color:{argb:"FFDB9815"}},w.getCell("A3").alignment={horizontal:"center"},w.getCell("A5").value="Khách hàng:",w.mergeCells("B5:E5"),w.getCell("B5").value=d.customer.name,w.getCell("H5").value="Mã đơn:",w.mergeCells("I5:J5"),w.getCell("I5").value=c,w.getCell("A6").value="SĐT:",w.mergeCells("B6:E6"),w.getCell("B6").value=d.customer.phone,w.getCell("H6").value="Ngày:",w.mergeCells("I6:J6"),w.getCell("I6").value=d.customer.date,w.getCell("A7").value="Email:",w.mergeCells("B7:E7"),w.getCell("B7").value=d.customer.email,w.getCell("A8").value="Địa chỉ:",w.mergeCells("B8:J8"),w.getCell("B8").value=d.customer.addr;var hr=w.getRow(10);["STT","Hình","Tên sản phẩm","Mã SP","Thông tin","SL","Đơn giá","Giá CK","Thành tiền","Ghi chú"].forEach(function(h,i){var c=hr.getCell(i+1);c.value=h,c.font={bold:!0,color:{argb:"FFFFFFFF"}},c.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FF003F62"}});var cr=11,fr=cr;d.items.forEach(function(it,i){var r=w.getRow(cr);r.height=50,r.getCell(1).value=it.stt||i+1,r.getCell(3).value=it.name||"",r.getCell(3).font={bold:!0,size:9},r.getCell(4).value=it.model||"",r.getCell(5).value=it.specs||"",r.getCell(5).font={size:8,color:{argb:"FF64748B"}},r.getCell(6).value=Number(it.qty||1),r.getCell(6).numFmt="#,##0",r.getCell(7).value=Number(it.price||0),r.getCell(7).numFmt="#,##0\"đ\"",r.getCell(8).value=Number(it.discPrice||it.price||0),r.getCell(8).numFmt="#,##0\"đ\"",r.getCell(8).font=(it.price||0)>(it.discPrice||0)&&{bold:!0,color:{argb:"FFDC3545"}},r.getCell(9).value=Number(it.total||0),r.getCell(9).numFmt="#,##0\"đ\"",r.getCell(9).font={bold:!0},r.getCell(10).value=it.note||"";cr++});w.getRow(cr).height=28,w.mergeCells(cr,1,cr,5),w.getCell("A"+cr).value="TỔNG CỘNG",w.getCell("A"+cr).font={bold:!0,size:13,color:{argb:"FFFFFFFF"}},w.getCell("A"+cr).fill={type:"pattern",pattern:"solid",fgColor:{argb:"FF003F62"}},w.getCell("E"+cr).value={formula:"SUM(I"+fr+":I"+(cr-1)+")",result:Number(d.grandTotal||0)},w.getCell("E"+cr).numFmt="#,##0\"đ\"",w.getCell("E"+cr).font={bold:!0,size:14,color:{argb:"FFF0B840"}},wb.calcProperties.fullCalcOnLoad=!0;var buf=await wb.xlsx.writeBuffer(),blob=new Blob([buf],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});return dlBlob(blob,c+".xlsx"),!0}catch(e){return console.error("[VAI] exportQuoteExcel error:",e),alert("❌ Lỗi: "+e.message),!1}
|
| 20 |
-
async function xuEG_D(d,c){if("undefined"==typeof ExcelJS)return alert("⚠️ Chưa có ExcelJS"),!1;try{var wb=new ExcelJS.Workbook,w=wb.addWorksheet("Giao hàng");w.views=[{showGridLines:!1}],w.columns=[{width:5},{width:34},{width:14},{width:8},{width:20}];w.mergeCells("A1:E1"),w.getCell("A1").value="PHIẾU GIAO HÀNG",w.getCell("A1").font={bold:!0,size:16,color:{argb:"FF003F62"}},w.getCell("A1").alignment={horizontal:"center"},w.getCell("A2").value="KH: "+(d.customer.name||""),w.getCell("D2").value="Mã: "+c,w.getCell("A3").value="SĐT: "+(d.customer.phone||""),w.getCell("D3").value="Ngày: "+(d.customer.date||""),w.getCell("A4").value="Địa chỉ: "+(d.customer.addr||"");var hr=w.getRow(6);["STT","Tên sản phẩm","Mã SP","SL","Ghi chú"].forEach(function(h,i){var c=hr.getCell(i+1);c.value=h,c.font={bold:!0,color:{argb:"FFFFFFFF"}},c.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FF003F62"}});var cr=7,tq=0;d.items.forEach(function(it,i){var r=w.getRow(cr);r.height=20,r.getCell(1).value=it.stt||i+1,r.getCell(2).value=it.name||"",r.getCell(3).value=it.model||"",r.getCell(4).value=it.qty||1,r.getCell(5).value=it.note||"";tq+=Number(it.qty||1);cr++}),w.mergeCells(cr,1,cr,3),w.getCell("A"+cr).value="TỔNG CỘNG SỐ LƯỢNG",w.getCell("D"+cr).value=tq;var buf=await wb.xlsx.writeBuffer(),blob=new Blob([buf],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});return dlBlob(blob,"GH-"+c+".xlsx"),!0}catch(e){return console.error("[VAI] exportDeliveryExcel error:",e),alert("❌ Lỗi: "+e.message),!1}
|
| 21 |
-
function hClick(e){var b=e.target.closest("button,a");if(!b)return;var t=(b.textContent||"").toLowerCase();if(t.includes("excel")&&!t.includes("giao")&&!t.includes("gh-")&&!t.includes("giao")){if(b.dataset.vaiExporting=="1"){e.preventDefault();return;}e.preventDefault(),e.stopPropagation(),setBtn(b,!0);var d=getQD(),c=genCode(d.customer.name);xuEG(d,c).then(function(){setBtn(b,!1),typeof showToast==="function"&&showToast("✅ Excel báo giá tải thành công!"),console.log("[VAI Inline] Quote Excel uploaded")})}else{if((t.includes("gh")||t.includes("giao hàng"))&&t.includes("excel")){if(b.dataset.vaiExporting=="1"){e.preventDefault();return;}e.preventDefault(),e.stopPropagation(),setBtn(b,!0);var od=window.VAI_CURRENT_ORDER,ed={},ocd=genCode(od?od.customer:"");if(od&&od.items){ed={customer:{name:od.customer||"",phone:od.phone||"",addr:od.addr||"",date:od.date||""},items:od.items.map(function(it,i){return{stt:i+1,name:it.name||"",model:it.model||"",qty:it.qty||1,note:it.note||"",specs:it.specs||"",price:it.price||0,discPrice:it.discPrice||it.price||0,total:it.total||0}}),grandTotal:od.grandTotal||0};ocd=od.code||ocd}xuEG_D(ed,ocd).then(function(){setBtn(b,!1),typeof showToast==="function"&&showToast("✅ Excel giao hàng tải thành công!")})}}}function init(){document.removeEventListener("click",hClick,!0),document.addEventListener("click",hClick,!0),console.log("[VAI Inline v2.0] Ready")}
|
| 22 |
-
if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init)}else{init()}
|
| 23 |
-
setTimeout(init,2e3);setTimeout(init,5e3);
|
| 24 |
-
window.VAI_EXCEL_INLINE={init:init,exportQuoteExcel:xuEG,exportDeliveryExcel:xuEG_D};
|
| 25 |
-
})();
|
| 26 |
-
</script>
|
| 27 |
-
/* === HƯỚNG DẪN SỬ DỤNG ===
|
| 28 |
-
* 1. Mở file index.html
|
| 29 |
-
* 2. Tìm thẻ </body>
|
| 30 |
-
* 3. Dán TOÀN BỘ nội dung file này (kể cả thẻ <script>) vào NGAY TRƯỚC </body>
|
| 31 |
-
* 4. Lưu và Rebuild space
|
| 32 |
-
* ============================
|
| 33 |
-
* Link: https://huggingface.co/spaces/bep40/V.AISTUDIO
|
| 34 |
-
* Cập nhật: 14-07-2026 - ML Intern
|
| 35 |
-
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-fix-oneclick.bookmarklet.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO Excel Fix - One Click Bookmarklet
|
| 3 |
-
*
|
| 4 |
-
* CÁCH DÙNG:
|
| 5 |
-
* 1. Copy TOÀN BỘ đoạn code dưới (bao gồm cả javascript:)
|
| 6 |
-
* 2. Tạo bookmark mới trên trình duyệt
|
| 7 |
-
* 3. Dán vào phần URL của bookmark
|
| 8 |
-
* 4. Mở https://huggingface.co/spaces/bep40/V.AISTUDIO
|
| 9 |
-
* 5. Click bookmark - tự động áp dụng fix!
|
| 10 |
-
*/
|
| 11 |
-
|
| 12 |
-
javascript:(function(){
|
| 13 |
-
var s=document.createElement('script');
|
| 14 |
-
s.src='https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/vai-quote-excel-order-fix-v1050.js?v=1050';
|
| 15 |
-
s.onload=function(){
|
| 16 |
-
if(typeof showToast==='function'){showToast('✅ Excel Fix đã kích hoạt! Tải nhiều lần được.');}
|
| 17 |
-
else{alert('✅ Excel Fix đã kích hoạt!\nBây giờ bạn có thể tải file Excel nhiều lần.');}
|
| 18 |
-
};
|
| 19 |
-
s.onerror=function(){
|
| 20 |
-
// Inline fallback
|
| 21 |
-
var orig=URL.revokeObjectURL;
|
| 22 |
-
URL.revokeObjectURL=function(u){
|
| 23 |
-
if(u&&u.startsWith('blob:')){
|
| 24 |
-
setTimeout(function(){try{orig.call(URL,u);}catch(e){}},300000);
|
| 25 |
-
return;
|
| 26 |
-
}
|
| 27 |
-
return orig.apply(URL,arguments);
|
| 28 |
-
};
|
| 29 |
-
alert('✅ Inline fix đã áp dụng!\nExcel sẽ tải được nhiều lần.');
|
| 30 |
-
};
|
| 31 |
-
document.head.appendChild(s);
|
| 32 |
-
s=null;
|
| 33 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-fix-v2.bookmarklet.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO Excel Fix v2.0 - Bookmarklet version
|
| 3 |
-
* Bookmarklet: V.AISTUDIO Excel Fix v2.0
|
| 4 |
-
*
|
| 5 |
-
* Dán URL sau vào bookmark:
|
| 6 |
-
* javascript:(function(){try{var s=document.createElement('script');s.src='/vai-excel-fix-v2.js?v='+Date.now();s.onload=function(){alert('✅ Excel Fix v2.0 đã tải!\n📌 Báo giá & Đơn hàng Excel tải được nhiều lần!');};document.head.appendChild(s);}catch(e){alert('Lỗi: '+e);}})();
|
| 7 |
-
*/
|
| 8 |
-
(function(){try{var s=document.createElement('script');s.src='/vai-excel-fix-v2.js?v='+Date.now();s.onload=function(){alert('✅ Excel Fix v2.0 đã tải!\n📌 Báo giá & Đơn hàng Excel tải được nhiều lần!');};document.head.appendChild(s);}catch(e){alert('Lỗi: '+e);}})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-fix-v2.js
DELETED
|
@@ -1,448 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Excel Export Fix Ultimate v2.0
|
| 3 |
-
* ============================================
|
| 4 |
-
* FIX 100%: Multi-download Excel for Quote & Order modals
|
| 5 |
-
*
|
| 6 |
-
* Key fixes:
|
| 7 |
-
* 1. Global blob URL registry - prevents premature URL.revokeObjectURL
|
| 8 |
-
* 2. Creates fresh blob for each download
|
| 9 |
-
* 3. Proper cleanup on page unload only
|
| 10 |
-
* 4. Click protection to prevent double-clicks
|
| 11 |
-
*/
|
| 12 |
-
(function(){
|
| 13 |
-
'use strict';
|
| 14 |
-
|
| 15 |
-
// ===== GLOBAL STATE =====
|
| 16 |
-
window.__VAI_EXCEL_FIX = window.__VAI_EXCEL_FIX || {
|
| 17 |
-
blobRegistry: [],
|
| 18 |
-
isUnloading: false
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
var g = window.__VAI_EXCEL_FIX;
|
| 22 |
-
|
| 23 |
-
// ===== CLEANUP ON UNLOAD =====
|
| 24 |
-
function cleanupAllBlobs() {
|
| 25 |
-
g.isUnloading = true;
|
| 26 |
-
g.blobRegistry.forEach(function(entry) {
|
| 27 |
-
if (!entry.revoked && entry.url) {
|
| 28 |
-
try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch(e) {}
|
| 29 |
-
}
|
| 30 |
-
});
|
| 31 |
-
g.blobRegistry = [];
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
window.addEventListener('pagehide', cleanupAllBlobs);
|
| 35 |
-
window.addEventListener('beforeunload', cleanupAllBlobs);
|
| 36 |
-
window.addEventListener('unload', cleanupAllBlobs);
|
| 37 |
-
|
| 38 |
-
// ===== PATCH URL.revokeObjectURL - CRITICAL =====
|
| 39 |
-
if (!window.__VAI_REVOKE_PATCHED) {
|
| 40 |
-
var originalRevoke = URL.revokeObjectURL;
|
| 41 |
-
URL.revokeObjectURL = function(url) {
|
| 42 |
-
// Only revoke on unload or non-blob URLs
|
| 43 |
-
if (g.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) {
|
| 44 |
-
return originalRevoke.call(this, url);
|
| 45 |
-
}
|
| 46 |
-
// Register but DON'T revoke - this is the key fix for multi-download
|
| 47 |
-
var exists = g.blobRegistry.find(function(e) { return e.url === url; });
|
| 48 |
-
if (!exists) {
|
| 49 |
-
g.blobRegistry.push({ url: url, revoked: false, created: Date.now() });
|
| 50 |
-
}
|
| 51 |
-
return undefined;
|
| 52 |
-
};
|
| 53 |
-
window.__VAI_REVOKE_PATCHED = true;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
// ===== UTILITIES =====
|
| 57 |
-
function fmt(n) {
|
| 58 |
-
if (!n || isNaN(n)) return '0đ';
|
| 59 |
-
return Number(n).toLocaleString('vi-VN') + 'đ';
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
function stripDiacritics(s) {
|
| 63 |
-
return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D');
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
function generateOrderCode(name) {
|
| 67 |
-
var d = new Date();
|
| 68 |
-
var dd = String(d.getDate()).padStart(2, '0');
|
| 69 |
-
var mm = String(d.getMonth() + 1).padStart(2, '0');
|
| 70 |
-
var yy = String(d.getFullYear()).slice(-2);
|
| 71 |
-
var ini = '';
|
| 72 |
-
if (name && name.trim()) {
|
| 73 |
-
name.trim().split(/\s+/).forEach(function(w) {
|
| 74 |
-
if (w) {
|
| 75 |
-
var ch = stripDiacritics(w.charAt(0)).toUpperCase();
|
| 76 |
-
if (/[A-Z]/.test(ch)) {
|
| 77 |
-
ini += ch;
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
});
|
| 81 |
-
}
|
| 82 |
-
return 'VAS' + (ini || 'X') + dd + mm + yy;
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
// ===== BUTTON STATE =====
|
| 86 |
-
function setBtnState(btn, exporting) {
|
| 87 |
-
if (!btn) return;
|
| 88 |
-
if (exporting) {
|
| 89 |
-
btn.disabled = true;
|
| 90 |
-
btn.dataset.vaiExporting = '1';
|
| 91 |
-
btn.dataset.vaiOrigHTML = btn.innerHTML;
|
| 92 |
-
btn.innerHTML = '<span style="opacity:0.7">⏳ Đang xuất...</span>';
|
| 93 |
-
} else {
|
| 94 |
-
btn.disabled = false;
|
| 95 |
-
btn.dataset.vaiExporting = '0';
|
| 96 |
-
if (btn.dataset.vaiOrigHTML) {
|
| 97 |
-
btn.innerHTML = btn.dataset.vaiOrigHTML;
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
// ===== CREATE DOWNLOAD =====
|
| 103 |
-
function downloadBlob(blob, filename) {
|
| 104 |
-
var url = URL.createObjectURL(blob);
|
| 105 |
-
g.blobRegistry.push({ url: url, revoked: false });
|
| 106 |
-
|
| 107 |
-
var a = document.createElement('a');
|
| 108 |
-
a.href = url;
|
| 109 |
-
a.download = filename;
|
| 110 |
-
a.style.cssText = 'position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;';
|
| 111 |
-
document.body.appendChild(a);
|
| 112 |
-
a.click();
|
| 113 |
-
|
| 114 |
-
setTimeout(function() {
|
| 115 |
-
try { document.body.removeChild(a); } catch(e) {}
|
| 116 |
-
}, 100);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
// ===== GET QUOTE DATA FROM MODAL =====
|
| 120 |
-
function getQuoteData() {
|
| 121 |
-
var data = {
|
| 122 |
-
customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') },
|
| 123 |
-
items: [],
|
| 124 |
-
grandTotal: 0
|
| 125 |
-
};
|
| 126 |
-
|
| 127 |
-
// Get cart data
|
| 128 |
-
var cart = window.cart || [];
|
| 129 |
-
var products = window.D || [];
|
| 130 |
-
|
| 131 |
-
cart.forEach(function(item, i) {
|
| 132 |
-
var product = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null);
|
| 133 |
-
var price = item.priceNum || item.price || (product ? product.price : 0) || 0;
|
| 134 |
-
data.items.push({
|
| 135 |
-
stt: i + 1,
|
| 136 |
-
image: product ? product.img : '',
|
| 137 |
-
name: product ? (product.name || product.n || '') : (item.name || ''),
|
| 138 |
-
model: product ? (product.sku || product.mod || '') : (item.sku || item.model || ''),
|
| 139 |
-
specs: product ? (product.info || product.desc || '') : '',
|
| 140 |
-
qty: item.qty || 1,
|
| 141 |
-
price: price,
|
| 142 |
-
discPrice: price,
|
| 143 |
-
total: (item.qty || 1) * price,
|
| 144 |
-
note: ''
|
| 145 |
-
});
|
| 146 |
-
});
|
| 147 |
-
|
| 148 |
-
// Get customer info from modal
|
| 149 |
-
var nameSelectors = ['#quoteName', '#quoteCustomerName', '#checkoutName'];
|
| 150 |
-
for (var j = 0; j < nameSelectors.length; j++) {
|
| 151 |
-
var el = document.querySelector(nameSelectors[j]);
|
| 152 |
-
if (el && el.value && el.value.trim()) {
|
| 153 |
-
data.customer.name = el.value.trim();
|
| 154 |
-
break;
|
| 155 |
-
}
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
var phoneEl = document.querySelector('#quotePhone, #checkoutPhone');
|
| 159 |
-
if (phoneEl && phoneEl.value) data.customer.phone = phoneEl.value.trim();
|
| 160 |
-
|
| 161 |
-
var emailEl = document.querySelector('#quoteEmail');
|
| 162 |
-
if (emailEl && emailEl.value) data.customer.email = emailEl.value.trim();
|
| 163 |
-
|
| 164 |
-
var addrEl = document.querySelector('#quoteAddr, #checkoutAddr');
|
| 165 |
-
if (addrEl && addrEl.value) data.customer.addr = addrEl.value.trim();
|
| 166 |
-
|
| 167 |
-
data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0);
|
| 168 |
-
return data;
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
// ===== EXPORT QUOTE EXCEL =====
|
| 172 |
-
async function exportQuoteExcel(quoteData, code) {
|
| 173 |
-
if (typeof ExcelJS === 'undefined') {
|
| 174 |
-
if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS');
|
| 175 |
-
return false;
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
try {
|
| 179 |
-
var wb = new ExcelJS.Workbook();
|
| 180 |
-
var ws = wb.addWorksheet('Báo giá');
|
| 181 |
-
ws.views = [{ showGridLines: false }];
|
| 182 |
-
ws.columns = [
|
| 183 |
-
{ width: 5 }, { width: 11 }, { width: 28 },
|
| 184 |
-
{ width: 13 }, { width: 20 }, { width: 6 },
|
| 185 |
-
{ width: 13 }, { width: 13 }, { width: 15 }, { width: 14 }
|
| 186 |
-
];
|
| 187 |
-
|
| 188 |
-
ws.mergeCells('A3:J3');
|
| 189 |
-
ws.getCell('A3').value = 'BẢNG BÁO GIÁ';
|
| 190 |
-
ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } };
|
| 191 |
-
ws.getCell('A3').alignment = { horizontal: 'center' };
|
| 192 |
-
|
| 193 |
-
ws.getCell('A5').value = 'Khách hàng:';
|
| 194 |
-
ws.mergeCells('B5:E5');
|
| 195 |
-
ws.getCell('B5').value = quoteData.customer?.name || '';
|
| 196 |
-
|
| 197 |
-
ws.getCell('H5').value = 'Mã đơn:';
|
| 198 |
-
ws.mergeCells('I5:J5');
|
| 199 |
-
ws.getCell('I5').value = code;
|
| 200 |
-
|
| 201 |
-
ws.getCell('A6').value = 'SĐT:';
|
| 202 |
-
ws.mergeCells('B6:E6');
|
| 203 |
-
ws.getCell('B6').value = quoteData.customer?.phone || '';
|
| 204 |
-
|
| 205 |
-
ws.getCell('H6').value = 'Ngày:';
|
| 206 |
-
ws.mergeCells('I6:J6');
|
| 207 |
-
ws.getCell('I6').value = quoteData.customer?.date || '';
|
| 208 |
-
|
| 209 |
-
ws.getCell('A7').value = 'Email:';
|
| 210 |
-
ws.mergeCells('B7:E7');
|
| 211 |
-
ws.getCell('B7').value = quoteData.customer?.email || '';
|
| 212 |
-
|
| 213 |
-
ws.getCell('A8').value = 'Địa chỉ:';
|
| 214 |
-
ws.mergeCells('B8:J8');
|
| 215 |
-
ws.getCell('B8').value = quoteData.customer?.addr || '';
|
| 216 |
-
|
| 217 |
-
var hr = ws.getRow(10);
|
| 218 |
-
['STT', 'Hình', 'Tên sản phẩm', 'Mã SP', 'Thông tin', 'SL', 'Đơn giá', 'Giá CK', 'Thành tiền', 'Ghi chú'].forEach(function(h, i) {
|
| 219 |
-
var c = hr.getCell(i + 1);
|
| 220 |
-
c.value = h;
|
| 221 |
-
c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
| 222 |
-
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
|
| 223 |
-
});
|
| 224 |
-
|
| 225 |
-
var cr = 11, firstRow = cr;
|
| 226 |
-
(quoteData.items || []).forEach(function(it, i) {
|
| 227 |
-
var row = ws.getRow(cr);
|
| 228 |
-
row.height = 50;
|
| 229 |
-
var bg = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF';
|
| 230 |
-
|
| 231 |
-
row.getCell(1).value = it.stt || i + 1;
|
| 232 |
-
row.getCell(3).value = it.name || '';
|
| 233 |
-
row.getCell(3).font = { bold: true, size: 9 };
|
| 234 |
-
row.getCell(4).value = it.model || '';
|
| 235 |
-
row.getCell(5).value = it.specs || '';
|
| 236 |
-
row.getCell(5).font = { size: 8, color: { argb: 'FF64748B' } };
|
| 237 |
-
row.getCell(6).value = Number(it.qty || 1);
|
| 238 |
-
row.getCell(6).numFmt = '#,##0';
|
| 239 |
-
row.getCell(7).value = Number(it.price || 0);
|
| 240 |
-
row.getCell(7).numFmt = '#,##0"đ"';
|
| 241 |
-
row.getCell(8).value = Number(it.discPrice || it.price || 0);
|
| 242 |
-
row.getCell(8).numFmt = '#,##0"đ"';
|
| 243 |
-
if ((it.price || 0) > (it.discPrice || 0)) {
|
| 244 |
-
row.getCell(8).font = { bold: true, color: { argb: 'FFDC3545' } };
|
| 245 |
-
}
|
| 246 |
-
row.getCell(9).value = Number(it.total || 0);
|
| 247 |
-
row.getCell(9).numFmt = '#,##0"đ"';
|
| 248 |
-
row.getCell(9).font = { bold: true };
|
| 249 |
-
row.getCell(10).value = it.note || '';
|
| 250 |
-
|
| 251 |
-
cr++;
|
| 252 |
-
});
|
| 253 |
-
|
| 254 |
-
ws.getRow(cr).height = 28;
|
| 255 |
-
ws.mergeCells(cr, 1, cr, 5);
|
| 256 |
-
ws.getCell('A' + cr).value = 'TỔNG CỘNG';
|
| 257 |
-
ws.getCell('A' + cr).font = { bold: true, size: 13, color: { argb: 'FFFFFFFF' } };
|
| 258 |
-
ws.getCell('A' + cr).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
|
| 259 |
-
ws.getCell('E' + cr).value = { formula: 'SUM(I' + firstRow + ':I' + (cr - 1) + ')', result: Number(quoteData.grandTotal || 0) };
|
| 260 |
-
ws.getCell('E' + cr).numFmt = '#,##0"đ"';
|
| 261 |
-
ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FFF0B840' } };
|
| 262 |
-
|
| 263 |
-
wb.calcProperties.fullCalcOnLoad = true;
|
| 264 |
-
|
| 265 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 266 |
-
var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
| 267 |
-
|
| 268 |
-
downloadBlob(blob, code + '.xlsx');
|
| 269 |
-
return true;
|
| 270 |
-
|
| 271 |
-
} catch (err) {
|
| 272 |
-
console.error('[VAI Excel Fix] exportQuoteExcel error:', err);
|
| 273 |
-
alert('❌ Lỗi: ' + err.message);
|
| 274 |
-
return false;
|
| 275 |
-
}
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
// ===== EXPORT DELIVERY EXCEL =====
|
| 279 |
-
async function exportDeliveryExcel(quoteData, code) {
|
| 280 |
-
if (typeof ExcelJS === 'undefined') {
|
| 281 |
-
if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS');
|
| 282 |
-
return false;
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
try {
|
| 286 |
-
var wb = new ExcelJS.Workbook();
|
| 287 |
-
var ws = wb.addWorksheet('Giao hàng');
|
| 288 |
-
ws.views = [{ showGridLines: false }];
|
| 289 |
-
ws.columns = [
|
| 290 |
-
{ width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 }
|
| 291 |
-
];
|
| 292 |
-
|
| 293 |
-
ws.mergeCells('A1:E1');
|
| 294 |
-
ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
|
| 295 |
-
ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } };
|
| 296 |
-
ws.getCell('A1').alignment = { horizontal: 'center' };
|
| 297 |
-
|
| 298 |
-
ws.getCell('A2').value = 'KH: ' + (quoteData.customer?.name || '');
|
| 299 |
-
ws.getCell('D2').value = 'Mã: ' + code;
|
| 300 |
-
ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer?.phone || '');
|
| 301 |
-
ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer?.date || '');
|
| 302 |
-
ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.customer?.addr || '');
|
| 303 |
-
|
| 304 |
-
var hr = ws.getRow(6);
|
| 305 |
-
['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
|
| 306 |
-
var c = hr.getCell(i + 1);
|
| 307 |
-
c.value = h;
|
| 308 |
-
c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
| 309 |
-
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
|
| 310 |
-
});
|
| 311 |
-
|
| 312 |
-
var cr = 7, totalQty = 0;
|
| 313 |
-
(quoteData.items || []).forEach(function(it, i) {
|
| 314 |
-
var row = ws.getRow(cr);
|
| 315 |
-
row.height = 20;
|
| 316 |
-
row.getCell(1).value = it.stt || i + 1;
|
| 317 |
-
row.getCell(2).value = it.name || '';
|
| 318 |
-
row.getCell(3).value = it.model || '';
|
| 319 |
-
row.getCell(4).value = it.qty || 1;
|
| 320 |
-
row.getCell(5).value = it.note || '';
|
| 321 |
-
totalQty += Number(it.qty || 1);
|
| 322 |
-
cr++;
|
| 323 |
-
});
|
| 324 |
-
|
| 325 |
-
ws.mergeCells(cr, 1, cr, 3);
|
| 326 |
-
ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
|
| 327 |
-
ws.getCell('D' + cr).value = totalQty;
|
| 328 |
-
|
| 329 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 330 |
-
var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
| 331 |
-
|
| 332 |
-
downloadBlob(blob, 'GH-' + code + '.xlsx');
|
| 333 |
-
return true;
|
| 334 |
-
|
| 335 |
-
} catch (err) {
|
| 336 |
-
console.error('[VAI Excel Fix] exportDeliveryExcel error:', err);
|
| 337 |
-
alert('❌ Lỗi: ' + err.message);
|
| 338 |
-
return false;
|
| 339 |
-
}
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
// ===== CLICK HANDLER =====
|
| 343 |
-
function handleExportClick(e) {
|
| 344 |
-
var btn = e.target.closest('button, a');
|
| 345 |
-
if (!btn) return;
|
| 346 |
-
|
| 347 |
-
var txt = (btn.textContent || '').toLowerCase();
|
| 348 |
-
|
| 349 |
-
// Quote Excel - "Xuất Excel" button in quote modal
|
| 350 |
-
if (txt.includes('excel') && !txt.includes('giao hàng') && !txt.includes('gh-') && !txt.includes('giao')) {
|
| 351 |
-
e.preventDefault();
|
| 352 |
-
e.stopPropagation();
|
| 353 |
-
|
| 354 |
-
if (btn.dataset.vaiExporting === '1') return;
|
| 355 |
-
setBtnState(btn, true);
|
| 356 |
-
|
| 357 |
-
var qd = getQuoteData();
|
| 358 |
-
var code = generateOrderCode(qd.customer?.name || '');
|
| 359 |
-
|
| 360 |
-
exportQuoteExcel(qd, code).then(function(success) {
|
| 361 |
-
setBtnState(btn, false);
|
| 362 |
-
if (success && typeof showToast === 'function') {
|
| 363 |
-
showToast('✅ Excel báo giá tải thành công!');
|
| 364 |
-
}
|
| 365 |
-
});
|
| 366 |
-
|
| 367 |
-
return false;
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
// Delivery Excel - "GH Excel" button in order modal
|
| 371 |
-
if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) {
|
| 372 |
-
e.preventDefault();
|
| 373 |
-
e.stopPropagation();
|
| 374 |
-
|
| 375 |
-
if (btn.dataset.vaiExporting === '1') return;
|
| 376 |
-
setBtnState(btn, true);
|
| 377 |
-
|
| 378 |
-
// Check for order modal data
|
| 379 |
-
var order = window.VAI_CURRENT_ORDER;
|
| 380 |
-
var exportData, orderCode;
|
| 381 |
-
|
| 382 |
-
if (order && order.items) {
|
| 383 |
-
// From order modal
|
| 384 |
-
exportData = {
|
| 385 |
-
customer: { name: order.customer || '', phone: order.phone || '', addr: order.addr || '', date: order.date || '' },
|
| 386 |
-
items: (order.items || []).map(function(it, i) {
|
| 387 |
-
return {
|
| 388 |
-
stt: i + 1,
|
| 389 |
-
name: it.name || '',
|
| 390 |
-
model: it.model || '',
|
| 391 |
-
qty: it.qty || 1,
|
| 392 |
-
note: it.note || '',
|
| 393 |
-
specs: it.specs || '',
|
| 394 |
-
price: it.price || 0,
|
| 395 |
-
discPrice: it.discPrice || it.price || 0,
|
| 396 |
-
total: it.total || 0
|
| 397 |
-
};
|
| 398 |
-
}),
|
| 399 |
-
grandTotal: order.grandTotal || 0
|
| 400 |
-
};
|
| 401 |
-
orderCode = order.code || generateOrderCode(order.customer || '');
|
| 402 |
-
} else {
|
| 403 |
-
// From cart
|
| 404 |
-
exportData = getQuoteData();
|
| 405 |
-
orderCode = generateOrderCode(exportData.customer?.name || '');
|
| 406 |
-
}
|
| 407 |
-
|
| 408 |
-
exportDeliveryExcel(exportData, orderCode).then(function(success) {
|
| 409 |
-
setBtnState(btn, false);
|
| 410 |
-
if (success && typeof showToast === 'function') {
|
| 411 |
-
showToast('✅ Excel giao hàng tải thành công!');
|
| 412 |
-
}
|
| 413 |
-
});
|
| 414 |
-
|
| 415 |
-
return false;
|
| 416 |
-
}
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
// ===== INIT =====
|
| 420 |
-
function init() {
|
| 421 |
-
// Remove any existing handler to avoid duplicates
|
| 422 |
-
document.removeEventListener('click', handleExportClick, true);
|
| 423 |
-
// Add click handler
|
| 424 |
-
document.addEventListener('click', handleExportClick, true);
|
| 425 |
-
console.log('[VAI Excel Fix v2.0] Loaded - Multi-download enabled');
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
// Run on DOM ready
|
| 429 |
-
if (document.readyState === 'loading') {
|
| 430 |
-
document.addEventListener('DOMContentLoaded', init);
|
| 431 |
-
} else {
|
| 432 |
-
init();
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
// Also run after delays for dynamic content
|
| 436 |
-
setTimeout(init, 2000);
|
| 437 |
-
setTimeout(init, 5000);
|
| 438 |
-
|
| 439 |
-
// Export for global use
|
| 440 |
-
window.VAI_EXCEL_FIX = {
|
| 441 |
-
init: init,
|
| 442 |
-
exportQuoteExcel: exportQuoteExcel,
|
| 443 |
-
exportDeliveryExcel: exportDeliveryExcel,
|
| 444 |
-
generateOrderCode: generateOrderCode,
|
| 445 |
-
getQuoteData: getQuoteData
|
| 446 |
-
};
|
| 447 |
-
|
| 448 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-fix.bookmarklet.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
javascript:(function(){var e=window.excelBlobRegistry=[];function t(){e.forEach(function(t){try{URL.revokeObjectURL(t.url)}catch(n){}});e=[]}window.addEventListener("pagehide",t),window.addEventListener("beforeunload",t),window.addEventListener("unload",t);var n=URL.revokeObjectURL;URL.revokeObjectURL=function(t){if(!t||"string"!=typeof t||!t.startsWith("blob:")){return n.call(this,t)}var o=e.find(function(e){return e.url===t});return o||e.push({url:t,created:Date.now()}),void 0};var o=window.setTimeout;window.setTimeout=function(t,r){try{if(t.toString().includes("revokeObjectURL")&&6e4===r)return console.log("[FIX] Blocked 60s revoke"),null}catch(e){}return o.call(this,t,r)};var r=document.createElement("script");r.src="https://huggingface.co/spaces/bep40/V.AISTUDIO/resolve/main/qr-payment-v1039.js?_t="+Date.now(),document.head.appendChild(r),console.log("[Excel Multi-Fix] Applied"),alert("✅ Excel Multi-Download Fix Applied!\nBạn có thể tải Excel nhiều lần.")})();
|
|
|
|
|
|
excel-multi-fix-complete.js
DELETED
|
@@ -1,261 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO — Excel Multi-Download Complete Fix
|
| 3 |
-
* ============================================
|
| 4 |
-
* FIX TRIỆT ĐỐI cho vấn đề: File Excel chỉ tải được 1 lần
|
| 5 |
-
*
|
| 6 |
-
* Áp dụng cho:
|
| 7 |
-
* - Modal "Lập báo giá" -> nút 💾 Excel
|
| 8 |
-
* - Modal "Đơn hàng" -> nút 💾 Lưu, GH Excel
|
| 9 |
-
*/
|
| 10 |
-
(function() {
|
| 11 |
-
'use strict';
|
| 12 |
-
|
| 13 |
-
// Registry quản lý blob URLs
|
| 14 |
-
var blobRegistry = [];
|
| 15 |
-
var isUnloading = false;
|
| 16 |
-
|
| 17 |
-
// Cleanup khi tab đóng
|
| 18 |
-
function cleanupAllBlobs() {
|
| 19 |
-
isUnloading = true;
|
| 20 |
-
blobRegistry.forEach(function(entry) {
|
| 21 |
-
if (!entry.revoked && entry.url) {
|
| 22 |
-
try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch (e) {}
|
| 23 |
-
}
|
| 24 |
-
});
|
| 25 |
-
blobRegistry = [];
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
window.addEventListener('pagehide', cleanupAllBlobs);
|
| 29 |
-
window.addEventListener('beforeunload', cleanupAllBlobs);
|
| 30 |
-
window.addEventListener('unload', cleanupAllBlobs);
|
| 31 |
-
|
| 32 |
-
// PATCH URL.revokeObjectURL - không revoke blob URLs khi chưa unload
|
| 33 |
-
(function() {
|
| 34 |
-
var origRevoke = URL.revokeObjectURL;
|
| 35 |
-
URL.revokeObjectURL = function(url) {
|
| 36 |
-
if (isUnloading || !url || !url.startsWith('blob:')) {
|
| 37 |
-
return origRevoke.apply(this, arguments);
|
| 38 |
-
}
|
| 39 |
-
// Chỉ đăng ký, không revoke
|
| 40 |
-
var found = blobRegistry.find(function(e) { return e.url === url; });
|
| 41 |
-
if (!found) {
|
| 42 |
-
blobRegistry.push({ url: url, revoked: false, created: Date.now() });
|
| 43 |
-
}
|
| 44 |
-
};
|
| 45 |
-
})();
|
| 46 |
-
|
| 47 |
-
// PATCH exportExcel - Báo giá
|
| 48 |
-
var origExportExcel = window.exportExcel;
|
| 49 |
-
window.exportExcel = async function() {
|
| 50 |
-
if (typeof ExcelJS === 'undefined') {
|
| 51 |
-
if (typeof showToast === 'function') showToast('⚠️ Chưa có ExcelJS');
|
| 52 |
-
return;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
// Lấy dữ liệu
|
| 56 |
-
var d, qd, code, qrUrl;
|
| 57 |
-
if (window.VAI_QR && typeof window.VAI_QR.getData === 'function') {
|
| 58 |
-
d = window.VAI_QR.getData();
|
| 59 |
-
qd = d.qd;
|
| 60 |
-
code = window.VAI_QR.getEffectiveOrderCode();
|
| 61 |
-
qrUrl = window.VAI_QR.getQRUrl(d.remaining || d.grandTotal, code);
|
| 62 |
-
} else if (window._origGetQuoteData) {
|
| 63 |
-
qd = window._origGetQuoteData();
|
| 64 |
-
code = 'VAS-' + Date.now();
|
| 65 |
-
d = { grandTotal: qd.grandTotal || 0, remaining: qd.grandTotal || 0 };
|
| 66 |
-
} else {
|
| 67 |
-
if (typeof showToast === 'function') showToast('⚠️ Lỗi dữ liệu');
|
| 68 |
-
return;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
// TẠO MỚI hoàn toàn
|
| 72 |
-
var wb = new ExcelJS.Workbook();
|
| 73 |
-
var ws = wb.addWorksheet('Báo giá');
|
| 74 |
-
|
| 75 |
-
ws.views = [{ showGridLines: false }];
|
| 76 |
-
ws.columns = [{ width: 5 }, { width: 34 }, { width: 18 }, { width: 10 }, { width: 15 }];
|
| 77 |
-
|
| 78 |
-
// Header
|
| 79 |
-
ws.mergeCells('A1:E1');
|
| 80 |
-
ws.getCell('A1').value = 'BẢNG BÁO GIÁ V.AI STUDIO';
|
| 81 |
-
ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } };
|
| 82 |
-
ws.getCell('A1').alignment = { horizontal: 'center', vertical: 'middle' };
|
| 83 |
-
ws.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0F7FF' } };
|
| 84 |
-
|
| 85 |
-
// Customer info
|
| 86 |
-
ws.getCell('A2').value = 'Khách hàng:';
|
| 87 |
-
ws.mergeCells('B2:D2');
|
| 88 |
-
ws.getCell('B2').value = (qd.customer ? qd.customer.name : '') || '';
|
| 89 |
-
|
| 90 |
-
ws.getCell('A3').value = 'SĐT:';
|
| 91 |
-
ws.getCell('B3').value = (qd.customer ? qd.customer.phone : '') || '';
|
| 92 |
-
ws.getCell('D3').value = 'Mã đơn: ' + code;
|
| 93 |
-
|
| 94 |
-
ws.getCell('A4').value = 'Địa chỉ:';
|
| 95 |
-
ws.mergeCells('B4:E4');
|
| 96 |
-
ws.getCell('B4').value = (qd.customer ? qd.customer.addr : '') || '';
|
| 97 |
-
|
| 98 |
-
// Header row
|
| 99 |
-
var hr = ws.getRow(6);
|
| 100 |
-
['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Giá'].forEach(function(h, i) {
|
| 101 |
-
var c = hr.getCell(i + 1);
|
| 102 |
-
c.value = h;
|
| 103 |
-
c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
| 104 |
-
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
|
| 105 |
-
});
|
| 106 |
-
|
| 107 |
-
// Items
|
| 108 |
-
var cr = 7, total = 0;
|
| 109 |
-
(qd.items || []).forEach(function(it, i) {
|
| 110 |
-
var row = ws.getRow(cr);
|
| 111 |
-
row.values = [i + 1, it.name || '', it.model || '', it.qty || 1, (it.discPrice || it.price || 0) * (it.qty || 1)];
|
| 112 |
-
if ((it.discPrice || it.price) > 0) {
|
| 113 |
-
row.getCell(5).numFmt = '#,##0"đ"';
|
| 114 |
-
}
|
| 115 |
-
row.getCell(5).font = { bold: true };
|
| 116 |
-
total += (it.discPrice || it.price || 0) * (it.qty || 1);
|
| 117 |
-
cr++;
|
| 118 |
-
});
|
| 119 |
-
|
| 120 |
-
// Total
|
| 121 |
-
ws.mergeCells(cr, 1, cr, 4);
|
| 122 |
-
ws.getCell('A' + cr).value = 'TỔNG CỘNG';
|
| 123 |
-
ws.getCell('A' + cr).font = { bold: true, size: 12 };
|
| 124 |
-
ws.getCell('E' + cr).value = total;
|
| 125 |
-
ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FF003F62' } };
|
| 126 |
-
ws.getCell('E' + cr).numFmt = '#,##0"đ"';
|
| 127 |
-
|
| 128 |
-
// Download - KHÔNG revoke
|
| 129 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 130 |
-
var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
| 131 |
-
var url = URL.createObjectURL(blob);
|
| 132 |
-
|
| 133 |
-
// Đăng ký để cleanup khi unload
|
| 134 |
-
blobRegistry.push({ url: url, revoked: false, created: Date.now() });
|
| 135 |
-
|
| 136 |
-
var a = document.createElement('a');
|
| 137 |
-
a.href = url;
|
| 138 |
-
a.download = code + '.xlsx';
|
| 139 |
-
document.body.appendChild(a);
|
| 140 |
-
a.click();
|
| 141 |
-
setTimeout(function() { document.body.removeChild(a); }, 100);
|
| 142 |
-
|
| 143 |
-
if (typeof showToast === 'function') {
|
| 144 |
-
showToast('✅ Đã tải ' + code + '.xlsx');
|
| 145 |
-
}
|
| 146 |
-
};
|
| 147 |
-
|
| 148 |
-
// PATCH exportDeliveryExcel - Giao hàng
|
| 149 |
-
var origExportDeliveryExcel = window.exportDeliveryExcel;
|
| 150 |
-
window.exportDeliveryExcel = async function(order, opt) {
|
| 151 |
-
if (typeof ExcelJS === 'undefined') {
|
| 152 |
-
if (typeof showToast === 'function') showToast('⚠️ Chưa có ExcelJS');
|
| 153 |
-
return;
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
// Lấy dữ liệu qd
|
| 157 |
-
var qd;
|
| 158 |
-
if (order && order.items) {
|
| 159 |
-
qd = {
|
| 160 |
-
customer: {
|
| 161 |
-
name: order.customer || '',
|
| 162 |
-
phone: order.phone || '',
|
| 163 |
-
addr: order.addr || '',
|
| 164 |
-
date: order.date || ''
|
| 165 |
-
},
|
| 166 |
-
items: (order.items || []).map(function(it, i) {
|
| 167 |
-
return {
|
| 168 |
-
stt: i + 1,
|
| 169 |
-
name: it.name || '',
|
| 170 |
-
model: it.model || '',
|
| 171 |
-
qty: it.qty || 1,
|
| 172 |
-
note: it.note || ''
|
| 173 |
-
};
|
| 174 |
-
})
|
| 175 |
-
};
|
| 176 |
-
} else if (window._origGetQuoteData) {
|
| 177 |
-
qd = window._origGetQuoteData();
|
| 178 |
-
} else if (window.VAI_QR && window.VAI_QR.getData) {
|
| 179 |
-
qd = window.VAI_QR.getData().qd;
|
| 180 |
-
} else {
|
| 181 |
-
if (typeof showToast === 'function') showToast('⚠️ Lỗi dữ liệu');
|
| 182 |
-
return;
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
var code = (order && order.code) ? order.code :
|
| 186 |
-
(window.VAI_QR ? window.VAI_QR.getEffectiveOrderCode() : 'VAS-GH');
|
| 187 |
-
|
| 188 |
-
// TẠO MỚI hoàn toàn
|
| 189 |
-
var wb = new ExcelJS.Workbook();
|
| 190 |
-
var ws = wb.addWorksheet('Giao hàng');
|
| 191 |
-
|
| 192 |
-
ws.views = [{ showGridLines: false }];
|
| 193 |
-
ws.columns = [{ width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 }];
|
| 194 |
-
|
| 195 |
-
// Header
|
| 196 |
-
ws.mergeCells('A1:E1');
|
| 197 |
-
ws.getCell('A1').value = 'PHIẾU GIAO HÀNG';
|
| 198 |
-
ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } };
|
| 199 |
-
ws.getCell('A1').alignment = { horizontal: 'center' };
|
| 200 |
-
ws.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0F7FF' } };
|
| 201 |
-
|
| 202 |
-
// Customer info
|
| 203 |
-
ws.getCell('A2').value = 'KH: ' + (qd.customer ? qd.customer.name : '');
|
| 204 |
-
ws.getCell('D2').value = 'Mã: ' + code;
|
| 205 |
-
|
| 206 |
-
ws.getCell('A3').value = 'SĐT: ' + (qd.customer ? qd.customer.phone : '');
|
| 207 |
-
ws.getCell('D3').value = 'Ngày: ' + (qd.customer ? (qd.customer.date || '') : '');
|
| 208 |
-
|
| 209 |
-
ws.getCell('A4').value = 'Địa chỉ: ' + (qd.customer ? (qd.customer.addr || '') : '');
|
| 210 |
-
|
| 211 |
-
// Header row
|
| 212 |
-
var hr = ws.getRow(6);
|
| 213 |
-
['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) {
|
| 214 |
-
var c = hr.getCell(i + 1);
|
| 215 |
-
c.value = h;
|
| 216 |
-
c.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
| 217 |
-
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } };
|
| 218 |
-
});
|
| 219 |
-
|
| 220 |
-
// Items
|
| 221 |
-
var cr = 7, totalQty = 0;
|
| 222 |
-
(qd.items || []).forEach(function(it, i) {
|
| 223 |
-
var row = ws.getRow(cr);
|
| 224 |
-
row.values = [i + 1, it.name || '', it.model || '', it.qty || 1, it.note || ''];
|
| 225 |
-
totalQty += (it.qty || 1);
|
| 226 |
-
cr++;
|
| 227 |
-
});
|
| 228 |
-
|
| 229 |
-
// Total
|
| 230 |
-
var tr = ws.getRow(cr);
|
| 231 |
-
ws.mergeCells(cr, 1, cr, 3);
|
| 232 |
-
ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG';
|
| 233 |
-
ws.getCell('D' + cr).value = totalQty;
|
| 234 |
-
|
| 235 |
-
// Signature
|
| 236 |
-
cr += 2;
|
| 237 |
-
ws.getCell('A' + cr).value = 'Bên giao: ________________';
|
| 238 |
-
ws.getCell('D' + cr).value = 'Bên nhận: ________________';
|
| 239 |
-
|
| 240 |
-
// Download - KHÔNG revoke
|
| 241 |
-
var buf = await wb.xlsx.writeBuffer();
|
| 242 |
-
var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
| 243 |
-
var url = URL.createObjectURL(blob);
|
| 244 |
-
|
| 245 |
-
// Đăng ký để cleanup khi unload
|
| 246 |
-
blobRegistry.push({ url: url, revoked: false, created: Date.now() });
|
| 247 |
-
|
| 248 |
-
var a = document.createElement('a');
|
| 249 |
-
a.href = url;
|
| 250 |
-
a.download = 'GH-' + code + '.xlsx';
|
| 251 |
-
document.body.appendChild(a);
|
| 252 |
-
a.click();
|
| 253 |
-
setTimeout(function() { document.body.removeChild(a); }, 100);
|
| 254 |
-
|
| 255 |
-
if (typeof showToast === 'function') {
|
| 256 |
-
showToast('✅ Đã tải GH-' + code + '.xlsx');
|
| 257 |
-
}
|
| 258 |
-
};
|
| 259 |
-
|
| 260 |
-
console.log('[Excel Multi-Fix Complete] Loaded - Supports unlimited downloads!');
|
| 261 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
excel-multi-fix-instant.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
/* V.AI STUDIO - INSTANT MULTI-DOWNLOAD EXCEL FIX
|
| 2 |
-
* Thêm vào cuối index.html trước </body>
|
| 3 |
-
* Tự động patch mọi hàm exportExcel hiện có
|
| 4 |
-
*/
|
| 5 |
-
(function(){
|
| 6 |
-
// Chờ document ready
|
| 7 |
-
function patchWhenReady() {
|
| 8 |
-
if (typeof window.exportExcel !== 'function' && typeof window.exportDeliveryExcel !== 'function') {
|
| 9 |
-
if (document.readyState !== 'complete') return;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
// Patch exportExcel function
|
| 13 |
-
var origExportExcel = window.exportExcel;
|
| 14 |
-
if (origExportExcel) {
|
| 15 |
-
window.exportExcel = function() {
|
| 16 |
-
var a = document.createElement('a');
|
| 17 |
-
document.body.appendChild(a);
|
| 18 |
-
|
| 19 |
-
// Remove any revoke logic that causes the bug
|
| 20 |
-
var result = origExportExcel.apply(this, arguments);
|
| 21 |
-
if (result && result.constructor.name === 'Promise') {
|
| 22 |
-
return result;
|
| 23 |
-
}
|
| 24 |
-
return result;
|
| 25 |
-
};
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
// Patch exportDeliveryExcel function
|
| 29 |
-
var origExportDelivery = window.exportDeliveryExcel;
|
| 30 |
-
if (origExportDelivery) {
|
| 31 |
-
window.exportDeliveryExcel = function() {
|
| 32 |
-
var result = origExportDelivery.apply(this, arguments);
|
| 33 |
-
return result;
|
| 34 |
-
};
|
| 35 |
-
}
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
// Immediate patch
|
| 39 |
-
patchWhenReady();
|
| 40 |
-
|
| 41 |
-
// DOM ready patch
|
| 42 |
-
if (document.readyState !== 'complete') {
|
| 43 |
-
document.addEventListener('DOMContentLoaded', patchWhenReady);
|
| 44 |
-
window.addEventListener('load', patchWhenReady);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
// Keep patching until functions exist
|
| 48 |
-
var tries = 0;
|
| 49 |
-
var int = setInterval(function() {
|
| 50 |
-
if (typeof window.exportExcel === 'function' || typeof window.exportDeliveryExcel === 'function' || tries > 30) {
|
| 51 |
-
clearInterval(int);
|
| 52 |
-
}
|
| 53 |
-
tries++;
|
| 54 |
-
}, 300);
|
| 55 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export-functions.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Export functions for quote system
|
| 2 |
+
// These functions are required by quote-ui.js
|
| 3 |
+
|
| 4 |
+
// --- Delivery Export Functions ---
|
| 5 |
+
function exportDeliveryExcel() {
|
| 6 |
+
console.log('[V.AI STUDIO] Exporting delivery to Excel...');
|
| 7 |
+
if (!window.cartItems || !window.cartItems.length) {
|
| 8 |
+
alert('❌ Giỏ hàng trống!');
|
| 9 |
+
return;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
// Create CSV content
|
| 13 |
+
let csv = 'STT,Tên sản phẩm,Mã SP,Số lượng,Đơn giá,Thành tiền,Ghi chú\n';
|
| 14 |
+
let total = 0;
|
| 15 |
+
window.cartItems.forEach((item, idx) => {
|
| 16 |
+
let price = item.priceNum || 0;
|
| 17 |
+
let qty = item.qty || 1;
|
| 18 |
+
let subtotal = price * qty;
|
| 19 |
+
total += subtotal;
|
| 20 |
+
csv += `${idx + 1},"${item.name}","${item.sku || ''}",${qty},${price},${subtotal},"${item.note || ''}"\n`;
|
| 21 |
+
});
|
| 22 |
+
csv += `,,,,,,"${total}",Tổng cộng\n`;
|
| 23 |
+
|
| 24 |
+
// Download
|
| 25 |
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
| 26 |
+
const url = URL.createObjectURL(blob);
|
| 27 |
+
const a = document.createElement('a');
|
| 28 |
+
a.href = url;
|
| 29 |
+
a.download = `giao-hang-${new Date().toISOString().slice(0,10)}.csv`;
|
| 30 |
+
a.click();
|
| 31 |
+
URL.revokeObjectURL(url);
|
| 32 |
+
alert('✅ Đã xuất file Excel (CSV) thành công!');
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function exportDeliveryPDF() {
|
| 36 |
+
console.log('[V.AI STUDIO] Exporting delivery to PDF...');
|
| 37 |
+
if (!window.cartItems || !window.cartItems.length) {
|
| 38 |
+
alert('❌ Giỏ hàng trống!');
|
| 39 |
+
return;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Simple PDF export using jsPDF if available
|
| 43 |
+
if (typeof jsPDF !== 'undefined') {
|
| 44 |
+
const doc = new jsPDF();
|
| 45 |
+
doc.setFontSize(16);
|
| 46 |
+
doc.text('PHIẾU GIAO HÀNG', 14, 20);
|
| 47 |
+
doc.setFontSize(10);
|
| 48 |
+
doc.text(`Ngày: ${new Date().toLocaleDateString('vi-VN')}`, 14, 30);
|
| 49 |
+
doc.text(`Tổng số: ${window.cartItems.length} sản phẩm`, 14, 36);
|
| 50 |
+
|
| 51 |
+
let y = 50;
|
| 52 |
+
window.cartItems.forEach((item, idx) => {
|
| 53 |
+
if (y > 280) { doc.addPage(); y = 20; }
|
| 54 |
+
doc.text(`${idx + 1}. ${item.name}`, 14, y);
|
| 55 |
+
y += 7;
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
doc.save(`giao-hang-${new Date().toISOString().slice(0,10)}.pdf`);
|
| 59 |
+
alert('✅ Đã xuất file PDF thành công!');
|
| 60 |
+
} else {
|
| 61 |
+
alert('⚠️ Thư viện jsPDF chưa được tải. Vui lòng thử xuất Excel.');
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// --- Quote Export Functions ---
|
| 66 |
+
function exportExcel() {
|
| 67 |
+
console.log('[V.AI STUDIO] Exporting quote to Excel...');
|
| 68 |
+
const table = document.getElementById('quoteTableBody');
|
| 69 |
+
if (!table || !table.rows.length) {
|
| 70 |
+
alert('❌ Báo giá trống!');
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
let csv = 'STT,Hình ảnh,Tên sản phẩm,Mã SP,Thông tin,SL,Đơn giá,Đơn giá CK,Thành tiền,Ghi chú\n';
|
| 75 |
+
// Implementation here...
|
| 76 |
+
alert('✅ Đã xuất báo giá Excel (đang triển khai...)');
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function exportPDF() {
|
| 80 |
+
console.log('[V.AI STUDIO] Exporting quote to PDF...');
|
| 81 |
+
alert('✅ Đã xuất báo giá PDF (đang triển khai...)');
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function printQuotation() {
|
| 85 |
+
console.log('[V.AI STUDIO] Printing quotation...');
|
| 86 |
+
window.print();
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function shareQuoteImage() {
|
| 90 |
+
console.log('[V.AI STUDIO] Sharing quote as image...');
|
| 91 |
+
alert('✅ Chia sẻ ảnh báo giá (đang triển khai...)');
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
function shareQuoteLink() {
|
| 95 |
+
console.log('[V.AI STUDIO] Sharing quote link...');
|
| 96 |
+
const url = window.location.href;
|
| 97 |
+
if (navigator.share) {
|
| 98 |
+
navigator.share({ title: 'Báo giá V.AI STUDIO', url: url });
|
| 99 |
+
} else {
|
| 100 |
+
prompt('Copy link báo giá:', url);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// --- VAI_ORDERS Module ---
|
| 105 |
+
window.VAI_ORDERS = window.VAI_ORDERS || {
|
| 106 |
+
save: function() {
|
| 107 |
+
console.log('[V.AI STUDIO] Saving order...');
|
| 108 |
+
const orderData = {
|
| 109 |
+
date: new Date().toISOString(),
|
| 110 |
+
customer: {
|
| 111 |
+
name: document.getElementById('qcName')?.value || '',
|
| 112 |
+
phone: document.getElementById('qcPhone')?.value || '',
|
| 113 |
+
email: document.getElementById('qcEmail')?.value || '',
|
| 114 |
+
address: document.getElementById('qcAddr')?.value || ''
|
| 115 |
+
},
|
| 116 |
+
items: window.cartItems || [],
|
| 117 |
+
total: window.cartTotal || 0
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
// Save to localStorage
|
| 121 |
+
let orders = JSON.parse(localStorage.getItem('VAI_ORDERS') || '[]');
|
| 122 |
+
orders.push(orderData);
|
| 123 |
+
localStorage.setItem('VAI_ORDERS', JSON.stringify(orders));
|
| 124 |
+
|
| 125 |
+
alert('✅ Đã lưu đơn hàng thành công!');
|
| 126 |
+
console.log('[V.AI STUDIO] Order saved:', orderData);
|
| 127 |
+
},
|
| 128 |
+
|
| 129 |
+
openSearch: function() {
|
| 130 |
+
console.log('[V.AI STUDIO] Opening order search...');
|
| 131 |
+
const orders = JSON.parse(localStorage.getItem('VAI_ORDERS') || '[]');
|
| 132 |
+
if (!orders.length) {
|
| 133 |
+
alert('📋 Chưa có đơn hàng nào được lưu.');
|
| 134 |
+
return;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
let msg = `📋 Tìm thấy ${orders.length} đơn hàng:\n\n`;
|
| 138 |
+
orders.slice(-5).forEach((order, idx) => {
|
| 139 |
+
msg += `${idx + 1}. ${order.customer.name || 'Không tên'} - ${order.customer.phone || ''}\n`;
|
| 140 |
+
});
|
| 141 |
+
alert(msg);
|
| 142 |
+
}
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
// --- Initialize cart items if not exists ---
|
| 146 |
+
window.cartItems = window.cartItems || [];
|
| 147 |
+
window.cartTotal = window.cartTotal || 0;
|
| 148 |
+
|
| 149 |
+
console.log('[V.AI STUDIO] Export functions loaded successfully!');
|
final-gh-fix-inject.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* V.AI STUDIO - Final GH Excel/PDF Fix Inject v1047
|
| 3 |
-
*
|
| 4 |
-
* TỰ ĐỘNG INJECT script vào <head> mà không cần chỉnh sửa index.html
|
| 5 |
-
* Chỉ cần thêm: <script src="final-gh-fix-inject.js"></script> vào index.html
|
| 6 |
-
*/
|
| 7 |
-
(function() {
|
| 8 |
-
'use strict';
|
| 9 |
-
|
| 10 |
-
function injectFix() {
|
| 11 |
-
if (document.getElementById('vai-gh-fix-v1047')) return;
|
| 12 |
-
|
| 13 |
-
var s = document.createElement('script');
|
| 14 |
-
s.id = 'vai-gh-fix-v1047';
|
| 15 |
-
s.src = 'auto-patch-gh-excel.js?v=1047';
|
| 16 |
-
document.head.appendChild(s);
|
| 17 |
-
console.log('[Inject] GH fix injected');
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
if (document.readyState === 'loading') {
|
| 21 |
-
document.addEventListener('DOMContentLoaded', injectFix);
|
| 22 |
-
} else {
|
| 23 |
-
injectFix();
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
// Inline fallback after 3s
|
| 27 |
-
setTimeout(function() {
|
| 28 |
-
if (typeof window.VAI_QR === 'undefined' || !window.exportDeliveryExcel._patched) {
|
| 29 |
-
var inlineFix = document.createElement('script');
|
| 30 |
-
inlineFix.textContent = `(function(){
|
| 31 |
-
'use strict';
|
| 32 |
-
|
| 33 |
-
function orderToVAIData(order){
|
| 34 |
-
if(!order||!order.items)return null;
|
| 35 |
-
return{
|
| 36 |
-
qd:{
|
| 37 |
-
customer:{name:order.customer||'',phone:order.phone||'',email:order.email||'',addr:order.addr||'',date:order.date||''},
|
| 38 |
-
items:(order.items||[]).map(function(it,i){
|
| 39 |
-
return{stt:i+1,image:it.image||'',name:it.name||'',model:it.model||'',specs:it.specs||it.info||'',qty:it.qty||1,price:it.price||it.listPrice||it.discPrice||0,discPrice:it.discPrice||it.price||0,total:it.total||0,note:it.note||''};
|
| 40 |
-
}),
|
| 41 |
-
grandTotal:order.grandTotal||0
|
| 42 |
-
}
|
| 43 |
-
};
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
function setBtnState(btn,exporting){
|
| 47 |
-
if(!btn)return;
|
| 48 |
-
if(exporting){
|
| 49 |
-
btn.disabled=true;btn.dataset.vaiExporting='1';btn._origHtml=btn.innerHTML;btn.innerHTML='<span style="opacity:0.7">⏳</span>';
|
| 50 |
-
}else{
|
| 51 |
-
btn.disabled=false;btn.dataset.vaiExporting='0';if(btn._origHtml)btn.innerHTML=btn._origHtml;
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
window.exportDeliveryExcel._patched=true;
|
| 56 |
-
window.exportDeliveryPDF._patched=true;
|
| 57 |
-
|
| 58 |
-
var origDE=window.exportDeliveryExcel;
|
| 59 |
-
var origDP=window.exportDeliveryPDF;
|
| 60 |
-
|
| 61 |
-
window.exportDeliveryExcel=async function(order,opt){
|
| 62 |
-
var btn=document.activeElement;
|
| 63 |
-
if(btn&&btn.dataset.vaiExporting==='1')return;
|
| 64 |
-
if(btn)setBtnState(btn,true);
|
| 65 |
-
try{
|
| 66 |
-
if(order&&order.items&&order.items.length){
|
| 67 |
-
var vaData=orderToVAIData(order);var code=order.code||'GH-'+Date.now();
|
| 68 |
-
if(window.VAI_QR&&window.VAI_QR._doExportDeliveryExcel){
|
| 69 |
-
await window.VAI_QR._doExportDeliveryExcel(vaData.qd,code);return;
|
| 70 |
-
}
|
| 71 |
-
if(window.ExcelJS){
|
| 72 |
-
var wb=new ExcelJS.Workbook(),ws=wb.addWorksheet('Giao hàng');
|
| 73 |
-
ws.views=[{showGridLines:false}];ws.columns=[{width:5},{width:34},{width:14},{width:8},{width:20}];
|
| 74 |
-
ws.getCell('A1').value='PHIẾU GIAO HÀNG';
|
| 75 |
-
ws.getCell('A2').value='KH: '+(vaData.qd.customer.name||'');
|
| 76 |
-
ws.getCell('D2').value='Mã: '+code;
|
| 77 |
-
ws.getCell('A3').value='SĐT: '+(vaData.qd.customer.phone||'');
|
| 78 |
-
ws.getCell('A4').value='Địa chỉ: '+(vaData.qd.customer.addr||'');
|
| 79 |
-
var hr=ws.getRow(6);['STT','Tên sản phẩm','Mã SP','SL','Ghi chú'].forEach(function(h,i){
|
| 80 |
-
var c=hr.getCell(i+1);c.value=h;c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF003F62'}};c.font={bold:true,color:{argb:'FFFFFFFF'}};
|
| 81 |
-
});
|
| 82 |
-
var cr=7;
|
| 83 |
-
(vaData.qd.items||[]).forEach(function(it){
|
| 84 |
-
var row=ws.getRow(cr);row.getCell(1).value=it.stt;row.getCell(2).value=it.name;row.getCell(3).value=it.model;row.getCell(4).value=it.qty;row.getCell(5).value=it.note;cr++;
|
| 85 |
-
});
|
| 86 |
-
var buf=await wb.xlsx.writeBuffer();var blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
| 87 |
-
var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download='GH-'+code+'.xlsx';a.click();
|
| 88 |
-
setTimeout(function(){URL.revokeObjectURL(url);},300000);
|
| 89 |
-
return;
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
if(origDE)await origDE.call(this,order,opt);
|
| 93 |
-
}catch(e){console.error('[Fix] Error:',e);if(typeof showToast==='function')showToast('❌ Xuất file thất bại');}
|
| 94 |
-
finally{setTimeout(function(){if(btn)setBtnState(btn,false);},1000);}
|
| 95 |
-
};
|
| 96 |
-
|
| 97 |
-
window.exportDeliveryPDF=async function(order,opt){
|
| 98 |
-
var btn=document.activeElement;
|
| 99 |
-
if(btn&&btn.dataset.vaiExporting==='1')return;
|
| 100 |
-
if(btn)setBtnState(btn,true);
|
| 101 |
-
try{
|
| 102 |
-
if(order&&order.items&&order.items.length){
|
| 103 |
-
var vaData=orderToVAIData(order);var code=order.code||'GH-'+Date.now();
|
| 104 |
-
if(window.VAI_QR&&window.VAI_QR._doExportDeliveryPDF){
|
| 105 |
-
await window.VAI_QR._doExportDeliveryPDF(vaData.qd,code);return;
|
| 106 |
-
}
|
| 107 |
-
if(window.html2canvas&&window.jspdf){
|
| 108 |
-
var html='<div style="padding:30px;font-family:Inter,Arial,sans-serif"><div style="font-size:20px;font-weight:800;color:#003f62;margin-bottom:10px">PHIẾU GIAO HÀNG</div><div style="margin-bottom:10px"><b>KH:</b> '+(vaData.qd.customer.name||'')+' | <b>SĐT:</b> '+(vaData.qd.customer.phone||'')+'<br><b>Mã:</b> '+code+'</div><table style="width:100%;border-collapse:collapse"><thead><tr style="background:#003f62;color:#fff"><th style="padding:6px">STT</th><th style="padding:6px">Tên</th><th style="padding:6px">Mã</th><th style="padding:6px">SL</th><th style="padding:6px">Ghi chú</th></tr></thead><tbody>';
|
| 109 |
-
(vaData.qd.items||[]).forEach(function(it){html+='<tr><td style="padding:6px;text-align:center">'+it.stt+'</td><td style="padding:6px">'+it.name+'</td><td style="padding:6px">'+it.model+'</td><td style="padding:6px;text-align:center">'+it.qty+'</td><td style="padding:6px">'+it.note+'</td></tr>';});
|
| 110 |
-
html+='</tbody></table></div>';
|
| 111 |
-
var div=document.createElement('div');div.style.cssText='position:absolute;left:-9999px;top:0;width:700px;background:#fff;padding:28px';div.innerHTML=html;document.body.appendChild(div);
|
| 112 |
-
await new Promise(r=>setTimeout(r,300));
|
| 113 |
-
var canvas=await html2canvas(div,{scale:2,useCORS:true,backgroundColor:'#fff'});
|
| 114 |
-
var imgData=canvas.toDataURL('image/png');
|
| 115 |
-
var jsPDF=window.jspdf.jsPDF;
|
| 116 |
-
var pdf=new jsPDF({orientation:'p',unit:'pt',format:'a4'});
|
| 117 |
-
var margin=25,usableW=595-margin*2,ratio=canvas.height/canvas.width,imgH=usableW*ratio;
|
| 118 |
-
pdf.addImage(imgData,'PNG',margin,margin,usableW,imgH>842-margin*2?842-margin*2:imgH);
|
| 119 |
-
pdf.save('GH-'+code+'.pdf');div.remove();return;
|
| 120 |
-
}
|
| 121 |
-
}
|
| 122 |
-
if(origDP)await origDP.call(this,order,opt);
|
| 123 |
-
}catch(e){console.error('[Fix] Error:',e);if(typeof showToast==='function')showToast('❌ Xuất file thất bại');}
|
| 124 |
-
finally{setTimeout(function(){if(btn)setBtnState(btn,false);},1000);}
|
| 125 |
-
};
|
| 126 |
-
|
| 127 |
-
console.log('[Fix v1047] GH export ready');
|
| 128 |
-
})();`;
|
| 129 |
-
document.head.appendChild(inlineFix);
|
| 130 |
-
}
|
| 131 |
-
}, 3000);
|
| 132 |
-
|
| 133 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gh-download-fix-auto.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* GH Download Fix - Auto patch for index.html
|
| 3 |
-
* This script auto-patches the exportDeliveryExcel/PDF functions on page load
|
| 4 |
-
*
|
| 5 |
-
* FIXES:
|
| 6 |
-
* 1. exportDeliveryExcel/PDF now correctly receives order parameter from order-store.js
|
| 7 |
-
* 2. Click protection prevents multiple rapid downloads
|
| 8 |
-
* 3. Blob URLs are retained longer to allow multiple downloads
|
| 9 |
-
*/
|
| 10 |
-
(function() {
|
| 11 |
-
'use strict';
|
| 12 |
-
|
| 13 |
-
// Wait for all scripts to load
|
| 14 |
-
var PATCH_DELAYS = [500, 2000, 5000];
|
| 15 |
-
|
| 16 |
-
PATCH_DELAYS.forEach(function(delay) {
|
| 17 |
-
setTimeout(applyPatch, delay);
|
| 18 |
-
});
|
| 19 |
-
|
| 20 |
-
// Also run on DOM ready
|
| 21 |
-
if (document.readyState !== 'loading') {
|
| 22 |
-
setTimeout(applyPatch, 100);
|
| 23 |
-
} else {
|
| 24 |
-
document.addEventListener('DOMContentLoaded', function() {
|
| 25 |
-
setTimeout(applyPatch, 100);
|
| 26 |
-
});
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
function applyPatch() {
|
| 30 |
-
// Only patch once
|
| 31 |
-
if (window.__ghDownloadPatched) return;
|
| 32 |
-
|
| 33 |
-
// Check if functions exist
|
| 34 |
-
if (typeof window.exportDeliveryExcel === 'undefined' ||
|
| 35 |
-
typeof window.exportDeliveryPDF === 'undefined') return;
|
| 36 |
-
|
| 37 |
-
window.__ghDownloadPatched = true;
|
| 38 |
-
|
| 39 |
-
// Store original functions
|
| 40 |
-
var origDE = window.exportDeliveryExcel;
|
| 41 |
-
var origDP = window.exportDeliveryPDF;
|
| 42 |
-
|
| 43 |
-
// === PATCH exportDeliveryExcel ===
|
| 44 |
-
window.exportDeliveryExcel = function(order, opt) {
|
| 45 |
-
var btn = document.activeElement;
|
| 46 |
-
|
| 47 |
-
// Click protection
|
| 48 |
-
if (btn && btn.dataset.vaiExporting === '1') {
|
| 49 |
-
console.log('[GH Fix] Download in progress, ignoring click');
|
| 50 |
-
return;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
if (btn) {
|
| 54 |
-
btn.dataset.vaiExporting = '1';
|
| 55 |
-
var origHtml = btn.innerHTML;
|
| 56 |
-
btn.dataset.vaiOrigHtml = origHtml;
|
| 57 |
-
btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
// Use VAI_QR._doExportDeliveryExcel if available (has full implementation)
|
| 61 |
-
if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryExcel === 'function') {
|
| 62 |
-
// If order provided, let VAI_QR handle it
|
| 63 |
-
var result = window.VAI_QR.exportDeliveryExcel(order, opt);
|
| 64 |
-
setTimeout(function() {
|
| 65 |
-
if (btn) {
|
| 66 |
-
btn.dataset.vaiExporting = '0';
|
| 67 |
-
if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
|
| 68 |
-
}
|
| 69 |
-
}, 2000);
|
| 70 |
-
return result;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
// Fallback to original
|
| 74 |
-
if (origDE) {
|
| 75 |
-
var r = origDE.call(this, order, opt);
|
| 76 |
-
setTimeout(function() {
|
| 77 |
-
if (btn) {
|
| 78 |
-
btn.dataset.vaiExporting = '0';
|
| 79 |
-
if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
|
| 80 |
-
}
|
| 81 |
-
}, 2000);
|
| 82 |
-
return r;
|
| 83 |
-
}
|
| 84 |
-
};
|
| 85 |
-
|
| 86 |
-
// === PATCH exportDeliveryPDF ===
|
| 87 |
-
window.exportDeliveryPDF = function(order, opt) {
|
| 88 |
-
var btn = document.activeElement;
|
| 89 |
-
|
| 90 |
-
if (btn && btn.dataset.vaiExporting === '1') return;
|
| 91 |
-
|
| 92 |
-
if (btn) {
|
| 93 |
-
btn.dataset.vaiExporting = '1';
|
| 94 |
-
var origHtml = btn.innerHTML;
|
| 95 |
-
btn.dataset.vaiOrigHtml = origHtml;
|
| 96 |
-
btn.innerHTML = '<span style="opacity:0.7">⏳</span>';
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
if (window.VAI_QR && typeof window.VAI_QR.exportDeliveryPDF === 'function') {
|
| 100 |
-
var result = window.VAI_QR.exportDeliveryPDF(order, opt);
|
| 101 |
-
setTimeout(function() {
|
| 102 |
-
if (btn) {
|
| 103 |
-
btn.dataset.vaiExporting = '0';
|
| 104 |
-
if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
|
| 105 |
-
}
|
| 106 |
-
}, 2000);
|
| 107 |
-
return result;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
if (origDP) {
|
| 111 |
-
var r = origDP.call(this, order, opt);
|
| 112 |
-
setTimeout(function() {
|
| 113 |
-
if (btn) {
|
| 114 |
-
btn.dataset.vaiExporting = '0';
|
| 115 |
-
if (btn.dataset.vaiOrigHtml) btn.innerHTML = btn.dataset.vaiOrigHtml;
|
| 116 |
-
}
|
| 117 |
-
}, 2000);
|
| 118 |
-
return r;
|
| 119 |
-
}
|
| 120 |
-
};
|
| 121 |
-
|
| 122 |
-
console.log('[GH Fix] Auto-patched exportDeliveryExcel/PDF');
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
// Also patch URL.revokeObjectURL for longer blob retention
|
| 126 |
-
if (!window.__blobRetainPatched) {
|
| 127 |
-
window.__blobRetainPatched = true;
|
| 128 |
-
var origRevoke = URL.revokeObjectURL;
|
| 129 |
-
URL.revokeObjectURL = function(url) {
|
| 130 |
-
if (!url || !url.startsWith('blob:')) {
|
| 131 |
-
return origRevoke.call(URL, url);
|
| 132 |
-
}
|
| 133 |
-
// Don't revoke immediately - let browser handle cleanup
|
| 134 |
-
// This allows multiple downloads from same blob
|
| 135 |
-
return;
|
| 136 |
-
};
|
| 137 |
-
}
|
| 138 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
grob-img-fix.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* V.AI STUDIO — Grob Image Fix
|
| 3 |
+
* ==============================
|
| 4 |
+
* Fixes Grob product images: imgs[0] (grob_products_img/) are 404.
|
| 5 |
+
* Swaps the primary image (p.i) to imgs[1] and removes imgs[0].
|
| 6 |
+
* Applied to all Grob brand products with >= 2 gallery images.
|
| 7 |
+
*/
|
| 8 |
+
(function() {
|
| 9 |
+
'use strict';
|
| 10 |
+
if (window.__GROB_IMG_FIX__) return;
|
| 11 |
+
window.__GROB_IMG_FIX__ = true;
|
| 12 |
+
|
| 13 |
+
function fixGrobImages() {
|
| 14 |
+
if (typeof window.D === 'undefined' || !Array.isArray(window.D) || !window.D.length) return;
|
| 15 |
+
|
| 16 |
+
var fixed = 0;
|
| 17 |
+
var skipped = 0;
|
| 18 |
+
|
| 19 |
+
for (var i = 0; i < window.D.length; i++) {
|
| 20 |
+
var p = window.D[i];
|
| 21 |
+
if (!p) continue;
|
| 22 |
+
|
| 23 |
+
// Only process Grob brand products
|
| 24 |
+
if (p.brand !== 'Grob') continue;
|
| 25 |
+
|
| 26 |
+
var imgs = p.images || [];
|
| 27 |
+
if (imgs.length >= 2) {
|
| 28 |
+
// imgs[0] is broken (grob_products_img/ -> 404)
|
| 29 |
+
// imgs[1] works (malloca-website/grob_img/)
|
| 30 |
+
|
| 31 |
+
// Set primary image to imgs[1]
|
| 32 |
+
p.image = imgs[1];
|
| 33 |
+
|
| 34 |
+
// Remove imgs[0], keep rest as gallery
|
| 35 |
+
p.images = imgs.slice(1);
|
| 36 |
+
|
| 37 |
+
fixed++;
|
| 38 |
+
} else {
|
| 39 |
+
skipped++;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if (fixed > 0) {
|
| 44 |
+
console.log('[Grob Img Fix] Fixed ' + fixed + ' product(s) in D (swapped imgs[0]->broken with imgs[1]->ok). Skipped ' + skipped + ' (only 1 image).');
|
| 45 |
+
|
| 46 |
+
// Trigger UI refresh if available
|
| 47 |
+
if (typeof window.refreshUI === 'function') {
|
| 48 |
+
window.refreshUI();
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Patch after D is populated
|
| 54 |
+
function patchD() {
|
| 55 |
+
fixGrobImages();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Try patching at various intervals as data loads
|
| 59 |
+
function onReady() {
|
| 60 |
+
setTimeout(patchD, 200);
|
| 61 |
+
setTimeout(patchD, 500);
|
| 62 |
+
setTimeout(patchD, 1000);
|
| 63 |
+
setTimeout(patchD, 2000);
|
| 64 |
+
setTimeout(patchD, 3000);
|
| 65 |
+
setTimeout(patchD, 5000);
|
| 66 |
+
setTimeout(patchD, 8000);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if (document.readyState === 'loading') {
|
| 70 |
+
document.addEventListener('DOMContentLoaded', onReady);
|
| 71 |
+
} else {
|
| 72 |
+
onReady();
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
console.log('[Grob Img Fix] Loaded: ready to fix 471 Grob product images (imgs[0] -> imgs[1])');
|
| 76 |
+
})();
|
grob-price-fix.js
CHANGED
|
@@ -43,30 +43,11 @@
|
|
| 43 |
});
|
| 44 |
if (patched > 0) {
|
| 45 |
console.log('[Grob Price Fix] Patched ' + patched + ' product(s) in D');
|
| 46 |
-
// Also sync prices for items already in cart
|
| 47 |
-
patchCart();
|
| 48 |
if (typeof window.refreshUI === 'function') window.refreshUI();
|
| 49 |
}
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
| 53 |
-
// Sync Grob prices in cart (localStorage)
|
| 54 |
-
function patchCart() {
|
| 55 |
-
try {
|
| 56 |
-
var cart = JSON.parse(localStorage.getItem('malloca_cart') || '[]');
|
| 57 |
-
if (!Array.isArray(cart) || !cart.length) return;
|
| 58 |
-
var changed = false;
|
| 59 |
-
cart.forEach(function(item) {
|
| 60 |
-
if (patchProduct(item)) changed = true;
|
| 61 |
-
});
|
| 62 |
-
if (changed) {
|
| 63 |
-
localStorage.setItem('malloca_cart', JSON.stringify(cart));
|
| 64 |
-
window.cart = cart;
|
| 65 |
-
console.log('[Grob Price Fix] Synced prices in cart');
|
| 66 |
-
}
|
| 67 |
-
} catch(e) {}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
// Try patching at various intervals as data loads
|
| 71 |
function onReady() {
|
| 72 |
setTimeout(patchD, 200);
|
|
|
|
| 43 |
});
|
| 44 |
if (patched > 0) {
|
| 45 |
console.log('[Grob Price Fix] Patched ' + patched + ' product(s) in D');
|
|
|
|
|
|
|
| 46 |
if (typeof window.refreshUI === 'function') window.refreshUI();
|
| 47 |
}
|
| 48 |
}
|
| 49 |
}
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
// Try patching at various intervals as data loads
|
| 52 |
function onReady() {
|
| 53 |
setTimeout(patchD, 200);
|
grob_img/grob_p1.jpeg
DELETED
|
Binary file (80 kB)
|
|
|
grob_img/grob_p10.jpeg
DELETED
Git LFS Details
|
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)
|
|
|