updates on the dispatch page preview

This commit is contained in:
2026-08-28 17:14:11 +05:30
parent 8d54a317cc
commit 3d6160e3f1
4 changed files with 224 additions and 140 deletions

View File

@@ -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;
};

View File

@@ -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 }) => (
<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 = ({
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 (
<div className="space-y-3.5 w-full pb-4">
{activeDeliveries.map((o) => {
<div className="zone-order-grid w-full pb-4">
{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 (
<div
key={o.orderid}
className={`zone-order-card going-on ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''}`}
role={canFocus ? 'button' : undefined}
tabIndex={canFocus ? 0 : undefined}
onClick={() => {
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 */}
<div
className="absolute top-0 left-0 right-0 h-1"
style={{ backgroundColor: color }}
/>
{/* 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}
</div>
<div className="min-w-0 flex-1">
<h4 className="font-bold text-slate-900 text-xs tracking-tight truncate" title={customer}>
{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>
{/* Header: Step Number Box + Order ID + Status Stack */}
<div className="zone-order-card-head">
<div
className="zone-order-num"
style={{ background: color, borderColor: color }}
title={`Planned step ${o.step || idx + 1}`}
>
{o.step || idx + 1}
</div>
<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 className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
{o.orderstatus || 'Active'}
</span>
</div>
{/* Middle: Rider Info & Locations */}
<div className="mt-3 pt-2.5 border-t border-slate-100 space-y-1.5 text-xs">
<div className="flex items-center gap-2 text-slate-700 font-semibold truncate">
<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" />
</div>
<span className="truncate">{riderName}</span>
<div className="zone-order-id-block">
<div className="zone-order-id">Order #{o.orderid}</div>
</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>
)}
{o.pickupcustomer && (
<div className="flex items-center gap-2 text-slate-500 text-[11px] truncate" title={o.pickupcustomer}>
<div className="w-5 h-5 rounded-md bg-slate-50 text-slate-400 flex items-center justify-center shrink-0">
<MdRestaurant className="w-3.5 h-3.5 text-amber-500" />
</div>
<span className="truncate">{o.pickupcustomer}</span>
</div>
)}
</div>
{/* Bottom Metrics Bar */}
<div className="mt-3 pt-2.5 border-t border-slate-100 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
{totalDistanceKm > 0 && (
<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">
<MdStraighten className="w-3 h-3 text-slate-400" />
{totalDistanceKm.toFixed(1)} km
</span>
)}
{deliveredAt && (
<span className="inline-flex items-center gap-1 text-[11px] font-mono text-slate-500">
<MdAccessTime className="w-3 h-3 text-slate-400" />
{deliveredAt}
</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>
</>
{(o.orderstatus || actual || expected) && (
<div className="zone-order-status-stack">
{o.orderstatus && (
<span
className="zone-order-status"
style={{ background: statusStyle.bg, color: statusStyle.fg }}
>
{statusStyle.label}
</span>
)}
{(actual || expected) && (
<span
className={`zone-order-time ${actual ? '' : 'is-expected'}`}
title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
>
<MdAccessTime />
{actual || expected}
</span>
)}
</div>
)}
</div>
{/* Customer */}
<div className="zone-order-customer">
<Ico>
<MdMarkunreadMailbox />
</Ico>
{o.deliverycustomer || o.customername || '—'}
</div>
{/* Kitchen */}
{o.pickupcustomer && (
<div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
<Ico>
<MdRestaurant />
</Ico>
{o.pickupcustomer}
</div>
)}
{/* Drop Location */}
{(o.deliverysuburb || o.deliveryaddress) && (
<div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
<Ico>
<MdLocationOn />
</Ico>
{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 className={`zone-order-chip ${isLoss ? 'is-loss' : 'is-profit'}`} title="Profit">
<Ico>
<MdAccountBalanceWallet />
</Ico>
{isLoss ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
</span>
<span className="zone-order-chip zone-order-trip">
T{o.trip_number || 1} · S{o.step || idx + 1}
</span>
</div>
</div>
);
})}

View File

@@ -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);
}
});

View File

@@ -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);