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 { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons'; import { Top50Badge } from './Top50Badge'; import { VendorStockBadge } from './VendorStockBadge'; import { BuyBoxWarningBadge } from './BuyBoxWarningBadge'; 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; wocFilter: string[]; onWocFilterChange: (newFilters: string[]) => void; buyBoxLostMap?: Map }>; } 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; buyBoxLostMap?: Map }>; }> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap }) => { 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} {/* Own Stock Indicator */} {stockMap && ( )} {/* WOC Indicator preserved */}
{/* Annual Forecast */} {(item.annualForecast || 0).toLocaleString('de-DE')} {/* Forecast (Period) */} {forecastPeriod.toLocaleString('de-DE')} {/* Actual Units Sales 2026 */} {actualUnits.toLocaleString('de-DE')} {/* FC 26 Achievement (Annual %) */}
{fcAchievement.toFixed(1)}%
OF ANNUAL {Math.round(annualForecast / 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, wocFilter, onWocFilterChange, top50Mode, buyBoxLostMap }) => { const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchTerm.toLowerCase()); }, 300); return () => clearTimeout(timer); }, [searchTerm]); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [displayCount, setDisplayCount] = useState(50); const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' }>({ key: 'actualUnits', direction: 'desc' }); const activeMonths = useMemo(() => { if (filters.month && filters.month.length > 0) { return filters.month.map(m => m.split('-')[0]); } const currentMonthIdx = new Date().getMonth(); return MONTH_ORDER.slice(0, currentMonthIdx + 1); }, [filters.month]); const handleSort = (key: string) => { setSortConfig(prev => ({ key, direction: prev.key === key && prev.direction === 'desc' ? 'asc' : 'desc' })); }; 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 calculatedData = useMemo(() => { return baseFilteredData.map(item => { const forecastPeriod = activeMonths.reduce((sum, month) => { return sum + (item.monthlyData?.[month]?.forecastUnits || 0); }, 0); const ytdAchievement = forecastPeriod > 0 ? ((item.actualUnits || 0) / forecastPeriod) * 100 : 0; const fcAchievement = (item.annualForecast || 0) > 0 ? ((item.actualUnits || 0) / item.annualForecast) * 100 : 0; return { ...item, forecastPeriod, ytdAchievement, fcAchievement }; }); }, [baseFilteredData, activeMonths]); const processedData = useMemo(() => { let result = [...calculatedData]; if (showOnlyTop50 && top50Ranking) { const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk; result = result.filter(p => currentRankMap.has(p.asin.toUpperCase())); } if (debouncedSearch) { result = result.filter(p => p.sku.toLowerCase().includes(debouncedSearch) || p.asin.toLowerCase().includes(debouncedSearch) || p.title.toLowerCase().includes(debouncedSearch) ); } // Apply WOC Filter if (wocFilter && wocFilter.length > 0 && vendorStockMap) { result = result.filter(p => { const asin = p.asin.trim().toUpperCase(); const stockData = vendorStockMap.get(asin); if (!stockData) return false; const stock = top50Mode === 'uk' ? stockData.uk : stockData.eu; const velocity = p.avgWeeklySales || 0; let woc: number = 0; if (velocity > 0) { woc = stock / velocity; } else if (stock > 0) { woc = 999; } return wocFilter.some(f => { if (f === '< 4 Weeks') return woc < 4; if (f === '> 4 Weeks') return woc >= 4; if (f === 'Out of Stock') return woc === 0; if (f === 'Infinite Cover') return woc === 999; return true; }); }); } // Apply Sorting const { key, direction } = sortConfig; result.sort((a: any, b: any) => { let valA = a[key]; let valB = b[key]; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); if (valA < valB) return direction === 'asc' ? -1 : 1; if (valA > valB) return direction === 'asc' ? 1 : -1; return 0; }); return result; }, [calculatedData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig]); const paginatedData = useMemo(() => processedData.slice(0, displayCount), [processedData, displayCount]); const handleExportExcel = useCallback(() => { const exportData = processedData.map(p => ({ SKU: p.sku, ASIN: p.asin, Title: p.title, Line: p.line, 'Annual Forecast': p.annualForecast, 'Forecast (Period)': p.forecastPeriod, 'Actual Units (2026)': p.actualUnits, 'FC 26 Achievement (%)': p.fcAchievement, 'YTD Achievement (%)': p.ytdAchievement })); 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`); }, [processedData]); const SortIndicator = ({ column }: { column: string }) => { if (sortConfig.key !== column) return ; return {sortConfig.direction === 'desc' ? '↓' : '↑'}; }; 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.5 optimized)

Showing {paginatedData.length} of {processedData.length}
{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" /> } /> } options={[ 'Out of Stock (0)', 'In Stock (>0)', 'In Stock (>20)', 'Low Stock (<10)', ]} /> } options={[ '< 4 Weeks', '> 4 Weeks', 'Out of Stock', 'Infinite Cover' ]} />
{paginatedData.map(item => ( ))}
handleSort('sku')}> Product Info handleSort('annualForecast')}> Annual Forecast handleSort('forecastPeriod')}> Forecast (Period) handleSort('actualUnits')}> Actual Units 2026 handleSort('fcAchievement')}> FC 26 Achievement handleSort('ytdAchievement')}> YTD Achievement
{displayCount < processedData.length && (
)} {processedData.length === 0 && (

No forecast data found for current filters

)}
); }; export default ForecastView;