feat: add cumulative YoY sales chart to Dashboard

New line chart shows accumulated sell-out (€) or units month by month,
comparing all available years on the same axis for like-for-like
year-over-year tracking.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-05-20 08:20:26 +02:00
co-authored by Claude Sonnet 4.6
parent b31911a9df
commit 9f9c0c2975
+64
View File
@@ -479,6 +479,22 @@ const Dashboard: React.FC<DashboardProps> = ({
}) => { }) => {
const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut'); const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut');
const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut'); const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut');
const [cumulativeMetric, setCumulativeMetric] = useState<'sellOut' | 'units'>('sellOut');
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]);
// Calculate Ads KPIs by Year // Calculate Ads KPIs by Year
const adsKPIsByYear = useMemo(() => { const adsKPIsByYear = useMemo(() => {
@@ -804,6 +820,54 @@ const Dashboard: React.FC<DashboardProps> = ({
</div> </div>
</ExpandableCard> </ExpandableCard>
{/* Cumulative Sales Chart - YoY comparison month by month */}
<ExpandableCard title="Cumulative Sales YoY (Month by Month)" 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={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. */} {/* 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"> <ExpandableCard title="Revenue Distribution by Customer" className="h-80">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">