fix: ensure Weekly Sales filters update totals and isolate products

- App.tsx: Add globalAsinMetadata to provide context for ad filtering
- dataProcessor.ts: Update filterAdsData to respect product line filters via metadata
- dataProcessor.ts: Update mergeSalesAndAdsData to use global metadata for ads-only records
- Ensures consistent calculation of totals and visibility when filtering by Line or ASIN
This commit is contained in:
Christian Vidal Wolf
2026-01-22 13:07:43 +01:00
parent 0214509ffe
commit 9357aed662
2 changed files with 52 additions and 22 deletions
+16 -3
View File
@@ -250,8 +250,21 @@ const App: React.FC = () => {
// Derive Data
const globalAsinMetadata = useMemo(() => {
const metaMap = new Map<string, { sku: string; title: string; line: string }>();
rawData.forEach(r => {
const asin = r.asin.trim().toUpperCase();
const existing = metaMap.get(asin);
// Keep most complete title
if (!existing || (r.title && r.title.length > (existing.title?.length || 0))) {
metaMap.set(asin, { sku: r.sku, title: r.title, line: r.line });
}
});
return metaMap;
}, [rawData]);
const filteredData = useMemo(() => filterData(rawData, filters), [rawData, filters]);
const filteredAdsData = useMemo(() => filterAdsData(adsData, filters), [adsData, filters]);
const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata), [adsData, filters, globalAsinMetadata]);
const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]);
// Calculate country-aware Top 50 Best Sellers for 2025
@@ -299,8 +312,8 @@ const App: React.FC = () => {
// Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => {
return mergeSalesAndAdsData(filteredData, filteredAdsData);
}, [filteredData, filteredAdsData]);
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata);
}, [filteredData, filteredAdsData, globalAsinMetadata]);
// Derive Context Data (Product Line Context when drilling down)
const contextAggregatedData = useMemo(() => {
+36 -19
View File
@@ -441,15 +441,19 @@ export const processAdsExcel = async (fileOrBuffer: File | ArrayBuffer): Promise
// --- DATA MERGING ---
export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecord[]): CombinedKPIs[] => {
export const mergeSalesAndAdsData = (
salesData: SalesRecord[],
adsData: AdsRecord[],
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>
): CombinedKPIs[] => {
// Key for both sales and ads: ASIN|Customer|Year|Week
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<string, { sku: string; title: string; line: string }>();
// 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales
const asinMetadata = asinMetadataMap || new Map<string, { sku: string; title: string; line: string }>();
// 1. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs)
// 2. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs)
const salesMap = new Map<string, {
sellOut: number;
units: number;
@@ -469,11 +473,13 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
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 no global map provided, build it on the fly
if (!asinMetadataMap) {
const metaKey = sale.asin.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)) {
@@ -502,7 +508,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
}
});
// 2. Aggregate Ads by ASIN|Customer|Year|Week
// 3. Aggregate Ads by ASIN|Customer|Year|Week
const adsMap = new Map<string, AdsRecord>();
adsData.forEach(ad => {
const key = createKey(ad.asin, ad.country, ad.year, ad.week);
@@ -522,7 +528,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
const mergedData: CombinedKPIs[] = [];
const processedKeys = new Set<string>();
// 3. Create ONE record per ASIN/Customer/Year/Week from sales
// 4. Create ONE record per ASIN/Customer/Year/Week from sales
salesMap.forEach((sale, key) => {
processedKeys.add(key);
const ad = adsMap.get(key);
@@ -552,7 +558,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
month: sale.month,
week: sale.week,
year: sale.year,
asin: sale.asin,
asin: sale.asin.trim().toUpperCase(),
title: sale.title,
line: sale.line,
sku: sale.sku,
@@ -576,11 +582,11 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
});
});
// 4. Add ads-only records - use O(1) metadata lookup instead of O(n) loop
// 5. Add ads-only records - use provided metadata for line/title
adsMap.forEach((ad, key) => {
if (!processedKeys.has(key)) {
const metaKey = `${ad.asin.trim().toUpperCase()}|${ad.country.trim().toUpperCase()}`;
const meta = asinMetadata.get(metaKey);
const asin = ad.asin.trim().toUpperCase();
const meta = asinMetadata.get(asin);
mergedData.push({
id: `ads-only-${key}`,
@@ -589,7 +595,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
month: 'N/A',
week: ad.week,
year: ad.year,
asin: ad.asin,
asin: asin,
title: meta?.title || ad.asin,
line: meta?.line || 'Unassigned',
sku: meta?.sku || '',
@@ -621,8 +627,12 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
// --- EXISTING HELPERS ---
// Filter Ads Data by Country, Year, Week, and ASIN
export const filterAdsData = (adsData: AdsRecord[], filters: FilterState): AdsRecord[] => {
// Filter Ads Data by Country, Year, Week, ASIN, and Product Line
export const filterAdsData = (
adsData: AdsRecord[],
filters: FilterState,
asinMetadata?: Map<string, { line: string }>
): AdsRecord[] => {
return adsData.filter(ad => {
// Country/Customer match (ads use 'country', sales use 'customer')
const countryMatch = filters.customer.length === 0
@@ -641,7 +651,14 @@ export const filterAdsData = (adsData: AdsRecord[], filters: FilterState): AdsRe
const asinMatch = filters.asin.length === 0 ||
filters.asin.some(a => a.toUpperCase() === ad.asin.toUpperCase());
return countryMatch && yearMatch && weekMatch && asinMatch;
// Line match (Requires metadata)
let lineMatch = true;
if (filters.line.length > 0 && asinMetadata) {
const meta = asinMetadata.get(ad.asin.trim().toUpperCase());
lineMatch = meta ? filters.line.includes(meta.line) : false;
}
return countryMatch && yearMatch && weekMatch && asinMatch && lineMatch;
});
};