Files

1019 lines
58 KiB
TypeScript

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<string, number>;
vendorStockMap?: Map<string, { eu: number; uk: number }>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
top50Mode?: 'eu' | 'uk';
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
velocityMap?: Map<string, number>;
}
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 (
<div className="fixed inset-0 z-50 bg-slate-950 px-3 pb-16 pt-16 md:px-6 md:pb-6 md:pt-32 flex flex-col animate-fade-in overflow-hidden">
{/* Fixed Close Button */}
<button
onClick={toggleExpand}
className="fixed top-3 right-3 z-[100] p-2 bg-red-600/90 hover:bg-red-500 active:bg-red-500 border border-red-400 rounded-full text-white transition-all shadow-2xl hover:scale-110 flex items-center gap-2 group"
title="Exit Fullscreen"
>
<span className="text-xs font-bold hidden group-hover:inline pr-1">CLOSE</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
<div className="flex justify-between items-center mb-3 md:mb-4 border-b border-slate-800 pb-3 md:pb-4 shrink-0">
<h3 className="text-base md:text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
</div>
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-3 md:p-6 border border-border custom-scrollbar scroll-touch">
{children}
</div>
</div>
);
}
return (
<div
className={`bg-surface border border-border rounded-xl p-3 md:p-6 shadow-sm flex flex-col group relative transition-all duration-300 hover:shadow-primary/5 ${className}`}
>
<div className="flex justify-between items-start mb-3 md:mb-4">
<h3 className="text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
<button
onClick={toggleExpand}
className="opacity-100 md:opacity-0 md:group-hover:opacity-100 text-slate-500 hover:text-primary active:text-primary transition-opacity"
title="Expand to Fullscreen"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>
</button>
</div>
<div className="flex-1 min-h-[200px] md:min-h-[250px] cursor-pointer" onClick={toggleExpand}>{children}</div>
</div>
);
};
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 (
<div className="bg-slate-900/40 border border-slate-800 rounded-xl p-5 md:p-6 flex flex-col justify-center shadow-lg">
<h3 className="text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wider mb-4">{displayTitle}</h3>
{sortedYears.length === 0 && <p className="text-2xl font-bold text-white">0</p>}
{sortedYears.length > 0 && (
<div className="space-y-4">
{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 = (
<span className={`text-xs ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
// 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 = (
<span className={`text-[10px] ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
}
}
return (
<div key={year} className="flex flex-col border-b border-slate-800/80 pb-3 last:border-0 last:pb-0">
<span className="text-xs text-slate-500 font-medium mb-1">{year}</span>
<div className="flex items-baseline">
<span className="text-2xl font-bold text-white tracking-tight leading-none">
{formatValue(currentValue)}
</span>
{growthElement}
</div>
{/* Context Row (Product Line Total) */}
{contextData && contextValue > 0 && (
<div className="mt-2 flex flex-wrap items-center justify-between bg-slate-850/50 p-2 rounded text-xs border border-slate-800">
<span className="text-slate-400 mr-2">Total Product Line:</span>
<div className="flex items-center gap-1">
<span className="text-slate-300 font-medium">{formatValue(contextValue)}</span>
{contextGrowthElement}
{/* Share of Line % */}
<span className="text-indigo-400 font-bold border-l border-slate-700 pl-2 ml-2 whitespace-nowrap">
{((currentValue / contextValue) * 100).toFixed(0)}% Share
</span>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
};
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-slate-900 border border-border p-3 rounded shadow-xl text-sm max-w-xs z-50">
<p className="font-bold text-slate-100 mb-2 border-b border-slate-700 pb-1">{label}</p>
{payload.map((p: any) => (
<p key={p.name} className="text-slate-300 flex justify-between gap-4" style={{ color: p.color }}>
<span>{p.name}:</span>
<span className="font-mono font-semibold">
{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')}
</span>
</p>
))}
</div>
);
}
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 (
<div className="bg-slate-900 border border-border p-3 rounded shadow-xl text-sm max-w-xs z-50">
<p className="font-bold text-slate-100 mb-2 border-b border-slate-700 pb-1">{label}</p>
{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 = (
<span className={`text-xs ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
}
return (
<div key={p.name} className="flex justify-between items-center gap-2 mb-1">
<span style={{ color: p.color }}>{p.name}:</span>
<div className="flex items-center">
<span className="font-mono font-semibold text-slate-200">
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
{growthEl}
</div>
</div>
);
})}
</div>
);
}
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 (
<div className="bg-slate-900 border border-border p-3 rounded shadow-xl text-sm max-w-xs z-50">
<p className="font-bold text-slate-100 mb-2 border-b border-slate-700 pb-1">{label}</p>
{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 = (
<span className={`text-xs ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
}
return (
<div key={year} className="flex justify-between items-center gap-2 mb-1">
<span style={{ color: p.color }}>{year}:</span>
<div className="flex items-center">
<span className="font-mono font-semibold text-slate-200">
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
{growthEl}
</div>
</div>
);
})}
</div>
);
}
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 (
<svg className="w-2.5 h-2.5 ml-1 text-slate-600 opacity-0 group-hover:opacity-50" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
);
}
return (
<svg className="w-2.5 h-2.5 ml-1 text-primary" fill="currentColor" viewBox="0 0 20 20">
{sortConfig.direction === 'asc'
? <path fillRule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clipRule="evenodd" />
: <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
}
</svg>
);
};
return (
<ExpandableCard title={title} className="h-full">
<div className="overflow-auto h-full relative">
<table className="w-full text-left text-sm h-full border-separate border-spacing-0">
<thead className="bg-slate-950 text-xs uppercase font-semibold text-slate-500 sticky top-0 z-10 shadow-sm">
<tr>
<th
className="px-4 py-3 bg-slate-950 min-w-[120px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('line')}
>
<div className="flex items-center font-bold">Product Line {getSortIndicator('line')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('previousYearSellOut')}
>
<div className="flex items-center justify-end font-bold">Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
onClick={() => requestSort('currentYearSellOut')}
>
<div className="flex items-center justify-end font-bold">Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('sellOutGrowthValue')}
>
<div className="flex items-center justify-end font-bold">SO Diff {getSortIndicator('sellOutGrowthValue')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('sellOutGrowthPercentage')}
>
<div className="flex items-center justify-end font-bold">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
</th>
</tr>
</thead>
<tbody className="divide-y divide-border text-slate-300">
{sortedData.length > 0 ? (
sortedData.map((item, idx) => (
<tr key={idx} className="hover:bg-slate-800/50">
<td className="px-4 py-2 font-medium">{item.line}</td>
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
<td className={`px-4 py-2 text-right font-medium ${item.sellOutGrowthValue >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{item.sellOutGrowthValue > 0 ? '+' : ''}{item.sellOutGrowthValue.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</td>
<td className="px-4 py-2 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-bold ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
{item.sellOutGrowthPercentage >= 0 ? '+' : ''}{item.sellOutGrowthPercentage.toFixed(0)}%
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={5} className="px-4 py-6 text-center text-slate-500 italic">
Insufficient data to calculate {type}. (Select at least 2 distinct years/periods)
</td>
</tr>
)}
</tbody>
</table>
</div>
</ExpandableCard>
);
};
const Dashboard: React.FC<DashboardProps> = ({
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<string, { cost: number; attributedSales: number; clicks: number; impressions: number }>();
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<string, { totalSpend: number; attributedSales: number; acos: number; roas: number; cpc: number; ctr: number }> = {};
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<string, number> = {};
years.forEach(y => { running[y] = 0; });
return source.map(point => {
const result: Record<string, number | string> = { 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 (
<div className="p-3 md:p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
{/* 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 <span className="text-slate-500 font-medium">-</span>;
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 <span className={`text-[10px] font-bold ${color}`}>{isPos ? '+' : ''}{pct.toFixed(1)}%</span>;
} 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 <span className={`text-[10px] font-bold ${color}`}>{isPos ? '+' : ''}{diff.toFixed(2)}pp</span>;
}
};
const renderRoasDiff = (currentVal: number, prevVal: number | undefined) => {
if (!prevVal || prevVal === 0) return <span className="text-slate-500 font-medium">-</span>;
const diff = currentVal - prevVal;
const isPos = diff >= 0;
return <span className={`text-[10px] font-bold ${isPos ? 'text-emerald-400' : 'text-red-400'}`}>{isPos ? '+' : ''}{diff.toFixed(2)}</span>;
};
return (
<div className="bg-slate-900/40 border border-slate-800 rounded-2xl p-5 shadow-xl">
<div className="flex items-center gap-2 mb-4 border-b border-slate-800/80 pb-3">
<span className="w-2 h-2 rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.8)]"></span>
<h3 className="text-xs font-black text-slate-300 uppercase tracking-widest">Advertising Performance</h3>
<div className="ml-auto text-[10px] text-slate-500 font-medium">
{adsData.length.toLocaleString('de-DE')} registros
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 divide-y lg:divide-y-0 lg:divide-x divide-slate-800">
{/* Período Actual */}
<div className="pb-4 lg:pb-0 lg:pr-6">
<div className="bg-slate-800/40 px-3 py-1.5 rounded text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
Current Period ({currentYear})
</div>
<div className="grid grid-cols-4 gap-2">
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sell Out</span>
<span className="text-base font-bold text-white block">
{currentKpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(currentKpi.totalSpend, prevKpi?.totalSpend, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
<span className="text-base font-bold text-white block">
{currentKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(currentKpi.attributedSales, prevKpi?.attributedSales, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Acos</span>
<span className="text-base font-bold text-white block">
{currentKpi.acos.toFixed(1)}%
</span>
{renderAdsGrowth(currentKpi.acos, prevKpi?.acos, false, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Roas</span>
<span className="text-base font-bold text-white block">
{currentKpi.roas.toFixed(2)}x
</span>
{renderRoasDiff(currentKpi.roas, prevKpi?.roas)}
</div>
</div>
</div>
{/* Período Anterior */}
<div className="pt-4 lg:pt-0 lg:pl-6">
<div className="bg-slate-800/40 px-3 py-1.5 rounded text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
Previous Period ({prevYear || 'N/A'})
</div>
{prevKpi ? (
<div className="grid grid-cols-4 gap-2">
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sell Out</span>
<span className="text-base font-bold text-white block">
{prevKpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(prevKpi.totalSpend, priorKpi?.totalSpend, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
<span className="text-base font-bold text-white block">
{prevKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(prevKpi.attributedSales, priorKpi?.attributedSales, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Acos</span>
<span className="text-base font-bold text-white block">
{prevKpi.acos.toFixed(1)}%
</span>
{renderAdsGrowth(prevKpi.acos, priorKpi?.acos, false, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Roas</span>
<span className="text-base font-bold text-white block">
{prevKpi.roas.toFixed(2)}x
</span>
{renderRoasDiff(prevKpi.roas, priorKpi?.roas)}
</div>
</div>
) : (
<div className="text-sm text-slate-500 italic py-2">No historical data available</div>
)}
</div>
</div>
</div>
);
})()}
{/* KPI Section with CraftAlley Card Layout */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Sell Out Revenue Card */}
<div className="bg-slate-900/40 border border-slate-800 rounded-xl p-5 md:p-6 flex flex-col justify-between shadow-lg relative">
<div className="flex justify-between items-start mb-2">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Sell Out Revenue</h3>
{sellOutStats.mainGrowthPercent && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
{sellOutStats.mainGrowthPercent}
</span>
)}
</div>
<div className="mb-4">
<span className="text-4xl font-extrabold text-white tracking-tight leading-none">
{sellOutStats.currentVal}
</span>
<p className="text-xs text-emerald-400 font-semibold mt-2">{sellOutStats.diffLabel}</p>
</div>
<div className="border-t border-slate-800/80 pt-4 mt-2">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Previous Year ({sellOutStats.prevYear})</span>
<span className="text-lg font-bold text-slate-200 block">{sellOutStats.prevVal}</span>
<span className="text-xs text-emerald-400 font-bold">{sellOutStats.prevGrowthPercent}</span>
</div>
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">2 Years Prior ({sellOutStats.twoYearsPrior || 'N/A'})</span>
<span className="text-lg font-bold text-slate-200 block">{sellOutStats.priorVal}</span>
<span className="text-xs text-slate-500 font-medium">reference</span>
</div>
</div>
</div>
</div>
{/* Units Sold Card */}
<div className="bg-slate-900/40 border border-slate-800 rounded-xl p-5 md:p-6 flex flex-col justify-between shadow-lg relative">
<div className="flex justify-between items-start mb-2">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Units Sold</h3>
{unitsStats.mainGrowthPercent && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
{unitsStats.mainGrowthPercent}
</span>
)}
</div>
<div className="mb-4">
<span className="text-4xl font-extrabold text-white tracking-tight leading-none">
{unitsStats.currentVal}
</span>
<p className="text-xs text-emerald-400 font-semibold mt-2">{unitsStats.diffLabel}</p>
</div>
<div className="border-t border-slate-800/80 pt-4 mt-2">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Previous Year ({unitsStats.prevYear})</span>
<span className="text-lg font-bold text-slate-200 block">{unitsStats.prevVal}</span>
<span className="text-xs text-emerald-400 font-bold">{unitsStats.prevGrowthPercent}</span>
</div>
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">2 Years Prior ({unitsStats.twoYearsPrior || 'N/A'})</span>
<span className="text-lg font-bold text-slate-200 block">{unitsStats.priorVal}</span>
<span className="text-xs text-slate-500 font-medium">reference</span>
</div>
</div>
</div>
</div>
</div>
{/* Row 1 Grid: Product Lines & Seasonality side-by-side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Horizontal Bar Chart (Product Lines Revenue) */}
<ExpandableCard title={contextData ? "Total Product Line Performance (Context)" : "Product Lines - Revenue"} className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-2">
{contextData && <span className="text-xs text-indigo-400 font-semibold uppercase tracking-wider">Showing Full Product Line Data</span>}
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs ml-auto">
<button
onClick={(e) => { e.stopPropagation(); setTop10Metric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${top10Metric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setTop10Metric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${top10Metric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
<div className="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
<div style={{ height: `${chartHeight}px` }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={displayData.topLinesSplit} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" horizontal={false} />
<XAxis
type="number"
stroke="#64748b"
tickFormatter={(val) => top10Metric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
orientation='top'
/>
<YAxis dataKey="name" type="category" width={100} stroke="#94a3b8" tick={{ fontSize: 12 }} />
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric={top10Metric} />} cursor={{ fill: '#1e293b' }} />
{displayData.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={`${year}_${top10Metric === 'sellOut' ? 'value' : 'units'}`}
name={year}
fill={getYearColor(year)}
radius={[0, 4, 4, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</ExpandableCard>
{/* Monthly Seasonality Chart */}
<ExpandableCard title="Monthly Seasonality - Sales" className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-end mb-2">
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
<button
onClick={(e) => { e.stopPropagation(); setSeasonalityMetric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${seasonalityMetric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setSeasonalityMetric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${seasonalityMetric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={seasonalityMetric === 'sellOut' ? data.seasonality : data.seasonalityUnits}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis
stroke="#64748b"
tickFormatter={(val) => seasonalityMetric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
/>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={seasonalityMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
</div>
{/* Row 2 Grid: Growth Tables side-by-side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Growth Table (Fastest Growing) */}
<div className="h-80">
<GrowthTable
title={contextData ? "Fastest Growing (Full Line Context)" : "Fastest Growing - YoY (€)"}
data={displayData.topMovers}
type="growth"
periods={displayData.comparisonPeriods}
/>
</div>
{/* Decline Table (Declining Lines) */}
<div className="h-80">
<GrowthTable
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines - YoY (€)"}
data={displayData.bottomMovers}
type="decline"
periods={displayData.comparisonPeriods}
/>
</div>
</div>
{/* Row 3: Full-width Cumulative Sales YoY Chart */}
<div className="w-full">
<ExpandableCard title="Cumulative Sales YoY - Mensual" className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-end mb-2">
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
<button
onClick={(e) => { e.stopPropagation(); setCumulativeMetric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${cumulativeMetric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setCumulativeMetric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${cumulativeMetric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={cumulativeData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis
stroke="#64748b"
tickFormatter={(val) => cumulativeMetric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
/>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={cumulativeMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
</div>
{/* Keep secondary charts/details below main redesigned mockup */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Units Chart (Split by Year) */}
<ExpandableCard title={contextData ? "Total Product Line Units (Context)" : "Units Sold by Product Line (Overview)"} className="h-96">
<div className="flex flex-col h-full">
{contextData && <div className="text-xs text-indigo-400 font-semibold uppercase tracking-wider mb-2 text-right">Showing Full Product Line Data</div>}
<div className="flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={displayData.byLineOverviewSplit} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" tick={{ fontSize: 10 }} interval={0} angle={-15} textAnchor="end" height={60} />
<YAxis
stroke="#64748b"
tickFormatter={(val) => {
if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
if (val >= 1000) return `${(val / 1000).toFixed(0)}k`;
return val;
}}
/>
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="units" />} cursor={{ fill: '#1e293b' }} />
{displayData.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={year}
name={year}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
{/* Country Chart */}
<ExpandableCard title="Revenue Distribution by Customer" className="h-96">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.byCustomerSplit}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis stroke="#64748b" tickFormatter={(val) => `€${(val / 1000).toFixed(0)}k`} />
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="sellOut" />} cursor={{ fill: '#1e293b' }} />
{data.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={year}
name={year}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</ExpandableCard>
</div>
{/* Top Movers Table - Full Width */}
{rawData && rawData.length > 0 && (
<div className="mt-6">
<TopMovers
data={rawData}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
buyBoxLostMap={buyBoxLostMap}
top50Mode={top50Mode}
velocityMap={velocityMap}
top50Ranking={top50Ranking}
/>
</div>
)}
</div>
);
};
export default Dashboard;