From ccdbcb282463d473b0f8bc4a3b13b9682cd1d118 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Mon, 26 Jan 2026 16:24:09 +0100 Subject: [PATCH] feat: enhance Forecast 2026 view with dynamic periods, top 50 badges and improved product details --- App.tsx | 2 +- components/ForecastView.tsx | 139 +++++++++++++++++++++++++++--------- services/dataProcessor.ts | 1 + types.ts | 1 + 4 files changed, 110 insertions(+), 33 deletions(-) diff --git a/App.tsx b/App.tsx index 9f3c6f8..4355432 100644 --- a/App.tsx +++ b/App.tsx @@ -538,7 +538,7 @@ const App: React.FC = () => { {view === 'weekly' && } {view === 'movers' && } {view === 'ads' && } - {view === 'forecast' && } + {view === 'forecast' && } diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index 3dadaa5..ae54a0b 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from 'react'; -import { ProductForecastData } from '../types'; +import { ProductForecastData, FilterState } from '../types'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend, ComposedChart, Area @@ -8,17 +8,60 @@ import { interface ForecastViewProps { data: ProductForecastData[]; + filters: FilterState; + top50Ranking?: { + eu: Map; + uk: Map; + }; } -const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9']; +const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; -const ForecastView: React.FC = ({ data }) => { +// Top 50 Badge Component (reused logic) +const Top50Badge: React.FC<{ rank: number; label: string; theme?: 'amber' | 'indigo' | 'blue' }> = ({ rank, label, theme = 'amber' }) => { + const themeClasses = { + amber: 'bg-amber-500/10 text-amber-500 border-amber-500/20', + indigo: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20', + blue: 'bg-blue-500/10 text-blue-400 border-blue-500/20' + }; + + return ( +
+ {label} + #{rank} +
+ ); +}; + +const ForecastView: React.FC = ({ data, filters, top50Ranking }) => { const [searchTerm, setSearchTerm] = useState(''); + // Determine the "Current Month" based on available 2026 data + const lastDataMonthIdx = useMemo(() => { + let maxIdx = 0; // Default Jan + data.forEach(p => { + p.monthlyData.forEach((md, idx) => { + if (md.actualUnits > 0 && idx > maxIdx) { + maxIdx = idx; + } + }); + }); + return maxIdx; + }, [data]); + + // Active months for "Monthly Forecast" calculation + const activeMonths = useMemo(() => { + if (filters.month && filters.month.length > 0) { + // Map e.g. "Apr-24" or just "Apr" to the short name + return filters.month.map(m => m.split('-')[0]); + } + // If no filter, use Jan to lastDataMonth + return MONTH_ORDER.slice(0, lastDataMonthIdx + 1); + }, [filters.month, lastDataMonthIdx]); + // Global Aggregate Data const globalMonthlyData = useMemo(() => { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - return months.map(m => { + return MONTH_ORDER.map(m => { let forecast = 0; let actual = 0; data.forEach(p => { @@ -145,50 +188,82 @@ const ForecastView: React.FC = ({ data }) => { Product Info - Annual Forecast - Actual 2026 - Fulfillment % - Monthly Status (YTD) + Forecast (Period) + Actual Units Sales 2026 + Fc 26 Achievement + YTD Achievement {sortedProducts.map((p) => { + const asin = p.asin.toUpperCase(); + + // Top 50 Badges logic + const ranks = []; + if (top50Ranking) { + const rankEU = top50Ranking.eu.get(asin); + const rankUK = top50Ranking.uk.get(asin); + if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' as const }); + if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' as const }); + } + + // Monthly Forecast Calculation + const filteredForecast = p.monthlyData + .filter(md => activeMonths.includes(md.month)) + .reduce((acc, md) => acc + md.forecastUnits, 0); + + // Actual Sales for 2026 (Selected Period) + const actualPeriod = p.monthlyData + .filter(md => activeMonths.includes(md.month)) + .reduce((acc, md) => acc + md.actualUnits, 0); + + // Total annual actual for achievement percentage const actualTotal = p.monthlyData.reduce((acc, md) => acc + md.actualUnits, 0); - const fulfillment = p.annualForecast > 0 ? (actualTotal / p.annualForecast) * 100 : 0; + const totalAchievement = p.annualForecast > 0 ? (actualTotal / p.annualForecast) * 100 : 0; + + // YTD Achievement (Achievement of the period) + const periodAchievement = filteredForecast > 0 ? (actualPeriod / filteredForecast) * 100 : 0; return ( -
- {p.asin} - {p.title} +
+
+ {ranks.map((r, i) => ( + + ))} + {p.sku} + {p.asin} +
+ {p.title} + {p.line}
- {p.annualForecast.toLocaleString('de-DE')} + {filteredForecast.toLocaleString('de-DE')} - {actualTotal.toLocaleString('de-DE')} + {actualPeriod.toLocaleString('de-DE')} - = 50 ? 'bg-emerald-500/10 text-emerald-400' : fulfillment >= 20 ? 'bg-indigo-500/10 text-indigo-400' : 'bg-slate-800 text-slate-500'}`}> - {fulfillment.toFixed(1)}% - +
+ = 50 ? 'bg-emerald-500/10 text-emerald-400' : totalAchievement >= 10 ? 'bg-indigo-500/10 text-indigo-400' : 'bg-slate-800 text-slate-500'}`}> + {totalAchievement.toFixed(1)}% + + of Annual {p.annualForecast.toLocaleString('de-DE')} +
- -
- {p.monthlyData.map((md, idx) => { - const isMet = md.actualUnits >= md.forecastUnits && md.forecastUnits > 0; - const height = md.forecastUnits > 0 ? (Math.min(1.5, md.actualUnits / md.forecastUnits) * 100) : 0; - return ( -
0 ? 'bg-indigo-500' : 'bg-slate-800'}`} - style={{ height: `${Math.max(10, height)}%` }} - title={`${md.month}: ${md.actualUnits} / ${md.forecastUnits}`} - >
- ); - })} + +
+
+
= 100 ? 'bg-emerald-500' : periodAchievement >= 50 ? 'bg-indigo-500' : 'bg-amber-500'}`} + style={{ width: `${Math.min(100, periodAchievement)}%` }} + >
+
+ = 100 ? 'text-emerald-400' : periodAchievement >= 50 ? 'text-indigo-400' : 'text-amber-400'}`}> + {periodAchievement.toFixed(1)}% +
diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 357420d..f5a6923 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1584,6 +1584,7 @@ export const calculateForecastViewData = ( asin: identifier, sku: meta?.sku || identifier, // Fallback to ASIN if SKU not found title: meta?.title || identifier, + line: meta?.line || "", annualForecast: fc.annualForecast, monthlyData }; diff --git a/types.ts b/types.ts index 850b181..76b6666 100644 --- a/types.ts +++ b/types.ts @@ -210,6 +210,7 @@ export interface ProductForecastData { asin: string; sku: string; title: string; + line: string; annualForecast: number; monthlyData: MonthlyForecastPoint[]; }