feat(dispatch): Phase 2 — DispatchLeadQueue with tabs, cards, and live drop-in

- DispatchLeadQueue: 4-tab filter (All / Unassigned / At Risk / Confirmed)
  with live counts; AnimatePresence popLayout for smooth card entrance/exit
- LeadCard: urgency badge (EMERGENCY pulse / HIGH / STANDARD), source icon,
  customer name, lead type, address, status pill, assigned rep initials, time ago
  — colored left border by urgency, blue ring when selected
- Storm mode: emergency leads float to top via sort; PRIORITY badge on
  emergency_tarp cards; amber Unassigned pill in storm context
- 20s interval drops new leads from 3-lead pool; NEW badge fades after 5s
- LynkDispatchPage: left panel now renders DispatchLeadQueue; handleSelectLead
  triggers 1.2s isProcessing state; right panel shows processing/selected/empty
  states ready for Phase 3
This commit is contained in:
Satyam
2026-03-21 21:53:00 +05:30
parent 4648561e7a
commit 1d6d587ebf
2 changed files with 392 additions and 47 deletions
+75 -47
View File
@@ -3,6 +3,7 @@ import { Zap, CloudLightning, Radio, Inbox, CheckCircle, Clock, AlertTriangle, B
import { motion, AnimatePresence } from 'framer-motion';
import { useTheme } from '../context/ThemeContext';
import { useMockStore, DISPATCH_REPS, DISPATCH_RECOMMENDATIONS } from '../data/mockStore';
import DispatchLeadQueue from '../components/dispatch/DispatchLeadQueue';
// ---------------------------------------------------------------------------
// Panel — themed container used by all 3 side panels
@@ -63,6 +64,13 @@ const LynkDispatchPage = () => {
const [stormMode, setStormMode] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const handleSelectLead = (lead) => {
if (selectedLead?.id === lead.id) return;
setSelectedLead(lead);
setIsProcessing(true);
setTimeout(() => setIsProcessing(false), 1200);
};
const accent = stormMode ? '#F59E0B' : '#3B82F6';
const unassigned = dispatchLeads.filter(l => l.status === 'unassigned').length;
@@ -140,43 +148,23 @@ const LynkDispatchPage = () => {
<Panel className="lg:overflow-hidden">
<PanelHeader
title="Lead Queue"
subtitle={`${unassigned} unassigned · ${dispatchLeads.length} total`}
subtitle="Live · auto-updates every 20s"
accent={accent}
right={
<span
className="text-xs font-bold px-2 py-0.5 rounded-full"
style={{ backgroundColor: `${accent}18`, color: accent }}
>
{dispatchLeads.length}
{dispatchLeads.length}+
</span>
}
/>
{/* Placeholder — Phase 2 builds this */}
<div className="flex-1 flex flex-col items-center justify-center gap-3 p-6 text-center">
<div className="w-12 h-12 rounded-2xl bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
<Inbox size={22} className="text-zinc-400" />
</div>
<div>
<p className="text-sm font-semibold text-zinc-700 dark:text-zinc-300">{dispatchLeads.length} leads loaded</p>
<p className="text-xs text-zinc-400 mt-1">Lead queue renders in Phase 2</p>
</div>
{/* Quick stat pills */}
<div className="flex flex-wrap justify-center gap-2 mt-2">
{[
{ label: 'Unassigned', count: dispatchLeads.filter(l => l.status === 'unassigned').length, color: '#EF4444' },
{ label: 'At Risk', count: dispatchLeads.filter(l => l.status === 'at_risk').length, color: '#F59E0B' },
{ label: 'Confirmed', count: dispatchLeads.filter(l => l.status === 'confirmed').length, color: '#10B981' },
].map(s => (
<span
key={s.label}
className="text-[11px] font-semibold px-2.5 py-1 rounded-full"
style={{ backgroundColor: `${s.color}15`, color: s.color }}
>
{s.count} {s.label}
</span>
))}
</div>
</div>
<DispatchLeadQueue
selectedLead={selectedLead}
onSelectLead={handleSelectLead}
stormMode={stormMode}
accent={accent}
/>
</Panel>
{/* CENTER — Map */}
@@ -235,26 +223,66 @@ const LynkDispatchPage = () => {
</div>
}
/>
{/* Empty state — Phase 3 builds this */}
{/* Placeholder — Phase 3 replaces this with DispatchRecommendationDrawer */}
<div className="flex-1 flex flex-col items-center justify-center gap-3 p-6 text-center">
<motion.div
animate={{ scale: [1, 1.05, 1] }}
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
className="w-14 h-14 rounded-2xl border-2 border-dashed border-zinc-200 dark:border-zinc-700 flex items-center justify-center"
>
<Bot size={24} className="text-zinc-300 dark:text-zinc-600" />
</motion.div>
<div>
<p className="text-sm font-semibold text-zinc-500 dark:text-zinc-400">
{selectedLead ? 'Processing...' : 'No lead selected'}
</p>
<p className="text-xs text-zinc-400 mt-1">
Click a lead in the queue to see AI-scored rep recommendations
</p>
</div>
<p className="text-[10px] text-zinc-300 dark:text-zinc-700 mt-2 font-medium uppercase tracking-widest">
Recommendation engine Phase 3
</p>
<AnimatePresence mode="wait">
{isProcessing ? (
<motion.div
key="processing"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center gap-3"
>
<div className="w-14 h-14 rounded-2xl border-2 border-dashed flex items-center justify-center animate-pulse"
style={{ borderColor: `${accent}60`, backgroundColor: `${accent}10` }}
>
<Bot size={24} style={{ color: accent }} />
</div>
<p className="text-sm font-semibold" style={{ color: accent }}>Analyzing lead...</p>
<p className="text-xs text-zinc-400">AI scoring reps</p>
</motion.div>
) : selectedLead ? (
<motion.div
key="selected"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center gap-2"
>
<div className="w-14 h-14 rounded-2xl flex items-center justify-center"
style={{ backgroundColor: `${accent}18`, color: accent }}
>
<Bot size={24} />
</div>
<p className="text-sm font-semibold text-zinc-700 dark:text-zinc-300">{selectedLead.id}</p>
<p className="text-xs text-zinc-400">{selectedLead.customer.name} · {selectedLead.property.address}</p>
<p className="text-[10px] text-zinc-300 dark:text-zinc-700 mt-2 font-medium uppercase tracking-widest">
Recommendation engine Phase 3
</p>
</motion.div>
) : (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center gap-3"
>
<motion.div
animate={{ scale: [1, 1.05, 1] }}
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
className="w-14 h-14 rounded-2xl border-2 border-dashed border-zinc-200 dark:border-zinc-700 flex items-center justify-center"
>
<Bot size={24} className="text-zinc-300 dark:text-zinc-600" />
</motion.div>
<div>
<p className="text-sm font-semibold text-zinc-500 dark:text-zinc-400">No lead selected</p>
<p className="text-xs text-zinc-400 mt-1">Click a lead in the queue to see AI-scored rep recommendations</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</Panel>
</div>