Fix Weeks of Coverage: use unfiltered velocity map for robust calculation

This commit is contained in:
Christian Vidal Wolf
2026-01-28 11:01:05 +01:00
parent 22f4a639c0
commit 57f1d66622
2 changed files with 66 additions and 61 deletions
+45 -56
View File
@@ -505,7 +505,8 @@ export const mergeSalesAndAdsData = (
salesData: SalesRecord[],
adsData: AdsRecord[],
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>,
trafficData?: TrafficRecord[]
trafficData?: TrafficRecord[],
velocityMap?: Map<string, number>
): CombinedKPIs[] => {
// Key for both sales and ads: ASIN|Customer|Year|Week
const createKey = (asin: string, customer: string, year: number, week: number) =>
@@ -598,35 +599,10 @@ export const mergeSalesAndAdsData = (
}
}
// 4. Calculate 4-Week Average Sales per ASIN
const latestYear = Math.max(...salesData.map(r => r.year).filter(y => y > 0));
const yearData = salesData.filter(r => r.year === latestYear);
const latestWeek = yearData.length > 0 ? Math.max(...yearData.map(r => r.week).filter(w => w !== undefined) as number[]) : 0;
const last4WeeksKeys = new Set<string>();
for (let i = 0; i < 4; i++) {
let w = latestWeek - i;
let y = latestYear;
if (w <= 0) {
w = 52 + w;
y = latestYear - 1;
}
last4WeeksKeys.add(`${y}|${w}`);
}
const asin4WeekSales = new Map<string, number>();
salesData.forEach(r => {
if (r.week === undefined) return;
if (last4WeeksKeys.has(`${r.year}|${r.week}`)) {
const key = r.asin.trim().toUpperCase();
asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + r.units);
}
});
const mergedData: CombinedKPIs[] = [];
const processedKeys = new Set<string>();
// 5. 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);
@@ -648,7 +624,7 @@ export const mergeSalesAndAdsData = (
const ctr = adImpressions > 0 ? (adClicks / adImpressions) * 100 : 0;
const cpc = adClicks > 0 ? adCost / adClicks : 0;
const cvrUnits = adClicks > 0 ? (adUnits / adClicks) * 100 : 0;
const avgWeeklySales = (asin4WeekSales.get(sale.asin.trim().toUpperCase()) || 0) / 4;
const avgWeeklySales = velocityMap?.get(sale.asin.trim().toUpperCase()) || 0;
mergedData.push({
id: `merged-${key}`,
@@ -689,7 +665,7 @@ export const mergeSalesAndAdsData = (
if (!processedKeys.has(key)) {
const asin = ad.asin.trim().toUpperCase();
const meta = asinMetadata.get(asin);
const avgWeeklySales = (asin4WeekSales.get(asin) || 0) / 4;
const avgWeeklySales = velocityMap?.get(asin) || 0;
mergedData.push({
id: `ads-only-${key}`,
@@ -1642,7 +1618,8 @@ export const calculateForecastViewData = (
rawData: SalesRecord[],
forecastData: ForecastRecord[],
asinMetadata: Map<string, { sku: string; title: string; line: string }>,
filters?: FilterState
filters?: FilterState,
velocityMap?: Map<string, number>
): ProductForecastData[] => {
const data2025 = rawData.filter(r => r.year === 2025);
const data2026 = rawData.filter(r => r.year === 2026);
@@ -1713,35 +1690,10 @@ export const calculateForecastViewData = (
monthMap.set(m, (monthMap.get(m) || 0) + r.units);
});
// 4-Week Average Sales Calculation
const latestYear = Math.max(...rawData.map(r => r.year).filter(y => y > 0));
const yearData = rawData.filter(r => r.year === latestYear);
const latestWeek = yearData.length > 0 ? Math.max(...yearData.map(r => r.week).filter(w => w !== undefined) as number[]) : 0;
const last4WeeksKeys = new Set<string>();
for (let i = 0; i < 4; i++) {
let w = latestWeek - i;
let y = latestYear;
if (w <= 0) {
w = 52 + w;
y = latestYear - 1;
}
last4WeeksKeys.add(`${y}|${w}`);
}
const asin4WeekSales = new Map<string, number>();
rawData.forEach(r => {
if (r.week === undefined) return;
if (last4WeeksKeys.has(`${r.year}|${r.week}`)) {
const key = r.asin.trim().toUpperCase();
asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + r.units);
}
});
return forecastData.map(fc => {
const identifier = fc.asin.toUpperCase();
const meta = asinMetadata.get(identifier);
const avgWeeklySales = (asin4WeekSales.get(identifier) || 0) / 4;
const avgWeeklySales = velocityMap?.get(identifier) || 0;
// 2. Determine weights for this ASIN
const productRecords2025 = dataByAsin2025.get(identifier) || [];
@@ -1858,6 +1810,43 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
}
};
export const calculateVelocityMap = (data: SalesRecord[]): Map<string, number> => {
// 4-Week Average Sales Calculation
const validYears = data.map(r => r.year).filter(y => y > 0);
if (validYears.length === 0) return new Map();
const latestYear = Math.max(...validYears);
const yearData = data.filter(r => r.year === latestYear);
const latestWeek = yearData.length > 0 ? Math.max(...yearData.map(r => r.week).filter(w => w !== undefined) as number[]) : 0;
const last4WeeksKeys = new Set<string>();
for (let i = 0; i < 4; i++) {
let w = latestWeek - i;
let y = latestYear;
if (w <= 0) {
w = 52 + w;
y = latestYear - 1;
}
last4WeeksKeys.add(`${y}|${w}`);
}
const asin4WeekSales = new Map<string, number>();
data.forEach(r => {
if (r.week === undefined) return;
if (last4WeeksKeys.has(`${r.year}|${r.week}`)) {
const key = r.asin.trim().toUpperCase();
asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + r.units);
}
});
const velocityMap = new Map<string, number>();
asin4WeekSales.forEach((total, asin) => {
velocityMap.set(asin, total / 4);
});
return velocityMap;
};
export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, number>> => {
try {
const arrayBuffer = fileOrBuffer instanceof File