diff --git a/components/AdsPerformance.tsx b/components/AdsPerformance.tsx index 0476e42..47df944 100644 --- a/components/AdsPerformance.tsx +++ b/components/AdsPerformance.tsx @@ -74,7 +74,7 @@ const AdsRow: React.FC<{ {stockMap && ( )} - + diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index 0bfce8f..f7b5bcd 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -66,7 +66,7 @@ const ForecastRow: React.FC<{ {stockMap && ( )} - + diff --git a/components/VendorStockBadge.tsx b/components/VendorStockBadge.tsx index b19ba46..cbbbab8 100644 --- a/components/VendorStockBadge.tsx +++ b/components/VendorStockBadge.tsx @@ -5,9 +5,10 @@ interface VendorStockBadgeProps { asin: string; vendorStockMap?: Map; mode: 'eu' | 'uk'; + avgWeeklySales?: number; } -export const VendorStockBadge: React.FC = ({ asin, vendorStockMap, mode }) => { +export const VendorStockBadge: React.FC = ({ asin, vendorStockMap, mode, avgWeeklySales }) => { if (!vendorStockMap || !asin) return null; const stockData = vendorStockMap.get(asin.toUpperCase()); @@ -15,48 +16,57 @@ export const VendorStockBadge: React.FC = ({ asin, vendor const stock = mode === 'uk' ? stockData.uk : stockData.eu; - // As per user request: "etiqueta serĂ¡ de color gris claro" - // Using a light gray/slate theme that fits the dashboard's premium look + // Calculate Weeks of Coverage (WOC) + const woc = (avgWeeklySales && avgWeeklySales > 0) ? stock / avgWeeklySales : null; + const wocColor = woc !== null ? (woc < 4 ? 'text-rose-500' : 'text-emerald-500') : 'text-slate-400'; + const themeClasses = "bg-slate-200 text-slate-800 border-slate-300 shadow-sm"; return ( -
- +
- - - - - -
- Vendor - {stock.toLocaleString('de-DE')} + + + + + + +
+ Vendor + {stock.toLocaleString('de-DE')} +
+ {woc !== null && ( +
+ {woc.toFixed(1)} Weeks Cover +
+ )}
); }; diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index 8df71af..18ec506 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -90,7 +90,7 @@ const WeeklyRow: React.FC<{ {stockMap && ( )} - +
{row.line} diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 9618d3d..1953562 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -598,10 +598,35 @@ 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(); + 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(); + 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(); - // 4. Create ONE record per ASIN/Customer/Year/Week from sales + // 5. Create ONE record per ASIN/Customer/Year/Week from sales salesMap.forEach((sale, key) => { processedKeys.add(key); const ad = adsMap.get(key); @@ -623,6 +648,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; mergedData.push({ id: `merged-${key}`, @@ -653,7 +679,8 @@ export const mergeSalesAndAdsData = ( ctr, cpc, cvrUnits, - glanceViews: trafficMap.get(key) || 0 + glanceViews: trafficMap.get(key) || 0, + avgWeeklySales }); }); @@ -662,6 +689,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; mergedData.push({ id: `ads-only-${key}`, @@ -692,7 +720,8 @@ export const mergeSalesAndAdsData = ( ctr: ad.impressions > 0 ? (ad.clicks / ad.impressions) * 100 : 0, cpc: ad.clicks > 0 ? ad.cost / ad.clicks : 0, cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0, - glanceViews: trafficMap.get(key) || 0 + glanceViews: trafficMap.get(key) || 0, + avgWeeklySales }); } }); @@ -1684,9 +1713,35 @@ 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(); + 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(); + 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; // 2. Determine weights for this ASIN const productRecords2025 = dataByAsin2025.get(identifier) || []; @@ -1735,6 +1790,7 @@ export const calculateForecastViewData = ( actualUnits: totalActualUnits, forecastUnits: totalForecastUnits, accuracy: Math.max(0, accuracy), + avgWeeklySales, monthlyData }; }); diff --git a/types.ts b/types.ts index 95ceff3..d6c9559 100644 --- a/types.ts +++ b/types.ts @@ -195,6 +195,7 @@ export interface CombinedKPIs { cpc: number; cvrUnits: number; glanceViews: number; // Traffic / page views + avgWeeklySales?: number; // Added for Weeks of Coverage } export interface ForecastRecord { @@ -220,5 +221,6 @@ export interface ProductForecastData { actualUnits: number; forecastUnits: number; accuracy: number; + avgWeeklySales: number; // Added for Weeks of Coverage monthlyData: Record; }