From e662154916e24e3630819af92a0f8fd492e78a0a Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Mon, 1 Jun 2026 15:16:45 +0530 Subject: [PATCH] updates on the design and added the riders route page --- src/pages/nearle/deliveries/deliveries.js | 2 +- src/pages/nearle/dispatch/Dispatch.css | 184 ++++++++++--- src/pages/nearle/dispatch/Dispatch.js | 98 ++++--- src/pages/nearle/login.js | 6 +- src/pages/nearle/reports/RidersRoutes.js | 316 ++++++++++++++++++---- src/pages/nearle/reports/ridersSummary.js | 91 ++++++- 6 files changed, 562 insertions(+), 135 deletions(-) diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js index f931042..7741b27 100644 --- a/src/pages/nearle/deliveries/deliveries.js +++ b/src/pages/nearle/deliveries/deliveries.js @@ -192,7 +192,7 @@ const KPI_META = [ const BATCH_OPTIONS = [ { id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' }, { id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 }, - { id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12 }, + { id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12:30 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12.5 }, { id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 } ]; diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index 62501c3..76efa51 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -3786,7 +3786,7 @@ background: rgba(15, 23, 42, 0.02); border: 1px solid rgba(15, 23, 42, 0.06); border-radius: 12px; - padding: 10px 14px; + padding: 18px 18px; gap: 16px; box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.02); } @@ -3840,16 +3840,11 @@ position: relative; } -/* Planned track overrides to align vertically centered since there are no ticks */ -.dispatch-container .compare-timeline-track.is-planned .compare-step { - gap: 0; - padding: 0; - height: 32px; -} - +/* Planned track now also carries a time tick under the circle, so it uses the + same column layout as the actual row (circle stacked above the tick). */ .dispatch-container .compare-timeline-track.is-planned .compare-step-spacer { - margin-bottom: 0; - align-self: center; + margin-bottom: 22px; + /* Centers spacer dynamically relative to the 32px circle (matches actual). */ } /* Actual track overrides for the spacer alignment */ @@ -6848,7 +6843,7 @@ } .dispatch-container .compare-timeline-container { - padding: 6px 10px; + padding: 12px 12px; gap: 12px; border-radius: 8px; } @@ -6894,14 +6889,10 @@ background: rgba(99, 102, 241, 0.5); } - /* Planned track overrides to align vertically centered since there are no ticks */ - .dispatch-container .compare-timeline-track.is-planned .compare-step { - height: 24px; - } - + /* Planned track now also carries a time tick under the circle, so the spacer + aligns the same way as the actual row (mirrors the 24px circle center). */ .dispatch-container .compare-timeline-track.is-planned .compare-step-spacer { - margin-bottom: 0; - align-self: center; + margin-bottom: 14px; } /* Actual track overrides for the spacer alignment */ @@ -7098,7 +7089,7 @@ } .dispatch-container .compare-timeline-container { - padding: 6px 10px; + padding: 12px 12px; gap: 12px; border-radius: 8px; } @@ -7124,14 +7115,10 @@ padding-bottom: 2px; } - /* Planned track overrides to align vertically centered since there are no ticks */ - .dispatch-container .compare-timeline-track.is-planned .compare-step { - height: 24px; - } - + /* Planned track now also carries a time tick under the circle, so the spacer + aligns the same way as the actual row (mirrors the 24px circle center). */ .dispatch-container .compare-timeline-track.is-planned .compare-step-spacer { - margin-bottom: 0; - align-self: center; + margin-bottom: 14px; } /* Actual track overrides for the spacer alignment */ @@ -7312,6 +7299,8 @@ gap: 12px; padding: 12px; background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%); + transition: grid-template-columns 0.32s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; } .dispatch-container #body.compare-mode #sidebar, @@ -7319,6 +7308,89 @@ display: none !important; } +/* Collapsed-data-panel state — drop the right column entirely so the map + claims the full body width. The panel itself is masked via overflow on + the body grid; the peek tab below stays visible to re-open. */ +.dispatch-container #body.compare-mode.compare-data-collapsed { + grid-template-columns: minmax(0, 1fr) 0; + gap: 0; +} + +.dispatch-container #body.compare-mode.compare-data-collapsed .compare-data-panel { + opacity: 0; + pointer-events: none; + transform: translateX(20px); +} + +.dispatch-container .compare-data-panel { + transition: opacity 0.24s ease, transform 0.32s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Peek tab for the right-side compare data panel — vertical pill mirroring + the left sidebar's toggle, but anchored to the panel's left edge. Tracks + the panel by sitting at right:0 when expanded (so it hugs the panel's + outside-left edge) and snaps flush to the viewport's right side when + collapsed. */ +.dispatch-container .compare-data-toggle-tab { + position: absolute; + top: 50%; + /* Anchor flush against the panel's outside-left edge. Panel max width is + 440px (see compare-mode grid-template-columns above) plus the 12px grid + gap; transform: translate(50%, …) re-centres the 22-wide pill on that + boundary so half of it sits on the panel side and half on the map side + — same visual treatment as the left sidebar's peek tab. */ + right: calc(440px + 12px); + transform: translate(50%, -50%); + width: 22px; + height: 56px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); + border-radius: 10px; + background: #fff; + color: var(--text, #0f172a); + font-size: 18px; + line-height: 1; + cursor: pointer; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12), + 0 1px 3px rgba(15, 23, 42, 0.06); + z-index: 1200; + transition: right 0.32s cubic-bezier(0.4, 0, 0.2, 1), + background 0.18s ease, + color 0.18s ease, + transform 0.18s ease, + box-shadow 0.18s ease; +} + +.dispatch-container .compare-data-toggle-tab:hover { + background: linear-gradient(135deg, #6366f1, #3b82f6); + color: #fff; + transform: translate(50%, -50%) scale(1.06); + box-shadow: 0 6px 16px rgba(99, 102, 241, 0.35); +} + +.dispatch-container .compare-data-toggle-tab:focus-visible { + outline: 2px solid var(--accent, #3b82f6); + outline-offset: 2px; +} + +.dispatch-container .compare-data-toggle-tab.is-collapsed { + right: 0; + transform: translate(0, -50%); + border-radius: 10px 0 0 10px; + border-right: none; +} + +.dispatch-container .compare-data-toggle-tab.is-collapsed:hover { + transform: translate(0, -50%) scale(1.06); +} + +.dispatch-container .compare-data-toggle-tab svg { + display: block; +} + /* Header strip — sits above the unified map (row 1, col 1) and carries the rider title, the step timeline + load progress, and the layer legend. The Sync toggle was removed when the second @@ -7973,6 +8045,12 @@ grid-row: 3; max-height: 50vh; } + + /* Single-column layout stacks the panel BELOW the map, so the + side-anchored peek tab no longer makes geometric sense — hide it. */ + .dispatch-container .compare-data-toggle-tab { + display: none; + } } /* Hide filter chrome when Compare takes over the screen — view-mode @@ -9062,46 +9140,53 @@ } /* ============================================================ - Top-level Live / Analysis tabs (standalone Dispatch only) + Top-level Live / Analysis tabs (pinned inside header, left of profit) ============================================================ */ .dispatch-container #dispatch-top-tabs { - display: flex; - gap: 6px; - padding: 8px 12px 0 12px; - border-bottom: 1px solid var(--border); - background: var(--bg); + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0; + background: transparent; flex-shrink: 0; } +.dispatch-container #dispatch-top-tabs.dtt-inline { + margin-right: 4px; +} + .dispatch-container .dtt-tab { display: inline-flex; align-items: center; gap: 6px; - padding: 8px 14px; - border: none; - background: transparent; - font-size: 13px; - font-weight: 600; + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg); + font-size: 12px; + font-weight: 700; color: var(--text-muted); cursor: pointer; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - transition: color 0.15s, border-color 0.15s; + line-height: 1; + transition: color 0.15s, background 0.15s, border-color 0.15s; } .dispatch-container .dtt-tab:hover { color: var(--text); + background: var(--bg-sub); } .dispatch-container .dtt-tab.active { - color: var(--accent); - border-bottom-color: var(--accent); + color: #fff; + background: var(--accent); + border-color: var(--accent); + box-shadow: 0 2px 6px rgba(59, 130, 246, 0.25); } .dispatch-container .dtt-icon { display: inline-flex; align-items: center; - font-size: 15px; + font-size: 14px; } /* ============================================================ @@ -9477,6 +9562,19 @@ gap: 8px; } +.dispatch-container .da-rec.da-rec-empty { + background: #f8fafc; + border: 1px dashed #e2e8f0; + color: #64748b; +} + +.dispatch-container .da-rec.da-rec-empty .da-rec-action { + color: #64748b; + text-transform: none; + font-weight: 500; + letter-spacing: 0; +} + .dispatch-container .da-rec-head { display: flex; justify-content: space-between; diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index ef28e76..f0cf83c 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -115,18 +115,18 @@ const hasValidPickup = (o) => Number.isFinite(toNum(pickupLat(o))) && Number.isF // FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported. // Three named batches, bucketed by assigntime per spec: // • Morning Batch: before 8 AM (00:00 → 08:00) -// • Afternoon Batch: 9 AM → 12 PM (09:00 → 12:00) +// • Afternoon Batch: 9 AM → 12:30 PM (09:00 → 12:30) // • Evening Batch: 4 PM → 7 PM (16:00 → 19:00) -// Gaps (8–9 AM, 12 PM–4 PM, 7 PM+) intentionally fall outside every batch. +// Gaps (8–9 AM, 12:30 PM–4 PM, 7 PM+) intentionally fall outside every batch. const BATCHES_DEFAULT_RAW = [ { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 }, - { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12 }, + { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 }, { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 } ]; -// v7: three-named-batch layout (Morning / Afternoon / Evening). -// Bumping the key drops cached 5-slot layouts from v6 and earlier. -const SLOTS_STORAGE_KEY = 'dispatch.slots.v7'; +// v8: afternoon batch extended to 12:30 PM. Bumping from v7 wipes the +// cached layouts that still hold the old endHour: 12 value. +const SLOTS_STORAGE_KEY = 'dispatch.slots.v8'; // Every prior storage key. Wiped once on mount so stale layouts // from earlier code versions can't reappear on the next page load. @@ -136,7 +136,8 @@ const LEGACY_SLOTS_STORAGE_KEYS = [ 'dispatch.slots.v3', 'dispatch.slots.v4', 'dispatch.slots.v5', - 'dispatch.slots.v6' + 'dispatch.slots.v6', + 'dispatch.slots.v7' ]; // Build a label like "Slot 1 · 8 AM" (or "Slot 2 · 12:30 PM") from a @@ -611,7 +612,7 @@ const Ico = ({ children }) => ( // to the X-Batch-Window header value the backend expects. const ANALYSIS_BATCH_WINDOWS = [ { key: 'morning', label: 'Morning', timeRange: '12:00 AM – 8:00 AM', sub: 'Early shift orders', color: '#f59e0b', bg: '#fffbeb', border: '#fde68a' }, - { key: 'afternoon', label: 'Noon', timeRange: '9:00 AM – 12:00 PM', sub: 'Lunch rush window', color: '#10b981', bg: '#ecfdf5', border: '#a7f3d0' }, + { key: 'afternoon', label: 'Noon', timeRange: '9:00 AM – 12:30 PM', sub: 'Lunch rush window', color: '#10b981', bg: '#ecfdf5', border: '#a7f3d0' }, { key: 'evening', label: 'Evening', timeRange: '4:00 PM – 7:00 PM', sub: 'Dinner & end-of-day', color: '#6366f1', bg: '#eef2ff', border: '#c7d2fe' } ]; @@ -1041,6 +1042,12 @@ const Dispatch = ({ const preCompareCollapsedRef = useRef(false); const prevCompareOpenRef = useRef(false); + // Compare-data-panel collapse — mirrors the left sidebar peek tab on the + // opposite edge so the operator can hide the right rail and let the map + // claim the full width during compare. Resets to expanded each time + // Compare opens fresh. + const [compareDataCollapsed, setCompareDataCollapsed] = useState(false); + // Compare UI — step focus on the unified compare map. // focusedCompareStep: null = "overall" (whole day); 1..N = drill into // that single delivery. The unified map zooms to that step's bounds @@ -1725,6 +1732,9 @@ const Dispatch = ({ if (compareOpen && !prevCompareOpenRef.current) { preCompareCollapsedRef.current = sidebarCollapsed; setSidebarCollapsed(true); + // Fresh compare-open always reveals the data panel — last-collapsed state + // shouldn't carry across compare sessions. + setCompareDataCollapsed(false); } else if (!compareOpen && prevCompareOpenRef.current) { setSidebarCollapsed(preCompareCollapsedRef.current); } @@ -2726,6 +2736,24 @@ const Dispatch = ({ )} )} +
+ + +
{/* Header right-cluster: profit/loss chip, total-orders pill, date picker. @@ -2970,27 +2998,6 @@ const Dispatch = ({ )} - {!embedded && ( -
- - -
- )} - {(embedded || topView === 'live') && (<>
@@ -3439,7 +3446,7 @@ const Dispatch = ({
) : ( -
+
+ {compareOpen && focusedRider && ( + + )}
- {rec && ( + {hasRec ? (
Top Recommendation
@@ -5289,6 +5315,16 @@ const Dispatch = ({ )}
+ ) : ( +
+
Top Recommendation
+
+
+ + Fleet is balanced, no reassignment needed right now. +
+
+
)} {riders.length > 0 && ( diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js index 39abd3e..bf7bba2 100644 --- a/src/pages/nearle/login.js +++ b/src/pages/nearle/login.js @@ -37,7 +37,7 @@ const Login = () => { useEffect(() => { if (localStorage.getItem('firstname')) { - navigate('/nearle/orders'); + navigate('/nearle/dispatch'); } }, []); @@ -97,7 +97,7 @@ const Login = () => { localStorage.setItem('userid', userinfo.userid); localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); fetchAppLocations(userinfo.userid); - navigate('/nearle/orders'); + navigate('/nearle/dispatch'); } else { OpenToast(res.data.message, 'error', 3000); } @@ -119,7 +119,7 @@ const Login = () => { localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); closeGlobalToast(); // to close the pin snackbar - navigate('/nearle/orders'); + navigate('/nearle/dispatch'); }; const opentoast = (message) => { diff --git a/src/pages/nearle/reports/RidersRoutes.js b/src/pages/nearle/reports/RidersRoutes.js index 1128341..be15e19 100644 --- a/src/pages/nearle/reports/RidersRoutes.js +++ b/src/pages/nearle/reports/RidersRoutes.js @@ -1,69 +1,285 @@ -import React, { useEffect, useMemo, useRef } from 'react'; -import { GoogleMap, Polyline, Marker, useJsApiLoader } from '@react-google-maps/api'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { GoogleMap, Polyline, Marker, InfoWindow, useJsApiLoader } from '@react-google-maps/api'; +import { Box, IconButton, Stack, Typography, CircularProgress } from '@mui/material'; +import { MdClose, MdRoute } from 'react-icons/md'; -const containerStyle = { - width: '100%', - height: '100%' -}; +const containerStyle = { width: '100%', height: '100%' }; -export default function RidersRoutes({ details }) { +// 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 mapRef = useRef(null); + const [focusedStep, setFocusedStep] = useState(null); + const [routePath, setRoutePath] = useState([]); + const [routeLoading, setRouteLoading] = useState(false); const { isLoaded } = useJsApiLoader({ googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY }); - // Convert dataset - const routePath = useMemo( - () => - details?.map((p) => ({ - lat: Number(p.latitude), - lng: Number(p.longitude) - })), + // Step-pin coordinates in planning order — what the polyline connects. + const dropPath = useMemo( + () => (details || []).map((d) => ({ lat: d.dropLat, lng: d.dropLng })), [details] ); - const bikeIcon = { - path: 'M12 2c-2.2 0-4 1.8-4 4v3H5l-1 2h2l3.6 7.59c.34.58.96.94 1.64.94h2.52c.68 0 1.3-.36 1.64-.94L19 11h2l-1-2h-3V6c0-2.2-1.8-4-4-4z', - fillColor: '#9c27b0', // 🔥 purple - fillOpacity: 1, - strokeWeight: 0, - scale: 1.4, - anchor: new window.google.maps.Point(12, 24) + + // Auto-fit map bounds to the full planned path once the map and data are + // both ready. Re-runs whenever the route changes (different rider / date). + useEffect(() => { + if (!isLoaded || !mapRef.current || dropPath.length === 0) return; + const bounds = new window.google.maps.LatLngBounds(); + dropPath.forEach((p) => bounds.extend(p)); + mapRef.current.fitBounds(bounds, 48); + }, [isLoaded, dropPath]); + + // Resolve the rider's planned waypoints into an actual road-following path + // via the Directions API. Without this, the polyline would cut across + // buildings / aerial lines — operators have no way to read the real route. + // Directions has a 25-waypoint limit per request, so we chunk and stitch. + useEffect(() => { + if (!isLoaded || dropPath.length < 2) { + setRoutePath([]); + return; + } + let cancelled = false; + const ds = new window.google.maps.DirectionsService(); + const MAX_WPS = 23; // origin + 23 waypoints + destination = 25 stops/chunk + + const fetchSegment = (origin, destination, waypoints) => + new Promise((resolve, reject) => { + ds.route( + { + origin, + destination, + waypoints: waypoints.map((p) => ({ location: p, stopover: true })), + travelMode: window.google.maps.TravelMode.DRIVING + }, + (result, status) => { + if (status === 'OK') resolve(result); + else reject(new Error(status)); + } + ); + }); + + (async () => { + setRouteLoading(true); + try { + const points = dropPath; + const all = []; + let i = 0; + while (i < points.length - 1) { + const remaining = points.length - 1 - i; + const take = Math.min(remaining, MAX_WPS + 1); + const origin = points[i]; + const destination = points[i + take]; + const waypoints = points.slice(i + 1, i + take); + const res = await fetchSegment(origin, destination, waypoints); + const seg = res.routes[0].overview_path.map((ll) => ({ + lat: ll.lat(), + lng: ll.lng() + })); + // Avoid duplicating the join point between adjacent chunks. + if (all.length > 0 && seg.length > 0) seg.shift(); + all.push(...seg); + i += take; + } + if (!cancelled) setRoutePath(all); + } catch { + // Fall back to the straight-line skeleton on failure (quota, no route, etc.). + if (!cancelled) setRoutePath([]); + } finally { + if (!cancelled) setRouteLoading(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [isLoaded, dropPath]); + + // Numbered step icon as a data URL — drawn fresh per render so we can pass + // the step number into the SVG without juggling external assets. Color is + // a fixed indigo to match the planned-route polyline below. + const stepIcon = (n, isFocused) => { + const size = isFocused ? 38 : 32; + const color = isFocused ? '#4338ca' : '#6366f1'; + const svg = encodeURIComponent( + `` + + `` + + `${n}` + + `` + ); + return `data:image/svg+xml;charset=UTF-8,${svg}`; }; - // Auto fit bounds - useEffect(() => { - if (!mapRef.current || routePath.length === 0) return; + const headerBar = ( + + + + + Planned route{riderName ? ` — ${riderName}` : ''} + + {dateRange && ( + {dateRange} + )} + + {details && details.length > 0 && ( + + {details.length} {details.length === 1 ? 'stop' : 'stops'} + {routeLoading ? ' · resolving route…' : ''} + + )} + {onClose && ( + + + + )} + + ); - const bounds = new window.google.maps.LatLngBounds(); - routePath.forEach((p) => bounds.extend(p)); - mapRef.current.fitBounds(bounds); - }, [routePath]); + // Loading state — route fetch in flight OR Google Maps script not ready yet. + if (loading || !isLoaded) { + return ( + + {headerBar} + + + + {loading ? 'Loading planned route…' : 'Loading map…'} + + + + ); + } - if (!isLoaded) return
Loading map...
; + // 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 ( - (mapRef.current = map)} center={routePath[0]} zoom={16}> - {/* Route line */} - + + {headerBar} + + (mapRef.current = map)} + center={dropPath[0]} + zoom={14} + options={{ + streetViewControl: false, + mapTypeControl: false, + fullscreenControl: false + }} + > + {routePath.length > 0 ? ( + <> + {/* Translucent backdrop so the route stays legible on busy tiles. */} + + {/* Road-following planned route from the Directions API. */} + + + ) : ( + // Fallback while Directions is in flight (or if it fails) — dashed + // straight-line skeleton between drop pins in step order. + + )} - {/* Start marker */} - - - {/* End marker */} - - + {details.map((d, i) => { + const stepNum = d.step || i + 1; + const isFocused = focusedStep === d.deliveryid; + return ( + setFocusedStep(isFocused ? null : d.deliveryid)} + zIndex={isFocused ? 1000 : stepNum} + > + {isFocused && ( + 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} + + )} + + + )} + + ); + })} + + + ); } diff --git a/src/pages/nearle/reports/ridersSummary.js b/src/pages/nearle/reports/ridersSummary.js index 6da7ab0..da92a42 100644 --- a/src/pages/nearle/reports/ridersSummary.js +++ b/src/pages/nearle/reports/ridersSummary.js @@ -193,6 +193,8 @@ export default function RidersSummary() { const [loading, setLoading] = useState(false); const [mapOpen, setMapOpen] = useState(false); const [logDetails, setLogDetails] = useState(null); + const [selectedRider, setSelectedRider] = useState(null); + const [routeLoading, setRouteLoading] = useState(false); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); @@ -244,15 +246,71 @@ export default function RidersSummary() { } }; - // ==============================|| rider delivery logs (for map) ||============================== // + // ==============================|| rider planned route (for map) ||============================== // + // Pulls every delivery the rider was assigned over the page's date range, then + // emits an ordered waypoint list sorted by `step` (the planning sequence). The + // map dialog renders this as the rider's PLANNED route — the path the + // optimizer told them to follow — not their actual GPS trail. const getuserdeliverylogs = async (userid) => { + setRouteLoading(true); try { - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getuserdeliverylogs/?userid=${userid}&fromdate=2026-01-28&todate=2026-01-28 ` - ); - setLogDetails(response.data.details); + // /deliveries/getdeliveries treats applocationid=0 differently from a + // real location id — when appId===0 ("All") the backend expects the + // logged-in operator's userid via appuserid instead. Mirrors the + // branching in api.js#fetchDeliveries. + const loggedInUserId = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0; + const scopeParam = appId === 0 + ? `appuserid=${loggedInUserId}` + : `applocationid=${appId}`; + const url = + `${process.env.REACT_APP_URL}/deliveries/getdeliveries/` + + `?${scopeParam}` + + `&status=all` + + `&fromdate=${startdate}` + + `&todate=${enddate}` + + `&pageno=1` + + `&pagesize=200` + + `&keyword=` + + `&tenantid=` + + `&locationid=` + + `&userid=${userid}`; + const response = await axios.get(url); + const rowsRaw = response?.data?.details || []; + const toNum = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; + const planned = rowsRaw + .map((o) => { + const dropLat = toNum(o.droplat ?? o.deliverylat); + const dropLng = toNum(o.droplon ?? o.deliverylong); + const pickLat = toNum(o.pickuplat ?? o.pickuplatitude); + const pickLng = toNum(o.pickuplon ?? o.pickuplong ?? o.picklongitude); + if (dropLat == null || dropLng == null) return null; + return { + step: Number(o.step) || 0, + orderid: o.orderid, + deliveryid: o.deliveryid, + customer: o.deliverycustomer || o.customername || `Order ${o.orderid}`, + address: o.deliveryaddress || o.deliverysuburb || '', + dropLat, + dropLng, + pickLat: pickLat ?? null, + pickLng: pickLng ?? null, + // Expected delivery clock — used as a label under the step pin so + // the operator can sanity-check sequencing without clicking each + // marker. + expectedTime: o.expecteddeliverytime || null + }; + }) + .filter(Boolean) + .sort((a, b) => a.step - b.step); + setLogDetails(planned); } catch (err) { OpenToast(err?.message, 'error', 2000); + setLogDetails([]); + } finally { + setRouteLoading(false); } }; @@ -668,10 +726,15 @@ export default function RidersSummary() { - + { + setSelectedRider({ + userid: row?.userid, + name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}` + }); + setLogDetails(null); setMapOpen(true); getuserdeliverylogs(row?.userid); }} @@ -921,9 +984,23 @@ export default function RidersSummary() { open={mapOpen} onClose={() => { setMapOpen(false); + setLogDetails(null); + setSelectedRider(null); }} > - {logDetails && } + + { + setMapOpen(false); + setLogDetails(null); + setSelectedRider(null); + }} + /> + {/* ============================================= || Date Filter Dialog || ============================================= */}