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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user