import React, { useMemo, useState, useEffect, useCallback } from 'react'; import * as XLSX from 'xlsx'; import { ProductForecastData, FilterState, CombinedKPIs } from '../types'; import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons'; import { StockBadge } from './StockBadge'; import { Top50Badge } from './Top50Badge'; import { VendorStockBadge } from './VendorStockBadge'; import { InColumnStockFilter } from './InColumnStockFilter'; import { PAN_EU_COUNTRIES } from '../services/dataProcessor'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend, ComposedChart, Area } from 'recharts'; interface ForecastViewProps { data: ProductForecastData[]; filters: FilterState; top50Ranking: { eu: Map; uk: Map }; stockMap: Map; top50Mode: 'eu' | 'uk'; vendorStockMap?: Map; stockFilter: string[]; onStockFilterChange: (newFilters: string[]) => void; vendorStockFilter: string[]; onVendorStockFilterChange: (newFilters: string[]) => void; } const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const ForecastRow: React.FC<{ item: ProductForecastData; activeMonths: string[]; top50Ranking?: { eu: Map; uk: Map }; top50Mode: 'eu' | 'uk'; stockMap?: Map; vendorStockMap?: Map; }> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap }) => { const asin = item.asin.trim().toUpperCase(); const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; if (top50Ranking) { if (top50Mode === 'eu') { const rankEU = top50Ranking.eu.get(asin); if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' }); } else { const rankUK = top50Ranking.uk.get(asin); if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' }); } } // Calculations for new columns // Forecast Period: Sum of forecast units for active months (YTD) const forecastPeriod = useMemo(() => { return activeMonths.reduce((sum, month) => { return sum + (item.monthlyData?.[month]?.forecastUnits || 0); }, 0); }, [item, activeMonths]); const actualUnits = item.actualUnits || 0; const annualForecast = item.annualForecast || 0; // FC 26 Achievement: Actual / Annual Forecast const fcAchievement = annualForecast > 0 ? (actualUnits / annualForecast) * 100 : 0; // YTD Achievement: Actual / Period Forecast // If activeMonths is basically "Year To Date" (which it usually is in this view unless filtered), this logic holds. // If user selects specific months, it becomes "Period Achievement". const ytdAchievement = forecastPeriod > 0 ? (actualUnits / forecastPeriod) * 100 : 0; let achievementColor = 'bg-slate-700'; if (ytdAchievement >= 100) achievementColor = 'bg-emerald-500'; else if (ytdAchievement >= 80) achievementColor = 'bg-emerald-400'; else if (ytdAchievement >= 50) achievementColor = 'bg-amber-400'; else achievementColor = 'bg-rose-500'; return ( {/* Product Info */}
{ranks.map((r, i) => ( ))} {item.sku}
{item.asin}
{item.title}
{item.line} {/* WOC Indicator preserved */}
{/* Forecast (Period) */} {forecastPeriod.toLocaleString('de-DE')} {/* Actual Units Sales 2026 */} {actualUnits.toLocaleString('de-DE')} {/* FC 26 Achievement (Annual %) */}
{ytdAchievement.toFixed(1)}%
OF PERIOD {Math.round(forecastPeriod / 1000)}K
{/* YTD Achievement (Period %) */}
= 100 ? 'text-emerald-400' : 'text-slate-300'}`}> {ytdAchievement.toFixed(1)}%
); }); const ForecastView: React.FC = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, vendorStockFilter, onVendorStockFilterChange, top50Mode }) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [displayCount, setDisplayCount] = useState(50); const lastDataMonthIdx = useMemo(() => { 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)) { maxDataMonth = i; break; } } const currentMonth = new Date().getMonth(); // 0 for Jan return Math.min(maxDataMonth, currentMonth); }, [data]); const activeMonths = useMemo(() => { if (filters.month && filters.month.length > 0) { return filters.month.map(m => m.split('-')[0]); } // 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; if (filters.line && filters.line.length > 0) result = result.filter(p => filters.line.includes(p.line)); if (filters.asin && filters.asin.length > 0) result = result.filter(p => filters.asin.includes(p.asin)); if (filters.sku && filters.sku.length > 0) result = result.filter(p => filters.sku.includes(p.sku)); return result; }, [data, filters.line, filters.asin, filters.sku]); const globalSummary = useMemo(() => { return baseFilteredData.reduce((acc, curr) => { const itemPeriodForecast = activeMonths.reduce((sum, month) => { return sum + (curr.monthlyData?.[month]?.forecastUnits || 0); }, 0); return { actualUnits: acc.actualUnits + (curr.actualUnits || 0), forecastUnits: acc.forecastUnits + itemPeriodForecast, annualForecast: acc.annualForecast + (curr.annualForecast || 0), }; }, { actualUnits: 0, forecastUnits: 0, annualForecast: 0 }); }, [baseFilteredData, activeMonths]); const chartData = useMemo(() => { const dataMap = new Map(); MONTH_ORDER.forEach(m => dataMap.set(m, { name: m, actual: 0, forecast: 0 })); baseFilteredData.forEach(item => { if (item.monthlyData) { Object.values(item.monthlyData).forEach((m: any) => { const entry = dataMap.get(m.month); if (entry) { entry.actual += m.actualUnits || 0; entry.forecast += m.forecastUnits || 0; } }); } }); return Array.from(dataMap.values()); }, [baseFilteredData]); const finalFilteredData = useMemo(() => { let result = baseFilteredData; if (showOnlyTop50 && top50Ranking) { const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk; result = result.filter(p => currentRankMap.has(p.asin.toUpperCase())); } if (searchTerm) { const s = searchTerm.toLowerCase(); result = result.filter(p => p.sku.toLowerCase().includes(s) || p.asin.toLowerCase().includes(s) || p.title.toLowerCase().includes(s)); } return result; }, [baseFilteredData, searchTerm, showOnlyTop50, top50Ranking, top50Mode]); const handleExportExcel = useCallback(() => { const exportData = finalFilteredData.map(p => ({ SKU: p.sku, ASIN: p.asin, Title: p.title, Line: p.line, 'Actual Units (2025)': p.actualUnits, 'Forecast Units (2025)': p.forecastUnits, 'Accuracy (%)': p.accuracy })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Forecast'); XLSX.writeFile(wb, `Forecast_Export_${new Date().toISOString().slice(0, 10)}.xlsx`); }, [finalFilteredData]); return (
Annual Forecast Total
{(globalSummary.annualForecast || 0).toLocaleString('de-DE')} Units
Total Forecast (Period)
{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')} Units
Total Actual Sales (Period)
{(globalSummary.actualUnits || 0).toLocaleString('de-DE')} Units
Fulfillment (Period)
{globalSummary.forecastUnits > 0 ? ((globalSummary.actualUnits / globalSummary.forecastUnits) * 100).toFixed(1) : '0.0'}%
0 ? ((globalSummary.actualUnits / globalSummary.forecastUnits) * 100) : 0)}%` }} />
Annual Fulfillment %
{globalSummary.annualForecast > 0 ? ((globalSummary.actualUnits / globalSummary.annualForecast) * 100).toFixed(1) : '0.0'}%

Monthly Evolution: Forecast vs Actual

Product Performance Comparison (v2.4 Final)

{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
)} setSearchTerm(e.target.value)} className="bg-slate-950 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:outline-none w-64" />
{finalFilteredData.map(item => ( ))}
Product Info Forecast (Period) Actual Units Sales 2026 FC 26 Achievement YTD Achievement
{finalFilteredData.length === 0 && (

No forecast data found for current filters

)}
); }; export default ForecastView;