mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 22:45:23 +02:00
75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
import React from 'react';
|
|||
|
|
import { formatCurrency, formatPercent, calculateProductMetrics } from './utils';
|
||
|
|
import { Product } from './types';
|
||
|
|
import { DollarSign, TrendingUp, AlertTriangle, Activity } from 'lucide-react';
|
||
|
|
|
||
|
|
interface KPICardsProps {
|
||
|
|
products: Product[];
|
||
|
|
includeCOGS: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const KPICards: React.FC<KPICardsProps> = ({ products, includeCOGS }) => {
|
||
|
|
const totals = products.reduce(
|
||
|
|
(acc, product) => {
|
||
|
|
const metrics = calculateProductMetrics(product, includeCOGS);
|
||
|
|
acc.grossSales += product.grossSales;
|
||
|
|
acc.netMargin += metrics.netMargin;
|
||
|
|
acc.ppcSpend += product.ppcSpend;
|
||
|
|
acc.chargebacks += product.chargebacks;
|
||
|
|
return acc;
|
||
|
|
},
|
||
|
|
{ grossSales: 0, netMargin: 0, ppcSpend: 0, chargebacks: 0 }
|
||
|
|
);
|
||
|
|
|
||
|
|
const marginPercent = totals.grossSales > 0 ? totals.netMargin / totals.grossSales : 0;
|
||
|
|
const tacos = totals.grossSales > 0 ? totals.ppcSpend / totals.grossSales : 0;
|
||
|
|
|
||
|
|
const cards = [
|
||
|
|
{
|
||
|
|
title: 'Total Sales',
|
||
|
|
value: formatCurrency(totals.grossSales),
|
||
|
|
icon: <DollarSign className="w-6 h-6 text-emerald-400" />,
|
||
|
|
description: 'Gross revenue for the period',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Est. Net Margin',
|
||
|
|
value: formatPercent(marginPercent),
|
||
|
|
subValue: formatCurrency(totals.netMargin),
|
||
|
|
icon: <TrendingUp className="w-6 h-6 text-indigo-400" />,
|
||
|
|
description: includeCOGS ? 'After COGS and expenses' : 'Before COGS',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'TACOS',
|
||
|
|
value: formatPercent(tacos),
|
||
|
|
icon: <Activity className="w-6 h-6 text-purple-400" />,
|
||
|
|
description: 'Total advertising cost / Sales',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Total Chargebacks',
|
||
|
|
value: formatCurrency(totals.chargebacks),
|
||
|
|
icon: <AlertTriangle className="w-6 h-6 text-rose-400" />,
|
||
|
|
description: 'Logistics issues / returns',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||
|
|
{cards.map((card, idx) => (
|
||
|
|
<div key={idx} className="bg-[#13161F] rounded-xl border border-[#1F2433] p-6 flex flex-col">
|
||
|
|
<div className="flex justify-between items-start mb-4">
|
||
|
|
<h3 className="text-sm font-medium text-slate-400 uppercase tracking-wider">{card.title}</h3>
|
||
|
|
<div className="p-2 bg-[#0A0C10] rounded-lg border border-[#1F2433]">{card.icon}</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-baseline gap-2">
|
||
|
|
<span className="text-2xl font-bold text-white">{card.value}</span>
|
||
|
|
{card.subValue && (
|
||
|
|
<span className="text-sm font-medium text-slate-400">({card.subValue})</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<p className="text-xs text-slate-500 mt-2">{card.description}</p>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|