mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
94 lines
3.9 KiB
TypeScript
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 →
|
|
</Link>
|
|
</div>
|
|
);
|
|
};
|