mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
feat: build Admin Team Dashboard with scoreboard, funnel, SLA, ROI, and integration health
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
src/components/admin/campaign-roi-cards.tsx
Normal file
93
src/components/admin/campaign-roi-cards.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user