updates on the dispatch page preview
This commit is contained in:
@@ -29,13 +29,17 @@ export const extractRiders = (previewData) => {
|
|||||||
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
|
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
|
||||||
}
|
}
|
||||||
const entry = map.get(key);
|
const entry = map.get(key);
|
||||||
(orders || []).forEach((o) => {
|
(orders || []).forEach((o, oIdx) => {
|
||||||
const oid = o?.orderid != null ? String(o.orderid) : null;
|
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 (oid) {
|
||||||
if (seenOrderIds.has(oid)) return;
|
if (seenOrderIds.has(oid)) return;
|
||||||
seenOrderIds.add(oid);
|
seenOrderIds.add(oid);
|
||||||
}
|
}
|
||||||
entry.orders.push(o);
|
entry.orders.push({
|
||||||
|
...o,
|
||||||
|
orderid: resolvedOrderId
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (!entry.rider_name && riderName) entry.rider_name = riderName;
|
if (!entry.rider_name && riderName) entry.rider_name = riderName;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
MdTwoWheeler,
|
|
||||||
MdLocationOn,
|
MdLocationOn,
|
||||||
MdRestaurant,
|
MdRestaurant,
|
||||||
MdMyLocation,
|
|
||||||
MdAccessTime,
|
MdAccessTime,
|
||||||
MdInventory2,
|
MdInventory2,
|
||||||
MdStraighten,
|
MdStraighten,
|
||||||
|
MdMarkunreadMailbox,
|
||||||
|
MdAccountBalanceWallet,
|
||||||
|
MdNotes
|
||||||
} from 'react-icons/md';
|
} from 'react-icons/md';
|
||||||
|
|
||||||
import { parseDoormileTimestamp } from '@/lib/doormileTimestamp';
|
import { getActiveOrder, getStatusStyle, onActivate } from './dispatchShared';
|
||||||
import { getActiveOrder } from './dispatchShared';
|
|
||||||
import { OpenToast } from '@/components/third-party/OpenToast';
|
import { OpenToast } from '@/components/third-party/OpenToast';
|
||||||
|
|
||||||
|
const Ico = ({ children }) => (
|
||||||
|
<span className="ico-inline" style={{ display: 'inline-flex', alignItems: 'center', verticalAlign: '-2px', marginRight: 4 }}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = ({
|
const ActiveSection = ({
|
||||||
visibleRiders = [],
|
visibleRiders = [],
|
||||||
riders = [],
|
riders = [],
|
||||||
@@ -22,7 +36,6 @@ const ActiveSection = ({
|
|||||||
setFocusedStop,
|
setFocusedStop,
|
||||||
calculateEstMeters,
|
calculateEstMeters,
|
||||||
getRiderColor,
|
getRiderColor,
|
||||||
formatMeters,
|
|
||||||
isLoading
|
isLoading
|
||||||
}) => {
|
}) => {
|
||||||
// Sort by live distance ascending (closest drop-off first; null/unknown last)
|
// 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);
|
OpenToast(`🛵 ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000);
|
||||||
}
|
}
|
||||||
if (old.status === 'active' && cur.status === 'delivered') {
|
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,139 +109,138 @@ const ActiveSection = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3.5 w-full pb-4">
|
<div className="zone-order-grid w-full pb-4">
|
||||||
{activeDeliveries.map((o) => {
|
{activeDeliveries.map((o, idx) => {
|
||||||
const rid = o.rider_id || o.userid;
|
const rid = o.rider_id || o.userid;
|
||||||
const rider = riders.find((r) => String(r.id) === String(rid));
|
const rider = riders.find((r) => String(r.id) === String(rid));
|
||||||
const color = getRiderColor(rid) || '#0284c7';
|
const color = getRiderColor(rid) || '#0284c7';
|
||||||
const lat = parseFloat(o.droplat || o.deliverylat);
|
const lat = parseFloat(o.droplat || o.deliverylat);
|
||||||
const lon = parseFloat(o.droplon || o.deliverylong);
|
const lon = parseFloat(o.droplon || o.deliverylong);
|
||||||
const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
|
const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
|
||||||
const estMeters = calculateEstMeters(rid, o);
|
const isStopActive = focusedStop && String(focusedStop.orderid) === String(o.orderid);
|
||||||
const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`;
|
const statusStyle = getStatusStyle(o.orderstatus);
|
||||||
const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || '';
|
const profit = parseFloat(o.profit || 0);
|
||||||
const riderName = o.rider_name || o.ridername || 'Unassigned';
|
const isLoss = profit < 0;
|
||||||
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 deliveredAt = o.deliverytime
|
const actual = formatTimeOnly(o.deliverytime);
|
||||||
? parseDoormileTimestamp(o.deliverytime).isValid()
|
const expected = formatTimeOnly(o.expecteddeliverytime);
|
||||||
? 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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={o.orderid}
|
key={o.orderid}
|
||||||
|
className={`zone-order-card going-on ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''}`}
|
||||||
|
role={canFocus ? 'button' : undefined}
|
||||||
|
tabIndex={canFocus ? 0 : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (rider) handleRiderFocus(rider);
|
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 ${
|
onKeyDown={
|
||||||
isSelected
|
canFocus
|
||||||
? 'border-blue-600 ring-2 ring-blue-500/20 bg-blue-50/20 shadow-sm'
|
? onActivate(() => {
|
||||||
: 'border-slate-200/90 hover:border-slate-300 hover:shadow-xs'
|
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: Step Number Box + Order ID + Status Stack */}
|
||||||
|
<div className="zone-order-card-head">
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 left-0 right-0 h-1"
|
className="zone-order-num"
|
||||||
style={{ backgroundColor: color }}
|
style={{ background: color, borderColor: color }}
|
||||||
/>
|
title={`Planned step ${o.step || idx + 1}`}
|
||||||
|
|
||||||
{/* Header: Rider Avatar + Customer Name + Status Badge */}
|
|
||||||
<div className="flex items-start justify-between gap-3 pt-1">
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
||||||
<div
|
|
||||||
className="w-8 h-8 rounded-lg shrink-0 flex items-center justify-center font-black text-white text-xs shadow-xs"
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
>
|
>
|
||||||
{initials}
|
{o.step || idx + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="zone-order-id-block">
|
||||||
<h4 className="font-bold text-slate-900 text-xs tracking-tight truncate" title={customer}>
|
<div className="zone-order-id">Order #{o.orderid}</div>
|
||||||
{customer}
|
|
||||||
</h4>
|
|
||||||
<div className="text-[10px] font-mono text-slate-400 mt-0.5">
|
|
||||||
Order #{o.orderid} {o.trip_number ? `· Trip ${o.trip_number}` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{(o.orderstatus || actual || expected) && (
|
||||||
</div>
|
<div className="zone-order-status-stack">
|
||||||
|
{o.orderstatus && (
|
||||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider px-2.5 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 shrink-0">
|
<span
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
className="zone-order-status"
|
||||||
{o.orderstatus || 'Active'}
|
style={{ background: statusStyle.bg, color: statusStyle.fg }}
|
||||||
|
>
|
||||||
|
{statusStyle.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
{(actual || expected) && (
|
||||||
{/* Middle: Rider Info & Locations */}
|
<span
|
||||||
<div className="mt-3 pt-2.5 border-t border-slate-100 space-y-1.5 text-xs">
|
className={`zone-order-time ${actual ? '' : 'is-expected'}`}
|
||||||
<div className="flex items-center gap-2 text-slate-700 font-semibold truncate">
|
title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
|
||||||
<div className="w-5 h-5 rounded-md bg-slate-100 text-slate-600 flex items-center justify-center shrink-0">
|
>
|
||||||
<MdTwoWheeler className="w-3.5 h-3.5" />
|
<MdAccessTime />
|
||||||
</div>
|
{actual || expected}
|
||||||
<span className="truncate">{riderName}</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{dropArea && (
|
|
||||||
<div className="flex items-center gap-2 text-slate-500 text-[11px] truncate" title={dropArea}>
|
|
||||||
<div className="w-5 h-5 rounded-md bg-slate-50 text-slate-400 flex items-center justify-center shrink-0">
|
|
||||||
<MdLocationOn className="w-3.5 h-3.5 text-orange-500" />
|
|
||||||
</div>
|
|
||||||
<span className="truncate">{dropArea}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Customer */}
|
||||||
|
<div className="zone-order-customer">
|
||||||
|
<Ico>
|
||||||
|
<MdMarkunreadMailbox />
|
||||||
|
</Ico>
|
||||||
|
{o.deliverycustomer || o.customername || '—'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kitchen */}
|
||||||
{o.pickupcustomer && (
|
{o.pickupcustomer && (
|
||||||
<div className="flex items-center gap-2 text-slate-500 text-[11px] truncate" title={o.pickupcustomer}>
|
<div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
|
||||||
<div className="w-5 h-5 rounded-md bg-slate-50 text-slate-400 flex items-center justify-center shrink-0">
|
<Ico>
|
||||||
<MdRestaurant className="w-3.5 h-3.5 text-amber-500" />
|
<MdRestaurant />
|
||||||
</div>
|
</Ico>
|
||||||
<span className="truncate">{o.pickupcustomer}</span>
|
{o.pickupcustomer}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Metrics Bar */}
|
{/* Drop Location */}
|
||||||
<div className="mt-3 pt-2.5 border-t border-slate-100 flex items-center justify-between gap-2">
|
{(o.deliverysuburb || o.deliveryaddress) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
|
||||||
{totalDistanceKm > 0 && (
|
<Ico>
|
||||||
<span className="inline-flex items-center gap-1 text-[11px] font-mono font-medium text-slate-600 bg-slate-50 px-2 py-0.5 rounded border border-slate-200/60">
|
<MdLocationOn />
|
||||||
<MdStraighten className="w-3 h-3 text-slate-400" />
|
</Ico>
|
||||||
{totalDistanceKm.toFixed(1)} km
|
{o.deliverysuburb || o.deliveryaddress}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
{o.ordernotes && (
|
||||||
|
<div className="zone-order-line zone-order-notes" title={o.ordernotes}>
|
||||||
|
<Ico>
|
||||||
|
<MdNotes />
|
||||||
|
</Ico>
|
||||||
|
{o.ordernotes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stats Chips */}
|
||||||
|
<div className="zone-order-stats">
|
||||||
|
<span className="zone-order-chip" title="Distance">
|
||||||
|
<Ico>
|
||||||
|
<MdStraighten />
|
||||||
|
</Ico>
|
||||||
|
{Number(o.actualkms || o.kms || 0).toFixed(2)} km
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className={`zone-order-chip ${isLoss ? 'is-loss' : 'is-profit'}`} title="Profit">
|
||||||
|
<Ico>
|
||||||
{deliveredAt && (
|
<MdAccountBalanceWallet />
|
||||||
<span className="inline-flex items-center gap-1 text-[11px] font-mono text-slate-500">
|
</Ico>
|
||||||
<MdAccessTime className="w-3 h-3 text-slate-400" />
|
{isLoss ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
|
||||||
{deliveredAt}
|
</span>
|
||||||
|
<span className="zone-order-chip zone-order-trip">
|
||||||
|
T{o.trip_number || 1} · S{o.step || idx + 1}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{estMeters !== null && (
|
|
||||||
<div className="inline-flex items-center gap-1.5 text-[11px] font-mono font-bold text-sky-900 bg-sky-50 px-2.5 py-0.5 rounded-full border border-sky-200/80">
|
|
||||||
<MdMyLocation className="w-3 h-3 text-sky-600 animate-spin-slow" />
|
|
||||||
<span>{formatMeters(estMeters)}</span>
|
|
||||||
{etaMin !== null && (
|
|
||||||
<>
|
|
||||||
<span className="text-sky-300">·</span>
|
|
||||||
<span className="text-sky-700">{etaMin < 1 ? '< 1m' : `${Math.ceil(etaMin)}m`}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1663,7 +1663,19 @@ const Dispatch = ({
|
|||||||
// delivers to, which kitchens it picks up from, and the order-status breakdown.
|
// delivers to, which kitchens it picks up from, and the order-status breakdown.
|
||||||
const zoneCards = useMemo(() => {
|
const zoneCards = useMemo(() => {
|
||||||
const source = data || liveData || { zones: [], zone_summary: [] };
|
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 = {};
|
const summaryByName = {};
|
||||||
(source.zone_summary || []).forEach((s) => {
|
(source.zone_summary || []).forEach((s) => {
|
||||||
summaryByName[s.zone_name] = s;
|
summaryByName[s.zone_name] = s;
|
||||||
@@ -1714,25 +1726,44 @@ const Dispatch = ({
|
|||||||
const { riders, kitchens, allOrders, stats } = useMemo(() => {
|
const { riders, kitchens, allOrders, stats } = useMemo(() => {
|
||||||
const source = data || liveData || { zones: [], zone_summary: [] };
|
const source = data || liveData || { zones: [], zone_summary: [] };
|
||||||
const orders = [];
|
const orders = [];
|
||||||
(source.zones || []).forEach((z) => {
|
(source.zones || []).forEach((z, zIdx) => {
|
||||||
(z.riders || []).forEach((r) => {
|
(z.riders || []).forEach((r, rIdx) => {
|
||||||
r.orders.forEach((o) => {
|
(r.orders || []).forEach((o, oIdx) => {
|
||||||
orders.push({ ...o, zone_name: z.zone_name, rider_name: r.rider_name, rider_id: r.rider_id });
|
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 = {};
|
const riderMap = {};
|
||||||
|
|
||||||
// First, populate riderMap with all riders present in the zones to capture those with 0 orders
|
// First, populate riderMap with all riders present in the zones to capture those with 0 orders
|
||||||
(source.zones || []).forEach((z) => {
|
(source.zones || []).forEach((z) => {
|
||||||
(z.riders || []).forEach((r) => {
|
(z.riders || []).forEach((r) => {
|
||||||
const key = r.rider_id;
|
const key = String(r.rider_id ?? r.userid ?? '');
|
||||||
if (!key || key === 'unassigned') return;
|
if (!key || key === 'unassigned' || key === '0') return;
|
||||||
if (!riderMap[key]) {
|
if (!riderMap[key]) {
|
||||||
riderMap[key] = {
|
riderMap[key] = {
|
||||||
id: key,
|
id: key,
|
||||||
riderName: r.rider_name || r.username || key,
|
riderName: r.rider_name || r.username || `Rider ${key}`,
|
||||||
orders: [],
|
orders: [],
|
||||||
color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length]
|
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
|
// Next, map the flat orders list to the riders
|
||||||
orders.forEach((o) => {
|
orders.forEach((o) => {
|
||||||
const key = o.rider_id || o.userid || 'unknown';
|
const key = String(o.rider_id ?? o.userid ?? '');
|
||||||
if (key === 'unassigned') return;
|
if (!key || key === 'unassigned' || key === '0') return;
|
||||||
if (!riderMap[key]) {
|
if (!riderMap[key]) {
|
||||||
riderMap[key] = {
|
riderMap[key] = {
|
||||||
id: key,
|
id: key,
|
||||||
riderName: o.rider_name || o.username || o.rider || key,
|
riderName: o.rider_name || o.username || o.rider || `Rider ${key}`,
|
||||||
orders: [],
|
orders: [],
|
||||||
color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length]
|
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);
|
riderMap[key].orders.push(o);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,38 @@ const tuningTypes = [
|
|||||||
{ tuneid: 4, type: 'Zone Strict', value: 'zone_strict' }
|
{ 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
|
// Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for
|
||||||
// the Reconcile tab UI and the reconcile-API payload.
|
// the Reconcile tab UI and the reconcile-API payload.
|
||||||
const extractRiders = (previewData) => {
|
const extractRiders = (previewData) => {
|
||||||
@@ -92,13 +124,17 @@ const extractRiders = (previewData) => {
|
|||||||
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
|
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
|
||||||
}
|
}
|
||||||
const entry = map.get(key);
|
const entry = map.get(key);
|
||||||
(orders || []).forEach((o) => {
|
(orders || []).forEach((o, oIdx) => {
|
||||||
const oid = o?.orderid != null ? String(o.orderid) : null;
|
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 (oid) {
|
||||||
if (seenOrderIds.has(oid)) return;
|
if (seenOrderIds.has(oid)) return;
|
||||||
seenOrderIds.add(oid);
|
seenOrderIds.add(oid);
|
||||||
}
|
}
|
||||||
entry.orders.push(o);
|
entry.orders.push({
|
||||||
|
...o,
|
||||||
|
orderid: resolvedOrderId
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (!entry.rider_name && riderName) entry.rider_name = riderName;
|
if (!entry.rider_name && riderName) entry.rider_name = riderName;
|
||||||
};
|
};
|
||||||
@@ -297,7 +333,7 @@ const Preview = () => {
|
|||||||
const stateData = location.state || {};
|
const stateData = location.state || {};
|
||||||
const isMobile = useIsNarrowViewport();
|
const isMobile = useIsNarrowViewport();
|
||||||
|
|
||||||
const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);
|
const [dispatchPreviewData, setDispatchPreviewData] = useState(() => normalizePreviewData(stateData.dispatchPreviewData));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stateData.dispatchPreviewData) {
|
if (!stateData.dispatchPreviewData) {
|
||||||
@@ -405,7 +441,7 @@ const Preview = () => {
|
|||||||
mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
|
mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
OpenToast('Orders Optimised Successfully', 'success', 2000);
|
OpenToast('Orders Optimised Successfully', 'success', 2000);
|
||||||
setDispatchPreviewData(data);
|
setDispatchPreviewData(normalizePreviewData(data));
|
||||||
setHasReconciled(false);
|
setHasReconciled(false);
|
||||||
setDirtyRiderIds(new Set());
|
setDirtyRiderIds(new Set());
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -443,7 +479,7 @@ const Preview = () => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
logger.debug('reconcile: response', data);
|
logger.debug('reconcile: response', data);
|
||||||
if (Array.isArray(data?.riders)) {
|
if (Array.isArray(data?.riders)) {
|
||||||
setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
|
setDispatchPreviewData((prev) => normalizePreviewData(applyReconcileResponse(prev, data)));
|
||||||
setHasReconciled(true);
|
setHasReconciled(true);
|
||||||
setDirtyRiderIds((prev) => {
|
setDirtyRiderIds((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|||||||
Reference in New Issue
Block a user