import React, { useState, useMemo, useEffect } from 'react'; import { AggregatedData, GrowthMetric, AdsRecord, SalesRecord } from '../types'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend } from 'recharts'; import TopMovers from './TopMovers'; interface DashboardProps { data: AggregatedData; contextData?: AggregatedData | null; adsData?: AdsRecord[]; rawData?: SalesRecord[]; stockMap?: Map; vendorStockMap?: Map; buyBoxLostMap?: Map }>; top50Mode?: 'eu' | 'uk'; top50Ranking?: { eu: Map; uk: Map }; velocityMap?: Map; } const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9']; const getYearColor = (year: string | number) => { const yStr = year.toString(); if (yStr === '2024') return '#6366f1'; // Indigo if (yStr === '2025') return '#ec4899'; // Pink if (yStr === '2026') return '#10b981'; // Emerald const yInt = parseInt(yStr); if (isNaN(yInt)) return '#6366f1'; return COLORS[yInt % COLORS.length]; }; // Reusable Expandable Card Component const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; className?: string }> = ({ title, children, className }) => { const [isExpanded, setIsExpanded] = useState(false); const toggleExpand = () => setIsExpanded(!isExpanded); // Auto-scroll to top of sticky filter bar when expanded useEffect(() => { if (isExpanded) { // Approximate height of the Header to scroll past (Logo + padding) // This ensures the Sticky FilterBar snaps to the top of the viewport const scrollTarget = 250; if (window.scrollY < scrollTarget) { window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); } } }, [isExpanded]); if (isExpanded) { return (
{/* Fixed Close Button */}

{title}

{children}
); } return (

{title}

{children}
); }; const MultiYearKPICard: React.FC<{ title: string; metric: 'sellOut' | 'units'; data: AggregatedData['totalsByYear']; availableYears: string[]; contextData?: AggregatedData['totalsByYear']; // Added context data }> = ({ title, metric, data, availableYears, contextData }) => { // Sort years descending to show most recent first const sortedYears = [...availableYears].sort((a, b) => parseInt(b) - parseInt(a)); const formatValue = (val: number) => { if (metric === 'sellOut') { return `€${val.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; } return val.toLocaleString('de-DE'); }; // Determine title based on context const displayTitle = contextData ? `${title} (Selected Item)` : title; return (

{displayTitle}

{sortedYears.length === 0 &&

0

} {sortedYears.length > 0 && (
{sortedYears.map((year, index) => { const currentValue = data[year] ? data[year][metric] : 0; const contextValue = contextData && contextData[year] ? contextData[year][metric] : 0; let growthElement = null; let contextGrowthElement = null; // Year-over-Year Growth comparison if (index < sortedYears.length - 1) { const prevYear = sortedYears[index + 1]; // Main Item Growth const prevValue = data[prevYear] ? data[prevYear][metric] : 0; if (prevValue > 0) { const pct = ((currentValue - prevValue) / prevValue) * 100; growthElement = ( = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% ); } // Context Item Growth (Product Line) if (contextData) { const prevContextValue = contextData[prevYear] ? contextData[prevYear][metric] : 0; if (prevContextValue > 0) { const pct = ((contextValue - prevContextValue) / prevContextValue) * 100; contextGrowthElement = ( = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% ); } } } return (
{year}
{formatValue(currentValue)} {growthElement}
{/* Context Row (Product Line Total) */} {contextData && contextValue > 0 && (
Total Product Line:
{formatValue(contextValue)} {contextGrowthElement} {/* Share of Line % */} {((currentValue / contextValue) * 100).toFixed(0)}% Share
)}
); })}
)}
); }; const CustomTooltip = ({ active, payload, label }: any) => { if (active && payload && payload.length) { return (

{label}

{payload.map((p: any) => (

{p.name}: {p.name.toString().toLowerCase().includes('sell out') || p.name.toString().toLowerCase().includes('year') || typeof p.value === 'number' && p.value > 1000 ? `€${Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` : Number(p.value).toLocaleString('de-DE')}

))}
); } return null; }; // Tooltip specifically for the Seasonality Chart to show YoY % const SeasonalityTooltip = ({ active, payload, label, metric }: any) => { if (active && payload && payload.length) { // Sort payload by year (name) to ensure we compare correctly const sortedPayload = [...payload].sort((a, b) => parseInt(a.name) - parseInt(b.name)); const isCurrency = metric === 'sellOut'; return (

{label}

{sortedPayload.map((p: any, index: number) => { let growthEl = null; // If there is a previous year in the list, calculate % change if (index > 0) { const prev = sortedPayload[index - 1]; const prevVal = Number(prev.value); const currVal = Number(p.value); if (prevVal > 0) { const pct = ((currVal - prevVal) / prevVal) * 100; growthEl = ( = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% ); } } return (
{p.name}:
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {growthEl}
); })}
); } return null; }; // Tooltip for the Top 10 Comparison Chart const ComparisonTooltip = ({ active, payload, label, metric }: any) => { if (active && payload && payload.length) { // Sort payload by the dataKey (which usually contains the year, e.g., "2023_value" or just "2023") const sortedPayload = [...payload].sort((a, b) => { const yearA = parseInt(a.dataKey.split('_')[0]); const yearB = parseInt(b.dataKey.split('_')[0]); return yearA - yearB; }); const isCurrency = metric === 'sellOut'; return (

{label}

{sortedPayload.map((p: any, index: number) => { const year = p.dataKey.split('_')[0]; let growthEl = null; if (index > 0) { const prev = sortedPayload[index - 1]; const prevVal = Number(prev.value); const currVal = Number(p.value); if (prevVal > 0) { const pct = ((currVal - prevVal) / prevVal) * 100; growthEl = ( = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% ); } } return (
{year}:
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {growthEl}
); })}
); } return null; }; const GrowthTable: React.FC<{ title: string; data: GrowthMetric[]; type: 'growth' | 'decline'; periods: { current: string; previous: string }; }> = ({ title, data, type, periods }) => { const [sortConfig, setSortConfig] = useState<{ key: keyof GrowthMetric | null; direction: 'asc' | 'desc' }>({ key: null, direction: 'desc' }); const sortedData = useMemo(() => { if (!sortConfig.key) return data; return [...data].sort((a, b) => { const aVal = a[sortConfig.key!] as number | string; const bVal = b[sortConfig.key!] as number | string; if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1; if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1; return 0; }); }, [data, sortConfig]); const requestSort = (key: keyof GrowthMetric) => { let direction: 'asc' | 'desc' = 'desc'; // If already sorting by this key, toggle direction if (sortConfig.key === key && sortConfig.direction === 'desc') { direction = 'asc'; } setSortConfig({ key, direction }); }; const getSortIndicator = (key: keyof GrowthMetric) => { if (sortConfig.key !== key) { return ( ); } return ( {sortConfig.direction === 'asc' ? : } ); }; return (
{sortedData.length > 0 ? ( sortedData.map((item, idx) => ( )) ) : ( )}
requestSort('line')} >
Product Line {getSortIndicator('line')}
requestSort('previousYearSellOut')} >
Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}
requestSort('currentYearSellOut')} >
Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}
requestSort('sellOutGrowthValue')} >
SO Diff {getSortIndicator('sellOutGrowthValue')}
requestSort('sellOutGrowthPercentage')} >
SO Growth % {getSortIndicator('sellOutGrowthPercentage')}
{item.line} €{item.previousYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })} €{item.currentYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })} = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {item.sellOutGrowthValue > 0 ? '+' : ''}€{item.sellOutGrowthValue.toLocaleString('de-DE', { maximumFractionDigits: 0 })} = 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}> {item.sellOutGrowthPercentage >= 0 ? '+' : ''}{item.sellOutGrowthPercentage.toFixed(0)}%
Insufficient data to calculate {type}. (Select at least 2 distinct years/periods)
); }; const Dashboard: React.FC = ({ data, contextData, adsData = [], rawData = [], stockMap, vendorStockMap, buyBoxLostMap, top50Mode = 'eu', top50Ranking, velocityMap }) => { const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut'); const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut'); const [cumulativeMetric, setCumulativeMetric] = useState<'sellOut' | 'units'>('sellOut'); // Calculate Ads KPIs by Year const adsKPIsByYear = useMemo(() => { if (!adsData || adsData.length === 0) return null; const yearMap = new Map(); adsData.forEach(ad => { const y = ad.year.toString(); if (!yearMap.has(y)) { yearMap.set(y, { cost: 0, attributedSales: 0, clicks: 0, impressions: 0 }); } const t = yearMap.get(y)!; t.cost += ad.cost || 0; t.attributedSales += ad.attributedSales30d; t.clicks += ad.clicks || 0; t.impressions += ad.impressions || 0; }); const result: Record = {}; yearMap.forEach((t, year) => { result[year] = { totalSpend: t.cost, attributedSales: t.attributedSales, acos: t.attributedSales > 0 ? (t.cost / t.attributedSales) * 100 : 0, roas: t.cost > 0 ? t.attributedSales / t.cost : 0, cpc: t.clicks > 0 ? t.cost / t.clicks : 0, ctr: t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0, }; }); return result; }, [adsData]); const availableAdsYears = useMemo(() => { if (!adsKPIsByYear) return []; return Object.keys(adsKPIsByYear).sort((a, b) => parseInt(b) - parseInt(a)); }, [adsKPIsByYear]); // Decide which data source to use for Product Line charts const displayData = contextData || data; // Calculate dynamic height for the All Product Lines chart to enable scrolling const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300); // Dynamic stats computation for KPI cards to match mockup V2 layout const renderKPICardData = (metric: 'sellOut' | 'units') => { const sorted = [...data.availableYears].sort((a, b) => parseInt(b) - parseInt(a)); const currentYear = sorted[0]; const prevYear = sorted[1]; const twoYearsPrior = sorted[2]; const currentVal = currentYear && data.totalsByYear[currentYear] ? data.totalsByYear[currentYear][metric] : 0; const prevVal = prevYear && data.totalsByYear[prevYear] ? data.totalsByYear[prevYear][metric] : 0; const priorVal = twoYearsPrior && data.totalsByYear[twoYearsPrior] ? data.totalsByYear[twoYearsPrior][metric] : 0; const formatVal = (v: number) => { if (metric === 'sellOut') { return `€${v.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; } return v.toLocaleString('de-DE'); }; const formatDiff = (curr: number, prev: number) => { const diff = curr - prev; const sign = diff >= 0 ? '+' : ''; if (metric === 'sellOut') { return `${sign}€${diff.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`; } return `${sign}${diff.toLocaleString('de-DE')}`; }; let mainGrowthPercent = ''; if (prevVal > 0) { const pct = ((currentVal - prevVal) / prevVal) * 100; mainGrowthPercent = `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`; } let prevGrowthPercent = ''; if (priorVal > 0) { const pct = ((prevVal - priorVal) / priorVal) * 100; prevGrowthPercent = `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`; } return { currentYear, prevYear, twoYearsPrior, currentVal: formatVal(currentVal), prevVal: formatVal(prevVal), priorVal: formatVal(priorVal), diffLabel: `${formatDiff(currentVal, prevVal)} vs prior year`, mainGrowthPercent, prevGrowthPercent }; }; const sellOutStats = renderKPICardData('sellOut'); const unitsStats = renderKPICardData('units'); // Dynamic color coding based on year relevance const getYearColor = (year: string | number) => { const sorted = [...data.availableYears].sort((a, b) => parseInt(b) - parseInt(a)); const index = sorted.indexOf(year.toString()); if (index === 0) return '#06b6d4'; // Cyan (current year, e.g., 2025/2026) if (index === 1) return '#6366f1'; // Indigo (previous year, e.g., 2024/2025) if (index === 2) return '#64748b'; // Muted Grey (two years prior) return COLORS[index % COLORS.length] || '#64748b'; }; const cumulativeData = useMemo(() => { const source = cumulativeMetric === 'sellOut' ? data.seasonality : data.seasonalityUnits; const years = data.availableYears; const running: Record = {}; years.forEach(y => { running[y] = 0; }); return source.map(point => { const result: Record = { name: point.name }; years.forEach(y => { running[y] = (running[y] || 0) + ((point[y] as number) || 0); result[y] = running[y]; }); return result; }); }, [data.seasonality, data.seasonalityUnits, data.availableYears, cumulativeMetric]); return (
{/* Ads Performance Section - Split Actual vs Anterior */} {adsKPIsByYear && availableAdsYears.length > 0 && (() => { const currentYear = availableAdsYears[0]; const prevYear = availableAdsYears[1]; const twoYearsPrior = availableAdsYears[2]; const currentKpi = adsKPIsByYear[currentYear]; const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null; const priorKpi = twoYearsPrior ? adsKPIsByYear[twoYearsPrior] : null; const renderAdsGrowth = (currentVal: number, prevVal: number | undefined, isPercentage: boolean = false, inverse: boolean = false) => { if (!prevVal || prevVal === 0) return -; const diff = currentVal - prevVal; if (isPercentage) { const pct = (diff / prevVal) * 100; const isPos = pct >= 0; const color = inverse ? (isPos ? 'text-red-400' : 'text-emerald-400') : (isPos ? 'text-emerald-400' : 'text-red-400'); return {isPos ? '+' : ''}{pct.toFixed(1)}%; } else { // For ACOS diff in percentage points const isPos = diff >= 0; const color = inverse ? (isPos ? 'text-red-400' : 'text-emerald-400') : (isPos ? 'text-emerald-400' : 'text-red-400'); return {isPos ? '+' : ''}{diff.toFixed(2)}pp; } }; const renderRoasDiff = (currentVal: number, prevVal: number | undefined) => { if (!prevVal || prevVal === 0) return -; const diff = currentVal - prevVal; const isPos = diff >= 0; return {isPos ? '+' : ''}{diff.toFixed(2)}; }; return (

Advertising Performance

{adsData.length.toLocaleString('de-DE')} registros
{/* Período Actual */}
Current Period ({currentYear})
Sell Out €{currentKpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })} {renderAdsGrowth(currentKpi.totalSpend, prevKpi?.totalSpend, true)}
Spend €{currentKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })} {renderAdsGrowth(currentKpi.attributedSales, prevKpi?.attributedSales, true)}
Acos {currentKpi.acos.toFixed(1)}% {renderAdsGrowth(currentKpi.acos, prevKpi?.acos, false, true)}
Roas {currentKpi.roas.toFixed(2)}x {renderRoasDiff(currentKpi.roas, prevKpi?.roas)}
{/* Período Anterior */}
Previous Period ({prevYear || 'N/A'})
{prevKpi ? (
Sell Out €{prevKpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })} {renderAdsGrowth(prevKpi.totalSpend, priorKpi?.totalSpend, true)}
Spend €{prevKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })} {renderAdsGrowth(prevKpi.attributedSales, priorKpi?.attributedSales, true)}
Acos {prevKpi.acos.toFixed(1)}% {renderAdsGrowth(prevKpi.acos, priorKpi?.acos, false, true)}
Roas {prevKpi.roas.toFixed(2)}x {renderRoasDiff(prevKpi.roas, priorKpi?.roas)}
) : (
No historical data available
)}
); })()} {/* KPI Section with CraftAlley Card Layout */}
{/* Sell Out Revenue Card */}

Sell Out Revenue

{sellOutStats.mainGrowthPercent && ( {sellOutStats.mainGrowthPercent} )}
{sellOutStats.currentVal}

{sellOutStats.diffLabel}

Previous Year ({sellOutStats.prevYear}) {sellOutStats.prevVal} {sellOutStats.prevGrowthPercent}
2 Years Prior ({sellOutStats.twoYearsPrior || 'N/A'}) {sellOutStats.priorVal} reference
{/* Units Sold Card */}

Units Sold

{unitsStats.mainGrowthPercent && ( {unitsStats.mainGrowthPercent} )}
{unitsStats.currentVal}

{unitsStats.diffLabel}

Previous Year ({unitsStats.prevYear}) {unitsStats.prevVal} {unitsStats.prevGrowthPercent}
2 Years Prior ({unitsStats.twoYearsPrior || 'N/A'}) {unitsStats.priorVal} reference
{/* Row 1 Grid: Product Lines & Seasonality side-by-side */}
{/* Horizontal Bar Chart (Product Lines Revenue) */}
{contextData && Showing Full Product Line Data}
top10Metric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')} orientation='top' /> } cursor={{ fill: '#1e293b' }} /> {displayData.availableYears.map((year, index) => ( ))}
{/* Monthly Seasonality Chart */}
seasonalityMetric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')} /> } /> {data.availableYears.map((year, index) => ( ))}
{/* Row 2 Grid: Growth Tables side-by-side */}
{/* Growth Table (Fastest Growing) */}
{/* Decline Table (Declining Lines) */}
{/* Row 3: Full-width Cumulative Sales YoY Chart */}
cumulativeMetric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')} /> } /> {data.availableYears.map((year, index) => ( ))}
{/* Keep secondary charts/details below main redesigned mockup */}
{/* Units Chart (Split by Year) */}
{contextData &&
Showing Full Product Line Data
}
{ if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`; if (val >= 1000) return `${(val / 1000).toFixed(0)}k`; return val; }} /> } cursor={{ fill: '#1e293b' }} /> {displayData.availableYears.map((year, index) => ( ))}
{/* Country Chart */} `€${(val / 1000).toFixed(0)}k`} /> } cursor={{ fill: '#1e293b' }} /> {data.availableYears.map((year, index) => ( ))}
{/* Top Movers Table - Full Width */} {rawData && rawData.length > 0 && (
)}
); }; export default Dashboard;