update on the preview build
This commit is contained in:
@@ -785,11 +785,7 @@ const Dispatch = ({
|
|||||||
selectedRiderId,
|
selectedRiderId,
|
||||||
onRiderSelect,
|
onRiderSelect,
|
||||||
// Highlight a single marker (e.g. on table-row hover). Adds a `.pulse` class to that cmark.
|
// Highlight a single marker (e.g. on table-row hover). Adds a `.pulse` class to that cmark.
|
||||||
pulseOrderId,
|
pulseOrderId
|
||||||
// Optional. When provided, focused-rider order cards render a small "change rider"
|
|
||||||
// icon in their header. Receiving callsite owns the rider-picker dialog. Standalone
|
|
||||||
// /dispatch usage leaves this undefined so the icon never appears there.
|
|
||||||
onChangeRider
|
|
||||||
}) => {
|
}) => {
|
||||||
// Default to "By Zone" when the caller passes pre-zoned data (AI preview); fall back to
|
// Default to "By Zone" when the caller passes pre-zoned data (AI preview); fall back to
|
||||||
// "By Rider" for the standalone live page where zones are synthesized but riders are primary.
|
// "By Rider" for the standalone live page where zones are synthesized but riders are primary.
|
||||||
@@ -4131,19 +4127,6 @@ const Dispatch = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{onChangeRider && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="zone-order-change-rider"
|
|
||||||
title="Change rider"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onChangeRider(o, focusedRider);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MdSwapHoriz />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="zone-order-customer">
|
<div className="zone-order-customer">
|
||||||
|
|||||||
@@ -1,35 +1,28 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Autocomplete,
|
|
||||||
Backdrop,
|
Backdrop,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Chip,
|
Chip,
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Stack,
|
Stack,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
TextField,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||||
import { IoReload } from 'react-icons/io5';
|
import { IoReload } from 'react-icons/io5';
|
||||||
import { MdTwoWheeler, MdSwapHoriz } from 'react-icons/md';
|
import { MdTwoWheeler } from 'react-icons/md';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createAutomationDeliveries,
|
createAutomationDeliveries,
|
||||||
createOptimisationDeliveries,
|
createOptimisationDeliveries,
|
||||||
fetchRidersList,
|
|
||||||
finalCreatedeliveries,
|
finalCreatedeliveries,
|
||||||
notifyRider,
|
notifyRider,
|
||||||
reconcileSteps
|
reconcileSteps
|
||||||
@@ -117,78 +110,6 @@ const flattenRiders = (riders) => {
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move one order from oldRiderId -> newRiderId inside dispatchPreviewData.
|
|
||||||
// Mutates both the zones[].riders[].orders[] tree (so the Dispatch tab
|
|
||||||
// renders the change) AND the flat details[] list (so Assign Orders picks
|
|
||||||
// it up). Returns a NEW preview object (immutable update).
|
|
||||||
const moveOrderInPreviewData = (preview, { orderId, newRiderId, newRiderName }) => {
|
|
||||||
if (!preview) return preview;
|
|
||||||
const next = JSON.parse(JSON.stringify(preview));
|
|
||||||
|
|
||||||
// 1) Update flat details list
|
|
||||||
if (Array.isArray(next.details)) {
|
|
||||||
next.details = next.details.map((o) =>
|
|
||||||
String(o.orderid) === String(orderId)
|
|
||||||
? { ...o, rider_id: newRiderId, userid: newRiderId, rider_name: newRiderName, rider: newRiderName }
|
|
||||||
: o
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Move within zones[].riders[].orders[]
|
|
||||||
if (Array.isArray(next.zones)) {
|
|
||||||
let movedOrder = null;
|
|
||||||
let homeZoneIdx = -1;
|
|
||||||
|
|
||||||
for (let zi = 0; zi < next.zones.length && !movedOrder; zi++) {
|
|
||||||
const zone = next.zones[zi];
|
|
||||||
if (!Array.isArray(zone.riders)) continue;
|
|
||||||
for (let ri = 0; ri < zone.riders.length && !movedOrder; ri++) {
|
|
||||||
const r = zone.riders[ri];
|
|
||||||
if (!Array.isArray(r.orders)) continue;
|
|
||||||
const oi = r.orders.findIndex((o) => String(o.orderid) === String(orderId));
|
|
||||||
if (oi !== -1) {
|
|
||||||
movedOrder = r.orders[oi];
|
|
||||||
r.orders.splice(oi, 1);
|
|
||||||
homeZoneIdx = zi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (movedOrder) {
|
|
||||||
const updated = {
|
|
||||||
...movedOrder,
|
|
||||||
rider_id: newRiderId,
|
|
||||||
userid: newRiderId,
|
|
||||||
rider_name: newRiderName,
|
|
||||||
rider: newRiderName
|
|
||||||
};
|
|
||||||
let placed = false;
|
|
||||||
for (const zone of next.zones) {
|
|
||||||
if (!Array.isArray(zone.riders)) continue;
|
|
||||||
const target = zone.riders.find(
|
|
||||||
(r) => String(r.rider_id ?? r.userid) === String(newRiderId)
|
|
||||||
);
|
|
||||||
if (target) {
|
|
||||||
target.orders = target.orders || [];
|
|
||||||
target.orders.push(updated);
|
|
||||||
placed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!placed && homeZoneIdx >= 0) {
|
|
||||||
next.zones[homeZoneIdx].riders.push({
|
|
||||||
rider_id: newRiderId,
|
|
||||||
userid: newRiderId,
|
|
||||||
rider_name: newRiderName,
|
|
||||||
orders: [updated]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into
|
// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into
|
||||||
// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
|
// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
|
||||||
// zone containment), then rebuilds the flat details list from the new tree.
|
// zone containment), then rebuilds the flat details list from the new tree.
|
||||||
@@ -319,12 +240,6 @@ const Preview = () => {
|
|||||||
// server-side step re-ordering only needs to see what actually changed.
|
// server-side step re-ordering only needs to see what actually changed.
|
||||||
const [dirtyRiderIds, setDirtyRiderIds] = useState(() => new Set());
|
const [dirtyRiderIds, setDirtyRiderIds] = useState(() => new Set());
|
||||||
|
|
||||||
// Change-rider dialog state
|
|
||||||
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
|
||||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
|
||||||
const [selectedOldRiderId, setSelectedOldRiderId] = useState(null);
|
|
||||||
const [selectedNewRider, setSelectedNewRider] = useState(null);
|
|
||||||
|
|
||||||
const aiMode = stateData.aiMode ?? 1;
|
const aiMode = stateData.aiMode ?? 1;
|
||||||
const selectedMode = stateData.selectedMode || null;
|
const selectedMode = stateData.selectedMode || null;
|
||||||
const deliveryData = stateData.deliveryData || [];
|
const deliveryData = stateData.deliveryData || [];
|
||||||
@@ -341,16 +256,8 @@ const Preview = () => {
|
|||||||
return 0;
|
return 0;
|
||||||
}, [stateData.appId]);
|
}, [stateData.appId]);
|
||||||
|
|
||||||
const { data: ridersList } = useQuery({
|
|
||||||
queryKey: ['ridersList', appId],
|
|
||||||
queryFn: fetchRidersList,
|
|
||||||
enabled: !!appId,
|
|
||||||
staleTime: 5 * 60 * 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Derived: rider list for the Reconcile tab. Recomputes whenever the cache
|
// Derived: rider list for the Reconcile tab. Recomputes whenever the cache
|
||||||
// (dispatchPreviewData) changes — so Change Rider / Reconcile both reflect
|
// (dispatchPreviewData) changes — so Reconcile reflects here without a separate state.
|
||||||
// here without a separate state.
|
|
||||||
const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]);
|
const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]);
|
||||||
|
|
||||||
// Derived: flat orders list used for the Assign Orders payload + CSV export.
|
// Derived: flat orders list used for the Assign Orders payload + CSV export.
|
||||||
@@ -508,47 +415,6 @@ const Preview = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openChangeRider = (oldRider, order) => {
|
|
||||||
const oldId =
|
|
||||||
oldRider?.rider_id ?? oldRider?.id ?? order?.rider_id ?? order?.userid ?? null;
|
|
||||||
setSelectedOldRiderId(oldId);
|
|
||||||
setSelectedOrder(order);
|
|
||||||
setSelectedNewRider(null);
|
|
||||||
setChangeDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmChangeRider = () => {
|
|
||||||
if (!selectedNewRider || !selectedOrder) return;
|
|
||||||
// Backend expects an int — coerce at the boundary so a string from the
|
|
||||||
// riders API doesn't propagate into the Assign Orders payload.
|
|
||||||
const newRiderId = Number(selectedNewRider.userid);
|
|
||||||
const newRiderName =
|
|
||||||
selectedNewRider.label ||
|
|
||||||
`${selectedNewRider.firstname || ''} ${selectedNewRider.lastname || ''}`.trim() ||
|
|
||||||
`Rider ${newRiderId}`;
|
|
||||||
|
|
||||||
setDispatchPreviewData((prev) =>
|
|
||||||
moveOrderInPreviewData(prev, {
|
|
||||||
orderId: selectedOrder.orderid,
|
|
||||||
oldRiderId: selectedOldRiderId,
|
|
||||||
newRiderId,
|
|
||||||
newRiderName
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// Both riders' step sequences are now potentially stale: the old rider
|
|
||||||
// lost a stop, the new rider gained one. Mark both as dirty so the next
|
|
||||||
// Reconcile sends exactly these two.
|
|
||||||
setDirtyRiderIds((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (selectedOldRiderId != null) next.add(String(selectedOldRiderId));
|
|
||||||
if (newRiderId != null && Number.isFinite(newRiderId)) next.add(String(newRiderId));
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
setHasReconciled(false);
|
|
||||||
setChangeDialogOpen(false);
|
|
||||||
OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -627,42 +493,13 @@ const Preview = () => {
|
|||||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
{dispatchPreviewData && (
|
{dispatchPreviewData && (
|
||||||
<Dispatch
|
<Dispatch
|
||||||
// The key forces a full re-mount when the cache reference changes
|
|
||||||
// (after Change Rider / Reconcile / Re-Assign) so Dispatch's
|
|
||||||
// internal state (focused rider, view mode, etc.) recomputes
|
|
||||||
// against the new orders. Without this, internal memos can stick
|
|
||||||
// to the previous data shape.
|
|
||||||
key={dispatchPreviewData?.__cacheKey || JSON.stringify(reconcileRiders.length)}
|
key={dispatchPreviewData?.__cacheKey || JSON.stringify(reconcileRiders.length)}
|
||||||
data={dispatchPreviewData}
|
data={dispatchPreviewData}
|
||||||
embedded
|
embedded
|
||||||
onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Dialog open={changeDialogOpen} onClose={() => setChangeDialogOpen(false)} maxWidth="xs" fullWidth>
|
|
||||||
<DialogTitle sx={{ fontWeight: 700 }}>Change Rider</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Typography sx={{ mb: 2, fontSize: 13, color: 'text.secondary' }}>
|
|
||||||
Move order #{selectedOrder?.orderid} (step {selectedOrder?.step ?? '—'}) to:
|
|
||||||
</Typography>
|
|
||||||
<Autocomplete
|
|
||||||
options={ridersList || []}
|
|
||||||
getOptionLabel={(o) =>
|
|
||||||
o?.label || `${o?.firstname || ''} ${o?.lastname || ''}`.trim() || ''
|
|
||||||
}
|
|
||||||
value={selectedNewRider}
|
|
||||||
onChange={(e, val) => setSelectedNewRider(val)}
|
|
||||||
renderInput={(params) => <TextField {...params} label="New rider" placeholder="Pick a rider" />}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
|
||||||
<Button onClick={() => setChangeDialogOpen(false)}>Cancel</Button>
|
|
||||||
<Button variant="contained" disabled={!selectedNewRider} onClick={confirmChangeRider}>
|
|
||||||
Change Rider
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user