Files
CrazeAnalytix/components/Dashboard.tsx
T
Christian 9ba63ab8f8 feat: Initialize Craze Analytix project structure
Sets up the project with Vite, React, Tailwind CSS, Gemini AI integration, and necessary dependencies for data analysis. Includes initial configuration for TypeScript, Tailwind, and project metadata.
2025-12-11 11:25:26 +01:00

712 lines
37 KiB
TypeScript

import React, { useState, useMemo, useEffect } from 'react';
import { AggregatedData, LineGrowthMetric } from '../types'; // Updated import for LineGrowthMetric
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
LineChart, Line, Legend
} from 'recharts';
import { DownloadIcon } from './Icons'; // Import DownloadIcon
interface DashboardProps {
data: AggregatedData;
contextData?: AggregatedData | null;
}
const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9'];
// Reusable Expandable Card Component
export const ExpandableCard: React.FC<{
title: string;
children: React.ReactNode;
className?: string;
onExport?: () => void; // Optional export function
exportFileName?: string; // Optional export file name
}> = ({ title, children, className, onExport, exportFileName }) => {
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-6 pb-6 pt-32 flex flex-col animate-fade-in overflow-hidden">
{/* Fixed Close Button - Positioned TOP RIGHT ON TOP OF FILTER BAR with z-[100] */}
<button
onClick={toggleExpand}
className="fixed top-3 right-3 z-[100] p-2 bg-red-600/90 hover: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-4 border-b border-slate-800 pb-4 shrink-0">
<h3 className="text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
{onExport && (
<button
onClick={onExport}
className="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors shadow-sm"
>
<DownloadIcon /> Export CSV
</button>
)}
</div>
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-6 border border-border custom-scrollbar">
{children}
</div>
</div>
);
}
return (
<div
className={`bg-surface border border-border rounded-xl 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-4">
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
<div className="flex items-center gap-2">
{onExport && (
<button
onClick={onExport}
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-indigo-400 transition-opacity"
title={`Export ${exportFileName || title} to CSV`}
>
<DownloadIcon />
</button>
)}
<button
onClick={toggleExpand}
className="opacity-0 group-hover:opacity-100 text-slate-500 hover: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>
<div className="flex-1 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(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
}
return val.toLocaleString();
};
// Determine title based on context
const displayTitle = contextData ? `${title} (Selected Item)` : title;
return (
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-6 flex flex-col justify-center">
<h3 className="text-sm font-medium text-slate-400 mb-3">{displayTitle}</h3>
{sortedYears.length === 0 && <p className="text-3xl 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 pb-2 last:border-0">
<div className="flex justify-between items-end">
<div className="flex flex-col">
<span className="text-xs text-slate-500 font-mono mb-0.5">{year}</span>
<div className="flex items-center">
<span className="text-xl font-bold text-slate-200 leading-none">
{formatValue(currentValue)}
</span>
{growthElement}
</div>
</div>
</div>
{/* Context Row (Product Line Total) */}
{contextData && contextValue > 0 && (
<div className="mt-1 flex flex-wrap items-center justify-between bg-slate-800/50 p-2 rounded text-xs">
<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(1)}% 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(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}`
: Number(p.value).toLocaleString()}
</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(undefined, {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(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}
</span>
{growthEl}
</div>
</div>
);
})}
</div>
);
}
return null;
};
const GrowthTable: React.FC<{
title: string;
data: LineGrowthMetric[]; // Updated to LineGrowthMetric
type: 'growth' | 'decline';
periods: { current: string; previous: string };
}> = ({ title, data, type, periods }) => {
const [sortConfig, setSortConfig] = useState<{ key: keyof LineGrowthMetric | 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 LineGrowthMetric) => {
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 LineGrowthMetric) => {
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 (
<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-[150px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('line')}
>
<div className="flex items-center">Product Line {getSortIndicator('line')}</div>
</th>
{/* Sell Out Columns */}
<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">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">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">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">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
</th>
{/* Units Columns */}
<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('previousYearUnits')}
>
<div className="flex items-center justify-end">Units {periods.previous} {getSortIndicator('previousYearUnits')}</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('currentYearUnits')}
>
<div className="flex items-center justify-end">Units {periods.current} {getSortIndicator('currentYearUnits')}</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('unitsGrowthValue')}
>
<div className="flex items-center justify-end">Units Diff {getSortIndicator('unitsGrowthValue')}</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('unitsGrowthPercentage')}
>
<div className="flex items-center justify-end">Units Growth % {getSortIndicator('unitsGrowthPercentage')}</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>
{/* Sell Out Columns */}
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearSellOut.toLocaleString(undefined, {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(undefined, {maximumFractionDigits: 0})}
{/* {item.sellOutGrowthValue.toFixed(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-medium ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
{item.sellOutGrowthPercentage.toFixed(1)}%
</span>
</td>
{/* Units Columns */}
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearUnits.toLocaleString()}</td>
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearUnits.toLocaleString()}</td>
<td className={`px-4 py-2 text-right font-medium ${item.unitsGrowthValue >= 0 ? 'text-violet-400' : 'text-orange-400'}`}>
{item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString()}
</td>
<td className="px-4 py-2 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.unitsGrowthPercentage >= 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}>
{item.unitsGrowthPercentage.toFixed(1)}%
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={9} 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>
);
}
const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut');
const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut');
// Decide which data source to use for Product Line charts
// If contextData is provided (drill down), we use that to show the "Total Line" view.
// Otherwise we use the standard filtered data.
const displayData = contextData || data;
// Calculate dynamic height for the All Product Lines chart to enable scrolling
// Assume ~60px per product line to give it enough space, minimum 300px
const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300);
return (
<div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
{/* KPI Section - Pass both specific data and context data */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<MultiYearKPICard
title="Sell Out Revenue"
metric="sellOut"
data={data.totalsByYear}
availableYears={data.availableYears}
contextData={contextData ? contextData.totalsByYear : undefined}
/>
<MultiYearKPICard
title="Units Sold"
metric="units"
data={data.totalsByYear}
availableYears={data.availableYears}
contextData={contextData ? contextData.totalsByYear : undefined}
/>
</div>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-6 flex flex-col">
{/* All Product Lines Revenue Chart */}
<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>
{/* Scrollable Container */}
<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()}
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={COLORS[index % COLORS.length]}
radius={[0, 4, 4, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</ExpandableCard>
{/* Growth Table */}
<ExpandableCard
title={contextData ? "Fastest Growing Lines (Full Line Context)" : "Fastest Growing Lines (YoY - €)"}
className="h-[400px]" // Provide a default height for the card
>
<GrowthTable
title={contextData ? "Fastest Growing Lines (Full Line Context)" : "Fastest Growing Lines (YoY - €)"}
data={displayData.topMovers}
type="growth"
periods={displayData.comparisonPeriods}
/>
</ExpandableCard>
{/* Decline Table */}
<ExpandableCard
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines (YoY - €)"}
className="h-[400px]" // Provide a default height for the card
>
<GrowthTable
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines (YoY - €)"}
data={displayData.bottomMovers}
type="decline"
periods={displayData.comparisonPeriods}
/>
</ExpandableCard>
</div>
{/* Right Column */}
<div className="space-y-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={COLORS[index % COLORS.length]}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
{/* Seasonality Chart - ALWAYS uses specific filtered data 'data' */}
<ExpandableCard title="Monthly Sales Seasonality (Selected Items)" 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()}
/>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={seasonalityMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={COLORS[index % COLORS.length]}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
{/* Country Chart - Uses Specific Data 'data' usually, unless we want to broaden it. Kept specific for now. */}
<ExpandableCard title="Revenue Distribution by Customer" className="h-80">
<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={COLORS[index % COLORS.length]}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</ExpandableCard>
</div>
</div>
</div>
);
};
export default Dashboard;