updates on the api and dispatch page and preview page
This commit is contained in:
@@ -63,6 +63,13 @@ const nearle = {
|
|||||||
icon: icons.FileDoneOutlined,
|
icon: icons.FileDoneOutlined,
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: 'dispatch',
|
||||||
|
title: <FormattedMessage id="dispatch" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/nearle/dispatch',
|
||||||
|
icon: icons.DirectionsBikeOutlinedIcon
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'orders',
|
id: 'orders',
|
||||||
title: <FormattedMessage id="orders" />,
|
title: <FormattedMessage id="orders" />,
|
||||||
@@ -151,13 +158,6 @@ const nearle = {
|
|||||||
type: 'item',
|
type: 'item',
|
||||||
url: '/nearle/invoice',
|
url: '/nearle/invoice',
|
||||||
icon: icons.ReceiptOutlinedIcon
|
icon: icons.ReceiptOutlinedIcon
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dispatch',
|
|
||||||
title: <FormattedMessage id="dispatch" />,
|
|
||||||
type: 'item',
|
|
||||||
url: '/nearle/dispatch',
|
|
||||||
icon: icons.DirectionsBikeOutlinedIcon
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -173,8 +173,20 @@ export const fetchBatchEfficiency = async ({ batch, tenantId }) => {
|
|||||||
// ==============================|| finalCreatedeliveries (orders) ||============================== //
|
// ==============================|| finalCreatedeliveries (orders) ||============================== //
|
||||||
|
|
||||||
export const finalCreatedeliveries = async (deliveryData) => {
|
export const finalCreatedeliveries = async (deliveryData) => {
|
||||||
console.log('deliveryData', deliveryData.deliveries);
|
// Go backend types Deliveries.userid (and rider_id) as int. Coerce at the
|
||||||
const response = await axios.post(`https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries`, deliveryData.deliveries);
|
// boundary so any upstream string — including ones that only surface in the
|
||||||
|
// deployed build's data flow — can't cause a 500 unmarshal error.
|
||||||
|
const toInt = (v) => {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : v;
|
||||||
|
};
|
||||||
|
const deliveries = (deliveryData.deliveries || []).map((d) => ({
|
||||||
|
...d,
|
||||||
|
userid: toInt(d.userid),
|
||||||
|
rider_id: toInt(d.rider_id)
|
||||||
|
}));
|
||||||
|
console.log('deliveryData', deliveries);
|
||||||
|
const response = await axios.post(`https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries`, deliveries);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
// ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== //
|
// ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== //
|
||||||
|
|||||||
@@ -2383,8 +2383,8 @@ const Dispatch = ({
|
|||||||
|
|
||||||
let ordersToRender = allOrders;
|
let ordersToRender = allOrders;
|
||||||
if (focusedZone) ordersToRender = focusedZone.orders;
|
if (focusedZone) ordersToRender = focusedZone.orders;
|
||||||
if (focusedRider) ordersToRender = focusedRider.orders;
|
|
||||||
if (focusedKitchen) ordersToRender = focusedKitchen.orders;
|
if (focusedKitchen) ordersToRender = focusedKitchen.orders;
|
||||||
|
if (focusedRider) ordersToRender = focusedRider.orders;
|
||||||
ordersToRender = ordersToRender.filter(hasValidDrop);
|
ordersToRender = ordersToRender.filter(hasValidDrop);
|
||||||
|
|
||||||
// Pre-build the deliveryid → sequenceStep lookup once per render so each
|
// Pre-build the deliveryid → sequenceStep lookup once per render so each
|
||||||
@@ -2418,6 +2418,7 @@ const Dispatch = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isRiderFocused = !!focusedRider;
|
const isRiderFocused = !!focusedRider;
|
||||||
|
const showNumbers = isRiderFocused || !!focusedKitchen;
|
||||||
const statusStyle = getStatusStyle(o.orderstatus);
|
const statusStyle = getStatusStyle(o.orderstatus);
|
||||||
const statusLow = String(o.orderstatus || '').toLowerCase();
|
const statusLow = String(o.orderstatus || '').toLowerCase();
|
||||||
const isDelivered = statusLow === 'delivered';
|
const isDelivered = statusLow === 'delivered';
|
||||||
@@ -2431,7 +2432,7 @@ const Dispatch = ({
|
|||||||
</svg>`
|
</svg>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const icon = isRiderFocused
|
const icon = showNumbers
|
||||||
? (() => {
|
? (() => {
|
||||||
const seq = compareSeq || o.step || (ordersToRender.indexOf(o) + 1);
|
const seq = compareSeq || o.step || (ordersToRender.indexOf(o) + 1);
|
||||||
const sz = 32;
|
const sz = 32;
|
||||||
@@ -2453,7 +2454,7 @@ const Dispatch = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
key={o.orderid}
|
key={`${o.orderid}-${showNumbers ? 'num' : 'flag'}`}
|
||||||
position={[parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]}
|
position={[parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
zIndexOffset={rid ? 100 : 0}
|
zIndexOffset={rid ? 100 : 0}
|
||||||
|
|||||||
@@ -255,6 +255,23 @@ const Preview = () => {
|
|||||||
// derives its rider list from it, and Assign Orders sends a flattened copy
|
// derives its rider list from it, and Assign Orders sends a flattened copy
|
||||||
// of it to the API.
|
// of it to the API.
|
||||||
const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);
|
const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);
|
||||||
|
|
||||||
|
// The AI response arrives via location.state, which the browser stores in
|
||||||
|
// history.state and persists across reloads. That means a reload of
|
||||||
|
// /dispatch/preview would re-hydrate the stale snapshot — including any
|
||||||
|
// pending edits the user thought they discarded. Bounce to /orders when
|
||||||
|
// there's no fresh response, and wipe the history snapshot once consumed
|
||||||
|
// so a later reload / back-forward also bounces instead of re-using it.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stateData.dispatchPreviewData) {
|
||||||
|
navigate('/nearle/orders', { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.history?.state) {
|
||||||
|
window.history.replaceState({ ...window.history.state, usr: null }, '');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
const [csvExportData, setCsvExportData] = useState([]);
|
const [csvExportData, setCsvExportData] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState(0);
|
||||||
@@ -262,6 +279,11 @@ const Preview = () => {
|
|||||||
const [reconcileLoading, setReconcileLoading] = useState(false);
|
const [reconcileLoading, setReconcileLoading] = useState(false);
|
||||||
const [hasReconciled, setHasReconciled] = useState(false);
|
const [hasReconciled, setHasReconciled] = useState(false);
|
||||||
|
|
||||||
|
// Tracks riders whose orders have been edited since the last AI response
|
||||||
|
// or successful reconcile. Only these are sent to the reconcile API — the
|
||||||
|
// server-side step re-ordering only needs to see what actually changed.
|
||||||
|
const [dirtyRiderIds, setDirtyRiderIds] = useState(() => new Set());
|
||||||
|
|
||||||
// Change-rider dialog state
|
// Change-rider dialog state
|
||||||
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
||||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||||
@@ -343,6 +365,7 @@ const Preview = () => {
|
|||||||
// Brand new response = brand new source of truth.
|
// Brand new response = brand new source of truth.
|
||||||
setDispatchPreviewData(data);
|
setDispatchPreviewData(data);
|
||||||
setHasReconciled(false);
|
setHasReconciled(false);
|
||||||
|
setDirtyRiderIds(new Set());
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -372,8 +395,17 @@ const Preview = () => {
|
|||||||
onMutate: () => setReconcileLoading(true),
|
onMutate: () => setReconcileLoading(true),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (Array.isArray(data?.riders)) {
|
if (Array.isArray(data?.riders)) {
|
||||||
|
// Merge: applyReconcileResponse replaces orders for riders present
|
||||||
|
// in the response and leaves the rest of the cache untouched.
|
||||||
setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
|
setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
|
||||||
setHasReconciled(true);
|
setHasReconciled(true);
|
||||||
|
// Clear only the riders we just reconciled from the dirty set, so
|
||||||
|
// any unrelated edits made meanwhile are preserved.
|
||||||
|
setDirtyRiderIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
data.riders.forEach((r) => next.delete(String(r.rider_id)));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
OpenToast('Steps reconciled — preview updated', 'success', 2000);
|
OpenToast('Steps reconciled — preview updated', 'success', 2000);
|
||||||
} else {
|
} else {
|
||||||
OpenToast('Reconcile returned no rider data', 'warning', 3000);
|
OpenToast('Reconcile returned no rider data', 'warning', 3000);
|
||||||
@@ -423,10 +455,18 @@ const Preview = () => {
|
|||||||
OpenToast('No riders to reconcile', 'warning', 3000);
|
OpenToast('No riders to reconcile', 'warning', 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Payload built straight from the cache — whatever Change Rider edited
|
// Only send riders that were edited since the last AI response / reconcile.
|
||||||
// is what gets sent.
|
// Their step ordering is the only thing that can be stale — untouched
|
||||||
|
// riders are skipped to keep the payload small.
|
||||||
|
const dirty = reconcileRiders.filter((r) =>
|
||||||
|
dirtyRiderIds.has(String(r.rider_id))
|
||||||
|
);
|
||||||
|
if (!dirty.length) {
|
||||||
|
OpenToast('No edits to reconcile', 'info', 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
reconcileMutation.mutate({
|
reconcileMutation.mutate({
|
||||||
riders: reconcileRiders.map((r) => ({
|
riders: dirty.map((r) => ({
|
||||||
rider_id: r.rider_id,
|
rider_id: r.rider_id,
|
||||||
orders: r.orders
|
orders: r.orders
|
||||||
}))
|
}))
|
||||||
@@ -460,6 +500,15 @@ const Preview = () => {
|
|||||||
newRiderName
|
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);
|
setHasReconciled(false);
|
||||||
setChangeDialogOpen(false);
|
setChangeDialogOpen(false);
|
||||||
OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500);
|
OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500);
|
||||||
@@ -653,10 +702,14 @@ const Preview = () => {
|
|||||||
size="large"
|
size="large"
|
||||||
startIcon={<MdSwapHoriz />}
|
startIcon={<MdSwapHoriz />}
|
||||||
onClick={handleReconcile}
|
onClick={handleReconcile}
|
||||||
disabled={reconcileLoading}
|
disabled={reconcileLoading || dirtyRiderIds.size === 0}
|
||||||
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
|
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
|
||||||
>
|
>
|
||||||
{reconcileLoading ? 'Reconciling...' : 'Reconcile'}
|
{reconcileLoading
|
||||||
|
? 'Reconciling...'
|
||||||
|
: dirtyRiderIds.size === 0
|
||||||
|
? 'Reconcile'
|
||||||
|
: `Reconcile (${dirtyRiderIds.size})`}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user