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 }) => (
{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;