mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:05:24 +02:00
perf: fix critical O(n²) bottleneck in mergeSalesAndAdsData
- The ads-only metadata lookup was doing a nested forEach (O(ads × sales)) - Now pre-builds asinMetadata map during sales aggregation for O(1) lookups - This reduces complexity from O(n²) to O(n) for large datasets - Should dramatically improve loading time for Weekly Sales tab
This commit is contained in:
+15
-14
@@ -446,6 +446,9 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
const createKey = (asin: string, customer: string, year: number, week: number) =>
|
const createKey = (asin: string, customer: string, year: number, week: number) =>
|
||||||
`${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`;
|
`${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<string, { sku: string; title: string; line: string }>();
|
||||||
|
|
||||||
// 1. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs)
|
// 1. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs)
|
||||||
const salesMap = new Map<string, {
|
const salesMap = new Map<string, {
|
||||||
sellOut: number;
|
sellOut: number;
|
||||||
@@ -462,15 +465,21 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
|
|
||||||
salesData.forEach(sale => {
|
salesData.forEach(sale => {
|
||||||
const weekNum = sale.week || 0;
|
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);
|
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)) {
|
if (salesMap.has(key)) {
|
||||||
const existing = salesMap.get(key)!;
|
const existing = salesMap.get(key)!;
|
||||||
existing.sellOut += sale.sellOut;
|
existing.sellOut += sale.sellOut;
|
||||||
existing.units += sale.units;
|
existing.units += sale.units;
|
||||||
// Keep the best metadata (longest title, first non-empty SKU)
|
|
||||||
if (sale.title && sale.title.length > (existing.title?.length || 0)) {
|
if (sale.title && sale.title.length > (existing.title?.length || 0)) {
|
||||||
existing.title = sale.title;
|
existing.title = sale.title;
|
||||||
}
|
}
|
||||||
@@ -513,7 +522,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
const mergedData: CombinedKPIs[] = [];
|
const mergedData: CombinedKPIs[] = [];
|
||||||
const processedKeys = new Set<string>();
|
const processedKeys = new Set<string>();
|
||||||
|
|
||||||
// 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) => {
|
salesMap.forEach((sale, key) => {
|
||||||
processedKeys.add(key);
|
processedKeys.add(key);
|
||||||
const ad = adsMap.get(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 salesOrganic = Math.max(0, salesTotal - adSales);
|
||||||
const unitsOrganic = Math.max(0, unitsTotal - adUnits);
|
const unitsOrganic = Math.max(0, unitsTotal - adUnits);
|
||||||
|
|
||||||
// KPIs
|
|
||||||
const acos = adSales > 0 ? (adCost / adSales) * 100 : 0;
|
const acos = adSales > 0 ? (adCost / adSales) * 100 : 0;
|
||||||
const tacos = salesTotal > 0 ? (adCost / salesTotal) * 100 : 0;
|
const tacos = salesTotal > 0 ? (adCost / salesTotal) * 100 : 0;
|
||||||
const roas = adCost > 0 ? adSales / adCost : 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) => {
|
adsMap.forEach((ad, key) => {
|
||||||
if (!processedKeys.has(key)) {
|
if (!processedKeys.has(key)) {
|
||||||
// Look up metadata from sales data for this ASIN (any week)
|
const metaKey = `${ad.asin.trim().toUpperCase()}|${ad.country.trim().toUpperCase()}`;
|
||||||
let meta: { sku: string; title: string; line: string } | undefined;
|
const meta = asinMetadata.get(metaKey);
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mergedData.push({
|
mergedData.push({
|
||||||
id: `ads-only-${key}`,
|
id: `ads-only-${key}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user