import React, { useState, useMemo } from 'react'; import { SalesRecord } from './types'; import { DownloadIcon } from './Icons'; interface TopMoversProps { data: SalesRecord[]; } type Metric = 'sellOut' | 'units'; interface SkuAggr { sku: string; title: string; line: string; previousValue: number; currentValue: number; diff: number; pct: number; } // Reusable Table Component const MoversTable: React.FC<{ title: string; data: SkuAggr[]; metric: Metric; previousYear: number; currentYear: number; type: 'growth' | 'decline'; }> = ({ title, data, metric, previousYear, currentYear, type }) => { const formatValue = (val: number) => { if (metric === 'sellOut') return `€${val.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`; return val.toLocaleString('de-DE'); }; const handleExport = () => { if (!data || data.length === 0) return; // Helper to force Comma as thousands separator (US Locale) const formatForCSV = (val: number) => { return val.toLocaleString('de-DE', { useGrouping: true, minimumFractionDigits: metric === 'sellOut' ? 2 : 0, maximumFractionDigits: metric === 'sellOut' ? 2 : 0, }); }; // Prepare data for CSV const csvData = data.map((item, index) => ({ Rank: index + 1, Title: item.title, SKU: item.sku, 'Product Line': item.line, [`${previousYear} ${metric === 'sellOut' ? 'Sell Out' : 'Units'}`]: formatForCSV(item.previousValue), [`${currentYear} ${metric === 'sellOut' ? 'Sell Out' : 'Units'}`]: formatForCSV(item.currentValue), 'Difference': formatForCSV(item.diff), '% Change': `${item.pct.toFixed(2)}%` })); // Generate CSV string // @ts-ignore - Papa is loaded globally via CDN const csv = Papa.unparse(csvData); // Create download link const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; const filename = `${title.replace(/\s+/g, '_')}_${currentYear}_vs_${previousYear}.csv`; link.setAttribute('download', filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const colorClass = type === 'growth' ? 'text-emerald-400' : 'text-rose-400'; const bgClass = type === 'growth' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'; const headerColor = type === 'growth' ? 'border-emerald-500/30' : 'border-rose-500/30'; return (

{type === 'growth' ? '🚀 ' : '📉 '} {title}

Top 20
{data.map((item, index) => { const isPositive = item.diff >= 0; return ( ); })} {data.length === 0 && ( )}
Rank SKU Details Product Line {previousYear} {currentYear} Diff % Change
{index + 1}
{item.title || 'Unknown Title'} SKU: {item.sku}
{item.line} {formatValue(item.previousValue)} {formatValue(item.currentValue)} {isPositive ? '+' : ''}{formatValue(item.diff)} {isPositive ? '↑' : '↓'} {Math.abs(item.pct).toFixed(1)}%
No records found matching this criteria.
); }; const TopMovers: React.FC = ({ data }) => { const [metric, setMetric] = useState('sellOut'); const [viewMode, setViewMode] = useState<'growth' | 'decline'>('growth'); // 1. Determine comparison years from filtered data const { currentYear, previousYear, availableYears } = useMemo(() => { const years = Array.from(new Set(data.map(d => d.year))).sort((a: number, b: number) => b - a); return { currentYear: years[0], previousYear: years[1], availableYears: years }; }, [data]); // 2. Aggregation Logic const { growers, decliners } = useMemo(() => { if (!currentYear || !previousYear) return { growers: [], decliners: [] }; // Map: SKU -> { currentVal, previousVal, metadata } const map = new Map(); data.forEach(row => { // Only care about the two comparison years if (row.year !== currentYear && row.year !== previousYear) return; if (!map.has(row.sku)) { map.set(row.sku, { current: 0, previous: 0, title: row.title, line: row.line }); } const entry = map.get(row.sku)!; const value = metric === 'sellOut' ? row.sellOut : row.units; if (row.year === currentYear) { entry.current += value; } else { entry.previous += value; } }); // Convert to Array and Calculate Deltas const list: SkuAggr[] = []; map.forEach((val, sku) => { // Filter out items that have 0 in BOTH years (irrelevant) if (val.current === 0 && val.previous === 0) return; const diff = val.current - val.previous; let pct = 0; if (val.previous !== 0) { pct = (diff / val.previous) * 100; } else if (val.current !== 0) { // Infinite growth (0 -> 100) pct = 100; } list.push({ sku, title: val.title, line: val.line, previousValue: val.previous, currentValue: val.current, diff, pct }); }); // Separate and Sort const growers = list .filter(i => i.diff > 0) .sort((a, b) => b.diff - a.diff) // Descending by Growth .slice(0, 20); const decliners = list .filter(i => i.diff < 0) .sort((a, b) => a.diff - b.diff) // Ascending by Decline (Most negative first) .slice(0, 20); return { growers, decliners }; }, [data, metric, currentYear, previousYear]); if (availableYears.length < 2) { return (

Insufficient Data for Comparison

To see Top Movers, please ensure your filters include at least two different years (e.g., 2024 and 2025).

Current Years Available: {availableYears.join(', ') || 'None'}

); } return (
{/* Controls Header */}

Analytics Overview

Comparing Performance: {previousYear} vs {currentYear}

{/* Gainers / Losers Toggle */}
{/* Metric Toggle */}
{/* Single Active Table */}
); }; export default TopMovers;