From 3d6160e3f18a42daa439d175e4fe89b09c9d2858 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Fri, 28 Aug 2026 17:14:11 +0530 Subject: [PATCH] updates on the dispatch page preview --- src/lib/dispatchPreview.js | 10 +- src/pages/doormile/dispatch/ActiveSection.jsx | 250 +++++++++--------- src/pages/doormile/dispatch/Dispatch.jsx | 56 +++- src/pages/doormile/dispatch/Preview.jsx | 48 +++- 4 files changed, 224 insertions(+), 140 deletions(-) diff --git a/src/lib/dispatchPreview.js b/src/lib/dispatchPreview.js index ee19099..3876a67 100644 --- a/src/lib/dispatchPreview.js +++ b/src/lib/dispatchPreview.js @@ -29,13 +29,17 @@ export const extractRiders = (previewData) => { map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] }); } const entry = map.get(key); - (orders || []).forEach((o) => { - const oid = o?.orderid != null ? String(o.orderid) : null; + (orders || []).forEach((o, oIdx) => { + const resolvedOrderId = o?.orderid || o?.bookingno || o?.bookingid || o?.order_id || o?.booking_id || o?.consignment_id || o?.id || `ord-${oIdx + 1}`; + const oid = String(resolvedOrderId); if (oid) { if (seenOrderIds.has(oid)) return; seenOrderIds.add(oid); } - entry.orders.push(o); + entry.orders.push({ + ...o, + orderid: resolvedOrderId + }); }); if (!entry.rider_name && riderName) entry.rider_name = riderName; }; diff --git a/src/pages/doormile/dispatch/ActiveSection.jsx b/src/pages/doormile/dispatch/ActiveSection.jsx index 3e9b8a0..e2fbe9c 100644 --- a/src/pages/doormile/dispatch/ActiveSection.jsx +++ b/src/pages/doormile/dispatch/ActiveSection.jsx @@ -1,19 +1,33 @@ import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; import { - MdTwoWheeler, MdLocationOn, MdRestaurant, - MdMyLocation, MdAccessTime, MdInventory2, MdStraighten, + MdMarkunreadMailbox, + MdAccountBalanceWallet, + MdNotes } from 'react-icons/md'; -import { parseDoormileTimestamp } from '@/lib/doormileTimestamp'; -import { getActiveOrder } from './dispatchShared'; +import { getActiveOrder, getStatusStyle, onActivate } from './dispatchShared'; import { OpenToast } from '@/components/third-party/OpenToast'; +const Ico = ({ children }) => ( + + {children} + +); + +const formatTimeOnly = (t) => { + if (!t) return null; + const d = dayjs(t); + if (!d.isValid()) return String(t); + return d.format('hh:mm A'); +}; + const ActiveSection = ({ visibleRiders = [], riders = [], @@ -22,7 +36,6 @@ const ActiveSection = ({ setFocusedStop, calculateEstMeters, getRiderColor, - formatMeters, isLoading }) => { // Sort by live distance ascending (closest drop-off first; null/unknown last) @@ -64,7 +77,7 @@ const ActiveSection = ({ OpenToast(`🛵 ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000); } if (old.status === 'active' && cur.status === 'delivered') { - OpenToast(`${cur.riderName} has reached ${cur.customer}'s location`, 'info', 3000); + OpenToast(`✅ ${cur.riderName} delivered to ${cur.customer}`, 'success', 3000); } }); } @@ -96,140 +109,139 @@ const ActiveSection = ({ } return ( -
- {activeDeliveries.map((o) => { +
+ {activeDeliveries.map((o, idx) => { const rid = o.rider_id || o.userid; const rider = riders.find((r) => String(r.id) === String(rid)); const color = getRiderColor(rid) || '#0284c7'; const lat = parseFloat(o.droplat || o.deliverylat); const lon = parseFloat(o.droplon || o.deliverylong); const canFocus = Number.isFinite(lat) && Number.isFinite(lon); - const estMeters = calculateEstMeters(rid, o); - const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`; - const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || ''; - const riderName = o.rider_name || o.ridername || 'Unassigned'; - const isSelected = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid); - const initials = - riderName - .split(/\s+/) - .filter(Boolean) - .slice(0, 2) - .map((w) => w[0]) - .join('') - .toUpperCase() || '•'; + const isStopActive = focusedStop && String(focusedStop.orderid) === String(o.orderid); + const statusStyle = getStatusStyle(o.orderstatus); + const profit = parseFloat(o.profit || 0); + const isLoss = profit < 0; - const deliveredAt = o.deliverytime - ? parseDoormileTimestamp(o.deliverytime).isValid() - ? parseDoormileTimestamp(o.deliverytime).format('hh:mm A') - : String(o.deliverytime) - : null; - - const etaMin = estMeters !== null ? (estMeters / 1000 / 20) * 60 : null; - const totalDistanceKm = Number(o.actualkms || o.kms || 0); + const actual = formatTimeOnly(o.deliverytime); + const expected = formatTimeOnly(o.expecteddeliverytime); return (
{ if (rider) handleRiderFocus(rider); - if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon }); + if (canFocus) setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }); }} - className={`group bg-white rounded-xl border p-4 transition-all duration-150 cursor-pointer shadow-2xs select-none relative overflow-hidden ${ - isSelected - ? 'border-blue-600 ring-2 ring-blue-500/20 bg-blue-50/20 shadow-sm' - : 'border-slate-200/90 hover:border-slate-300 hover:shadow-xs' - }`} + onKeyDown={ + canFocus + ? onActivate(() => { + if (rider) handleRiderFocus(rider); + setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }); + }) + : undefined + } + title={ + canFocus + ? isStopActive + ? 'Click to show full trip' + : `Show ${o.deliverycustomer || o.customername || `order #${o.orderid}`} on map` + : undefined + } > - {/* Top Accent Strip */} -
- - {/* Header: Rider Avatar + Customer Name + Status Badge */} -
-
-
- {initials} -
-
-

- {customer} -

-
- Order #{o.orderid} {o.trip_number ? `· Trip ${o.trip_number}` : ''} -
-
+ {/* Header: Step Number Box + Order ID + Status Stack */} +
+
+ {o.step || idx + 1}
- - - - {o.orderstatus || 'Active'} - -
- - {/* Middle: Rider Info & Locations */} -
-
-
- -
- {riderName} +
+
Order #{o.orderid}
- - {dropArea && ( -
-
- -
- {dropArea} -
- )} - - {o.pickupcustomer && ( -
-
- -
- {o.pickupcustomer} -
- )} -
- - {/* Bottom Metrics Bar */} -
-
- {totalDistanceKm > 0 && ( - - - {totalDistanceKm.toFixed(1)} km - - )} - - {deliveredAt && ( - - - {deliveredAt} - - )} -
- - {estMeters !== null && ( -
- - {formatMeters(estMeters)} - {etaMin !== null && ( - <> - · - {etaMin < 1 ? '< 1m' : `${Math.ceil(etaMin)}m`} - + {(o.orderstatus || actual || expected) && ( +
+ {o.orderstatus && ( + + {statusStyle.label} + + )} + {(actual || expected) && ( + + + {actual || expected} + )}
)}
+ + {/* Customer */} +
+ + + + {o.deliverycustomer || o.customername || '—'} +
+ + {/* Kitchen */} + {o.pickupcustomer && ( +
+ + + + {o.pickupcustomer} +
+ )} + + {/* Drop Location */} + {(o.deliverysuburb || o.deliveryaddress) && ( +
+ + + + {o.deliverysuburb || o.deliveryaddress} +
+ )} + + {/* Notes */} + {o.ordernotes && ( +
+ + + + {o.ordernotes} +
+ )} + + {/* Stats Chips */} +
+ + + + + {Number(o.actualkms || o.kms || 0).toFixed(2)} km + + + + + + {isLoss ? '-' : ''}₹{Math.abs(profit).toFixed(0)} + + + T{o.trip_number || 1} · S{o.step || idx + 1} + +
); })} diff --git a/src/pages/doormile/dispatch/Dispatch.jsx b/src/pages/doormile/dispatch/Dispatch.jsx index be66125..ba85308 100644 --- a/src/pages/doormile/dispatch/Dispatch.jsx +++ b/src/pages/doormile/dispatch/Dispatch.jsx @@ -1663,7 +1663,19 @@ const Dispatch = ({ // delivers to, which kitchens it picks up from, and the order-status breakdown. const zoneCards = useMemo(() => { const source = data || liveData || { zones: [], zone_summary: [] }; - const zonesArr = source.zones || []; + const rawZones = source.zones || []; + const zonesArr = rawZones.map((z, zIdx) => ({ + ...z, + riders: (z.riders || []).map((r, rIdx) => ({ + ...r, + orders: (r.orders || []).map((o, oIdx) => ({ + ...o, + orderid: o.orderid || o.bookingno || o.bookingid || o.order_id || o.booking_id || o.consignment_id || o.id || `ord-${zIdx}-${rIdx}-${oIdx + 1}`, + trip_number: o.trip_number || 1, + step: o.step || oIdx + 1 + })) + })) + })); const summaryByName = {}; (source.zone_summary || []).forEach((s) => { summaryByName[s.zone_name] = s; @@ -1714,25 +1726,44 @@ const Dispatch = ({ const { riders, kitchens, allOrders, stats } = useMemo(() => { const source = data || liveData || { zones: [], zone_summary: [] }; const orders = []; - (source.zones || []).forEach((z) => { - (z.riders || []).forEach((r) => { - r.orders.forEach((o) => { - orders.push({ ...o, zone_name: z.zone_name, rider_name: r.rider_name, rider_id: r.rider_id }); + (source.zones || []).forEach((z, zIdx) => { + (z.riders || []).forEach((r, rIdx) => { + (r.orders || []).forEach((o, oIdx) => { + const resolvedOrderId = o.orderid || o.bookingno || o.bookingid || o.order_id || o.booking_id || o.consignment_id || o.id || `ord-${zIdx}-${rIdx}-${oIdx + 1}`; + orders.push({ + ...o, + orderid: resolvedOrderId, + zone_name: z.zone_name, + rider_name: r.rider_name || o.rider_name, + rider_id: r.rider_id ?? o.rider_id ?? o.userid + }); }); }); }); + if (orders.length === 0 && Array.isArray(source.details)) { + source.details.forEach((o, oIdx) => { + const resolvedOrderId = o.orderid || o.bookingno || o.bookingid || o.order_id || o.booking_id || o.consignment_id || o.id || `ord-${oIdx + 1}`; + orders.push({ + ...o, + orderid: resolvedOrderId, + rider_id: o.rider_id ?? o.userid, + rider_name: o.rider_name || o.ridername || o.rider + }); + }); + } + const riderMap = {}; // First, populate riderMap with all riders present in the zones to capture those with 0 orders (source.zones || []).forEach((z) => { (z.riders || []).forEach((r) => { - const key = r.rider_id; - if (!key || key === 'unassigned') return; + const key = String(r.rider_id ?? r.userid ?? ''); + if (!key || key === 'unassigned' || key === '0') return; if (!riderMap[key]) { riderMap[key] = { id: key, - riderName: r.rider_name || r.username || key, + riderName: r.rider_name || r.username || `Rider ${key}`, orders: [], color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length] }; @@ -1742,17 +1773,18 @@ const Dispatch = ({ // Next, map the flat orders list to the riders orders.forEach((o) => { - const key = o.rider_id || o.userid || 'unknown'; - if (key === 'unassigned') return; + const key = String(o.rider_id ?? o.userid ?? ''); + if (!key || key === 'unassigned' || key === '0') return; if (!riderMap[key]) { riderMap[key] = { id: key, - riderName: o.rider_name || o.username || o.rider || key, + riderName: o.rider_name || o.username || o.rider || `Rider ${key}`, orders: [], color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length] }; } - if (!riderMap[key].orders.some((existing) => existing.orderid === o.orderid)) { + const orderIdStr = String(o.orderid); + if (!riderMap[key].orders.some((existing) => String(existing.orderid) === orderIdStr)) { riderMap[key].orders.push(o); } }); diff --git a/src/pages/doormile/dispatch/Preview.jsx b/src/pages/doormile/dispatch/Preview.jsx index 1d3309d..2b8ad83 100644 --- a/src/pages/doormile/dispatch/Preview.jsx +++ b/src/pages/doormile/dispatch/Preview.jsx @@ -79,6 +79,38 @@ const tuningTypes = [ { tuneid: 4, type: 'Zone Strict', value: 'zone_strict' } ]; +// Normalise preview data so every order carries an explicit `orderid` (falling +// back to bookingno/bookingid/id) and step/trip fields. +export const normalizePreviewData = (previewData) => { + if (!previewData) return null; + const next = JSON.parse(JSON.stringify(previewData)); + if (Array.isArray(next.zones)) { + next.zones.forEach((z, zIdx) => { + if (Array.isArray(z.riders)) { + z.riders.forEach((r, rIdx) => { + if (Array.isArray(r.orders)) { + r.orders = r.orders.map((o, oIdx) => ({ + ...o, + orderid: o.orderid || o.bookingno || o.bookingid || o.order_id || o.booking_id || o.consignment_id || o.id || `ord-${zIdx}-${rIdx}-${oIdx + 1}`, + step: o.step || oIdx + 1, + trip_number: o.trip_number || 1 + })); + } + }); + } + }); + } + if (Array.isArray(next.details)) { + next.details = next.details.map((o, oIdx) => ({ + ...o, + orderid: o.orderid || o.bookingno || o.bookingid || o.order_id || o.booking_id || o.consignment_id || o.id || `ord-${oIdx + 1}`, + step: o.step || oIdx + 1, + trip_number: o.trip_number || 1 + })); + } + return next; +}; + // Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for // the Reconcile tab UI and the reconcile-API payload. const extractRiders = (previewData) => { @@ -92,13 +124,17 @@ const extractRiders = (previewData) => { map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] }); } const entry = map.get(key); - (orders || []).forEach((o) => { - const oid = o?.orderid != null ? String(o.orderid) : null; + (orders || []).forEach((o, oIdx) => { + const resolvedOrderId = o?.orderid || o?.bookingno || o?.bookingid || o?.order_id || o?.booking_id || o?.consignment_id || o?.id || `ord-${oIdx + 1}`; + const oid = String(resolvedOrderId); if (oid) { if (seenOrderIds.has(oid)) return; seenOrderIds.add(oid); } - entry.orders.push(o); + entry.orders.push({ + ...o, + orderid: resolvedOrderId + }); }); if (!entry.rider_name && riderName) entry.rider_name = riderName; }; @@ -297,7 +333,7 @@ const Preview = () => { const stateData = location.state || {}; const isMobile = useIsNarrowViewport(); - const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null); + const [dispatchPreviewData, setDispatchPreviewData] = useState(() => normalizePreviewData(stateData.dispatchPreviewData)); useEffect(() => { if (!stateData.dispatchPreviewData) { @@ -405,7 +441,7 @@ const Preview = () => { mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries, onSuccess: (data) => { OpenToast('Orders Optimised Successfully', 'success', 2000); - setDispatchPreviewData(data); + setDispatchPreviewData(normalizePreviewData(data)); setHasReconciled(false); setDirtyRiderIds(new Set()); setIsLoading(false); @@ -443,7 +479,7 @@ const Preview = () => { onSuccess: (data) => { logger.debug('reconcile: response', data); if (Array.isArray(data?.riders)) { - setDispatchPreviewData((prev) => applyReconcileResponse(prev, data)); + setDispatchPreviewData((prev) => normalizePreviewData(applyReconcileResponse(prev, data))); setHasReconciled(true); setDirtyRiderIds((prev) => { const next = new Set(prev);