import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Zap, CloudLightning, Inbox, CheckCircle, Clock, AlertTriangle, Bot, ChevronDown, History, BarChart2 } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useMockStore, DISPATCH_REPS } from '../data/mockStore'; import DispatchLeadQueue from '../components/dispatch/DispatchLeadQueue'; import DispatchRecommendationDrawer from '../components/dispatch/DispatchRecommendationDrawer'; import DispatchMapPanel from '../components/dispatch/DispatchMapPanel'; import LeadQuickViewModal from '../components/dispatch/LeadQuickViewModal'; import DispatchLogDrawer from '../components/dispatch/DispatchLogDrawer'; import DispatchChartModal from '../components/dispatch/DispatchChartModal'; // --------------------------------------------------------------------------- // Panel — themed container // --------------------------------------------------------------------------- const Panel = ({ children, className = '', style, stormMode }) => (
{children}
); // PanelHeader — supports optional collapse toggle on mobile/tablet const PanelHeader = ({ title, subtitle, accent, right, collapsible, isCollapsed, onToggle }) => (

{title}

{subtitle &&

{subtitle}

}
{right && (
collapsible && e.stopPropagation()}> {right}
)} {collapsible && ( )}
); // --------------------------------------------------------------------------- // KPI Card // --------------------------------------------------------------------------- const KpiCard = ({ label, value, icon: Icon, accent, stormMode, pulse }) => (

{label}

{value}

{stormMode && pulse && ( )}
); // --------------------------------------------------------------------------- // ResizableHandle — desktop only (xl+) // --------------------------------------------------------------------------- const ResizableHandle = ({ onDrag }) => { const dragging = useRef(false); const lastX = useRef(0); const onMouseDown = useCallback((e) => { dragging.current = true; lastX.current = e.clientX; e.preventDefault(); const onMove = (ev) => { if (!dragging.current) return; const delta = ev.clientX - lastX.current; lastX.current = ev.clientX; onDrag(delta); }; const onUp = () => { dragging.current = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }, [onDrag]); return (
); }; // Animated collapse wrapper const CollapsePanel = ({ open, children }) => ( {open && ( {children} )} ); // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- const LynkDispatchPage = () => { const { dispatchLeads, assignDispatchLead } = useMockStore(); const [viewDetailLead, setViewDetailLead] = useState(null); const [dispatchLog, setDispatchLog] = useState([]); const [selectedLead, setSelectedLead] = useState(null); const [stormMode, setStormMode] = useState(false); const [logDrawerOpen, setLogDrawerOpen] = useState(false); const [chartModalOpen, setChartModalOpen] = useState(false); // Rep status live cycling — overrides static DISPATCH_REPS.status const [repStatuses, setRepStatuses] = useState( () => Object.fromEntries(DISPATCH_REPS.map(r => [r.id, r.status])) ); // Dispatch efficiency tracking — seeded with today's prior activity const [aiCount, setAiCount] = useState(8); const [totalCount, setTotalCount] = useState(9); const [isProcessing, setIsProcessing] = useState(false); const [leftWidth, setLeftWidth] = useState(300); const [rightWidth, setRightWidth] = useState(320); // xl = 1280px — treats all iPads (inc. Air 820/1180 & Pro 11" 834/1194) as tablet const [isDesktop, setIsDesktop] = useState(() => window.innerWidth >= 1280); const aiPanelRef = useRef(null); // Responsive map height for non-desktop — explicit px needed so Leaflet // gets a reliable offsetHeight regardless of CollapsePanel's height:auto chain const calcMapHeight = () => { const w = window.innerWidth; if (w >= 1280) return null; // desktop: DispatchMapPanel handles its own height if (w >= 1024) return '520px'; // iPad Pro 11" landscape / large tablet if (w >= 768) return '460px'; // iPad Air / md tablet if (w >= 640) return '380px'; // small tablet / large phone landscape return '300px'; // mobile portrait }; const [mapHeight, setMapHeight] = useState(calcMapHeight); useEffect(() => { const handler = () => { setIsDesktop(window.innerWidth >= 1280); setMapHeight(calcMapHeight()); }; window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); // Collapse state — map & AI default-collapsed on small phones only const [collapsed, setCollapsed] = useState(() => ({ queue: false, map: window.innerWidth < 640, ai: window.innerWidth < 640, })); const toggle = (key) => setCollapsed(p => ({ ...p, [key]: !p[key] })); const dragLeft = useCallback((delta) => setLeftWidth(w => Math.min(440, Math.max(220, w + delta))), []); const dragRight = useCallback((delta) => setRightWidth(w => Math.min(440, Math.max(260, w - delta))), []); // When the map panel expands on mobile/tablet, fire a synthetic resize // after the CollapsePanel animation (220ms) completes so Leaflet // invalidateSize() in MapController gets a correct container height. useEffect(() => { if (!collapsed.map && !isDesktop) { const t = setTimeout(() => window.dispatchEvent(new Event('resize')), 260); return () => clearTimeout(t); } }, [collapsed.map, isDesktop]); const handleSelectLead = (lead) => { if (selectedLead?.id === lead.id) return; setSelectedLead(lead); setIsProcessing(true); setTimeout(() => setIsProcessing(false), 1200); // Mobile/tablet: expand AI panel and scroll to it if (!isDesktop) { setCollapsed(p => ({ ...p, ai: false })); setTimeout(() => { aiPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 120); } }; const handleAssign = useCallback((leadId, repId, isManual = false) => { assignDispatchLead(leadId, repId); // Cycle rep status: en_route for 60s, then back to available setRepStatuses(prev => ({ ...prev, [repId]: 'en_route' })); setTimeout(() => { setRepStatuses(prev => ({ ...prev, [repId]: 'available' })); }, 60000); // Track AI vs manual efficiency setTotalCount(n => n + 1); if (!isManual) setAiCount(n => n + 1); const lead = dispatchLeads.find(l => l.id === leadId); const rep = DISPATCH_REPS.find(r => r.id === repId); if (lead && rep) { setDispatchLog(prev => [{ id: Date.now(), repFirstName: rep.name.split(' ')[0], repInitials: rep.initials, repStatus: 'en_route', customerName: lead.customer.name, address: `${lead.property.address}, ${lead.property.city}`, leadType: lead.leadType, urgency: lead.urgency, timestamp: Date.now(), }, ...prev].slice(0, 20)); } }, [assignDispatchLead, dispatchLeads]); const handleDismissAfterAssign = useCallback(() => { setSelectedLead(null); }, []); const accent = stormMode ? '#F59E0B' : '#3B82F6'; // Merge live statuses into rep objects — passed to map + AI drawer const effectiveReps = DISPATCH_REPS.map(r => ({ ...r, status: repStatuses[r.id] ?? r.status, })); // Efficiency score — what % of dispatches were AI-recommended (not manual override) const efficiencyPct = totalCount > 0 ? Math.round((aiCount / totalCount) * 100) : 0; // ── Live KPI derivations ── const unassigned = dispatchLeads.filter(l => l.status === 'unassigned').length; const assignedToday = dispatchLeads.filter(l => ['assigned', 'confirmed'].includes(l.status)).length; const atRiskCount = dispatchLeads.filter(l => l.status === 'at_risk').length; const emergencyCount = dispatchLeads.filter(l => l.urgency === 'emergency').length; const alertCount = stormMode ? emergencyCount + atRiskCount : atRiskCount; const avgWait = (() => { const pending = dispatchLeads.filter(l => l.status === 'unassigned'); if (!pending.length) return '—'; const avg = Math.round( pending.reduce((sum, l) => sum + (l.arrivedMinsAgo || 0), 0) / pending.length ); return `${avg} min`; })(); return (
{/* Storm Banner */} {stormMode && (
Storm Mode Active — Exterior Inspections Restricted — Route Planning Adjusted
)}
{/* ── Header ── */}

LynkDispatch

{stormMode ? ( Storm Protocol Active ) : ( AI Dispatch Engine )} {totalCount > 0 && ( {efficiencyPct}% AI-Dispatched )}
{/* LIVE / STORM ACTIVE badge */} {stormMode ? ( Storm Active ) : ( Live )} {/* Log + Chart buttons */}
{/* Storm Mode Toggle */}
{/* ── 3-Panel Layout ──────────────────────────────────────────────── */} {/* xl+ (1280px): flex row with drag handles (all iPads = tablet) */} {/* < xl: single column, collapsible panels */}
{/* LEFT — Lead Queue */} toggle('queue')} right={ stormMode && emergencyCount > 0 ? ( {emergencyCount} critical ) : ( {dispatchLeads.length}+ ) } /> {isDesktop ? ( ) : ( )} {/* CENTER — Territory Map */} toggle('map')} right={
{DISPATCH_REPS.filter(r => r.status === 'available').length} available
} /> {isDesktop ? ( ) : ( )}
{/* RIGHT — AI Recommendations */}
toggle('ai')} right={
} /> {isDesktop ? ( ) : ( )}
{/* ── Lead Quick View Modal ── */} {viewDetailLead && ( setViewDetailLead(null)} accent={accent} /> )} {/* ── KPI Bar ── */}
0} /> 0 ? '#F59E0B' : '#6B7280'} stormMode={stormMode} pulse={stormMode && alertCount > 0} />
{/* ── Dispatch Log Drawer ── */} {logDrawerOpen && ( setLogDrawerOpen(false)} /> )} {/* ── Weekly Chart Modal ── */} {chartModalOpen && ( setChartModalOpen(false)} /> )}
); }; export default LynkDispatchPage;