updates on the dispatch page

This commit is contained in:
2026-05-28 19:37:04 +05:30
parent 8cc7cc75f9
commit 39b562ddb5
8 changed files with 3937 additions and 1714 deletions

View File

@@ -140,6 +140,36 @@ export const createOptimisationDeliveries = async (deliveryData) => {
const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/createdeliveries`, deliveryData.deliveries); const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/createdeliveries`, deliveryData.deliveries);
return response.data; return response.data;
}; };
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
export const reconcileSteps = async ({ riders }) => {
const response = await axios.post(
`https://routes.workolik.com/api/v1/optimization/reconcile-steps`,
{ riders }
);
return response.data;
};
// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== //
// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }.
// `batch` is one of: 'morning' | 'afternoon' | 'evening'.
export const fetchBatchEfficiency = async ({ batch, tenantId }) => {
const response = await axios.post(
`https://routes.workolik.com/api/v1/batch/efficiency`,
{
batch,
tenant_id: tenantId
},
{
headers: { 'Content-Type': 'application/json' },
// Let success:false envelopes flow through so the UI can surface the
// server's own error message (instead of axios throwing on 4xx/5xx).
validateStatus: () => true
}
);
return response.data;
};
// ==============================|| finalCreatedeliveries (orders) ||============================== // // ==============================|| finalCreatedeliveries (orders) ||============================== //
export const finalCreatedeliveries = async (deliveryData) => { export const finalCreatedeliveries = async (deliveryData) => {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,717 @@
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 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 {
createAutomationDeliveries,
createOptimisationDeliveries,
fetchRidersList,
finalCreatedeliveries,
notifyRider,
reconcileSteps
} from '../../api/api';
import { OpenToast } from 'components/third-party/OpenToast';
import CSVExport from 'components/third-party/ReactTable';
import CircularLoader from 'components/CircularLoader';
import Dispatch from './Dispatch';
import { stepColor } from './dispatchShared';
const tuningTypes = [
{ tuneid: 1, type: 'Balanced', value: 'balanced' },
{ tuneid: 2, type: 'Aggressive Speed', value: 'aggressive_speed' },
{ tuneid: 3, type: 'Fuel Saver', value: 'fuel_saver' },
{ tuneid: 4, type: 'Zone Strict', value: 'zone_strict' }
];
// 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) => {
if (!previewData) return [];
const map = new Map();
const push = (riderId, riderName, orders) => {
if (riderId == null) return;
const key = String(riderId);
if (!map.has(key)) {
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
}
const entry = map.get(key);
entry.orders.push(...(orders || []));
if (!entry.rider_name && riderName) entry.rider_name = riderName;
};
if (Array.isArray(previewData.zones) && previewData.zones.length) {
previewData.zones.forEach((z) => {
(z.riders || []).forEach((r) => {
const id = r.rider_id ?? r.userid;
const name = r.rider_name || r.username || `Rider ${id}`;
push(id, name, r.orders);
});
});
} else if (Array.isArray(previewData.details)) {
previewData.details.forEach((o) => {
const id = o.rider_id ?? o.userid;
const name = o.rider_name || o.ridername || `Rider ${id}`;
push(id, name, [o]);
});
}
return Array.from(map.values());
};
// Reverse of extractRiders — flatten rider-grouped list into a details-style
// array (used as the Assign Orders payload).
const flattenRiders = (riders) => {
const out = [];
riders.forEach((r) => {
(r.orders || []).forEach((o) => {
out.push({
...o,
rider_id: r.rider_id,
userid: r.rider_id,
rider_name: r.rider_name,
rider: r.rider_name
});
});
});
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.
const applyReconcileResponse = (preview, response) => {
if (!preview || !Array.isArray(response?.riders)) return preview;
const next = JSON.parse(JSON.stringify(preview));
const newOrdersByRider = new Map(
response.riders.map((r) => [String(r.rider_id), r.orders || []])
);
if (Array.isArray(next.zones) && next.zones.length) {
next.zones.forEach((zone) => {
if (!Array.isArray(zone.riders)) return;
zone.riders.forEach((r) => {
const key = String(r.rider_id ?? r.userid);
if (newOrdersByRider.has(key)) {
r.orders = newOrdersByRider.get(key);
newOrdersByRider.delete(key);
} else {
// Rider wasn't in the response — keep their existing orders untouched.
}
});
});
// Any response riders we didn't place attach to the first zone.
if (newOrdersByRider.size) {
const target = next.zones[0];
target.riders = target.riders || [];
newOrdersByRider.forEach((orders, riderKey) => {
target.riders.push({
rider_id: Number(riderKey) || riderKey,
rider_name: orders[0]?.rider_name || `Rider ${riderKey}`,
orders
});
});
}
} else {
next.zones = [
{
zone_name: 'Reconciled',
riders: response.riders.map((r) => ({
rider_id: r.rider_id,
rider_name: r.rider_name || `Rider ${r.rider_id}`,
orders: r.orders || []
}))
}
];
}
// Rebuild flat details from the updated zones->riders->orders tree.
const flatDetails = [];
next.zones.forEach((zone) => {
(zone.riders || []).forEach((r) => {
(r.orders || []).forEach((o) => {
flatDetails.push({
...o,
rider_id: r.rider_id,
userid: r.rider_id,
rider_name: r.rider_name,
rider: r.rider_name
});
});
});
});
next.details = flatDetails;
return next;
};
const Preview = () => {
const navigate = useNavigate();
const location = useLocation();
const stateData = location.state || {};
// SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes
// through this state. The Dispatch tab renders from it, the Reconcile tab
// derives its rider list from it, and Assign Orders sends a flattened copy
// of it to the API.
const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);
const [csvExportData, setCsvExportData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [tabValue, setTabValue] = useState(0);
const [reconcileLoading, setReconcileLoading] = useState(false);
const [hasReconciled, setHasReconciled] = useState(false);
// 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 || [];
const autoRiders = stateData.autoRiders || [];
const absentRidersPayload = stateData.absentRidersPayload || [];
const rider = stateData.rider || null;
const appId = useMemo(() => {
if (stateData.appId) return stateData.appId;
if (typeof window !== 'undefined') {
const v = localStorage.getItem('applocationid');
return v ? Number(v) : 0;
}
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.
const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]);
// Derived: flat orders list used for the Assign Orders payload + CSV export.
// Always reflects the latest cache state.
const finaldeliveryList = useMemo(() => {
const flat = flattenRiders(reconcileRiders);
if (flat.length) return computeDeliveryAmounts(flat);
if (Array.isArray(dispatchPreviewData?.details)) {
return computeDeliveryAmounts(dispatchPreviewData.details);
}
return [];
}, [reconcileRiders, dispatchPreviewData]);
useEffect(() => {
const filtered = finaldeliveryList.map((item) => ({
zone_name: item.zone_name,
ordernotes: item.ordernotes,
rider: item.rider,
step: item.step,
ordertype: item.ordertype,
orderamount: item.orderamount,
riderkms: item.riderkms,
cumulativekms: item.cumulativekms,
baseprice: item.baseprice,
minkm: item.minkm,
priceperkm: item.priceperkm,
kms: item.kms,
actualkms: item.actualkms,
rider_charge: item.rider_charge,
deliveryamt: item.deliveryamt,
deliverycharges: item.deliverycharges,
profit: item.profit
}));
setCsvExportData(filtered);
}, [finaldeliveryList]);
const notifyRiderMutation = useMutation({
mutationFn: notifyRider,
onSuccess: () => OpenToast('Notification sent Successfully', 'success', 2000),
onError: (error) => OpenToast(error.message, 'error', 2000)
});
const createDeliveryMutation = useMutation({
mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
onSuccess: (data) => {
OpenToast('Orders Optimised Successfully', 'success', 2000);
// Brand new response = brand new source of truth.
setDispatchPreviewData(data);
setHasReconciled(false);
setIsLoading(false);
},
onError: (error) => {
OpenToast(error.message, 'error', 4000);
setIsLoading(false);
},
onSettled: () => setIsLoading(false)
});
const createFinalDeliveryMutation = useMutation({
mutationFn: finalCreatedeliveries,
onSuccess: () => {
OpenToast('Delivery Created Successfully', 'success', 2000);
setIsLoading(false);
if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
navigate('/nearle/deliveries');
},
onError: (error) => {
OpenToast(error.message, 'error', 4000);
setIsLoading(false);
},
onSettled: () => setIsLoading(false)
});
const reconcileMutation = useMutation({
mutationFn: reconcileSteps,
onMutate: () => setReconcileLoading(true),
onSuccess: (data) => {
if (Array.isArray(data?.riders)) {
setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
setHasReconciled(true);
OpenToast('Steps reconciled — preview updated', 'success', 2000);
} else {
OpenToast('Reconcile returned no rider data', 'warning', 3000);
}
},
onError: (error) => {
OpenToast(error.message || 'Reconcile failed', 'error', 4000);
},
onSettled: () => setReconcileLoading(false)
});
const handleCreateDelivery = (tune) => {
setIsLoading(true);
if (aiMode == 0) {
createDeliveryMutation.mutate({ deliveries: deliveryData });
} else if (selectedMode && selectedMode?.value == 1) {
createDeliveryMutation.mutate({
deliveries: deliveryData,
hypertuning_params: tune || null,
selectedMode,
absent_riders: absentRidersPayload
});
} else {
createDeliveryMutation.mutate({
data: {
orders: deliveryData,
riders: autoRiders,
config: { pay_type: 'hourly', base_pay: 300.0, strategy: 'multi_trip' },
absent_riders: absentRidersPayload
},
selectedMode
});
}
};
const handleFinalCreateDelivery = () => {
if (!finaldeliveryList?.length) {
OpenToast('No deliveries to assign', 'error', 3000);
return;
}
setIsLoading(true);
createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList });
};
const handleReconcile = () => {
if (!reconcileRiders.length) {
OpenToast('No riders to reconcile', 'warning', 3000);
return;
}
// Payload built straight from the cache — whatever Change Rider edited
// is what gets sent.
reconcileMutation.mutate({
riders: reconcileRiders.map((r) => ({
rider_id: r.rider_id,
orders: r.orders
}))
});
};
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;
const newRiderId = selectedNewRider.userid;
const newRiderName =
selectedNewRider.label ||
`${selectedNewRider.firstname || ''} ${selectedNewRider.lastname || ''}`.trim() ||
`Rider ${newRiderId}`;
setDispatchPreviewData((prev) =>
moveOrderInPreviewData(prev, {
orderId: selectedOrder.orderid,
oldRiderId: selectedOldRiderId,
newRiderId,
newRiderName
})
);
setHasReconciled(false);
setChangeDialogOpen(false);
OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500);
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden', position: 'relative' }}>
<Backdrop
sx={{ position: 'absolute', color: '#fff', zIndex: (theme) => theme.zIndex.modal + 1 }}
open={isLoading}
>
<CircularLoader color="inherit" />
</Backdrop>
<Box sx={{ py: 1.25, px: 2, borderBottom: '1px solid #eef2f6' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
<IconButton
onClick={() => navigate('/nearle/orders')}
sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }}
>
<HiOutlineArrowLeft size={20} />
</IconButton>
</Tooltip>
<Typography variant="h3" fontWeight={600}>
Assign Orders
</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<Autocomplete
options={tuningTypes || []}
getOptionLabel={(option) => option.type}
sx={{ minWidth: 250, maxWidth: 600, flex: 1 }}
renderInput={(params) => <TextField {...params} label="Hyper Tuning" />}
onChange={(e, val, reason) => {
if (reason === 'clear') handleCreateDelivery(null);
else handleCreateDelivery(val.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<IoReload />}
onClick={() => {
setIsLoading(true);
handleCreateDelivery('reshuffle');
}}
>
Re-Assign
</Button>
<CSVExport
data={csvExportData}
filename={`Orders_Detail_${dayjs().format('YYYY-MM-DD_HHmmss')}.csv`}
label=" CSV"
style={{ m: 1 }}
/>
</Stack>
</Stack>
</Box>
<Box sx={{ px: 2, borderBottom: '1px solid #eef2f6' }}>
<Tabs value={tabValue} onChange={(e, v) => setTabValue(v)} sx={{ minHeight: 40 }}>
<Tab label="Dispatch" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
<Tab label="Reconcile" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
</Tabs>
</Box>
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{tabValue === 0 && 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)}
/>
)}
{tabValue === 1 && (
<Box sx={{ flex: 1, overflow: 'auto', p: 2, bgcolor: '#f8fafc' }}>
{reconcileRiders.length === 0 ? (
<Typography sx={{ color: '#94a3b8', textAlign: 'center', mt: 4 }}>
No rider data available to reconcile.
</Typography>
) : (
<Stack spacing={1.75}>
<Box
sx={{
bgcolor: hasReconciled ? '#ecfdf5' : '#fffbeb',
border: `1px solid ${hasReconciled ? '#a7f3d0' : '#fde68a'}`,
color: hasReconciled ? '#065f46' : '#92400e',
borderRadius: '10px',
px: 1.5,
py: 1,
fontSize: 13
}}
>
{hasReconciled
? 'Steps have been reconciled. The Dispatch tab and Assign payload are updated.'
: 'Click a numbered step to change its rider. Hit Reconcile to verify the corrected steps with the server.'}
</Box>
{reconcileRiders.map((r) => {
const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
return (
<Card key={r.rider_id} sx={{ p: 2, borderRadius: '12px', boxShadow: '0 1px 3px rgba(15,23,42,0.06)' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.25 }}>
<Stack direction="row" alignItems="center" gap={1.25}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
bgcolor: '#eef2ff',
color: '#4f46e5',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<MdTwoWheeler size={18} />
</Box>
<Box>
<Typography sx={{ fontWeight: 700, fontSize: 14, color: '#1e293b' }}>
{r.rider_name}
</Typography>
<Typography sx={{ fontSize: 11.5, color: '#64748b' }}>
ID: {r.rider_id}
</Typography>
</Box>
</Stack>
<Stack direction="row" gap={1}>
<Chip size="small" label={`${r.orders.length} stops`} sx={{ fontWeight: 600 }} />
<Chip size="small" label={`${totalKms.toFixed(1)} km`} variant="outlined" />
</Stack>
</Stack>
<Stack direction="row" gap={1.25} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
{r.orders.map((o, idx) => {
const stepNum = o.step ?? idx + 1;
const color = stepColor(Number(stepNum) - 1);
return (
<Tooltip
key={`${o.orderid}-${idx}`}
title={
<Box>
<div>Order #{o.orderid}</div>
<div>{o.deliveryaddress || o.deliverysuburb || ''}</div>
<div style={{ marginTop: 4, opacity: 0.8 }}>Click to change rider</div>
</Box>
}
>
<Box
onClick={() => openChangeRider(r, o)}
sx={{
width: 36,
height: 36,
borderRadius: '50%',
bgcolor: color,
color: '#fff',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 800,
fontSize: 14,
cursor: 'pointer',
boxShadow:
'0 0 0 2px rgba(255,255,255,0.6), 0 1px 3px rgba(15,23,42,0.15)',
transition: 'transform 0.15s',
'&:hover': { transform: 'scale(1.08)' }
}}
>
{stepNum}
</Box>
</Tooltip>
);
})}
</Stack>
</Card>
);
})}
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 1.5, pb: 2 }}>
<Button
variant="contained"
color="primary"
size="large"
startIcon={<MdSwapHoriz />}
onClick={handleReconcile}
disabled={reconcileLoading}
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
>
{reconcileLoading ? 'Reconciling...' : 'Reconcile'}
</Button>
</Box>
</Stack>
)}
</Box>
)}
</Box>
<Box sx={{ px: 2, py: 1.25, borderTop: '1px solid #eef2f6' }}>
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
<Button
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
onClick={() => navigate(-1)}
>
Back
</Button>
<Button variant="contained" onClick={handleFinalCreateDelivery}>
Assign Orders
</Button>
</Stack>
</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>
);
};
// Mirrors the orders.js deliveryamt recalc — applied at render-time so the
// Assign payload always reflects the current cache without a useEffect.
function computeDeliveryAmounts(list) {
return list.map((item) => {
const cumulativeKms = Number(item.cumulativekms || 0);
const minKm = Number(item.minkm || 0);
const basePrice = Number(item.baseprice || 0);
const pricePerKm = Number(item.priceperkm || 0);
if (cumulativeKms <= minKm) return { ...item, deliveryamt: basePrice };
return { ...item, deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice };
});
}
export default Preview;

View File

@@ -1096,13 +1096,11 @@
} }
.gradient-btn-create:hover { .gradient-btn-create:hover {
transform: translateY(-1px) !important;
filter: brightness(1.04); filter: brightness(1.04);
box-shadow: 0 8px 18px -4px rgba(24, 144, 255, 0.40), 0 3px 8px rgba(101, 56, 122, 0.18) !important; box-shadow: 0 8px 18px -4px rgba(24, 144, 255, 0.40), 0 3px 8px rgba(101, 56, 122, 0.18) !important;
} }
.gradient-btn-create:active { .gradient-btn-create:active {
transform: translateY(0) !important;
filter: brightness(0.98); filter: brightness(0.98);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -142,6 +142,10 @@ const Orders = () => {
const [finaldeliveryList, setFinalDeliveryList] = useState([]); const [finaldeliveryList, setFinalDeliveryList] = useState([]);
const aiModeRef = useRef(0); const aiModeRef = useRef(0);
// Caches the inputs of the most recent AI Assign call so we can forward them
// to the /nearle/dispatch/preview page via navigate state (the page re-uses
// them for its Re-Assign button).
const aiMutationContextRef = useRef(null);
const rowsPerPage = 100; const rowsPerPage = 100;
const transportOptions = [ const transportOptions = [
@@ -417,19 +421,25 @@ const Orders = () => {
setZoneData(data?.zones); setZoneData(data?.zones);
setMetaData(data?.meta); setMetaData(data?.meta);
setDispatchPreviewData(data); setDispatchPreviewData(data);
setAiDialog(true); // Route the AI Assign result to the dedicated Preview page (Dispatch
// navigate('/nearle/orders/optimisedpreview', { // view + Reconcile tab + Change Rider). The previous in-page dialog
// state: { // (aiDialog) is left intact but no longer opened — the page replaces
// zoneSummary: data?.zone_analysis, // it. All inputs needed for Re-Assign / Assign Orders are forwarded.
// deliverylist: data?.details, // to deliveryDetails const ctx = aiMutationContextRef.current || {};
// zoneData: data?.zones, navigate('/nearle/dispatch/preview', {
// metaData: data?.meta, state: {
// riderToken: rider.userfcmtoken, dispatchPreviewData: data,
// appId, aiMode: ctx.aiMode ?? aiModeRef.current,
// aiMode: aiModeRef.current, selectedMode: ctx.selectedMode || selectedMode,
// reassignOrders deliveryData: ctx.deliveryData || [],
// } autoRiders: ctx.autoRiders || autoRiders || [],
// }); absentRidersPayload: ctx.absentRidersPayload || [],
rider: ctx.rider || rider,
appId: ctx.appId ?? appId,
tenantId: ctx.tenantid ?? tenantid,
startdate
}
});
} }
}, },
onError: (error) => { onError: (error) => {
@@ -562,6 +572,19 @@ const Orders = () => {
`Rider ${r.userid}` `Rider ${r.userid}`
})); }));
// Remember the inputs so the Preview page can re-run Re-Assign with the
// same payload without us having to re-derive it from current Orders state.
aiMutationContextRef.current = {
deliveryData,
absentRidersPayload,
autoRiders,
selectedMode,
aiMode: aiModeRef.current,
rider,
appId,
tenantid
};
if (aiModeRef.current == 0) { if (aiModeRef.current == 0) {
// manual assign // manual assign
createDeliveryMutation.mutate({ createDeliveryMutation.mutate({

View File

@@ -49,6 +49,7 @@ const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders')));
const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider'))); const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider')));
const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider'))); const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider')));
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch'))); const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
// ==============================|| MAIN ROUTING ||============================== // // ==============================|| MAIN ROUTING ||============================== //
@@ -170,6 +171,10 @@ const MainRoutes = {
{ {
path: 'dispatch', path: 'dispatch',
element: <Dispatch /> element: <Dispatch />
},
{
path: 'dispatch/preview',
element: <DispatchPreview />
} }
] ]
}, },