update on the preview build
This commit is contained in:
@@ -1,35 +1,28 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Autocomplete,
|
||||
Backdrop,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Stack,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||
import { IoReload } from 'react-icons/io5';
|
||||
import { MdTwoWheeler, MdSwapHoriz } from 'react-icons/md';
|
||||
import { MdTwoWheeler } from 'react-icons/md';
|
||||
|
||||
import {
|
||||
createAutomationDeliveries,
|
||||
createOptimisationDeliveries,
|
||||
fetchRidersList,
|
||||
finalCreatedeliveries,
|
||||
notifyRider,
|
||||
reconcileSteps
|
||||
@@ -117,78 +110,6 @@ const flattenRiders = (riders) => {
|
||||
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
|
||||
// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
|
||||
// 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.
|
||||
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 selectedMode = stateData.selectedMode || null;
|
||||
const deliveryData = stateData.deliveryData || [];
|
||||
@@ -341,16 +256,8 @@ const Preview = () => {
|
||||
return 0;
|
||||
}, [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
|
||||
// (dispatchPreviewData) changes — so Change Rider / Reconcile both reflect
|
||||
// here without a separate state.
|
||||
// (dispatchPreviewData) changes — so Reconcile reflects here without a separate state.
|
||||
const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]);
|
||||
|
||||
// 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 (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -627,42 +493,13 @@ const Preview = () => {
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{dispatchPreviewData && (
|
||||
<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)}
|
||||
data={dispatchPreviewData}
|
||||
embedded
|
||||
onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user