+ {/* Header: Step Number Box + Order ID + Status Stack */}
+
-
-
-
-
-
{riderName}
+
-
- {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);