From 0138b787b26dcbbe95e6501670e99b5c21e2c9ed Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 21 Jan 2026 12:49:21 +0100 Subject: [PATCH] feat: multi-year ads performance and YoY growth in Dashboard --- components/Dashboard.tsx | 161 ++++++++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 44 deletions(-) diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index 067d664..d2c94e5 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -459,27 +459,43 @@ const Dashboard: React.FC = ({ data, contextData, adsData = [] } const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut'); const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut'); - // Calculate Ads KPIs - const adsKPIs = useMemo(() => { + // Calculate Ads KPIs by Year + const adsKPIsByYear = useMemo(() => { if (!adsData || adsData.length === 0) return null; - const totals = adsData.reduce((acc, ad) => ({ - cost: acc.cost + ad.cost, - attributedSales: acc.attributedSales + ad.attributedSales30d, - clicks: acc.clicks + ad.clicks, - impressions: acc.impressions + ad.impressions, - }), { cost: 0, attributedSales: 0, clicks: 0, impressions: 0 }); + const yearMap = new Map(); - return { - totalSpend: totals.cost, - attributedSales: totals.attributedSales, - acos: totals.attributedSales > 0 ? (totals.cost / totals.attributedSales) * 100 : 0, - roas: totals.cost > 0 ? totals.attributedSales / totals.cost : 0, - cpc: totals.clicks > 0 ? totals.cost / totals.clicks : 0, - ctr: totals.impressions > 0 ? (totals.clicks / totals.impressions) * 100 : 0, - }; + 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; + t.attributedSales += ad.attributedSales30d; + t.clicks += ad.clicks; + t.impressions += ad.impressions; + }); + + 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 // If contextData is provided (drill down), we use that to show the "Total Line" view. // Otherwise we use the standard filtered data. @@ -492,35 +508,92 @@ const Dashboard: React.FC = ({ data, contextData, adsData = [] } return (
- {/* Ads Performance Section - Only shown when ads data is loaded */} - {adsKPIs && ( -
-
- -

Advertising Performance

- {adsData.length.toLocaleString('de-DE')} ad records loaded + {/* Ads Performance Section - Supports Multiple Years and Growth */} + {adsKPIsByYear && availableAdsYears.length > 0 && ( +
+
+
+ + +
+

Advertising Performance Breakdown

+
+ {adsData.length.toLocaleString('de-DE')} AD RECORDS LOADED +
-
-
- Total Ad Spend - €{adsKPIs.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })} -
-
- Attributed Sales - €{adsKPIs.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })} -
-
- ACOS - - {adsKPIs.acos.toFixed(1)}% - -
-
- ROAS - = 3 ? 'text-emerald-400' : adsKPIs.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}> - {adsKPIs.roas.toFixed(2)}x - -
+ +
+ {availableAdsYears.map((year, idx) => { + const kpi = adsKPIsByYear[year]; + const prevYear = availableAdsYears[idx + 1]; + const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null; + + const renderGrowth = (current: number, previous: number | undefined, inverse: boolean = false) => { + if (!previous || previous === 0) return null; + const pct = ((current - previous) / previous) * 100; + const isPositive = pct >= 0; + const colorClass = inverse + ? (isPositive ? 'text-red-400' : 'text-emerald-400') + : (isPositive ? 'text-emerald-400' : 'text-red-400'); + + return ( + + {isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(1)}% + + ); + }; + + return ( +
+
+ {year} +
+
+
+ {/* Ad Spend */} +
+ Ad Spend +
+ + €{kpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })} + + {renderGrowth(kpi.totalSpend, prevKpi?.totalSpend)} +
+
+ {/* Attributed Sales */} +
+ Attr. Sales +
+ + €{kpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })} + + {renderGrowth(kpi.attributedSales, prevKpi?.attributedSales)} +
+
+ {/* ACOS */} +
+ ACOS +
+ + {kpi.acos.toFixed(1)}% + + {renderGrowth(kpi.acos, prevKpi?.acos, true)} +
+
+ {/* ROAS */} +
+ ROAS +
+ = 3 ? 'text-emerald-400' : kpi.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}> + {kpi.roas.toFixed(2)}x + + {renderGrowth(kpi.roas, prevKpi?.roas)} +
+
+
+
+ ); + })}
)}