V.AISTUDIO2 / comprehensive-fix.js
bep40's picture
Khôi phục file từ commit a3b87f83469d
94211e0 verified
Raw
History Blame Contribute Delete
16.9 kB
/**
* 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!');
});
})();