mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:25:23 +02:00
272 lines
15 KiB
TypeScript
272 lines
15 KiB
TypeScript
|
|
import React, { useMemo } from 'react';
|
|
import { CombinedKPIs } from '../types';
|
|
import {
|
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
|
LineChart, Line, Legend, ComposedChart, Area
|
|
} from 'recharts';
|
|
|
|
interface AdvertisingDashboardProps {
|
|
data: CombinedKPIs[];
|
|
}
|
|
|
|
// Helper to aggregate data for charts
|
|
const aggregateByMonth = (data: CombinedKPIs[]) => {
|
|
const map = new Map<string, any>();
|
|
|
|
// Sort chronologically if needed, but assuming input might be mixed
|
|
// We'll create keys like "Jan 23", "Feb 23" and sort them later
|
|
|
|
data.forEach(item => {
|
|
const key = item.month; // e.g. "May-24"
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
name: key,
|
|
salesTotal: 0,
|
|
salesAds: 0,
|
|
cost: 0,
|
|
salesOrganic: 0,
|
|
impressions: 0,
|
|
clicks: 0,
|
|
unitsTotal: 0,
|
|
unitsAds: 0
|
|
});
|
|
}
|
|
const entry = map.get(key);
|
|
entry.salesTotal += item.salesTotal;
|
|
entry.salesAds += item.salesAds;
|
|
entry.cost += item.cost;
|
|
entry.salesOrganic += item.salesOrganic;
|
|
entry.impressions += item.impressions;
|
|
entry.clicks += item.clicks;
|
|
entry.unitsTotal += item.unitsTotal;
|
|
entry.unitsAds += item.unitsAds;
|
|
});
|
|
|
|
const result = Array.from(map.values()).map(r => ({
|
|
...r,
|
|
acos: r.salesAds > 0 ? (r.cost / r.salesAds) * 100 : 0,
|
|
tacos: r.salesTotal > 0 ? (r.cost / r.salesTotal) * 100 : 0,
|
|
ctr: r.impressions > 0 ? (r.clicks / r.impressions) * 100 : 0,
|
|
cpc: r.clicks > 0 ? r.cost / r.clicks : 0
|
|
}));
|
|
|
|
// Sort by Date
|
|
return result.sort((a, b) => {
|
|
const [mA, yA] = a.name.split('-');
|
|
const [mB, yB] = b.name.split('-');
|
|
|
|
// Year comparison
|
|
if (yA !== yB) return parseInt(yA) - parseInt(yB);
|
|
|
|
// Month comparison
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
return months.indexOf(mA) - months.indexOf(mB);
|
|
});
|
|
};
|
|
|
|
const KPICard = ({ title, value, subValue, type = 'currency' }: { title: string, value: number, subValue?: string, type?: 'currency' | 'percent' | 'number' }) => {
|
|
const formatted = type === 'currency'
|
|
? `€${value.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`
|
|
: type === 'percent'
|
|
? `${value.toFixed(2)}%`
|
|
: value.toLocaleString('de-DE');
|
|
|
|
return (
|
|
<div className="bg-surface border border-border rounded-xl p-5 flex flex-col shadow-sm hover:shadow-md transition-shadow">
|
|
<span className="text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">{title}</span>
|
|
<span className="text-2xl font-bold text-slate-100">{formatted}</span>
|
|
{subValue && <span className="text-xs text-slate-500 mt-1">{subValue}</span>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const AdvertisingDashboard: React.FC<AdvertisingDashboardProps> = ({ data }) => {
|
|
|
|
const aggregated = useMemo(() => aggregateByMonth(data), [data]);
|
|
|
|
const totals = useMemo(() => {
|
|
return data.reduce((acc, curr) => ({
|
|
salesTotal: acc.salesTotal + curr.salesTotal,
|
|
salesAds: acc.salesAds + curr.salesAds,
|
|
cost: acc.cost + curr.cost,
|
|
clicks: acc.clicks + curr.clicks,
|
|
impressions: acc.impressions + curr.impressions,
|
|
salesOrganic: acc.salesOrganic + curr.salesOrganic
|
|
}), { salesTotal: 0, salesAds: 0, cost: 0, clicks: 0, impressions: 0, salesOrganic: 0 });
|
|
}, [data]);
|
|
|
|
const kpiAcos = totals.salesAds > 0 ? (totals.cost / totals.salesAds) * 100 : 0;
|
|
const kpiTacos = totals.salesTotal > 0 ? (totals.cost / totals.salesTotal) * 100 : 0;
|
|
const kpiRoas = totals.cost > 0 ? totals.salesAds / totals.cost : 0;
|
|
|
|
if (data.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-96 text-slate-500">
|
|
<p className="text-lg font-medium">No Advertising Data Available</p>
|
|
<p className="text-sm">Please upload an Ads CSV file via "Connect Data".</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-7xl mx-auto pb-24 animate-fade-in">
|
|
|
|
{/* KPI Grid */}
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
<KPICard title="Ad Spend" value={totals.cost} />
|
|
<KPICard title="Ad Sales" value={totals.salesAds} subValue={`${((totals.salesAds / totals.salesTotal) * 100).toFixed(1)}% of Total`} />
|
|
<KPICard title="Total Sales" value={totals.salesTotal} />
|
|
<KPICard title="ACOS" value={kpiAcos} type="percent" />
|
|
<KPICard title="TACOS" value={kpiTacos} type="percent" />
|
|
<KPICard title="ROAS" value={kpiRoas} type="number" subValue="Return on Ad Spend" />
|
|
</div>
|
|
|
|
{/* Charts Row 1 */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
{/* Spend vs Sales Trend */}
|
|
<div className="bg-surface border border-border rounded-xl p-6 h-96 flex flex-col">
|
|
<h3 className="text-sm font-bold text-slate-300 uppercase mb-4">Ad Spend vs Ad Sales Trend</h3>
|
|
<div className="flex-1 min-h-0">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ComposedChart data={aggregated}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
|
<XAxis dataKey="name" stroke="#64748b" />
|
|
<YAxis yAxisId="left" stroke="#64748b" tickFormatter={(v) => `€${v / 1000}k`} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
|
formatter={(val: number) => `€${val.toLocaleString('de-DE')}`}
|
|
/>
|
|
<Legend />
|
|
<Bar yAxisId="left" dataKey="cost" name="Ad Spend" fill="#f43f5e" radius={[4, 4, 0, 0]} />
|
|
<Line yAxisId="left" type="monotone" dataKey="salesAds" name="Ad Sales" stroke="#10b981" strokeWidth={3} />
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ACOS vs TACOS Trend */}
|
|
<div className="bg-surface border border-border rounded-xl p-6 h-96 flex flex-col">
|
|
<h3 className="text-sm font-bold text-slate-300 uppercase mb-4">Efficiency: ACOS vs TACOS</h3>
|
|
<div className="flex-1 min-h-0">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={aggregated}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
|
<XAxis dataKey="name" stroke="#64748b" />
|
|
<YAxis stroke="#64748b" unit="%" />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
|
formatter={(val: number) => `${val.toFixed(2)}%`}
|
|
/>
|
|
<Legend />
|
|
<Line type="monotone" dataKey="acos" name="ACOS" stroke="#f59e0b" strokeWidth={2} dot={{ r: 4 }} />
|
|
<Line type="monotone" dataKey="tacos" name="TACOS" stroke="#3b82f6" strokeWidth={2} dot={{ r: 4 }} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Charts Row 2 */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
{/* Organic vs Paid Sales */}
|
|
<div className="bg-surface border border-border rounded-xl p-6 h-96 flex flex-col">
|
|
<h3 className="text-sm font-bold text-slate-300 uppercase mb-4">Total Sales Composition (Organic vs Paid)</h3>
|
|
<div className="flex-1 min-h-0">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={aggregated}>
|
|
<defs>
|
|
<linearGradient id="colorOrganic" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
|
</linearGradient>
|
|
<linearGradient id="colorPaid" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#ec4899" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="#ec4899" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
|
<XAxis dataKey="name" stroke="#64748b" />
|
|
<YAxis stroke="#64748b" tickFormatter={(v) => `€${v / 1000}k`} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
|
formatter={(val: number) => `€${val.toLocaleString('de-DE')}`}
|
|
/>
|
|
<Legend />
|
|
<Area type="monotone" dataKey="salesOrganic" name="Organic Sales" stroke="#6366f1" fillOpacity={1} fill="url(#colorOrganic)" stackId="1" />
|
|
<Area type="monotone" dataKey="salesAds" name="Paid Sales" stroke="#ec4899" fillOpacity={1} fill="url(#colorPaid)" stackId="1" />
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Funnel: Impressions -> Clicks */}
|
|
<div className="bg-surface border border-border rounded-xl p-6 h-96 flex flex-col">
|
|
<h3 className="text-sm font-bold text-slate-300 uppercase mb-4">Marketing Funnel (Impressions & Clicks)</h3>
|
|
<div className="flex-1 min-h-0">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ComposedChart data={aggregated}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
|
<XAxis dataKey="name" stroke="#64748b" />
|
|
<YAxis yAxisId="left" stroke="#8b5cf6" tickFormatter={(v) => `${(v / 1000).toFixed(0)}k`} label={{ value: 'Impressions', angle: -90, position: 'insideLeft', fill: '#8b5cf6' }} />
|
|
<YAxis yAxisId="right" orientation="right" stroke="#0ea5e9" label={{ value: 'Clicks', angle: 90, position: 'insideRight', fill: '#0ea5e9' }} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
|
/>
|
|
<Legend />
|
|
<Bar yAxisId="left" dataKey="impressions" name="Impressions" fill="#8b5cf6" radius={[4, 4, 0, 0]} />
|
|
<Line yAxisId="right" type="monotone" dataKey="clicks" name="Clicks" stroke="#0ea5e9" strokeWidth={2} />
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Detailed Table */}
|
|
<div className="bg-surface border border-border rounded-xl shadow-lg overflow-hidden flex flex-col">
|
|
<div className="p-4 border-b border-border bg-slate-900/50">
|
|
<h3 className="font-bold text-slate-200">Monthly Advertising Breakdown</h3>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm text-slate-300">
|
|
<thead className="bg-slate-950 text-xs uppercase font-semibold text-slate-500">
|
|
<tr>
|
|
<th className="px-4 py-3">Period</th>
|
|
<th className="px-4 py-3 text-right">Spend</th>
|
|
<th className="px-4 py-3 text-right">Ad Sales</th>
|
|
<th className="px-4 py-3 text-right">Total Sales</th>
|
|
<th className="px-4 py-3 text-right">ACOS</th>
|
|
<th className="px-4 py-3 text-right">TACOS</th>
|
|
<th className="px-4 py-3 text-right">Clicks</th>
|
|
<th className="px-4 py-3 text-right">CPC</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border">
|
|
{[...aggregated].reverse().map((row, idx) => (
|
|
<tr key={idx} className="hover:bg-slate-800/50">
|
|
<td className="px-4 py-3 font-medium text-slate-200">{row.name}</td>
|
|
<td className="px-4 py-3 text-right">€{row.cost.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
|
|
<td className="px-4 py-3 text-right text-emerald-400">€{row.salesAds.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
|
|
<td className="px-4 py-3 text-right">€{row.salesTotal.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
|
|
<td className="px-4 py-3 text-right">{row.acos.toFixed(2)}%</td>
|
|
<td className="px-4 py-3 text-right">{row.tacos.toFixed(2)}%</td>
|
|
<td className="px-4 py-3 text-right">{row.clicks.toLocaleString('de-DE')}</td>
|
|
<td className="px-4 py-3 text-right">€{row.cpc.toFixed(2)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Fix for AreaChart reference error in some bundlers, just export
|
|
import { AreaChart } from 'recharts';
|
|
|
|
export default AdvertisingDashboard;
|