diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index 669ce9e..7d537d9 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -361,13 +361,20 @@ const ForecastView: React.FC = ({ return sum + (curr.monthlyData?.[month]?.forecastUnits || 0); }, 0); + const asin = curr.asin.trim().toUpperCase(); + const stockData = vendorStockMap?.get(asin); + const stock = stockData ? (top50Mode === 'uk' ? stockData.uk : stockData.eu) : 0; + const velocity = curr.avgWeeklySales || 0; + return { actualUnits: acc.actualUnits + (curr.actualUnits || 0), forecastUnits: acc.forecastUnits + itemPeriodForecast, annualForecast: acc.annualForecast + (curr.annualForecast || 0), + totalStock: acc.totalStock + stock, + totalVelocity: acc.totalVelocity + velocity }; - }, { actualUnits: 0, forecastUnits: 0, annualForecast: 0 }); - }, [processedData, activeMonths]); + }, { actualUnits: 0, forecastUnits: 0, annualForecast: 0, totalStock: 0, totalVelocity: 0 }); + }, [processedData, activeMonths, vendorStockMap, top50Mode]); // [MOVED HERE] Chart Data - Now respects all filters including Search/WOC const chartData = useMemo(() => { @@ -442,6 +449,21 @@ const ForecastView: React.FC = ({
{globalSummary.annualForecast > 0 ? ((globalSummary.actualUnits / globalSummary.annualForecast) * 100).toFixed(1) : '0.0'}%
+ {/* New Aggregate WOC Card */} +
+ Average Week Coverage +
+
0 && (globalSummary.totalStock / globalSummary.totalVelocity) < 4 ? 'text-rose-400' : 'text-emerald-400'}`}> + {globalSummary.totalVelocity > 0 + ? (globalSummary.totalStock / globalSummary.totalVelocity).toFixed(1) + : (globalSummary.totalStock > 0 ? '> 52' : '0.0')} +
+ Weeks +
+
+ Total Stock: {globalSummary.totalStock.toLocaleString('de-DE')} | Velocity: {globalSummary.totalVelocity.toFixed(1)}/wk +
+
diff --git a/components/VendorStockBadge.tsx b/components/VendorStockBadge.tsx index 5e6ced1..a853db0 100644 --- a/components/VendorStockBadge.tsx +++ b/components/VendorStockBadge.tsx @@ -37,9 +37,9 @@ export const VendorStockBadge: React.FC = ({ asin, vendor const themeClasses = "bg-slate-200 text-slate-800 border-slate-300 shadow-sm"; return ( -
+
@@ -48,12 +48,14 @@ export const VendorStockBadge: React.FC = ({ asin, vendor {stock.toLocaleString('de-DE')}
- - {wocText} - + + {wocText} + +
); }; diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index f8393fd..9f16716 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -559,6 +559,26 @@ const WeeklyGrid: React.FC = ({ return totals; }, [rows, weeks]); + const aggregateWoc = useMemo(() => { + if (!vendorStockMap || !velocityMap || filteredRows.length === 0) return null; + + let totalStock = 0; + let totalVelocity = 0; + + filteredRows.forEach(row => { + const asin = row.asin.trim().toUpperCase(); + const stockData = vendorStockMap.get(asin); + if (stockData) { + totalStock += (top50Mode === 'uk' ? stockData.uk : stockData.eu); + } + totalVelocity += (velocityMap.get(asin) || 0); + }); + + if (totalVelocity > 0) return totalStock / totalVelocity; + if (totalStock > 0) return 999; + return 0; + }, [filteredRows, vendorStockMap, velocityMap, top50Mode]); + // 1. Filter by search term, Top 50, and growth (using debounced value) const filteredRows = useMemo(() => { let result = rows.slice(); @@ -1112,7 +1132,19 @@ const WeeklyGrid: React.FC = ({ ))} - TOTALS + +
+ TOTALS + {aggregateWoc !== null && ( +
+ Week Coverage (Avg) + + {aggregateWoc === 999 ? '> 52 Weeks' : `${aggregateWoc.toFixed(1)} Weeks`} + +
+ )} +
+ {weeks.map((week, idx) => (
diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 6a59e7e..94c5cb6 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -2470,11 +2470,24 @@ export const calculateVelocityMap = (data: SalesRecord[]): Map = if (validYears.length === 0) return new Map(); const latestYear = validYears.reduce((a, b) => Math.max(a, b), 0); + + // Find the latest week with actual units > 0 in the latest year + // This avoids using future weeks with 0 sales that might be in the data const yearData = data.filter(r => r.year === latestYear); - const latestWeek = yearData.length > 0 - ? (yearData.map(r => r.week).filter(w => w !== undefined) as number[]) - .reduce((a, b) => Math.max(a, b), 0) - : 0; + const weeksWithSales = yearData + .filter(r => (r.units || 0) > 0 && r.week !== undefined) + .map(r => r.week as number); + + let latestWeek = 0; + if (weeksWithSales.length > 0) { + latestWeek = Math.max(...weeksWithSales); + } else { + // Fallback to highest week if no sales found + latestWeek = yearData.length > 0 + ? (yearData.map(r => r.week).filter(w => w !== undefined) as number[]) + .reduce((a, b) => Math.max(a, b), 0) + : 0; + } const last4WeeksKeys = new Set(); for (let i = 0; i < 4; i++) { @@ -2492,7 +2505,7 @@ export const calculateVelocityMap = (data: SalesRecord[]): Map = 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); + asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + (r.units || 0)); } });