diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 5a5b30e..45c820d 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -446,6 +446,9 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor const createKey = (asin: string, customer: string, year: number, week: number) => `${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`; + // Pre-build metadata lookup map for O(1) access (instead of O(n) nested loop) + const asinMetadata = new Map(); + // 1. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs) const salesMap = new Map { const weekNum = sale.week || 0; - if (weekNum === 0) return; // Skip records without week data for weekly analysis + if (weekNum === 0) return; const key = createKey(sale.asin, sale.customer, sale.year, weekNum); + // Build metadata map: ASIN|Customer -> best metadata + const metaKey = `${sale.asin.trim().toUpperCase()}|${sale.customer.trim().toUpperCase()}`; + const existingMeta = asinMetadata.get(metaKey); + if (!existingMeta || (sale.title && sale.title.length > (existingMeta.title?.length || 0))) { + asinMetadata.set(metaKey, { sku: sale.sku, title: sale.title, line: sale.line }); + } + if (salesMap.has(key)) { const existing = salesMap.get(key)!; existing.sellOut += sale.sellOut; existing.units += sale.units; - // Keep the best metadata (longest title, first non-empty SKU) if (sale.title && sale.title.length > (existing.title?.length || 0)) { existing.title = sale.title; } @@ -513,7 +522,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor const mergedData: CombinedKPIs[] = []; const processedKeys = new Set(); - // 3. Create ONE record per ASIN/Customer/Year/Week from sales, attach ads if available + // 3. Create ONE record per ASIN/Customer/Year/Week from sales salesMap.forEach((sale, key) => { processedKeys.add(key); const ad = adsMap.get(key); @@ -529,7 +538,6 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor const salesOrganic = Math.max(0, salesTotal - adSales); const unitsOrganic = Math.max(0, unitsTotal - adUnits); - // KPIs const acos = adSales > 0 ? (adCost / adSales) * 100 : 0; const tacos = salesTotal > 0 ? (adCost / salesTotal) * 100 : 0; const roas = adCost > 0 ? adSales / adCost : 0; @@ -568,18 +576,11 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor }); }); - // 4. Add ads-only records (ASINs with ads but no sales in the filtered data) + // 4. Add ads-only records - use O(1) metadata lookup instead of O(n) loop adsMap.forEach((ad, key) => { if (!processedKeys.has(key)) { - // Look up metadata from sales data for this ASIN (any week) - let meta: { sku: string; title: string; line: string } | undefined; - salesMap.forEach((sale, saleKey) => { - if (saleKey.startsWith(ad.asin.trim().toUpperCase() + '|' + ad.country.trim().toUpperCase())) { - if (!meta || sale.title?.length > meta.title?.length) { - meta = { sku: sale.sku, title: sale.title, line: sale.line }; - } - } - }); + const metaKey = `${ad.asin.trim().toUpperCase()}|${ad.country.trim().toUpperCase()}`; + const meta = asinMetadata.get(metaKey); mergedData.push({ id: `ads-only-${key}`,