Files
helix-engage/src/components/admin/campaign-roi-cards.tsx

94 lines
3.9 KiB
TypeScript

import { useMemo } from 'react';
import { Link } from 'react-router';
import { formatCurrency } from '@/lib/format';
import { cx } from '@/utils/cx';
import type { Campaign } from '@/types/entities';
interface CampaignRoiCardsProps {
campaigns: Campaign[];
}
type CampaignWithCac = Campaign & {
cac: number;
conversionRate: number;
budgetProgress: number;
};
export const CampaignRoiCards = ({ campaigns }: CampaignRoiCardsProps) => {
const sorted = useMemo((): CampaignWithCac[] => {
return campaigns
.map((campaign) => {
const spent = campaign.amountSpent?.amountMicros ?? 0;
const converted = campaign.convertedCount ?? 0;
const leadCount = campaign.leadCount ?? 0;
const budgetMicros = campaign.budget?.amountMicros ?? 1;
const cac = converted > 0 ? spent / converted : Infinity;
const conversionRate = leadCount > 0 ? converted / leadCount : 0;
const budgetProgress = budgetMicros > 0 ? spent / budgetMicros : 0;
return { ...campaign, cac, conversionRate, budgetProgress };
})
.sort((a, b) => a.cac - b.cac);
}, [campaigns]);
const getHealthColor = (rate: number): string => {
if (rate >= 0.1) return 'bg-success-500';
if (rate >= 0.05) return 'bg-warning-500';
return 'bg-error-500';
};
return (
<div className="space-y-3">
<h3 className="text-sm font-bold text-primary">Campaign ROI</h3>
<div className="flex gap-4 overflow-x-auto pb-1">
{sorted.map((campaign) => (
<div
key={campaign.id}
className="min-w-[220px] flex-shrink-0 rounded-xl border border-secondary bg-primary p-4"
>
<div className="flex items-center gap-2">
<span
className={cx('size-2 shrink-0 rounded-full', getHealthColor(campaign.conversionRate))}
/>
<span className="truncate text-sm font-semibold text-primary">
{campaign.campaignName}
</span>
</div>
<div className="mt-3 flex items-center gap-3 text-xs text-tertiary">
<span>{campaign.leadCount ?? 0} leads</span>
<span className="text-quaternary">|</span>
<span>{campaign.convertedCount ?? 0} converted</span>
</div>
<div className="mt-2">
<span className="text-sm font-bold text-primary">
{campaign.cac === Infinity
? '—'
: formatCurrency(campaign.cac)}
</span>
<span className="ml-1 text-xs text-tertiary">CAC</span>
</div>
<div className="mt-3 h-1 w-full overflow-hidden rounded-full bg-tertiary">
<div
className="h-full rounded-full bg-brand-solid"
style={{ width: `${Math.min(campaign.budgetProgress * 100, 100)}%` }}
/>
</div>
<span className="mt-1 block text-xs text-quaternary">
{Math.round(campaign.budgetProgress * 100)}% budget used
</span>
</div>
))}
</div>
<Link to="/campaigns" className="text-sm font-medium text-brand-secondary hover:text-brand-secondary_hover">
View All Campaigns &rarr;
</Link>
</div>
);
};