feat(leadverification): port verification page + components (table, cards, summary, assign/verify modals)

This commit is contained in:
Satyam Rastogi
2026-05-29 21:16:46 +05:30
parent 56f6075ceb
commit f579769e19
9 changed files with 1163 additions and 0 deletions
@@ -0,0 +1,159 @@
import React, { useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { X, User, Repeat, UserPlus, Loader2 } from 'lucide-react';
import { useMockStore } from '../../data/mockStore';
import Select from '../ui/Select';
// One modal handles both "Assign" and "Reassign" — `mode` differentiates copy
// and target action. `mode='reassign'` is the In Progress flow.
// Assignee list is sourced from `users` (store key) filtered to role === 'ADMIN'.
// Field names match our store shape: assigneeId / assigneeName (not assignedToId/assignedToName).
const AssignLeadModal = ({ isOpen, onClose, lead, mode = 'assign' }) => {
const { users, assignLeadVerification, reassignLeadVerification } = useMockStore();
const [selectedId, setSelectedId] = useState('');
const [submitting, setSubmitting] = useState(false);
const isReassign = mode === 'reassign';
// Build the assignee list from admin users, excluding the currently assigned one.
const adminUsers = useMemo(
() => (users || []).filter(u => u.role === 'ADMIN'),
[users],
);
const available = useMemo(
() => adminUsers.filter(p => p.id !== lead?.assigneeId),
[adminUsers, lead],
);
useEffect(() => {
if (isOpen) {
setSelectedId('');
setSubmitting(false);
}
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', onKey);
return () => {
document.body.style.overflow = prevOverflow;
window.removeEventListener('keydown', onKey);
};
}, [isOpen, onClose]);
if (!isOpen || !lead) return null;
const handleConfirm = async () => {
if (!selectedId) return;
const target = adminUsers.find(a => a.id === selectedId);
if (!target) return;
setSubmitting(true);
await new Promise(r => setTimeout(r, 250));
if (isReassign) reassignLeadVerification(lead.id, target.id, target.name);
else assignLeadVerification(lead.id, target.id, target.name);
setSubmitting(false);
onClose();
};
const Icon = isReassign ? Repeat : UserPlus;
const headerIconCls = isReassign
? 'bg-purple-500/10 text-purple-500'
: 'bg-blue-500/10 text-blue-500';
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4" role="dialog" aria-modal="true">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={submitting ? undefined : onClose}
/>
<div className="relative w-full max-w-md bg-white dark:bg-[#121214] rounded-2xl shadow-2xl border border-zinc-200 dark:border-white/10 overflow-hidden animate-in zoom-in-95 duration-200">
<div className="px-5 py-4 border-b border-zinc-200 dark:border-white/10 flex items-center justify-between bg-zinc-50/50 dark:bg-white/5">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-xl ${headerIconCls}`}>
<Icon size={18} />
</div>
<h2 className="text-base font-bold text-zinc-900 dark:text-white uppercase tracking-wider">
{isReassign ? 'Reassign Lead' : 'Assign Lead'}
</h2>
</div>
<button
onClick={onClose}
disabled={submitting}
aria-label="Close"
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:text-red-500 transition-colors disabled:opacity-50"
>
<X size={18} />
</button>
</div>
<div className="p-5 space-y-4">
<div className="text-sm text-zinc-600 dark:text-zinc-400">
{isReassign ? 'Reassign ' : 'Assign '}
<span className="font-bold text-zinc-900 dark:text-white">{lead.customerName}</span>
{' '}({lead.leadId})
{lead.assigneeName && (
<>
{' currently with '}
<span className="font-bold text-zinc-900 dark:text-white">{lead.assigneeName}</span>
</>
)}
.
</div>
<div className="space-y-1.5">
<label
htmlFor="lv-assignee"
className="text-xs font-bold uppercase tracking-wider text-zinc-600 dark:text-zinc-400 flex items-center gap-1.5"
>
<User size={12} /> Assignee
</label>
<Select
id="lv-assignee"
ariaLabel="Assignee"
value={selectedId}
onChange={setSelectedId}
placeholder="Select an assignee…"
options={available.map(a => ({ value: a.id, label: `${a.name}${a.role}` }))}
/>
{available.length === 0 && (
<p className="text-xs text-zinc-500 mt-1">
No other assignees available.
</p>
)}
</div>
</div>
<div className="px-5 py-4 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5 flex justify-end gap-2">
<button
type="button"
onClick={onClose}
disabled={submitting}
className="px-4 py-2 text-sm font-bold rounded-xl border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
onClick={handleConfirm}
disabled={!selectedId || submitting}
className={`inline-flex items-center gap-2 px-5 py-2 text-sm font-bold rounded-xl text-white shadow-lg transition-all disabled:opacity-50 ${
isReassign
? 'bg-purple-600 hover:bg-purple-500 shadow-purple-500/20'
: 'bg-blue-600 hover:bg-blue-500 shadow-blue-500/20'
}`}
>
{submitting && <Loader2 size={14} className="animate-spin" />}
{isReassign ? 'Reassign' : 'Assign'}
</button>
</div>
</div>
</div>,
document.body,
);
};
export default AssignLeadModal;
@@ -0,0 +1,76 @@
import React from 'react';
import { ShieldCheck, ShieldX, UserPlus, Repeat, Clock, Eye } from 'lucide-react';
const MenuItem = ({ icon: Icon, children, onClick, disabled, variant }) => (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold transition-colors disabled:opacity-40 disabled:cursor-not-allowed text-left ${
variant === 'success'
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/10'
: variant === 'danger'
? 'text-red-600 dark:text-red-400 hover:bg-red-500/10'
: 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/5'
}`}
>
<Icon size={13} className="shrink-0" />
{children}
</button>
);
// Reused both by table row's dropdown and the mobile card's stack of buttons.
// `canManage` mirrors the existing PermissionGate pattern (leads:verify).
// Field names match our store shape: assigneeId (not assignedToId).
export const LeadActionsMenu = ({
lead,
canManage,
onView,
onAssign,
onReassign,
onVerify,
onUnverify,
onMarkPending,
}) => {
const status = lead.status;
const isVerified = status === 'Verified';
const isUnverified = status === 'Unverified';
const isInProgress = status === 'In Progress';
const hasAssignee = Boolean(lead.assigneeId);
return (
<>
<MenuItem icon={Eye} onClick={onView}>View Details</MenuItem>
<MenuItem icon={ShieldCheck} onClick={onVerify} disabled={!canManage || isVerified} variant="success">
Verify Lead
</MenuItem>
<MenuItem icon={ShieldX} onClick={onUnverify} disabled={!canManage || isUnverified} variant="danger">
Mark Unverified
</MenuItem>
<MenuItem
icon={UserPlus}
onClick={onAssign}
disabled={!canManage || isVerified}
>
{hasAssignee ? 'Change Assignee' : 'Assign'}
</MenuItem>
<MenuItem
icon={Repeat}
onClick={onReassign}
disabled={!canManage || !isInProgress}
title={!isInProgress ? 'Reassign is only available while In Progress' : undefined}
>
Reassign (In Progress)
</MenuItem>
<MenuItem
icon={Clock}
onClick={onMarkPending}
disabled={!canManage || status === 'Pending' || isVerified}
>
Move to Pending
</MenuItem>
</>
);
};
export default LeadActionsMenu;
@@ -0,0 +1,103 @@
import React from 'react';
import {
Eye, ShieldCheck, ShieldX, UserPlus, Repeat, Clock, Phone, MapPin, User,
} from 'lucide-react';
import { LeadStatusBadge, VerificationStatusPill } from './LeadStatusBadge';
const formatDate = (iso) => {
if (!iso) return '—';
try {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
});
} catch { return iso; }
};
const CardActionButton = ({ icon: Icon, children, onClick, disabled, variant }) => (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
variant === 'success'
? 'text-emerald-600 dark:text-emerald-400 border-emerald-500/20 hover:bg-emerald-500/10'
: variant === 'danger'
? 'text-red-600 dark:text-red-400 border-red-500/20 hover:bg-red-500/10'
: 'text-zinc-700 dark:text-zinc-300 border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5'
}`}
>
<Icon size={12} />
{children}
</button>
);
// Field names match our store shape: assigneeId / assigneeName (not assignedToId/assignedToName).
const LeadCard = ({
lead,
canManage,
onView,
onAssign,
onReassign,
onVerify,
onUnverify,
onMarkPending,
}) => {
const isVerified = lead.status === 'Verified';
const isUnverified = lead.status === 'Unverified';
const isInProgress = lead.status === 'In Progress';
const hasAssignee = Boolean(lead.assigneeId);
return (
<div className="p-4 active:bg-zinc-50 dark:active:bg-white/5 transition-colors">
<button type="button" onClick={onView} className="w-full text-left">
<div className="flex justify-between items-start gap-3 mb-2">
<div className="min-w-0 flex-1">
<h4 className="font-semibold text-zinc-900 dark:text-white truncate">
{lead.customerName}
</h4>
<p className="text-[11px] text-zinc-500 font-mono mt-0.5">{lead.leadId}</p>
</div>
<LeadStatusBadge status={lead.status} />
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-zinc-500 mb-2">
<span className="flex items-center gap-1">
<Phone size={11} />
{lead.phone}
</span>
{lead.address && (
<span className="flex items-center gap-1 max-w-full">
<MapPin size={11} />
<span className="truncate max-w-[180px]">{lead.address}</span>
</span>
)}
</div>
<div className="flex flex-wrap items-center gap-2 mb-1">
<VerificationStatusPill label={lead.verificationStatus} />
<span className="flex items-center gap-1 text-[11px] text-zinc-500">
<User size={11} />
{lead.assigneeName || 'Unassigned'}
</span>
<span className="text-[11px] text-zinc-400 font-mono">
{formatDate(lead.createdAt)} · {lead.source}
</span>
</div>
</button>
<div className="mt-3 flex flex-wrap gap-2 pt-3 border-t border-zinc-100 dark:border-white/5">
<CardActionButton icon={Eye} onClick={onView}>View</CardActionButton>
<CardActionButton icon={ShieldCheck} onClick={onVerify} disabled={!canManage || isVerified} variant="success">Verify</CardActionButton>
<CardActionButton icon={ShieldX} onClick={onUnverify} disabled={!canManage || isUnverified} variant="danger">Unverify</CardActionButton>
<CardActionButton icon={UserPlus} onClick={onAssign} disabled={!canManage || isVerified}>
{hasAssignee ? 'Reassign' : 'Assign'}
</CardActionButton>
<CardActionButton icon={Repeat} onClick={onReassign} disabled={!canManage || !isInProgress}>
Reassign (IP)
</CardActionButton>
<CardActionButton icon={Clock} onClick={onMarkPending} disabled={!canManage || lead.status === 'Pending' || isVerified}>
Pending
</CardActionButton>
</div>
</div>
);
};
export default LeadCard;
@@ -0,0 +1,151 @@
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';
import {
X, Phone, Mail, MapPin, User, FileText, Clock, Hash, History as HistoryIcon,
} from 'lucide-react';
import { LeadStatusBadge, VerificationStatusPill } from './LeadStatusBadge';
const formatDateTime = (iso) => {
if (!iso) return '—';
try {
return new Date(iso).toLocaleString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: 'numeric', minute: '2-digit',
});
} catch { return iso; }
};
const Row = ({ icon: Icon, label, value }) => (
<div className="flex items-start gap-3 py-2 border-b border-zinc-100 dark:border-white/5 last:border-0">
<div className="shrink-0 p-1.5 rounded-lg bg-zinc-100 dark:bg-white/5 text-zinc-500">
<Icon size={14} />
</div>
<div className="min-w-0 flex-1">
<div className="text-[10px] uppercase tracking-wider text-zinc-400 dark:text-zinc-500 font-bold">
{label}
</div>
<div className="text-sm text-zinc-900 dark:text-white font-medium break-words">
{value || '—'}
</div>
</div>
</div>
);
// Field names match our store shape: assigneeName (not assignedToName).
const LeadDetailsModal = ({ isOpen, onClose, lead }) => {
useEffect(() => {
if (!isOpen) return;
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', onKey);
return () => {
document.body.style.overflow = prevOverflow;
window.removeEventListener('keydown', onKey);
};
}, [isOpen, onClose]);
if (!isOpen || !lead) return null;
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-end sm:items-center justify-center p-0 sm:p-4" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full sm:max-w-2xl max-h-[92vh] sm:max-h-[88vh] flex flex-col bg-white dark:bg-[#121214] sm:rounded-2xl rounded-t-2xl shadow-2xl border border-zinc-200 dark:border-white/10 overflow-hidden animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="px-5 py-4 border-b border-zinc-200 dark:border-white/10 flex items-start justify-between gap-3 bg-zinc-50/50 dark:bg-white/5">
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h2 className="text-base font-bold text-zinc-900 dark:text-white truncate">
{lead.customerName}
</h2>
<LeadStatusBadge status={lead.status} />
<VerificationStatusPill label={lead.verificationStatus} />
</div>
<p className="mt-0.5 text-[11px] font-mono text-zinc-500">{lead.leadId}</p>
</div>
<button
onClick={onClose}
aria-label="Close"
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:text-red-500 transition-colors shrink-0"
>
<X size={18} />
</button>
</div>
{/* Body */}
<div className="overflow-y-auto custom-scrollbar p-5 space-y-6">
{/* Contact Info */}
<section>
<h3 className="text-xs font-bold uppercase tracking-widest text-zinc-500 mb-2 flex items-center gap-1.5">
<User size={12} /> Contact
</h3>
<div className="rounded-xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-white/5 px-3">
<Row icon={Phone} label="Phone" value={lead.phone} />
<Row icon={Mail} label="Email" value={lead.email} />
<Row icon={MapPin} label="Address" value={lead.address} />
<Row icon={Hash} label="Source" value={lead.source} />
</div>
</section>
{/* Assignment */}
<section>
<h3 className="text-xs font-bold uppercase tracking-widest text-zinc-500 mb-2 flex items-center gap-1.5">
<User size={12} /> Assignment
</h3>
<div className="rounded-xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-white/5 px-3">
<Row icon={User} label="Assigned To" value={lead.assigneeName} />
<Row icon={Clock} label="Created" value={formatDateTime(lead.createdAt)} />
<Row icon={Clock} label="Verified At" value={formatDateTime(lead.verifiedAt)} />
</div>
</section>
{/* Notes */}
<section>
<h3 className="text-xs font-bold uppercase tracking-widest text-zinc-500 mb-2 flex items-center gap-1.5">
<FileText size={12} /> Verification Notes
</h3>
<div className="rounded-xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-white/5 p-3 text-sm text-zinc-700 dark:text-zinc-300 leading-relaxed whitespace-pre-wrap">
{lead.verificationNotes || 'No notes yet.'}
</div>
</section>
{/* History */}
{lead.history?.length > 0 && (
<section>
<h3 className="text-xs font-bold uppercase tracking-widest text-zinc-500 mb-2 flex items-center gap-1.5">
<HistoryIcon size={12} /> Activity
</h3>
<ol className="relative border-l-2 border-zinc-100 dark:border-white/10 ml-1.5 space-y-3 pl-4">
{lead.history.map((h, i) => (
<li key={i} className="relative">
<span className="absolute -left-[22px] top-1.5 w-3 h-3 rounded-full bg-blue-500 ring-4 ring-white dark:ring-[#121214]" />
<p className="text-sm text-zinc-900 dark:text-white font-medium">
{h.action}
</p>
<p className="text-[11px] text-zinc-500 mt-0.5">
{formatDateTime(h.at)} · {h.by}
</p>
</li>
))}
</ol>
</section>
)}
</div>
{/* Footer */}
<div className="px-5 py-3 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5 flex justify-end">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-bold rounded-xl border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors"
>
Close
</button>
</div>
</div>
</div>,
document.body,
);
};
export default LeadDetailsModal;
@@ -0,0 +1,23 @@
import React from 'react';
import { LV_STATUS_CONFIG, lvVerificationStatusCls } from './statusConfig';
export const LeadStatusBadge = ({ status }) => {
const cfg = LV_STATUS_CONFIG[status] || LV_STATUS_CONFIG.Pending;
const Icon = cfg.icon;
const spin = status === 'In Progress';
return (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wide ${cfg.cls}`}>
<Icon size={11} className={spin ? 'animate-spin' : ''} />
{cfg.label}
</span>
);
};
export const VerificationStatusPill = ({ label }) => {
if (!label) return null;
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-semibold uppercase tracking-wide border ${lvVerificationStatusCls(label)}`}>
{label}
</span>
);
};
@@ -0,0 +1,131 @@
import React from 'react';
import { MoreVertical, Phone, Eye } from 'lucide-react';
import { LeadStatusBadge, VerificationStatusPill } from './LeadStatusBadge';
import LeadActionsMenu from './LeadActionsMenu';
const formatDate = (iso) => {
if (!iso) return '—';
try {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
});
} catch { return iso; }
};
// Field names match our store shape: assigneeId / assigneeName (not assignedToId/assignedToName).
const LeadTableRow = ({
lead,
openMenu,
setOpenMenu,
canManage,
onView,
onAssign,
onReassign,
onVerify,
onUnverify,
onMarkPending,
}) => {
return (
<tr className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors group">
<td className="px-5 py-4 whitespace-nowrap">
<button type="button" onClick={onView} className="text-left">
<div className="font-mono text-[11px] text-zinc-500">{lead.leadId}</div>
<div className="text-[10px] text-zinc-400 mt-0.5">
{formatDate(lead.createdAt)}
</div>
</button>
</td>
<td className="px-5 py-4">
<button type="button" onClick={onView} className="text-left flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-blue-500/10 text-blue-500 flex items-center justify-center text-xs font-bold shrink-0">
{lead.customerName?.charAt(0)}
</div>
<div className="min-w-0">
<div className="font-semibold text-sm text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors truncate max-w-[180px]">
{lead.customerName}
</div>
<div className="text-[11px] text-zinc-500 truncate max-w-[180px]">
{lead.address}
</div>
</div>
</button>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<a
href={`tel:${lead.phone}`}
className="inline-flex items-center gap-1.5 text-sm text-zinc-700 dark:text-zinc-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
<Phone size={12} className="text-zinc-400" />
{lead.phone}
</a>
</td>
<td className="px-5 py-4">
<span className="text-sm text-zinc-700 dark:text-zinc-300">{lead.source}</span>
</td>
<td className="px-5 py-4">
{lead.assigneeName ? (
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center text-[10px] font-bold shrink-0">
{lead.assigneeName?.charAt(0)}
</div>
<span className="text-sm text-zinc-700 dark:text-zinc-300 truncate max-w-[140px]">
{lead.assigneeName}
</span>
</div>
) : (
<span className="text-xs italic text-zinc-400">Unassigned</span>
)}
</td>
<td className="px-5 py-4">
<LeadStatusBadge status={lead.status} />
</td>
<td className="px-5 py-4">
<VerificationStatusPill label={lead.verificationStatus} />
</td>
<td className="px-5 py-4 whitespace-nowrap text-sm text-zinc-500">
{formatDate(lead.createdAt)}
</td>
<td className="px-5 py-4 text-right relative">
<div className="inline-flex items-center gap-1">
<button
type="button"
onClick={onView}
className="p-1.5 rounded-lg text-zinc-500 hover:text-blue-600 hover:bg-blue-500/10 transition-colors"
aria-label="View details"
title="View"
>
<Eye size={14} />
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); setOpenMenu(!openMenu); }}
className="p-1.5 rounded-lg text-zinc-500 hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors"
aria-label="More actions"
aria-expanded={openMenu}
>
<MoreVertical size={14} />
</button>
</div>
{openMenu && (
<div
onClick={(e) => e.stopPropagation()}
className="absolute right-5 top-full mt-1 w-52 bg-white dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl shadow-xl z-20 overflow-hidden text-left"
>
<LeadActionsMenu
lead={lead}
canManage={canManage}
onView={() => { setOpenMenu(false); onView(); }}
onAssign={() => { setOpenMenu(false); onAssign(); }}
onReassign={() => { setOpenMenu(false); onReassign(); }}
onVerify={() => { setOpenMenu(false); onVerify(); }}
onUnverify={() => { setOpenMenu(false); onUnverify(); }}
onMarkPending={() => { setOpenMenu(false); onMarkPending(); }}
/>
</div>
)}
</td>
</tr>
);
};
export default LeadTableRow;
@@ -0,0 +1,35 @@
import React from 'react';
import { SpotlightCard } from '../SpotlightCard';
import { ShieldCheck, ShieldX, UserCheck, Loader2, Clock } from 'lucide-react';
const TILE_DEF = [
{ key: 'verified', label: 'Verified', icon: ShieldCheck, accent: 'text-emerald-500', bg: 'bg-emerald-500/10' },
{ key: 'inProgress', label: 'In Progress', icon: Loader2, accent: 'text-blue-500', bg: 'bg-blue-500/10' },
{ key: 'assigned', label: 'Assigned', icon: UserCheck, accent: 'text-amber-500', bg: 'bg-amber-500/10' },
{ key: 'pending', label: 'Pending', icon: Clock, accent: 'text-zinc-500', bg: 'bg-zinc-400/15' },
{ key: 'unverified', label: 'Unverified', icon: ShieldX, accent: 'text-red-500', bg: 'bg-red-500/10' },
];
export default function SummaryCards({ stats }) {
return (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
{TILE_DEF.map(({ key, label, icon: Icon, accent, bg }) => (
<SpotlightCard key={key} className="p-4 sm:p-5">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-[10px] font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 truncate">
{label}
</p>
<p className="mt-1 text-2xl sm:text-3xl font-extrabold text-zinc-900 dark:text-white tabular-nums">
{stats[key] ?? 0}
</p>
</div>
<div className={`shrink-0 p-2.5 rounded-xl ${bg} ${accent}`}>
<Icon size={18} className={key === 'inProgress' ? 'animate-spin' : ''} />
</div>
</div>
</SpotlightCard>
))}
</div>
);
}
@@ -0,0 +1,57 @@
import { Clock, UserCheck, Loader2, ShieldCheck, ShieldX } from 'lucide-react';
// Centralised lead-verification status visuals + icons. Reused by table, cards,
// summary tiles, and modals so the badge always looks identical.
export const LV_STATUS_CONFIG = {
Pending: {
label: 'Pending',
icon: Clock,
cls: 'bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-300',
dot: 'bg-zinc-400',
accent: 'text-zinc-500',
},
Assigned: {
label: 'Assigned',
icon: UserCheck,
cls: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400',
dot: 'bg-amber-500',
accent: 'text-amber-500',
},
'In Progress': {
label: 'In Progress',
icon: Loader2,
cls: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400',
dot: 'bg-blue-500',
accent: 'text-blue-500',
},
Verified: {
label: 'Verified',
icon: ShieldCheck,
cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400',
dot: 'bg-emerald-500',
accent: 'text-emerald-500',
},
Unverified: {
label: 'Unverified',
icon: ShieldX,
cls: 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400',
dot: 'bg-red-500',
accent: 'text-red-500',
},
};
export const LV_VERIFICATION_STATUS_CLS = {
Verified: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:border-emerald-500/20',
'Awaiting Confirmation': 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-500/10 dark:text-blue-300 dark:border-blue-500/20',
'Verification In Progress': 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-500/10 dark:text-blue-300 dark:border-blue-500/20',
'Verifying Identity': 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-500/10 dark:text-purple-300 dark:border-purple-500/20',
'Reviewing Insurance': 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-500/10 dark:text-purple-300 dark:border-purple-500/20',
'Pending Outreach': 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:border-amber-500/20',
Unassigned: 'bg-zinc-50 text-zinc-600 border-zinc-200 dark:bg-white/5 dark:text-zinc-300 dark:border-white/10',
'Failed Contact': 'bg-red-50 text-red-700 border-red-200 dark:bg-red-500/10 dark:text-red-300 dark:border-red-500/20',
'Emergency — Awaiting Verifier':'bg-red-50 text-red-700 border-red-200 dark:bg-red-500/10 dark:text-red-300 dark:border-red-500/20',
};
export const lvVerificationStatusCls = (label) =>
LV_VERIFICATION_STATUS_CLS[label] ||
'bg-zinc-50 text-zinc-600 border-zinc-200 dark:bg-white/5 dark:text-zinc-300 dark:border-white/10';