From 9d732eeba3f8e5219ab59ee5511f063574bdd6c4 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 28 Jan 2026 14:55:35 +0100 Subject: [PATCH] Restore ForecastView UI, Fix default YTD logic, and Refine Seasonality (Specific > Global fallback) --- components/ForecastView.tsx | 26 +++++++------------------- services/dataProcessor.ts | 10 ++-------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index f11abec..1475e90 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -147,6 +147,7 @@ const ForecastRow: React.FC<{ ); }); + const ForecastView: React.FC = ({ data, filters, @@ -164,7 +165,6 @@ const ForecastView: React.FC = ({ const [displayCount, setDisplayCount] = useState(50); const lastDataMonthIdx = useMemo(() => { - // Find the last month with actual data let maxDataMonth = 0; for (let i = MONTH_ORDER.length - 1; i >= 0; i--) { if (data.some(p => (p.monthlyData?.[MONTH_ORDER[i]]?.actualUnits || 0) > 0)) { @@ -172,12 +172,7 @@ const ForecastView: React.FC = ({ break; } } - - // Clamp to current month to ensure we don't show future months in "Period" view - // This fixes the issue where "Period" shows Annual forecast if there is any stray future data const currentMonth = new Date().getMonth(); // 0 for Jan - // We assume the data is for the current year if we are in FC 26 view. - // If we want to support past years, we'd need to check the year context, but for now this fixes the user's immediate issue. return Math.min(maxDataMonth, currentMonth); }, [data]); @@ -185,8 +180,10 @@ const ForecastView: React.FC = ({ if (filters.month && filters.month.length > 0) { return filters.month.map(m => m.split('-')[0]); } - return MONTH_ORDER.slice(0, lastDataMonthIdx + 1); - }, [filters.month, lastDataMonthIdx]); + // Default to YTD: Current month and all before it this year + const currentMonthIdx = new Date().getMonth(); + return MONTH_ORDER.slice(0, currentMonthIdx + 1); + }, [filters.month]); const baseFilteredData = useMemo(() => { let result = data; @@ -198,7 +195,6 @@ const ForecastView: React.FC = ({ const globalSummary = useMemo(() => { return baseFilteredData.reduce((acc, curr) => { - // Calculate Forecast Period (YTD) for this item const itemPeriodForecast = activeMonths.reduce((sum, month) => { return sum + (curr.monthlyData?.[month]?.forecastUnits || 0); }, 0); @@ -260,26 +256,20 @@ const ForecastView: React.FC = ({ return (
- {/* Top Section: Metrics & Graph */}
- {/* Metrics Cards */}
- {/* Annual Forecast */}
Annual Forecast Total
{(globalSummary.annualForecast || 0).toLocaleString('de-DE')} Units
- {/* Period Forecast */}
Total Forecast (Period)
{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')} Units
- {/* Actual Sales */}
Total Actual Sales (Period)
{(globalSummary.actualUnits || 0).toLocaleString('de-DE')} Units
- {/* Fulfillment */}
Fulfillment (Period) @@ -295,7 +285,6 @@ const ForecastView: React.FC = ({
- {/* Graph */}

Monthly Evolution: Forecast vs Actual @@ -329,11 +318,10 @@ const ForecastView: React.FC = ({

- {/* Bottom Section: Table */}

- Product Performance Comparison (v2.2 Updated) + Product Performance Comparison (v2.4 Final)

{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && ( @@ -374,7 +362,7 @@ const ForecastView: React.FC = ({ 0 ? filters.month : MONTH_ORDER} + activeMonths={activeMonths} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 591b8da..34e49bb 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1628,7 +1628,6 @@ export const calculateForecastViewData = ( const getWeights = (records: SalesRecord[]): number[] | null => { const weights = new Array(12).fill(0); let total = 0; - const seenMonths = new Set(); records.forEach(r => { const m = r.month.split('-')[0]; @@ -1636,18 +1635,13 @@ export const calculateForecastViewData = ( if (idx !== -1) { weights[idx] += r.units; total += r.units; - if (r.units > 0) seenMonths.add(m); } }); - // 1. No data -> Fallback + // If ASIN has any 2025 sales, we trust its specific seasonality. + // Return null ONLY if there's no data at all for this ASIN in 2025. if (total === 0) return null; - // 2. Sparse Data Check (< 4 months) - // If an ASIN has very little history (e.g. only Jan), using its own curve implies 100% seasonality in Jan. - // The user requested to use the "General Catalog Seasonality" in these cases. - if (seenMonths.size < 4) return null; - return weights.map(w => w / total); };