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(
+ `
`
+ );
+ 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 || ============================================= */}