import React, { useEffect, useMemo, useState } from 'react'; import { MapContainer, TileLayer, Polyline, Marker, Popup, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { Box, IconButton, Stack, Typography, CircularProgress } from '@mui/material'; import { MdClose, MdRoute } from 'react-icons/md'; const containerStyle = { width: '100%', height: '100%' }; // Numbered step icon — brand red to match the planned-route polyline below. // Drawn fresh per render as a data URL so the step number can be baked into // the SVG without juggling external marker assets. const stepIcon = (n, isFocused) => { const size = isFocused ? 38 : 32; const color = isFocused ? '#910E1D' : '#C01227'; const svg = encodeURIComponent( `` + `` + `${n}` + `` ); return new L.Icon({ iconUrl: `data:image/svg+xml;charset=UTF-8,${svg}`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); }; // Fits the map to the planned path once both the map and data are ready. // Re-runs whenever the route changes (different rider / date). const FitBoundsController = ({ dropPath }) => { const map = useMap(); useEffect(() => { if (!dropPath.length) return; if (dropPath.length === 1) { map.setView(dropPath[0], 14); } else { map.fitBounds(dropPath, { padding: [48, 48] }); } }, [dropPath, map]); return null; }; // Renders a single rider's PLANNED route for the date range chosen on the // Riders Summary page. `details` is an ordered array of waypoints (sorted by // the planning step number) shaped as: // { step, orderid, deliveryid, customer, address, // dropLat, dropLng, pickLat, pickLng, expectedTime } // `dropLat/dropLng` are required; pickup coords are optional and rendered as // faded pre-stops if present. export default function RidersRoutes({ details, loading, riderName, dateRange, onClose }) { const [focusedStep, setFocusedStep] = useState(null); const [routePath, setRoutePath] = useState([]); const [routeLoading, setRouteLoading] = useState(false); // Step-pin coordinates in planning order — what the polyline connects. const dropPath = useMemo(() => (details || []).map((d) => [d.dropLat, d.dropLng]), [details]); // Resolve the rider's planned waypoints into an actual road-following path // via OSRM. Without this, the polyline would cut across buildings / aerial // lines — operators have no way to read the real route. useEffect(() => { if (dropPath.length < 2) { setRoutePath([]); return; } let cancelled = false; (async () => { setRouteLoading(true); try { const coords = dropPath.map(([lat, lng]) => `${lng},${lat}`).join(';'); const url = `https://router.project-osrm.org/route/v1/driving/${coords}?overview=full&geometries=geojson`; const res = await fetch(url); const data = await res.json(); if (!cancelled && data.routes?.length) { const points = data.routes[0].geometry.coordinates.map(([lng, lat]) => [lat, lng]); setRoutePath(points); } else if (!cancelled) { setRoutePath([]); } } catch (e) { console.warn('OSRM route error:', e); if (!cancelled) setRoutePath([]); } finally { if (!cancelled) setRouteLoading(false); } })(); return () => { cancelled = true; }; }, [dropPath]); const headerBar = ( Planned route{riderName ? ` — ${riderName}` : ''} {dateRange && {dateRange}} {details && details.length > 0 && ( {details.length} {details.length === 1 ? 'stop' : 'stops'} {routeLoading ? ' · resolving route…' : ''} )} {onClose && ( )} ); // Loading state — parent is still fetching the planned route data. if (loading) { return ( {headerBar} Loading planned route… ); } // Empty state — fetched but rider has no deliveries with drop coords in the // selected window. if (!details || details.length === 0) { return ( {headerBar} No planned route for this rider There are no deliveries with drop coordinates assigned to this rider for the selected date range. ); } return ( {headerBar} {routePath.length > 0 ? ( <> {/* Translucent backdrop so the route stays legible on busy tiles. */} {/* Road-following planned route from OSRM. */} ) : ( // Fallback while OSRM is in flight (or if it fails) — dashed // straight-line skeleton between drop pins in step order. )} {details.map((d, i) => { const stepNum = d.step || i + 1; const isFocused = focusedStep === d.deliveryid; return ( setFocusedStep(isFocused ? null : d.deliveryid) }} zIndexOffset={isFocused ? 1000 : stepNum} > setFocusedStep(null)}> Step {stepNum} · {d.customer} {d.address && {d.address}} {d.expectedTime && ( ETA {String(d.expectedTime).slice(11, 16) || d.expectedTime} )} {d.orderid && Order #{d.orderid}} ); })} ); }