Spaces:
Running
Running
| /** | |
| * V.AI STUDIO — Comprehensive Fix for Brand Discounts, Excel Formulas & Notes | |
| * ======================================================================== | |
| * Fixes: | |
| * 1. Brand-level discount parsing: "Malloca Ck% 35%", "eurogold ck% 50%" → itemDiscounts | |
| * 2. Excel discount column (Giá CK) uses FORMULA (G*0.65) not hardcoded value | |
| * 3. Notes from AI prompt are filtered out from Excel/modal (no "Ghi chú:" row) | |
| * 4. Fees (giao hàng, lắp đặt, cọc, bốc xếp) appear as separate lines in Excel | |
| * 5. Applies to both _doExportExcel and _doExportDeliveryExcel | |
| */ | |
| (function() { | |
| 'use strict'; | |
| if (window.__COMPREHENSIVE_FIX__) return; | |
| window.__COMPREHENSIVE_FIX__ = true; | |
| // Wait for VAIR_QR to be available | |
| function waitForVAIQR() { | |
| return new Promise(resolve => { | |
| if (window.VAI_QR) { | |
| resolve(window.VAI_QR); | |
| return; | |
| } | |
| let attempts = 0; | |
| const check = setInterval(() => { | |
| attempts++; | |
| if (window.VAI_QR) { | |
| clearInterval(check); | |
| resolve(window.VAI_QR); | |
| } else if (attempts > 100) { // 10 seconds | |
| clearInterval(check); | |
| console.warn('[Comprehensive Fix] VAI_QR not found after 10s'); | |
| resolve(null); | |
| } | |
| }, 100); | |
| }); | |
| } | |
| waitForVAIQR().then(VAI_QR => { | |
| if (!VAI_QR) return; | |
| // ===================================================================== | |
| // 1. PATCH parsePrompt TO HANDLE BRAND-LEVEL DISCOUNTS | |
| // ===================================================================== | |
| if (VAI_QR.parsePrompt && !VAI_QR.parsePrompt._patchedBrandDiscount) { | |
| const origParsePrompt = VAI_QR.parsePrompt; | |
| VAI_QR.parsePrompt = function(text) { | |
| // First, extract brand-level discounts BEFORE the original parser runs | |
| // Pattern: "BrandName Ck% XX%" or "BrandName ck% XX" or "BrandName chiết khấu XX%" | |
| const brandDiscounts = {}; | |
| const brandDiscountRegex = /\b([a-zA-Z][a-zA-Z0-9\s\-]{1,30}?)\s+(?:ck|chiết\s*khấu|chiet\s*khau|giảm|giam|discount)\s*([\d.,]+)\s*%/gi; | |
| let match; | |
| while ((match = brandDiscountRegex.exec(text)) !== null) { | |
| const brand = match[1].trim().toLowerCase(); | |
| const pct = parseFloat(match[2].replace(/\./g, '').replace(',', '.')); | |
| if (pct > 0 && pct <= 100) { | |
| brandDiscounts[brand] = pct; | |
| } | |
| } | |
| // Remove brand discount lines from text so they don't get parsed as fees | |
| let cleanText = text; | |
| if (Object.keys(brandDiscounts).length > 0) { | |
| cleanText = text.replace(brandDiscountRegex, ''); | |
| } | |
| // Call original parser with cleaned text | |
| const result = origParsePrompt.call(this, cleanText); | |
| // Merge brand discounts into itemDiscounts | |
| // Store them in a special property for later matching | |
| result._brandDiscounts = brandDiscounts; | |
| // Also merge into itemDiscounts for backward compatibility | |
| if (Object.keys(brandDiscounts).length > 0) { | |
| result.itemDiscounts = { ...result.itemDiscounts, ...brandDiscounts }; | |
| } | |
| return result; | |
| }; | |
| VAI_QR.parsePrompt._patchedBrandDiscount = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.parsePrompt for brand-level discounts'); | |
| } | |
| // ===================================================================== | |
| // 2. PATCH matchItemDiscount TO HANDLE BRAND MATCHING | |
| // ===================================================================== | |
| if (VAI_QR.matchItemDiscount && !VAI_QR.matchItemDiscount._patchedBrandMatch) { | |
| const origMatchItemDiscount = VAI_QR.matchItemDiscount; | |
| VAI_QR.matchItemDiscount = function(it, itemDiscounts) { | |
| // First try original matching (SKU/model/code matching) | |
| const originalResult = origMatchItemDiscount.call(this, it, itemDiscounts); | |
| if (originalResult > 0) return originalResult; | |
| // If no match, try brand matching from _brandDiscounts | |
| // Check if itemDiscounts has brand keys (lowercase, no special chars) | |
| const brandKeys = Object.keys(itemDiscounts || {}).filter(k => | |
| !k.match(/[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d/) // Not a product code pattern | |
| ); | |
| if (brandKeys.length === 0) return 0; | |
| // Get item's brand from product data | |
| let itemBrand = ''; | |
| if (it.brand) { | |
| itemBrand = it.brand.toLowerCase(); | |
| } else if (window.D) { | |
| // Try to find product in D array | |
| const idx = it.idx != null ? it.idx : (it.productIdx != null ? it.productIdx : null); | |
| let product = null; | |
| if (idx != null && window.D[idx]) { | |
| product = window.D[idx]; | |
| } else if (it.model || it.sku || it.ma || it.name) { | |
| const ck = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| product = window.D.find(x => { | |
| const xck = (x.model || x.sku || x.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| return xck.includes(ck) || ck.includes(xck); | |
| }); | |
| } | |
| if (product && product.brand) { | |
| itemBrand = product.brand.toLowerCase(); | |
| } | |
| } | |
| // Match against brand discounts | |
| const cleanItemBrand = itemBrand.replace(/[^a-z0-9]/g, ''); | |
| for (const bk of brandKeys) { | |
| const cleanBk = bk.replace(/[^a-z0-9]/g, ''); | |
| if (cleanBk && cleanItemBrand && (cleanItemBrand.includes(cleanBk) || cleanBk.includes(cleanItemBrand))) { | |
| return itemDiscounts[bk]; | |
| } | |
| } | |
| return 0; | |
| }; | |
| VAI_QR.matchItemDiscount._patchedBrandMatch = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.matchItemDiscount for brand matching'); | |
| } | |
| // ===================================================================== | |
| // 3. PATCH applyPromptDiscounts TO PROPAGATE BRAND DISCOUNTS | |
| // ===================================================================== | |
| if (VAI_QR.applyPromptDiscounts && !VAI_QR.applyPromptDiscounts._patchedBrand) { | |
| const origApplyPromptDiscounts = VAI_QR.applyPromptDiscounts; | |
| VAI_QR.applyPromptDiscounts = function(qd, parsed) { | |
| // Call original | |
| const result = origApplyPromptDiscounts.call(this, qd, parsed); | |
| // If we have brand discounts but no item-level discounts were applied, | |
| // apply them to matching items | |
| if (parsed && parsed._brandDiscounts && Object.keys(parsed._brandDiscounts).length > 0) { | |
| if (result && result.items) { | |
| result.items.forEach(it => { | |
| // Check if this item already has a discount | |
| if (!it.discountPercent || it.discountPercent === 0) { | |
| // Try to match brand | |
| let itemBrand = ''; | |
| if (it.brand) { | |
| itemBrand = it.brand.toLowerCase(); | |
| } else if (window.D) { | |
| const idx = it.idx != null ? it.idx : (it.productIdx != null ? it.productIdx : null); | |
| let product = null; | |
| if (idx != null && window.D[idx]) { | |
| product = window.D[idx]; | |
| } else if (it.model || it.sku || it.ma || it.name) { | |
| const ck = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| product = window.D.find(x => { | |
| const xck = (x.model || x.sku || x.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| return xck.includes(ck) || ck.includes(xck); | |
| }); | |
| } | |
| if (product && product.brand) { | |
| itemBrand = product.brand.toLowerCase(); | |
| } | |
| } | |
| const cleanItemBrand = itemBrand.replace(/[^a-z0-9]/g, ''); | |
| for (const [brand, pct] of Object.entries(parsed._brandDiscounts)) { | |
| const cleanBrand = brand.replace(/[^a-z0-9]/g, ''); | |
| if (cleanBrand && cleanItemBrand && (cleanItemBrand.includes(cleanBrand) || cleanBrand.includes(cleanItemBrand))) { | |
| it.discPrice = Math.round((it.price || 0) * (1 - pct / 100)); | |
| it.discountPercent = pct; | |
| it.total = (it.discPrice || it.price || 0) * (it.qty || 1); | |
| it._promptDiscount = true; | |
| break; | |
| } | |
| } | |
| } | |
| }); | |
| // Recalculate grandTotal | |
| let productTotal = 0; | |
| result.items.forEach(it => { | |
| productTotal += Number(it.total || 0); | |
| }); | |
| result.grandTotal = productTotal; | |
| } | |
| } | |
| return result; | |
| }; | |
| VAI_QR.applyPromptDiscounts._patchedBrand = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.applyPromptDiscounts for brand discounts'); | |
| } | |
| // ===================================================================== | |
| // 4. PATCH _doExportExcel FOR FORMULA-BASED DISCOUNT & NO NOTES ROW | |
| // ===================================================================== | |
| if (VAI_QR.exportExcel && !VAI_QR.exportExcel._patchedComprehensive) { | |
| const orig = VAI_QR.exportExcel; | |
| VAI_QR.exportExcel = async function(d, qd, code, qr) { | |
| // Ensure items have correct discPrice from prompt discounts | |
| if (qd && qd.items) { | |
| qd.items.forEach((it, i) => { | |
| // If item has discountPercent from prompt, ensure discPrice reflects it | |
| if (it.discountPercent && it.discountPercent > 0 && it.price) { | |
| it.discPrice = Math.round(it.price * (1 - it.discountPercent / 100)); | |
| it.total = it.discPrice * (it.qty || 1); | |
| } | |
| // Mark that this item has prompt-based discount (for formula) | |
| it._promptDiscount = (it.discountPercent && it.discountPercent > 0) || | |
| (d.itemDiscounts && matchItemDiscountForPrompt(it, d.itemDiscounts)) || | |
| (d._brandDiscounts && matchBrandDiscountForPrompt(it, d._brandDiscounts)); | |
| }); | |
| } | |
| // SUPPRESS NOTES ROW - set d.notes to empty array | |
| // Fees (giao hàng, lắp đặt, cọc, bốc xếp) are in d.fees and SHOULD appear | |
| const originalNotes = d.notes; | |
| d.notes = []; | |
| try { | |
| return await orig.call(this, d, qd, code, qr); | |
| } finally { | |
| // Restore | |
| d.notes = originalNotes; | |
| } | |
| }; | |
| VAI_QR.exportExcel._patchedComprehensive = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.exportExcel for formula discount & no notes'); | |
| } | |
| // Helper: Check if item has prompt-based item discount (product code matching) | |
| function matchItemDiscountForPrompt(it, itemDiscounts) { | |
| if (!itemDiscounts || !it) return false; | |
| const keys = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| for (const code in itemDiscounts) { | |
| const ck = code.toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| if (ck && keys && (keys.includes(ck) || ck.includes(keys)) && | |
| code.match(/[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d/)) { // product code pattern | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Helper: Check if item has prompt-based brand discount | |
| function matchBrandDiscountForPrompt(it, brandDiscounts) { | |
| if (!brandDiscounts || !it) return false; | |
| let itemBrand = ''; | |
| if (it.brand) { | |
| itemBrand = it.brand.toLowerCase(); | |
| } else if (window.D) { | |
| const idx = it.idx != null ? it.idx : (it.productIdx != null ? it.productIdx : null); | |
| let product = null; | |
| if (idx != null && window.D[idx]) { | |
| product = window.D[idx]; | |
| } else if (it.model || it.sku || it.ma || it.name) { | |
| const ck = (it.model || it.sku || it.ma || it.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| product = window.D.find(x => { | |
| const xck = (x.model || x.sku || x.name || '').toString().toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| return xck.includes(ck) || ck.includes(xck); | |
| }); | |
| } | |
| if (product && product.brand) { | |
| itemBrand = product.brand.toLowerCase(); | |
| } | |
| } | |
| const cleanItemBrand = itemBrand.replace(/[^a-z0-9]/g, ''); | |
| for (const brand in brandDiscounts) { | |
| const cleanBrand = brand.replace(/[^a-z0-9]/g, ''); | |
| if (cleanBrand && cleanItemBrand && (cleanItemBrand.includes(cleanBrand) || cleanBrand.includes(cleanItemBrand))) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // ===================================================================== | |
| // 5. PATCH _doExportDeliveryExcel FOR NO NOTES ROW | |
| // ===================================================================== | |
| if (VAI_QR.exportDeliveryExcel && !VAI_QR.exportDeliveryExcel._patchedComprehensive) { | |
| const orig2 = VAI_QR.exportDeliveryExcel; | |
| VAI_QR.exportDeliveryExcel = async function(qd, code) { | |
| // Suppress notes row | |
| const originalNotes = qd.notes; | |
| qd.notes = []; | |
| try { | |
| return await orig2.call(this, qd, code); | |
| } finally { | |
| qd.notes = originalNotes; | |
| } | |
| }; | |
| VAI_QR.exportDeliveryExcel._patchedComprehensive = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.exportDeliveryExcel for no notes'); | |
| } | |
| // ===================================================================== | |
| // 6. PATCH getData TO MARK PROMPT DISCOUNTS | |
| // ===================================================================== | |
| if (VAI_QR.getData && !VAI_QR.getData._patchedComprehensive) { | |
| const origGetData = VAI_QR.getData; | |
| VAI_QR.getData = function() { | |
| const result = origGetData.apply(this, arguments); | |
| // Mark items with prompt discount (global, item-level, or brand-level) | |
| if (result.qd && result.qd.items && (result.itemDiscounts || result.discountPercent || result._brandDiscounts)) { | |
| result.qd.items.forEach(it => { | |
| if (it.discountPercent && it.discountPercent > 0) { | |
| it._promptDiscount = true; | |
| } | |
| }); | |
| } | |
| // The notes are already in result.notes (from parsed.notes) | |
| // Excel patch will suppress them by setting d.notes = [] | |
| // Fees in result.fees will still appear (giao hàng, lắp đặt, cọc, bốc xếp) | |
| return result; | |
| }; | |
| VAI_QR.getData._patchedComprehensive = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.getData for prompt discount marking'); | |
| } | |
| // ===================================================================== | |
| // 7. PATCH buildQuoteHTML (PDF/Image export) TO SUPPRESS NOTES | |
| // ===================================================================== | |
| if (VAI_QR.buildQuoteHTML && !VAI_QR.buildQuoteHTML._patchedComprehensive) { | |
| const origBuildQuoteHTML = VAI_QR.buildQuoteHTML; | |
| VAI_QR.buildQuoteHTML = function(d, code, qrUrl) { | |
| // Suppress notes in HTML export too | |
| const originalNotes = d.notes; | |
| d.notes = []; | |
| try { | |
| return origBuildQuoteHTML.call(this, d, code, qrUrl); | |
| } finally { | |
| d.notes = originalNotes; | |
| } | |
| }; | |
| VAI_QR.buildQuoteHTML._patchedComprehensive = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.buildQuoteHTML for no notes'); | |
| } | |
| // ===================================================================== | |
| // 8. PATCH buildDeliveryHTML (PDF/Image export) TO SUPPRESS NOTES | |
| // ===================================================================== | |
| if (VAI_QR.buildDeliveryHTML && !VAI_QR.buildDeliveryHTML._patchedComprehensive) { | |
| const origBuildDeliveryHTML = VAI_QR.buildDeliveryHTML; | |
| VAI_QR.buildDeliveryHTML = function(d, code, qrUrl) { | |
| // Suppress notes in delivery HTML export too | |
| const originalNotes = d.notes; | |
| d.notes = []; | |
| try { | |
| return origBuildDeliveryHTML.call(this, d, code, qrUrl); | |
| } finally { | |
| d.notes = originalNotes; | |
| } | |
| }; | |
| VAI_QR.buildDeliveryHTML._patchedComprehensive = true; | |
| console.log('[Comprehensive Fix] Patched VAI_QR.buildDeliveryHTML for no notes'); | |
| } | |
| console.log('[Comprehensive Fix] All 8 patches applied successfully!'); | |
| }); | |
| })(); |