import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { Box, Typography, Card, CardContent, Avatar, Chip, Stack, Button, Grid, IconButton, List, ListItemButton, ListItemText, Collapse, Tooltip, Divider, LinearProgress, Menu, MenuItem, Drawer, Paper } from '@mui/material'; import { alpha } from '@mui/material/styles'; import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip as LTooltip, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded'; import PauseRoundedIcon from '@mui/icons-material/PauseRounded'; import ReplayRoundedIcon from '@mui/icons-material/ReplayRounded'; import SpeedRoundedIcon from '@mui/icons-material/SpeedRounded'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded'; import WarehouseRoundedIcon from '@mui/icons-material/WarehouseRounded'; import FlagRoundedIcon from '@mui/icons-material/FlagRounded'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; import CancelRoundedIcon from '@mui/icons-material/CancelRounded'; import DeliveryDiningRoundedIcon from '@mui/icons-material/DeliveryDiningRounded'; import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'; import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; import StraightenRoundedIcon from '@mui/icons-material/StraightenRounded'; import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined'; import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import ScaleOutlinedIcon from '@mui/icons-material/ScaleOutlined'; import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; import NotesOutlinedIcon from '@mui/icons-material/NotesOutlined'; import CallOutlinedIcon from '@mui/icons-material/CallOutlined'; import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; // ════════════════════════════════════════════════════════════════════════════════ // Leaflet base-icon fix (same approach as TrackingMap.jsx) // ════════════════════════════════════════════════════════════════════════════════ delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', }); // Keeps Leaflet's canvas sized correctly inside a flex layout / on sidebar toggle. function MapResizeHandler() { const map = useMap(); useEffect(() => { const fix = () => map.invalidateSize(); const t = setTimeout(fix, 250); window.addEventListener('resize', fix); const ro = new ResizeObserver(fix); ro.observe(map.getContainer()); return () => { clearTimeout(t); window.removeEventListener('resize', fix); ro.disconnect(); }; }, [map]); return null; } // Imperatively fits the map to a set of latlng points whenever they change. function FitBounds({ points }) { const map = useMap(); useEffect(() => { if (!points || points.length === 0) return; const bounds = L.latLngBounds(points.map((p) => [p.lat, p.lng])); if (bounds.isValid()) map.fitBounds(bounds, { padding: [60, 60], maxZoom: 15 }); }, [points, map]); return null; } // Pans/zooms to a single point when an order is focused (clicked). function FlyTo({ target }) { const map = useMap(); useEffect(() => { if (target) map.flyTo([target.lat, target.lng], Math.max(map.getZoom(), 14), { duration: 0.6 }); }, [target, map]); return null; } // ════════════════════════════════════════════════════════════════════════════════ // Leaflet DivIcons // ════════════════════════════════════════════════════════════════════════════════ const hubIcon = new L.DivIcon({ className: 'rr-icon', html: `
`, iconSize: [34, 34], iconAnchor: [17, 17], }); const flagIcon = new L.DivIcon({ className: 'rr-icon', html: `
`, iconSize: [30, 30], iconAnchor: [15, 15], }); // Numbered stop pin in the rider's colour. Pickups get a square pin, deliveries a // round one — so the two journey types are distinguishable at a glance on the map. const stopIcon = (n, color, focused, isPickup) => new L.DivIcon({ className: 'rr-icon', html: `
${n}
`, iconSize: focused ? [30, 30] : [24, 24], iconAnchor: focused ? [15, 15] : [12, 12], }); // The animated "moving rider" puck in the rider's colour. const moverIcon = (color) => new L.DivIcon({ className: 'rr-icon rr-mover', html: `
`, iconSize: [30, 30], iconAnchor: [15, 15], }); // ════════════════════════════════════════════════════════════════════════════════ // Mock data — every rider has a DELIVERY trip and a PICKUP trip for the day, each // with its own ordered stops, timings and distance. Every order stop carries the // full detail shown in the side drawer when clicked. // // Delivery: hub → customer drops. Pickup: merchant collections → hub. // ════════════════════════════════════════════════════════════════════════════════ const HUB = { lat: 28.6139, lng: 77.2090, label: 'Delhi Operations Hub' }; const hubStop = (time) => ({ kind: 'hub', label: HUB.label, lat: HUB.lat, lng: HUB.lng, time }); const RIDERS = [ { id: 'RDR-8012', name: 'Muthu Kumar', color: '#1A73E8', vehicle: 'Electric Bike', vehicleNo: 'DL-04-EB-1234', phone: '+91 98765 43210', delivery: { startTime: '08:12', endTime: '13:40', distanceKm: 18.4, stops: [ hubStop('08:12'), { kind: 'order', orderId: 'ORD-100231', customer: 'Aarav Mehta', phone: '+91 90011 22334', address: 'Flat 402, Dwarka Sector 12, New Delhi', lat: 28.5921, lng: 77.0460, time: '08:48', status: 'Picked', items: 2, weight: '1.4 kg', slot: '08:00–10:00', cod: 0, payment: 'Prepaid', legKm: 5.2, instructions: 'Leave at reception if not home.' }, { kind: 'order', orderId: 'ORD-100244', customer: 'Priya Nair', phone: '+91 90022 33445', address: 'House 18, Janakpuri B-Block, New Delhi', lat: 28.6219, lng: 77.0878, time: '09:36', status: 'Picked', items: 1, weight: '0.6 kg', slot: '09:00–11:00', cod: 1200, payment: 'COP', legKm: 4.1, instructions: 'Call on arrival.' }, { kind: 'order', orderId: 'ORD-100258', customer: 'Rohit Sethi', phone: '+91 90033 44556', address: 'Shop 7, Rajouri Garden Market, New Delhi', lat: 28.6492, lng: 77.1207, time: '10:25', status: 'Failed', items: 3, weight: '2.8 kg', slot: '10:00–12:00', cod: 0, payment: 'Prepaid', legKm: 4.6, instructions: 'Customer unreachable — reattempt tomorrow.' }, { kind: 'order', orderId: 'ORD-100269', customer: 'Sana Kapoor', phone: '+91 90044 55667', address: 'A-22, Karol Bagh, New Delhi', lat: 28.6512, lng: 77.1907, time: '11:30', status: 'Picked', items: 1, weight: '0.9 kg', slot: '11:00–13:00', cod: 850, payment: 'COP', legKm: 4.5, instructions: '' }, ], }, pickup: { startTime: '14:10', endTime: '16:50', distanceKm: 11.2, stops: [ { kind: 'order', orderId: 'PCK-50118', customer: 'TrendKart Store', phone: '+91 98801 11222', address: 'Tilak Nagar Main Rd, New Delhi', lat: 28.6363, lng: 77.0945, time: '14:35', status: 'Picked', items: 6, weight: '4.2 kg', slot: '14:00–16:00', cod: 0, payment: 'Merchant', legKm: 6.0, instructions: 'Collect from back gate.' }, { kind: 'order', orderId: 'PCK-50126', customer: 'FreshLeaf Organics', phone: '+91 98802 22333', address: 'Subhash Nagar, New Delhi', lat: 28.6404, lng: 77.1199, time: '15:30', status: 'Picked', items: 3, weight: '5.5 kg', slot: '15:00–17:00', cod: 0, payment: 'Merchant', legKm: 3.0, instructions: '' }, { kind: 'order', orderId: 'PCK-50131', customer: 'GadgetHub', phone: '+91 98803 33444', address: 'Moti Nagar, New Delhi', lat: 28.6580, lng: 77.1450, time: '16:15', status: 'Missed', items: 2, weight: '1.1 kg', slot: '16:00–18:00', cod: 0, payment: 'Merchant', legKm: 2.2, instructions: 'Shop was closed at pickup time.' }, hubStop('16:50'), ], }, }, { id: 'RDR-8041', name: 'Sana Sheikh', color: '#8E24AA', vehicle: 'Motorcycle', vehicleNo: 'DL-02-MC-1199', phone: '+91 98765 43214', delivery: { startTime: '08:00', endTime: '14:05', distanceKm: 22.9, stops: [ hubStop('08:00'), { kind: 'order', orderId: 'ORD-100277', customer: 'Imran Qureshi', phone: '+91 90055 66778', address: 'N-Block, Connaught Place, New Delhi', lat: 28.6315, lng: 77.2167, time: '08:30', status: 'Picked', items: 1, weight: '0.4 kg', slot: '08:00–10:00', cod: 0, payment: 'Prepaid', legKm: 2.1, instructions: '' }, { kind: 'order', orderId: 'ORD-100283', customer: 'Neha Gupta', phone: '+91 90066 77889', address: 'C-44, Lajpat Nagar II, New Delhi', lat: 28.5677, lng: 77.2433, time: '09:20', status: 'Picked', items: 2, weight: '1.7 kg', slot: '09:00–11:00', cod: 1450, payment: 'COP', legKm: 7.4, instructions: 'Ring twice.' }, { kind: 'order', orderId: 'ORD-100291', customer: 'Vikas Rao', phone: '+91 90077 88990', address: 'J-Block, Saket, New Delhi', lat: 28.5245, lng: 77.2066, time: '10:40', status: 'Picked', items: 1, weight: '0.8 kg', slot: '10:00–12:00', cod: 0, payment: 'Prepaid', legKm: 6.2, instructions: '' }, { kind: 'order', orderId: 'ORD-100305', customer: 'Diya Shah', phone: '+91 90088 99001', address: 'Malviya Nagar Main Market, New Delhi', lat: 28.5355, lng: 77.2110, time: '11:55', status: 'Picked', items: 4, weight: '3.1 kg', slot: '11:00–13:00', cod: 2300, payment: 'COP', legKm: 1.4, instructions: 'Heavy parcel — handle with care.' }, ], }, pickup: { startTime: '14:30', endTime: '17:20', distanceKm: 14.6, stops: [ { kind: 'order', orderId: 'PCK-50140', customer: 'Bloom & Co Florists', phone: '+91 98804 44555', address: 'Greater Kailash I, New Delhi', lat: 28.5494, lng: 77.2426, time: '15:00', status: 'Picked', items: 4, weight: '2.0 kg', slot: '14:30–16:30', cod: 0, payment: 'Merchant', legKm: 8.1, instructions: '' }, { kind: 'order', orderId: 'PCK-50147', customer: 'BookNook', phone: '+91 98805 55666', address: 'Hauz Khas Village, New Delhi', lat: 28.5535, lng: 77.1944, time: '16:05', status: 'Picked', items: 9, weight: '7.8 kg', slot: '15:30–17:30', cod: 0, payment: 'Merchant', legKm: 4.5, instructions: 'Multiple boxes.' }, hubStop('17:20'), ], }, }, { id: 'RDR-8015', name: 'Rajesh Sharma', color: '#1E8E3E', vehicle: 'Cargo Van', vehicleNo: 'DL-01-CV-9876', phone: '+91 98765 43211', delivery: { startTime: '07:50', endTime: '15:10', distanceKm: 31.2, stops: [ hubStop('07:50'), { kind: 'order', orderId: 'ORD-100312', customer: 'Anil Verma', phone: '+91 90099 00112', address: 'Sector 7, Rohini, New Delhi', lat: 28.7042, lng: 77.1025, time: '08:55', status: 'Picked', items: 5, weight: '6.2 kg', slot: '08:00–10:00', cod: 0, payment: 'Prepaid', legKm: 12.4, instructions: '' }, { kind: 'order', orderId: 'ORD-100320', customer: 'Meera Iyer', phone: '+91 90100 11223', address: 'NSP, Pitampura, New Delhi', lat: 28.6996, lng: 77.1314, time: '09:50', status: 'Picked', items: 2, weight: '2.0 kg', slot: '09:00–11:00', cod: 990, payment: 'COP', legKm: 3.2, instructions: '' }, { kind: 'order', orderId: 'ORD-100334', customer: 'Sahil Khan', phone: '+91 90111 22334', address: 'Model Town III, New Delhi', lat: 28.7158, lng: 77.1910, time: '11:10', status: 'Failed', items: 1, weight: '0.7 kg', slot: '10:00–12:00', cod: 0, payment: 'Prepaid', legKm: 6.1, instructions: 'Wrong address provided.' }, { kind: 'order', orderId: 'ORD-100349', customer: 'Tara Bose', phone: '+91 90122 33445', address: 'Civil Lines, New Delhi', lat: 28.6796, lng: 77.2240, time: '12:30', status: 'Picked', items: 3, weight: '4.4 kg', slot: '12:00–14:00', cod: 3100, payment: 'COP', legKm: 5.0, instructions: '' }, ], }, pickup: { startTime: '15:40', endTime: '18:30', distanceKm: 19.8, stops: [ { kind: 'order', orderId: 'PCK-50155', customer: 'MegaMart Warehouse', phone: '+91 98806 66777', address: 'Wazirpur Industrial Area, New Delhi', lat: 28.6991, lng: 77.1612, time: '16:20', status: 'Picked', items: 24, weight: '38.0 kg', slot: '16:00–18:00', cod: 0, payment: 'Merchant', legKm: 11.0, instructions: 'Use loading dock 4.' }, { kind: 'order', orderId: 'PCK-50163', customer: 'HomeStyle Furnishings', phone: '+91 98807 77888', address: 'Ashok Vihar, New Delhi', lat: 28.6924, lng: 77.1760, time: '17:25', status: 'Picked', items: 8, weight: '22.5 kg', slot: '17:00–19:00', cod: 0, payment: 'Merchant', legKm: 3.0, instructions: '' }, hubStop('18:30'), ], }, }, { id: 'RDR-8044', name: 'Harpreet Gill', color: '#E8710A', vehicle: 'Cycle', vehicleNo: '—', phone: '+91 98765 43215', delivery: { startTime: '09:20', endTime: '13:15', distanceKm: 9.7, stops: [ hubStop('09:20'), { kind: 'order', orderId: 'ORD-100356', customer: 'Kabir Anand', phone: '+91 90133 44556', address: 'Mayur Vihar Phase I, New Delhi', lat: 28.6090, lng: 77.2920, time: '10:05', status: 'Picked', items: 1, weight: '0.5 kg', slot: '10:00–12:00', cod: 600, payment: 'COP', legKm: 9.0, instructions: '' }, { kind: 'order', orderId: 'ORD-100361', customer: 'Ritu Saxena', phone: '+91 90144 55667', address: 'Mayur Vihar Phase III, New Delhi', lat: 28.6135, lng: 77.3215, time: '11:10', status: 'Picked', items: 2, weight: '1.2 kg', slot: '11:00–13:00', cod: 0, payment: 'Prepaid', legKm: 3.0, instructions: '' }, { kind: 'order', orderId: 'ORD-100370', customer: 'Farhan Ali', phone: '+91 90155 66778', address: 'Patparganj, New Delhi', lat: 28.6280, lng: 77.2960, time: '12:20', status: 'Picked', items: 1, weight: '0.8 kg', slot: '12:00–14:00', cod: 450, payment: 'COP', legKm: 2.9, instructions: '' }, ], }, pickup: { startTime: '13:40', endTime: '15:30', distanceKm: 6.4, stops: [ { kind: 'order', orderId: 'PCK-50170', customer: 'Cafe Mosaic', phone: '+91 98808 88999', address: 'Mayur Vihar Phase I Market, New Delhi', lat: 28.6055, lng: 77.2985, time: '14:10', status: 'Picked', items: 2, weight: '1.5 kg', slot: '14:00–16:00', cod: 0, payment: 'Merchant', legKm: 4.0, instructions: '' }, hubStop('15:30'), ], }, }, ]; // ── helpers ──────────────────────────────────────────────────────────────────── const initials = (n) => n.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase(); const inr = (n) => `₹${(Number(n) || 0).toLocaleString('en-IN')}`; const lerpPoint = (a, b, t) => ({ lat: a.lat + (b.lat - a.lat) * t, lng: a.lng + (b.lng - a.lng) * t }); const STATUS_META = { Picked: { color: '#1A73E8', icon: CheckCircleRoundedIcon, label: 'Picked up' }, Missed: { color: '#D93025', icon: CancelRoundedIcon, label: 'Missed' }, }; const MODES = { pickup: { label: 'Pickups', icon: StorefrontOutlinedIcon, doneLabel: 'Picked up', doneStatus: 'Picked', failStatus: 'Missed', pointLabel: 'Pickup', dash: '8 8' }, }; // ════════════════════════════════════════════════════════════════════════════════ // Route resolution — OSRM road geometry (free, no API key); straight-line fallback. // ════════════════════════════════════════════════════════════════════════════════ async function fetchRoadRoute(stops) { const coords = stops.map((s) => `${s.lng},${s.lat}`).join(';'); const url = `https://router.project-osrm.org/route/v1/driving/${coords}?overview=full&geometries=geojson`; const res = await fetch(url); const json = await res.json(); if (json.routes && json.routes[0]) { return json.routes[0].geometry.coordinates.map(([lng, lat]) => ({ lat, lng })); } throw new Error('No route'); } // ════════════════════════════════════════════════════════════════════════════════ // Small presentational pieces // ════════════════════════════════════════════════════════════════════════════════ function KpiCard({ icon: Icon, label, value, sub, color, bg }) { return ( {label} {value} {sub && {sub}} ); } function DetailRow({ icon: Icon, label, value, valueColor }) { if (value === undefined || value === null || value === '') return null; return ( {label} {value} ); } // Full order/pickup detail panel — rendered INLINE in the left column (not a modal) // so the map stays visible and zoomed to the stop while these details are read. function OrderDetailPanel({ data, onBack }) { const { order, rider, mode, index } = data; const meta = STATUS_META[order.status] || {}; const StatusIcon = meta.icon || CheckCircleRoundedIcon; const isPickup = mode === 'pickup'; return ( {/* Header */} : )} label={isPickup ? 'Pickup' : 'Delivery'} sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700, '& .MuiChip-icon': { color: '#fff' } }} /> } label={meta.label || order.status} sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700 }} /> {order.customer} #{order.orderId} {/* Body */} {/* Contact */} {isPickup ? 'MERCHANT / SENDER' : 'CUSTOMER'} {/* Pickup summary */} PICKUP SUMMARY {order.cod > 0 && ( CASH ON PICKUP {inr(order.cod)} )} {/* Route & location */} ROUTE & LOCATION {order.instructions && } {/* Assigned miler */} ASSIGNED MILER {initials(rider.name)} {rider.name} {rider.vehicle} · {rider.vehicleNo} · Stop {index} {/* Footer */} ); } // ════════════════════════════════════════════════════════════════════════════════ // MAIN COMPONENT // ════════════════════════════════════════════════════════════════════════════════ export default function RiderRoutes() { const [mode] = useState('pickup'); // pickups only const [visible, setVisible] = useState(() => Object.fromEntries(RIDERS.map((r) => [r.id, true]))); const [routes, setRoutes] = useState({}); // keyed `${id}__${mode}` const [expanded, setExpanded] = useState(RIDERS[0].id); const [focusedStop, setFocusedStop] = useState(null); // `${riderId}-${index}` const [detail, setDetail] = useState(null); // { order, rider, mode, index } const [flyTarget, setFlyTarget] = useState(null); // Animation state. const [playing, setPlaying] = useState(null); const [progress, setProgress] = useState(0); const [speed, setSpeed] = useState(1); const [speedAnchor, setSpeedAnchor] = useState(null); const rafRef = useRef(null); const lastTsRef = useRef(0); const resolvedRef = useRef({}); // keys we've already fetched/attempted const modeCfg = MODES[mode]; // Resolve road routes for the active mode (cached per id+mode). A ref guards // against re-fetching keys we've already resolved when the tab is revisited. useEffect(() => { let cancelled = false; (async () => { await Promise.all( RIDERS.map(async (r) => { const key = `${r.id}__${mode}`; if (resolvedRef.current[key]) return; resolvedRef.current[key] = true; const stops = r[mode].stops; let path; try { path = await fetchRoadRoute(stops); } catch { path = stops.map((s) => ({ lat: s.lat, lng: s.lng })); } if (!cancelled) setRoutes((prev) => ({ ...prev, [key]: path })); }) ); })(); return () => { cancelled = true; }; }, [mode]); const pathFor = useCallback( (r) => routes[`${r.id}__${mode}`] || r[mode].stops.map((s) => ({ lat: s.lat, lng: s.lng })), [routes, mode] ); const stopsOf = useCallback((r) => r[mode].stops, [mode]); // Auto-fit points = visible riders' stops for the active mode. const fitPoints = useMemo(() => { const pts = []; RIDERS.forEach((r) => { if (visible[r.id]) stopsOf(r).forEach((s) => pts.push(s)); }); return pts; }, [visible, stopsOf]); // ── Analysis KPIs for the active mode ───────────────────────────────────────── const kpi = useMemo(() => { let orders = 0, done = 0, fail = 0, km = 0, cod = 0, activeRiders = 0; RIDERS.forEach((r) => { const trip = r[mode]; const orderStops = trip.stops.filter((s) => s.kind === 'order'); if (orderStops.length) activeRiders += 1; orders += orderStops.length; done += orderStops.filter((s) => s.status === modeCfg.doneStatus).length; fail += orderStops.filter((s) => s.status === modeCfg.failStatus).length; km += trip.distanceKm; cod += orderStops.reduce((s, o) => s + (o.cod || 0), 0); }); return { orders, done, fail, km: km.toFixed(1), cod, activeRiders }; }, [mode, modeCfg]); // ── Animation driver ────────────────────────────────────────────────────────── useEffect(() => { if (!playing) { if (rafRef.current) cancelAnimationFrame(rafRef.current); return; } lastTsRef.current = 0; const DURATION = 14000; const tick = (ts) => { if (!lastTsRef.current) lastTsRef.current = ts; const dt = ts - lastTsRef.current; lastTsRef.current = ts; setProgress((p) => { const nextP = p + (dt * speed) / DURATION; if (nextP >= 1) { setPlaying(null); return 1; } return nextP; }); rafRef.current = requestAnimationFrame(tick); }; rafRef.current = requestAnimationFrame(tick); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [playing, speed]); // Stop any animation when switching tabs. useEffect(() => { setPlaying(null); setProgress(0); }, [mode]); const startAnim = (riderId) => { setVisible((v) => ({ ...v, [riderId]: true })); if (playing === riderId) { setPlaying(null); return; } if (playing !== riderId) setProgress(0); setPlaying(riderId); }; const resetAnim = () => { setPlaying(null); setProgress(0); }; const toggleVisible = (id) => setVisible((v) => ({ ...v, [id]: !v[id] })); const openDetail = (rider, order, index) => { setDetail({ order, rider, mode, index }); setExpanded(rider.id); setFocusedStop(`${rider.id}-${index}`); setFlyTarget({ lat: order.lat, lng: order.lng }); }; const closeDetail = () => { setDetail(null); setFlyTarget(null); setFocusedStop(null); }; const playState = useMemo(() => { if (!playing) return null; const rider = RIDERS.find((r) => r.id === playing); const path = pathFor(rider); if (path.length < 2) return null; const totalSegs = path.length - 1; const exact = progress * totalSegs; const i = Math.min(Math.floor(exact), totalSegs - 1); const frac = exact - i; const pos = lerpPoint(path[i], path[i + 1], frac); const travelled = [...path.slice(0, i + 1), pos]; return { rider, path, travelled, pos }; }, [playing, progress, pathFor]); return ( {/* ── Header ── */} Rider Routes Pickups each miler covered today open any order for full details, or press play to replay the trip. {/* ── Analysis KPI strip ── */} {[ { icon: GroupsOutlinedIcon, label: 'Active Milers', value: kpi.activeRiders, sub: `of ${RIDERS.length}`, color: '#1A73E8', bg: '#E8F0FE' }, { icon: modeCfg.icon, label: modeCfg.label, value: kpi.orders, sub: 'pickups covered', color: '#C01227', bg: alpha('#C01227', 0.1) }, { icon: CheckCircleRoundedIcon, label: modeCfg.doneLabel, value: kpi.done, sub: `${kpi.fail} missed`, color: '#1E8E3E', bg: '#E6F4EA' }, { icon: StraightenRoundedIcon, label: 'Distance', value: `${kpi.km} km`, sub: 'fleet total today', color: '#8E24AA', bg: '#F3E5F5' }, ].map((k, i) => ( ))} {/* ── Left control panel — the milers list; clicking an order opens the full detail in a right-side drawer (below). ── */} Milers & their {modeCfg.label.toLowerCase()} {(playing || progress > 0) && ( )} {RIDERS.map((rider) => { const trip = rider[mode]; const orders = trip.stops.filter((s) => s.kind === 'order'); const done = orders.filter((o) => o.status === modeCfg.doneStatus).length; const isOpen = expanded === rider.id; const isPlaying = playing === rider.id; const isVisible = visible[rider.id]; return ( {initials(rider.name)} setExpanded(isOpen ? null : rider.id)} sx={{ flex: 1, borderRadius: 2, px: 1, py: 0.5, minWidth: 0 }}> {rider.name}} secondary={{done}/{orders.length} {modeCfg.doneLabel.toLowerCase()} · {trip.distanceKm} km} /> {isOpen ? : } {/* per-rider stat chips (analysis) */} } label={`${orders.length} ${modeCfg.label.toLowerCase()}`} sx={{ height: 24, fontWeight: 600, bgcolor: '#F1F3F5' }} /> } label={done} sx={{ height: 24, fontWeight: 600, bgcolor: alpha('#1E8E3E', 0.1), color: '#1E8E3E' }} /> } label={`${trip.distanceKm} km`} sx={{ height: 24, fontWeight: 600, bgcolor: '#F1F3F5' }} /> } label={`${trip.startTime}–${trip.endTime}`} sx={{ height: 24, fontWeight: 600, bgcolor: '#F1F3F5' }} /> {/* action row */} toggleVisible(rider.id)} sx={{ border: '1px solid #E9ECEF', borderRadius: 2 }}> {isVisible ? : } {isPlaying && ( )} {/* expandable stops — rich order cards; click to open detail */} {trip.stops.map((s, i) => { const key = `${rider.id}-${i}`; const isHub = s.kind === 'hub'; const meta = STATUS_META[s.status]; const StatusIcon = meta?.icon; // Hub return — compact row, not a card. if (isHub) { return ( {s.label} Back to hub · {s.time} ); } return ( setFocusedStop(key)} onMouseLeave={() => setFocusedStop((f) => (f === key ? null : f))} onClick={() => openDetail(rider, s, i)} sx={{ p: 1.5, borderRadius: 2, cursor: 'pointer', transition: 'all .15s', border: '1px solid', borderColor: focusedStop === key ? alpha(rider.color, 0.55) : '#ECEEF1', bgcolor: focusedStop === key ? alpha(rider.color, 0.04) : '#fff', boxShadow: '0 1px 3px rgba(0,0,0,0.03)', '&:hover': { borderColor: alpha(rider.color, 0.55), boxShadow: '0 4px 14px rgba(0,0,0,0.07)' }, }} > {/* order id + status */} {i} Order #{s.orderId} {meta && ( } label={meta.label} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 700, color: meta.color, bgcolor: alpha(meta.color, 0.1), flexShrink: 0 }} /> )} {/* rider + time */} {rider.name} {s.time} {/* merchant + address */} {s.customer} {s.address} {/* metric chips */} } label={`${s.legKm} km`} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#F1F3F5' }} /> } label={s.weight} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#F1F3F5' }} /> } label={`${s.items} ${s.items === 1 ? 'parcel' : 'parcels'}`} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#F1F3F5' }} /> ); })} ); })} {/* ── Map ── */} {playState && ( {initials(playState.rider.name)} {playState.rider.name} Replaying {modeCfg.label.toLowerCase().replace(/s$/, '')} · {Math.round(progress * 100)}% setPlaying(null)}> setSpeedAnchor(e.currentTarget)}> )} setSpeedAnchor(null)}> {[0.5, 1, 2, 4].map((s) => ( { setSpeed(s); setSpeedAnchor(null); }}>{s}× speed))} {!playing && !flyTarget && } {HUB.label}
Pickups return here
{RIDERS.map((rider) => { if (!visible[rider.id]) return null; const path = pathFor(rider); const stops = stopsOf(rider); const dimmed = playing && playing !== rider.id; const endStop = stops[stops.length - 1]; return ( [p.lat, p.lng])} pathOptions={{ color: rider.color, weight: 5, opacity: dimmed ? 0.15 : 0.85, dashArray: modeCfg.dash }} /> {stops.map((s, i) => { if (s.kind === 'hub') return null; const key = `${rider.id}-${i}`; return ( openDetail(rider, s, i) }} > {modeCfg.pointLabel} {i} · {s.customer} {s.address} #{s.orderId} · {s.time} · {STATUS_META[s.status]?.label} openDetail(rider, s, i)}>View full details → ); })} {!dimmed && ( {mode === 'pickup' ? 'Returned to hub' : 'Trip end'} · {rider[mode].endTime} )} ); })} {playState && ( <> [p.lat, p.lng])} pathOptions={{ color: playState.rider.color, weight: 7, opacity: 1 }} /> {playState.rider.name} )}
{/* Legend */} {RIDERS.map((r) => ( toggleVisible(r.id)}> {r.name} ))} {mode === 'pickup' ? 'Hub return' : 'Trip end'} Hub
{/* ── Order detail — right-side drawer ── */} {detail && }
); }