mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:35:24 +02:00
feat: multi-year ads performance and YoY growth in Dashboard
This commit is contained in:
+117
-44
@@ -459,27 +459,43 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData, adsData = [] }
|
|||||||
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');
|
||||||
|
|
||||||
// Calculate Ads KPIs
|
// Calculate Ads KPIs by Year
|
||||||
const adsKPIs = useMemo(() => {
|
const adsKPIsByYear = useMemo(() => {
|
||||||
if (!adsData || adsData.length === 0) return null;
|
if (!adsData || adsData.length === 0) return null;
|
||||||
|
|
||||||
const totals = adsData.reduce((acc, ad) => ({
|
const yearMap = new Map<string, { cost: number; attributedSales: number; clicks: number; impressions: number }>();
|
||||||
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 });
|
|
||||||
|
|
||||||
return {
|
adsData.forEach(ad => {
|
||||||
totalSpend: totals.cost,
|
const y = ad.year.toString();
|
||||||
attributedSales: totals.attributedSales,
|
if (!yearMap.has(y)) {
|
||||||
acos: totals.attributedSales > 0 ? (totals.cost / totals.attributedSales) * 100 : 0,
|
yearMap.set(y, { cost: 0, attributedSales: 0, clicks: 0, impressions: 0 });
|
||||||
roas: totals.cost > 0 ? totals.attributedSales / totals.cost : 0,
|
}
|
||||||
cpc: totals.clicks > 0 ? totals.cost / totals.clicks : 0,
|
const t = yearMap.get(y)!;
|
||||||
ctr: totals.impressions > 0 ? (totals.clicks / totals.impressions) * 100 : 0,
|
t.cost += ad.cost;
|
||||||
};
|
t.attributedSales += ad.attributedSales30d;
|
||||||
|
t.clicks += ad.clicks;
|
||||||
|
t.impressions += ad.impressions;
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
}, [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
|
// 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.
|
// If contextData is provided (drill down), we use that to show the "Total Line" view.
|
||||||
// Otherwise we use the standard filtered data.
|
// Otherwise we use the standard filtered data.
|
||||||
@@ -492,35 +508,92 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData, adsData = [] }
|
|||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
|
<div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
|
||||||
|
|
||||||
{/* Ads Performance Section - Only shown when ads data is loaded */}
|
{/* Ads Performance Section - Supports Multiple Years and Growth */}
|
||||||
{adsKPIs && (
|
{adsKPIsByYear && availableAdsYears.length > 0 && (
|
||||||
<div className="bg-gradient-to-r from-fuchsia-900/20 to-indigo-900/20 border border-fuchsia-500/30 rounded-xl p-6 animate-fade-in">
|
<div className="bg-gradient-to-br from-fuchsia-950/20 to-indigo-950/30 border border-fuchsia-500/30 rounded-2xl p-6 animate-fade-in shadow-xl">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<span className="w-2 h-2 rounded-full bg-fuchsia-400 animate-pulse"></span>
|
<div className="relative">
|
||||||
<h3 className="text-sm font-bold text-fuchsia-400 uppercase tracking-wider">Advertising Performance</h3>
|
<span className="absolute inset-0 bg-fuchsia-400 rounded-full animate-ping opacity-20"></span>
|
||||||
<span className="text-xs text-slate-500 ml-auto">{adsData.length.toLocaleString('de-DE')} ad records loaded</span>
|
<span className="relative block w-2.5 h-2.5 rounded-full bg-fuchsia-400 shadow-[0_0_8px_rgba(232,121,249,0.8)]"></span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-black text-fuchsia-400 uppercase tracking-[0.2em]">Advertising Performance Breakdown</h3>
|
||||||
|
<div className="ml-auto px-3 py-1 bg-slate-900/50 rounded-full border border-fuchsia-500/20 text-[10px] text-fuchsia-300 font-bold">
|
||||||
|
{adsData.length.toLocaleString('de-DE')} AD RECORDS LOADED
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4">
|
<div className="space-y-6">
|
||||||
<span className="text-xs text-slate-400 block mb-1">Total Ad Spend</span>
|
{availableAdsYears.map((year, idx) => {
|
||||||
<span className="text-2xl font-bold text-fuchsia-400">€{adsKPIs.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</span>
|
const kpi = adsKPIsByYear[year];
|
||||||
</div>
|
const prevYear = availableAdsYears[idx + 1];
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4">
|
const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null;
|
||||||
<span className="text-xs text-slate-400 block mb-1">Attributed Sales</span>
|
|
||||||
<span className="text-2xl font-bold text-emerald-400">€{adsKPIs.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</span>
|
const renderGrowth = (current: number, previous: number | undefined, inverse: boolean = false) => {
|
||||||
</div>
|
if (!previous || previous === 0) return null;
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4">
|
const pct = ((current - previous) / previous) * 100;
|
||||||
<span className="text-xs text-slate-400 block mb-1">ACOS</span>
|
const isPositive = pct >= 0;
|
||||||
<span className={`text-2xl font-bold ${adsKPIs.acos <= 30 ? 'text-emerald-400' : adsKPIs.acos <= 50 ? 'text-amber-400' : 'text-red-400'}`}>
|
const colorClass = inverse
|
||||||
{adsKPIs.acos.toFixed(1)}%
|
? (isPositive ? 'text-red-400' : 'text-emerald-400')
|
||||||
</span>
|
: (isPositive ? 'text-emerald-400' : 'text-red-400');
|
||||||
</div>
|
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4">
|
return (
|
||||||
<span className="text-xs text-slate-400 block mb-1">ROAS</span>
|
<span className={`text-[10px] font-black ml-1.5 ${colorClass}`}>
|
||||||
<span className={`text-2xl font-bold ${adsKPIs.roas >= 3 ? 'text-emerald-400' : adsKPIs.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}>
|
{isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(1)}%
|
||||||
{adsKPIs.roas.toFixed(2)}x
|
</span>
|
||||||
</span>
|
);
|
||||||
</div>
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={year} className="relative">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<span className="text-xs font-black text-slate-500 font-mono tracking-widest">{year}</span>
|
||||||
|
<div className="h-[1px] flex-1 bg-gradient-to-r from-slate-800 to-transparent"></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{/* Ad Spend */}
|
||||||
|
<div className="bg-slate-900/40 rounded-xl p-4 border border-white/5 transition-all hover:border-fuchsia-500/20">
|
||||||
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-1">Ad Spend</span>
|
||||||
|
<div className="flex items-baseline">
|
||||||
|
<span className="text-xl font-black text-fuchsia-400">
|
||||||
|
€{kpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
|
||||||
|
</span>
|
||||||
|
{renderGrowth(kpi.totalSpend, prevKpi?.totalSpend)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Attributed Sales */}
|
||||||
|
<div className="bg-slate-900/40 rounded-xl p-4 border border-white/5 transition-all hover:border-emerald-500/20">
|
||||||
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-1">Attr. Sales</span>
|
||||||
|
<div className="flex items-baseline">
|
||||||
|
<span className="text-xl font-black text-emerald-400">
|
||||||
|
€{kpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
|
||||||
|
</span>
|
||||||
|
{renderGrowth(kpi.attributedSales, prevKpi?.attributedSales)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* ACOS */}
|
||||||
|
<div className="bg-slate-900/40 rounded-xl p-4 border border-white/5 transition-all hover:border-amber-500/20">
|
||||||
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-1">ACOS</span>
|
||||||
|
<div className="flex items-baseline">
|
||||||
|
<span className={`text-xl font-black ${kpi.acos <= 30 ? 'text-emerald-400' : kpi.acos <= 50 ? 'text-amber-400' : 'text-red-400'}`}>
|
||||||
|
{kpi.acos.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
{renderGrowth(kpi.acos, prevKpi?.acos, true)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* ROAS */}
|
||||||
|
<div className="bg-slate-900/40 rounded-xl p-4 border border-white/5 transition-all hover:border-indigo-500/20">
|
||||||
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-1">ROAS</span>
|
||||||
|
<div className="flex items-baseline">
|
||||||
|
<span className={`text-xl font-black ${kpi.roas >= 3 ? 'text-emerald-400' : kpi.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}>
|
||||||
|
{kpi.roas.toFixed(2)}x
|
||||||
|
</span>
|
||||||
|
{renderGrowth(kpi.roas, prevKpi?.roas)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user