mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:45:24 +02:00
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:
@@ -250,8 +250,21 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
|
|
||||||
// Derive Data
|
// 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 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]);
|
const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]);
|
||||||
|
|
||||||
// Calculate country-aware Top 50 Best Sellers for 2025
|
// 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
|
// Combine Sales & Ads Data dynamically based on current filters
|
||||||
const combinedAdsData = useMemo(() => {
|
const combinedAdsData = useMemo(() => {
|
||||||
return mergeSalesAndAdsData(filteredData, filteredAdsData);
|
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata);
|
||||||
}, [filteredData, filteredAdsData]);
|
}, [filteredData, filteredAdsData, globalAsinMetadata]);
|
||||||
|
|
||||||
// Derive Context Data (Product Line Context when drilling down)
|
// Derive Context Data (Product Line Context when drilling down)
|
||||||
const contextAggregatedData = useMemo(() => {
|
const contextAggregatedData = useMemo(() => {
|
||||||
|
|||||||
+33
-16
@@ -441,15 +441,19 @@ export const processAdsExcel = async (fileOrBuffer: File | ArrayBuffer): Promise
|
|||||||
|
|
||||||
// --- DATA MERGING ---
|
// --- 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
|
// Key for both sales and ads: ASIN|Customer|Year|Week
|
||||||
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)
|
// 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales
|
||||||
const asinMetadata = new Map<string, { sku: string; title: string; line: string }>();
|
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, {
|
const salesMap = new Map<string, {
|
||||||
sellOut: number;
|
sellOut: number;
|
||||||
units: number;
|
units: number;
|
||||||
@@ -469,12 +473,14 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
|
|
||||||
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
|
// If no global map provided, build it on the fly
|
||||||
const metaKey = `${sale.asin.trim().toUpperCase()}|${sale.customer.trim().toUpperCase()}`;
|
if (!asinMetadataMap) {
|
||||||
|
const metaKey = sale.asin.trim().toUpperCase();
|
||||||
const existingMeta = asinMetadata.get(metaKey);
|
const existingMeta = asinMetadata.get(metaKey);
|
||||||
if (!existingMeta || (sale.title && sale.title.length > (existingMeta.title?.length || 0))) {
|
if (!existingMeta || (sale.title && sale.title.length > (existingMeta.title?.length || 0))) {
|
||||||
asinMetadata.set(metaKey, { sku: sale.sku, title: sale.title, line: sale.line });
|
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)!;
|
||||||
@@ -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>();
|
const adsMap = new Map<string, AdsRecord>();
|
||||||
adsData.forEach(ad => {
|
adsData.forEach(ad => {
|
||||||
const key = createKey(ad.asin, ad.country, ad.year, ad.week);
|
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 mergedData: CombinedKPIs[] = [];
|
||||||
const processedKeys = new Set<string>();
|
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) => {
|
salesMap.forEach((sale, key) => {
|
||||||
processedKeys.add(key);
|
processedKeys.add(key);
|
||||||
const ad = adsMap.get(key);
|
const ad = adsMap.get(key);
|
||||||
@@ -552,7 +558,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
month: sale.month,
|
month: sale.month,
|
||||||
week: sale.week,
|
week: sale.week,
|
||||||
year: sale.year,
|
year: sale.year,
|
||||||
asin: sale.asin,
|
asin: sale.asin.trim().toUpperCase(),
|
||||||
title: sale.title,
|
title: sale.title,
|
||||||
line: sale.line,
|
line: sale.line,
|
||||||
sku: sale.sku,
|
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) => {
|
adsMap.forEach((ad, key) => {
|
||||||
if (!processedKeys.has(key)) {
|
if (!processedKeys.has(key)) {
|
||||||
const metaKey = `${ad.asin.trim().toUpperCase()}|${ad.country.trim().toUpperCase()}`;
|
const asin = ad.asin.trim().toUpperCase();
|
||||||
const meta = asinMetadata.get(metaKey);
|
const meta = asinMetadata.get(asin);
|
||||||
|
|
||||||
mergedData.push({
|
mergedData.push({
|
||||||
id: `ads-only-${key}`,
|
id: `ads-only-${key}`,
|
||||||
@@ -589,7 +595,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
month: 'N/A',
|
month: 'N/A',
|
||||||
week: ad.week,
|
week: ad.week,
|
||||||
year: ad.year,
|
year: ad.year,
|
||||||
asin: ad.asin,
|
asin: asin,
|
||||||
title: meta?.title || ad.asin,
|
title: meta?.title || ad.asin,
|
||||||
line: meta?.line || 'Unassigned',
|
line: meta?.line || 'Unassigned',
|
||||||
sku: meta?.sku || '',
|
sku: meta?.sku || '',
|
||||||
@@ -621,8 +627,12 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
|
|
||||||
// --- EXISTING HELPERS ---
|
// --- EXISTING HELPERS ---
|
||||||
|
|
||||||
// Filter Ads Data by Country, Year, Week, and ASIN
|
// Filter Ads Data by Country, Year, Week, ASIN, and Product Line
|
||||||
export const filterAdsData = (adsData: AdsRecord[], filters: FilterState): AdsRecord[] => {
|
export const filterAdsData = (
|
||||||
|
adsData: AdsRecord[],
|
||||||
|
filters: FilterState,
|
||||||
|
asinMetadata?: Map<string, { line: string }>
|
||||||
|
): AdsRecord[] => {
|
||||||
return adsData.filter(ad => {
|
return adsData.filter(ad => {
|
||||||
// Country/Customer match (ads use 'country', sales use 'customer')
|
// Country/Customer match (ads use 'country', sales use 'customer')
|
||||||
const countryMatch = filters.customer.length === 0
|
const countryMatch = filters.customer.length === 0
|
||||||
@@ -641,7 +651,14 @@ export const filterAdsData = (adsData: AdsRecord[], filters: FilterState): AdsRe
|
|||||||
const asinMatch = filters.asin.length === 0 ||
|
const asinMatch = filters.asin.length === 0 ||
|
||||||
filters.asin.some(a => a.toUpperCase() === ad.asin.toUpperCase());
|
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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user