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' }); } } const monthlySales = activeMonths.map(m => (item.monthlyData && item.monthlyData[m]?.units) || 0); const avgMonthly = monthlySales.length > 0 ? monthlySales.reduce((a, b) => a + b, 0) / activeMonths.length : 0; const peakMonthly = monthlySales.length > 0 ? Math.max(...monthlySales) : 0; return (
{ranks.map((r, i) => ( ))} {item.sku}
{item.asin}
{item.title}
{item.line} {stockMap && ( )}
{(item.actualUnits || 0).toLocaleString('de-DE')} {(item.forecastUnits || 0).toLocaleString('de-DE')}
{(Math.round(avgMonthly) || 0).toLocaleString('de-DE')} Peak: {(Math.round(peakMonthly) || 0).toLocaleString('de-DE')}
({ name: m, units: (item.monthlyData && item.monthlyData[m]?.units) || 0 }))}>
= 80 ? 'text-emerald-400' : item.accuracy >= 50 ? 'text-amber-400' : 'text-rose-400'}`}> {item.accuracy}%
= 80 ? 'bg-emerald-500' : item.accuracy >= 50 ? 'bg-amber-500' : 'bg-rose-500'}`} style={{ width: `${item.accuracy}%` }} />
); }); 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(() => { for (let i = MONTH_ORDER.length - 1; i >= 0; i--) { if (data.some(p => p.monthlyData[MONTH_ORDER[i]]?.units > 0)) return i; } return 0; }, [data]); const activeMonths = useMemo(() => { 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]); 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) => ({ actualUnits: acc.actualUnits + curr.actualUnits, forecastUnits: acc.forecastUnits + curr.forecastUnits, }), { actualUnits: 0, forecastUnits: 0 }); }, [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 (
Global Actuals
{(globalSummary.actualUnits || 0).toLocaleString('de-DE')}
Global Forecast
{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')}

Product Performance Comparison

{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.slice(0, displayCount).map(p => ( ))}
Product Details
0)', 'In Stock (>20)', 'Low Stock (<10)', ]} />
Actuals Forecast Avg/Mo Trend Accuracy
{displayCount < finalFilteredData.length && (
)}
); }; export default ForecastView;