mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +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:
83
src/components/admin/lead-funnel.tsx
Normal file
83
src/components/admin/lead-funnel.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
interface LeadFunnelProps {
|
||||
leads: Lead[];
|
||||
}
|
||||
|
||||
type FunnelStage = {
|
||||
label: string;
|
||||
count: number;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export const LeadFunnel = ({ leads }: LeadFunnelProps) => {
|
||||
const stages = useMemo((): FunnelStage[] => {
|
||||
const total = leads.length;
|
||||
|
||||
const contacted = leads.filter((lead) =>
|
||||
lead.leadStatus === 'CONTACTED' ||
|
||||
lead.leadStatus === 'QUALIFIED' ||
|
||||
lead.leadStatus === 'NURTURING' ||
|
||||
lead.leadStatus === 'APPOINTMENT_SET' ||
|
||||
lead.leadStatus === 'CONVERTED',
|
||||
).length;
|
||||
|
||||
const appointmentSet = leads.filter((lead) =>
|
||||
lead.leadStatus === 'APPOINTMENT_SET' ||
|
||||
lead.leadStatus === 'CONVERTED',
|
||||
).length;
|
||||
|
||||
const converted = leads.filter((lead) =>
|
||||
lead.leadStatus === 'CONVERTED',
|
||||
).length;
|
||||
|
||||
return [
|
||||
{ label: 'Generated', count: total, color: 'bg-brand-600' },
|
||||
{ label: 'Contacted', count: contacted, color: 'bg-brand-500' },
|
||||
{ label: 'Appointment Set', count: appointmentSet, color: 'bg-brand-400' },
|
||||
{ label: 'Converted', count: converted, color: 'bg-success-500' },
|
||||
];
|
||||
}, [leads]);
|
||||
|
||||
const maxCount = stages[0]?.count || 1;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-secondary bg-primary p-5">
|
||||
<h3 className="text-sm font-bold text-primary">Lead Funnel · This Week</h3>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{stages.map((stage, index) => {
|
||||
const widthPercent = maxCount > 0 ? (stage.count / maxCount) * 100 : 0;
|
||||
const previousCount = index > 0 ? stages[index - 1].count : null;
|
||||
const conversionRate =
|
||||
previousCount !== null && previousCount > 0
|
||||
? ((stage.count / previousCount) * 100).toFixed(0)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key={stage.label} className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-secondary">{stage.label}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold text-primary">{stage.count}</span>
|
||||
{conversionRate !== null && (
|
||||
<span className="text-xs text-tertiary">({conversionRate}%)</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-6 w-full overflow-hidden rounded-md bg-secondary">
|
||||
<div
|
||||
className={cx('h-full rounded-md transition-all', stage.color)}
|
||||
style={{ width: `${Math.max(widthPercent, 2)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user