import React, { useState, useMemo, useCallback } from 'react'; import * as XLSX from 'xlsx'; import { SalesRecord } from '../types'; import { DownloadIcon } from './Icons'; import { StockBadge } from './StockBadge'; import { VendorStockBadge } from './VendorStockBadge'; import { BuyBoxWarningBadge } from './BuyBoxWarningBadge'; interface TopMoversProps { data: SalesRecord[]; stockMap?: Map; vendorStockMap?: Map; buyBoxLostMap?: Map }>; top50Mode?: 'eu' | 'uk'; } type Metric = 'sellOut' | 'units'; interface SkuAggr { sku: string; asin: 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'; stockMap?: Map; vendorStockMap?: Map; buyBoxLostMap?: Map }>; top50Mode?: 'eu' | 'uk'; }> = ({ title, data, metric, previousYear, currentYear, type, stockMap, vendorStockMap, buyBoxLostMap, top50Mode = 'eu' }) => { const formatValue = (val: number) => { if (metric === 'sellOut') return `€${val.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`; return val.toLocaleString('de-DE'); }; const handleExport = useCallback(() => { if (!data || data.length === 0) return; const exportData = data.map((item, index) => ({ Rank: index + 1, Title: item.title, SKU: item.sku, 'Product Line': item.line, [`${previousYear} ${metric === 'sellOut' ? 'Sell Out' : 'Units'}`]: item.previousValue, [`${currentYear} ${metric === 'sellOut' ? 'Sell Out' : 'Units'}`]: item.currentValue, 'Difference': item.diff, '% Change': Number(item.pct.toFixed(2)) })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Top Movers'); XLSX.writeFile(wb, `${title.replace(/\s+/g, '_')}_${currentYear}_vs_${previousYear}.xlsx`); }, [data, title, currentYear, previousYear, metric]); 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 ( {/* Stock Badge */} {/* Vendor Stock Badge */} {/* Buy Box Warning Badge */} ); })} {data.length === 0 && ( )}
Rank SKU Details Product Line Stock Vendor Buy Box {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, stockMap, vendorStockMap, buyBoxLostMap, top50Mode = 'eu' }) => { 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, asin: row.asin }); } 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, asin: val.asin, 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;