/** * 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!'); }); })();