mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
};
|