updates on the orders page ai assign to the specific rider

This commit is contained in:
2026-09-03 12:37:46 +05:30
parent ac790d28cb
commit d71c899f02
8 changed files with 818 additions and 146 deletions

View File

@@ -426,6 +426,158 @@ export const createAutomationDeliveries = async (variables) => {
return response.data;
};
// ==============================|| Nagercoil auto-assign ||============================== //
// Base for the routes solver. Overridable per-environment so a staging build
// can point at a staging solver; falls back to the live host the other
// optimisation calls in this file already hardcode.
const ROUTES_API_BASE = process.env.REACT_APP_ROUTES_URL || 'https://routes.workolik.com/api/v1';
// Pull the orders list out of whatever envelope the solver returns. The
// nagercoil endpoint may answer with a bare array, or wrap the rows under
// any of these keys — first array-like wins.
const pickOrdersArray = (raw) => {
if (Array.isArray(raw)) return raw;
if (!raw || typeof raw !== 'object') return [];
return [raw.details, raw.deliveries, raw.orders, raw.assignments, raw.data].find((c) => Array.isArray(c)) || [];
};
// Flatten a zones->riders->orders tree into the flat `details` list that
// Preview's Assign payload is built from.
const flattenZones = (zones) => {
const out = [];
(zones || []).forEach((z) => {
(z.riders || []).forEach((r) => {
(r.orders || []).forEach((o) => {
out.push({
...o,
zone_name: z.zone_name,
rider_id: r.rider_id,
userid: r.rider_id,
rider_name: r.rider_name,
rider: r.rider_name
});
});
});
});
return out;
};
// Coerce any assign response into the { zones, zone_summary, details } shape
// that Preview.js and the embedded Dispatch map consume. Three inputs are
// handled: an already-zoned response (passed through, with `details`
// backfilled), a flat list of sequenced orders (grouped into zones by
// delivery suburb, then by rider), and an empty/no-work response (returns
// empty collections so callers can detect "nothing to assign" without
// guessing at the message text).
export const normaliseAssignResponse = (raw) => {
const base = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
if (Array.isArray(raw?.zones) && raw.zones.length) {
const details = Array.isArray(raw.details) && raw.details.length ? raw.details : flattenZones(raw.zones);
return { ...raw, details };
}
const orders = pickOrdersArray(raw);
if (!orders.length) return { ...base, zones: [], zone_summary: [], details: [] };
// Step assignment happens PER RIDER ACROSS THE WHOLE RESPONSE, before any
// zone bucketing. The Nagercoil endpoint routes every order to one rider as
// a single sequenced run, and that run frequently spans several delivery
// suburbs. Numbering within each zone instead would restart the sequence at
// each suburb (1,2 | 1) and misrepresent the solver's actual ordering on the
// map pins and route lines — so the global sequence is fixed here and the
// zone buckets below simply inherit it.
const normOrder = (o) => {
const riderId = o.rider_id ?? o.userid ?? null;
const riderName = o.rider_name || o.ridername || o.username || (riderId != null ? `Rider ${riderId}` : 'Unassigned');
return {
...o,
rider_id: riderId,
userid: riderId,
rider_name: riderName,
rider: riderName,
trip_number: o.trip_number || 1,
// `step` drives route ordering and the numbered map pins. Accept the
// solver's own naming variants; backfilled just below when absent.
step: o.step ?? o.sequence ?? o.stop_number ?? null
};
};
const byRider = new Map();
orders.map(normOrder).forEach((o) => {
const riderKey = String(o.rider_id ?? 'unassigned');
if (!byRider.has(riderKey)) byRider.set(riderKey, []);
byRider.get(riderKey).push(o);
});
// Sort each rider's full run by the solver's sequence (rows without one keep
// their arrival order), then backfill a contiguous 1..N step.
const stepped = new Map();
byRider.forEach((list, riderKey) => {
const sorted = [...list].sort((a, b) => {
const sA = a.step == null ? Number.MAX_SAFE_INTEGER : Number(a.step);
const sB = b.step == null ? Number.MAX_SAFE_INTEGER : Number(b.step);
return sA - sB;
});
stepped.set(riderKey, sorted.map((o, idx) => ({ ...o, step: o.step ?? idx + 1 })));
});
// Now bucket into zones for display. Orders carry their global step through
// unchanged, so a rider split across suburbs still reads as one route.
const zoneBuckets = new Map();
stepped.forEach((list) => {
list.forEach((o) => {
const zoneName = String(o.zone_name || o.deliverysuburb || o.locationsuburb || 'Nagercoil').trim() || 'Nagercoil';
const zoneKey = zoneName.toLowerCase();
const riderKey = String(o.rider_id ?? 'unassigned');
if (!zoneBuckets.has(zoneKey)) zoneBuckets.set(zoneKey, { zone_name: zoneName, riders: new Map() });
const zone = zoneBuckets.get(zoneKey);
if (!zone.riders.has(riderKey)) {
zone.riders.set(riderKey, { rider_id: o.rider_id, rider_name: o.rider_name, orders: [] });
}
zone.riders.get(riderKey).orders.push(o);
});
});
const zones = Array.from(zoneBuckets.values()).map((z) => ({
zone_name: z.zone_name,
riders: Array.from(z.riders.values()).map((rd) => ({
...rd,
orders: [...rd.orders].sort((a, b) => Number(a.step) - Number(b.step))
}))
}));
const zone_summary = zones.map((z) => {
const allOrds = z.riders.flatMap((r) => r.orders);
const assigned = allOrds.filter((o) => o.userid || o.rider_id).length;
return {
zone_name: z.zone_name,
total_orders: allOrds.length,
assigned_orders: assigned,
unassigned_orders_count: allOrds.length - assigned,
active_riders_count: z.riders.filter((r) => r.rider_id != null).length,
total_delivery_kms: allOrds.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
total_profit: allOrds.reduce((s, o) => s + parseFloat(o.profit || 0), 0)
};
});
return { ...base, zones, zone_summary, details: flattenZones(zones) };
};
// Nagercoil auto-assign. Every order routes to the hub's single rider,
// sequenced with ETAs, server-side. Passing `deliveries` scopes the run to
// the rows the operator is looking at; omitting them (empty body) lets the
// server pull the created orders itself.
export const createNagercoilDeliveries = async (variables = {}) => {
const body = Array.isArray(variables.deliveries) && variables.deliveries.length
? { deliveries: variables.deliveries }
: {};
const response = await axios.post(`${ROUTES_API_BASE}/optimization/nagercoil/riderassign`, body);
return normaliseAssignResponse(response.data);
};
// Push a notification to a rider after assignment.
export const notifyRider = async (riderToken) => {
if (!riderToken) {

View File

@@ -0,0 +1,440 @@
/**
* Tests for the Nagercoil auto-assign path.
*
* Covers the two pieces added for the fixed-rider endpoint:
* • normaliseAssignResponse — coerces whatever the solver returns into the
* { zones, zone_summary, details } shape Preview.js and the embedded
* Dispatch map consume.
* • createNagercoilDeliveries — request shape, and that the response is
* normalised on the way out.
*
* Plus the contract orders.js's empty-batch guard depends on, so a change to
* the normaliser that breaks that guard fails here rather than in production.
*/
jest.mock('axios', () => ({
__esModule: true,
default: { post: jest.fn(), get: jest.fn(), put: jest.fn() }
}));
import axios from 'axios';
import { normaliseAssignResponse, createNagercoilDeliveries } from './api';
const RIDER = { id: 852, name: 'Sivakumar Subramani' };
// Convenience: read back "which rider got which steps" from a normalised
// response, which is what almost every assertion below cares about.
const stepsByRider = (res) => {
const out = {};
(res.zones || []).forEach((z) => {
(z.riders || []).forEach((r) => {
const key = `${r.rider_name}#${r.rider_id}`;
out[key] = (out[key] || []).concat(r.orders.map((o) => o.step));
});
});
return out;
};
const allOrders = (res) => (res.zones || []).flatMap((z) => (z.riders || []).flatMap((r) => r.orders));
beforeEach(() => {
jest.clearAllMocks();
});
// ==========================================================================
// normaliseAssignResponse — already-zoned input
// ==========================================================================
describe('normaliseAssignResponse — zoned response', () => {
const zoned = {
zones: [
{
zone_name: 'Nagercoil',
riders: [
{
rider_id: RIDER.id,
rider_name: RIDER.name,
orders: [{ orderid: 1, step: 1 }, { orderid: 2, step: 2 }]
}
]
}
],
zone_summary: []
};
it('passes the zone tree through untouched', () => {
const res = normaliseAssignResponse(zoned);
expect(res.zones).toEqual(zoned.zones);
});
it('backfills details from the tree when the response omits it', () => {
const res = normaliseAssignResponse(zoned);
expect(res.details).toHaveLength(2);
expect(res.details[0]).toMatchObject({
orderid: 1,
zone_name: 'Nagercoil',
rider_id: RIDER.id,
userid: RIDER.id,
rider_name: RIDER.name,
rider: RIDER.name
});
});
it('keeps a details list the server already sent', () => {
const withDetails = { ...zoned, details: [{ orderid: 99 }] };
expect(normaliseAssignResponse(withDetails).details).toEqual([{ orderid: 99 }]);
});
it('falls through to grouping when zones is present but empty', () => {
const res = normaliseAssignResponse({
zones: [],
details: [{ orderid: 7, userid: RIDER.id, username: RIDER.name }]
});
expect(res.zones).toHaveLength(1);
expect(res.details).toHaveLength(1);
});
});
// ==========================================================================
// normaliseAssignResponse — flat list input
// ==========================================================================
describe('normaliseAssignResponse — flat list', () => {
it('groups orders into zones by delivery suburb', () => {
const res = normaliseAssignResponse({
details: [
{ orderid: 10, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Vadasery', step: 1 },
{ orderid: 11, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Vadasery', step: 2 },
{ orderid: 12, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Ozhuginasery', step: 3 }
]
});
expect(res.zones.map((z) => z.zone_name).sort()).toEqual(['Ozhuginasery', 'Vadasery']);
expect(res.details).toHaveLength(3);
});
it('buckets suburbs case/whitespace-insensitively', () => {
const res = normaliseAssignResponse([
{ orderid: 1, userid: RIDER.id, deliverysuburb: 'Vadasery' },
{ orderid: 2, userid: RIDER.id, deliverysuburb: ' vadasery ' }
]);
expect(res.zones).toHaveLength(1);
});
it('sequences steps globally per rider, not per zone', () => {
// The regression this guards: Nagercoil sends one rider a single run that
// spans suburbs. Numbering inside each zone would restart at 1 and
// misrepresent the solver's ordering on the map pins and route lines.
const res = normaliseAssignResponse({
details: [
{ orderid: 10, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Vadasery' },
{ orderid: 11, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Vadasery' },
{ orderid: 12, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Ozhuginasery' }
]
});
const steps = allOrders(res)
.map((o) => o.step)
.sort((a, b) => a - b);
expect(steps).toEqual([1, 2, 3]);
});
it("honours the solver's own step ordering when rows arrive out of order", () => {
const res = normaliseAssignResponse([
{ orderid: 'c', userid: RIDER.id, step: 3 },
{ orderid: 'a', userid: RIDER.id, step: 1 },
{ orderid: 'b', userid: RIDER.id, step: 2 }
]);
expect(allOrders(res).map((o) => o.orderid)).toEqual(['a', 'b', 'c']);
});
it('accepts `sequence` and `stop_number` as step aliases', () => {
const bySequence = normaliseAssignResponse([
{ orderid: 'y', userid: RIDER.id, sequence: 2 },
{ orderid: 'x', userid: RIDER.id, sequence: 1 }
]);
expect(allOrders(bySequence).map((o) => o.orderid)).toEqual(['x', 'y']);
const byStopNumber = normaliseAssignResponse([
{ orderid: 'y', userid: RIDER.id, stop_number: 2 },
{ orderid: 'x', userid: RIDER.id, stop_number: 1 }
]);
expect(allOrders(byStopNumber).map((o) => o.orderid)).toEqual(['x', 'y']);
});
it('sinks orders with no sequence below sequenced ones, keeping arrival order', () => {
const res = normaliseAssignResponse([
{ orderid: 'no-step-1', userid: RIDER.id },
{ orderid: 'seq-2', userid: RIDER.id, step: 2 },
{ orderid: 'no-step-2', userid: RIDER.id },
{ orderid: 'seq-1', userid: RIDER.id, step: 1 }
]);
expect(allOrders(res).map((o) => o.orderid)).toEqual(['seq-1', 'seq-2', 'no-step-1', 'no-step-2']);
});
it('sequences each rider independently', () => {
const res = normaliseAssignResponse([
{ orderid: 1, userid: 852, username: 'Sivakumar Subramani' },
{ orderid: 2, userid: 852, username: 'Sivakumar Subramani' },
{ orderid: 3, userid: 900, username: 'Other Rider' }
]);
expect(stepsByRider(res)).toEqual({
'Sivakumar Subramani#852': [1, 2],
'Other Rider#900': [1]
});
});
it('stamps rider identity onto every order in both spellings', () => {
// The Assign payload reads rider_id/userid; the map reads rider_name/rider.
const res = normaliseAssignResponse([{ orderid: 1, userid: RIDER.id, username: RIDER.name }]);
expect(allOrders(res)[0]).toMatchObject({
rider_id: RIDER.id,
userid: RIDER.id,
rider_name: RIDER.name,
rider: RIDER.name
});
});
it('reads rider name from any of rider_name / ridername / username', () => {
const variants = [
{ orderid: 1, userid: 852, rider_name: 'A' },
{ orderid: 2, userid: 853, ridername: 'B' },
{ orderid: 3, userid: 854, username: 'C' }
];
const names = allOrders(normaliseAssignResponse(variants)).map((o) => o.rider_name);
expect(names.sort()).toEqual(['A', 'B', 'C']);
});
it('falls back to a synthetic rider name when none is given', () => {
const res = normaliseAssignResponse([{ orderid: 1, userid: 852 }]);
expect(allOrders(res)[0].rider_name).toBe('Rider 852');
});
it('labels orders with no rider as Unassigned rather than dropping them', () => {
const res = normaliseAssignResponse([{ orderid: 1, deliverysuburb: 'Vadasery' }]);
expect(allOrders(res)).toHaveLength(1);
expect(allOrders(res)[0].rider_name).toBe('Unassigned');
expect(res.zone_summary[0].unassigned_orders_count).toBe(1);
});
it('defaults trip_number to 1 and preserves it when present', () => {
const res = normaliseAssignResponse([
{ orderid: 1, userid: RIDER.id },
{ orderid: 2, userid: RIDER.id, trip_number: 2 }
]);
const trips = allOrders(res).map((o) => o.trip_number);
expect(trips).toEqual([1, 2]);
});
it('preserves solver fields the map and pricing depend on', () => {
const res = normaliseAssignResponse([
{ orderid: 1, userid: RIDER.id, kms: '3.5', droplat: '8.17', droplon: '77.43', eta: '12:40', cumulativekms: 3.5 }
]);
expect(allOrders(res)[0]).toMatchObject({
kms: '3.5',
droplat: '8.17',
droplon: '77.43',
eta: '12:40',
cumulativekms: 3.5
});
});
it('defaults the zone name to Nagercoil when the order carries no suburb', () => {
const res = normaliseAssignResponse([{ orderid: 1, userid: RIDER.id }]);
expect(res.zones[0].zone_name).toBe('Nagercoil');
});
it('prefers an explicit zone_name over the delivery suburb', () => {
const res = normaliseAssignResponse([
{ orderid: 1, userid: RIDER.id, zone_name: 'Zone A', deliverysuburb: 'Vadasery' }
]);
expect(res.zones[0].zone_name).toBe('Zone A');
});
it('computes zone_summary totals', () => {
const res = normaliseAssignResponse([
{ orderid: 1, userid: RIDER.id, deliverysuburb: 'Vadasery', kms: '2', profit: '10' },
{ orderid: 2, userid: RIDER.id, deliverysuburb: 'Vadasery', actualkms: '3', profit: '5' }
]);
expect(res.zone_summary[0]).toMatchObject({
zone_name: 'Vadasery',
total_orders: 2,
assigned_orders: 2,
unassigned_orders_count: 0,
active_riders_count: 1,
total_delivery_kms: 5,
total_profit: 15
});
});
});
// ==========================================================================
// normaliseAssignResponse — envelope tolerance
// ==========================================================================
describe('normaliseAssignResponse — envelope variants', () => {
const one = [{ orderid: 1, userid: RIDER.id, username: RIDER.name }];
it.each([['details'], ['deliveries'], ['orders'], ['assignments'], ['data']])(
'unwraps orders held under `%s`',
(key) => {
const res = normaliseAssignResponse({ [key]: one });
expect(res.details).toHaveLength(1);
expect(res.zones).toHaveLength(1);
}
);
it('accepts a bare array', () => {
const res = normaliseAssignResponse(one);
expect(res.details).toHaveLength(1);
expect(stepsByRider(res)).toEqual({ 'Sivakumar Subramani#852': [1] });
});
it('carries top-level metadata through onto the result', () => {
const res = normaliseAssignResponse({ request_id: 'abc123', details: one });
expect(res.request_id).toBe('abc123');
});
});
// ==========================================================================
// normaliseAssignResponse — empty and malformed input
// ==========================================================================
describe('normaliseAssignResponse — empty and malformed', () => {
it('returns empty collections for the live empty-batch response', () => {
const res = normaliseAssignResponse({
userid: RIDER.id,
username: RIDER.name,
message: 'No created orders to assign'
});
expect(res.zones).toEqual([]);
expect(res.zone_summary).toEqual([]);
expect(res.details).toEqual([]);
});
it('preserves the server message on an empty batch so the UI can show it', () => {
const res = normaliseAssignResponse({ message: 'No created orders to assign' });
expect(res.message).toBe('No created orders to assign');
});
it.each([[null], [undefined], [''], [0], [false], [[]], [{}], ['nonsense'], [42]])(
'returns an empty, well-formed result for %p instead of throwing',
(input) => {
const res = normaliseAssignResponse(input);
expect(res.zones).toEqual([]);
expect(res.zone_summary).toEqual([]);
expect(res.details).toEqual([]);
}
);
it('does not mutate the response it was handed', () => {
const input = { details: [{ orderid: 1, userid: RIDER.id }] };
const snapshot = JSON.parse(JSON.stringify(input));
normaliseAssignResponse(input);
expect(input).toEqual(snapshot);
});
});
// ==========================================================================
// createNagercoilDeliveries — request/response wiring
// ==========================================================================
describe('createNagercoilDeliveries', () => {
const URL = 'https://routes.workolik.com/api/v1/optimization/nagercoil/riderassign';
it('posts to the nagercoil riderassign endpoint', async () => {
axios.post.mockResolvedValue({ data: { details: [] } });
await createNagercoilDeliveries({ deliveries: [{ orderid: 1 }] });
expect(axios.post).toHaveBeenCalledTimes(1);
expect(axios.post.mock.calls[0][0]).toBe(URL);
});
it('sends the deliveries list when the operator has rows loaded', async () => {
axios.post.mockResolvedValue({ data: { details: [] } });
const deliveries = [{ orderid: 1 }, { orderid: 2 }];
await createNagercoilDeliveries({ deliveries });
expect(axios.post.mock.calls[0][1]).toEqual({ deliveries });
});
it.each([
['no argument at all', undefined],
['an empty object', {}],
['an empty deliveries array', { deliveries: [] }],
['a non-array deliveries value', { deliveries: 'nope' }]
])('sends an empty body for %s, so the server pulls created orders itself', async (_label, input) => {
axios.post.mockResolvedValue({ data: { details: [] } });
await createNagercoilDeliveries(input);
expect(axios.post.mock.calls[0][1]).toEqual({});
});
it('normalises a flat solver response before returning it', async () => {
axios.post.mockResolvedValue({
data: {
details: [
{ orderid: 10, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Vadasery' },
{ orderid: 11, userid: RIDER.id, username: RIDER.name, deliverysuburb: 'Ozhuginasery' }
]
}
});
const res = await createNagercoilDeliveries({ deliveries: [{ orderid: 10 }] });
expect(res.zones).toHaveLength(2);
expect(res.details).toHaveLength(2);
expect(stepsByRider(res)).toEqual({ 'Sivakumar Subramani#852': [1, 2] });
});
it('returns an empty-but-valid result for the empty-batch response', async () => {
axios.post.mockResolvedValue({
data: { userid: RIDER.id, username: RIDER.name, message: 'No created orders to assign' }
});
const res = await createNagercoilDeliveries({ deliveries: [] });
expect(res.details).toEqual([]);
expect(res.message).toBe('No created orders to assign');
});
it('propagates a transport error to the mutation onError handler', async () => {
axios.post.mockRejectedValue(new Error('Network Error'));
await expect(createNagercoilDeliveries({ deliveries: [] })).rejects.toThrow('Network Error');
});
it('honours REACT_APP_ROUTES_URL when set', async () => {
const prev = process.env.REACT_APP_ROUTES_URL;
process.env.REACT_APP_ROUTES_URL = 'https://staging.example.com/api/v1';
try {
let api;
jest.isolateModules(() => {
// eslint-disable-next-line global-require
api = require('./api');
});
axios.post.mockResolvedValue({ data: { details: [] } });
await api.createNagercoilDeliveries({ deliveries: [] });
expect(axios.post.mock.calls[0][0]).toBe(
'https://staging.example.com/api/v1/optimization/nagercoil/riderassign'
);
} finally {
if (prev === undefined) delete process.env.REACT_APP_ROUTES_URL;
else process.env.REACT_APP_ROUTES_URL = prev;
}
});
});
// ==========================================================================
// The contract orders.js's empty-batch guard relies on
// ==========================================================================
describe('orders.js empty-batch guard contract', () => {
// orders.js branches on `!data?.details?.length` to decide between showing
// an info snackbar and navigating to /nearle/dispatch/preview. These pin
// that contract so a normaliser change can't silently push an empty preview.
const guardFires = (data) => !data?.details?.length;
it('fires for the empty-batch response', () => {
expect(guardFires(normaliseAssignResponse({ message: 'No created orders to assign' }))).toBe(true);
});
it('fires for a malformed response', () => {
expect(guardFires(normaliseAssignResponse(null))).toBe(true);
});
it('does not fire for a populated flat response', () => {
expect(guardFires(normaliseAssignResponse([{ orderid: 1, userid: RIDER.id }]))).toBe(false);
});
it('does not fire for a populated zoned response', () => {
const zoned = {
zones: [{ zone_name: 'Nagercoil', riders: [{ rider_id: RIDER.id, rider_name: RIDER.name, orders: [{ orderid: 1 }] }] }]
};
expect(guardFires(normaliseAssignResponse(zoned))).toBe(false);
});
});

View File

@@ -62,7 +62,7 @@ import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { useNavigate, useLocation } from 'react-router-dom';
import {
fetchPercentageData,
createAutomationDeliveries,
createNagercoilDeliveries,
cancelMultipleOrder,
getallriders,
fetchorderscount
@@ -353,12 +353,26 @@ const Orders = () => {
// ==============================|| Mutations ||============================== //
const createDeliveryMutation = useMutation({
mutationFn: createAutomationDeliveries,
mutationFn: createNagercoilDeliveries,
onSuccess: (data, variables) => {
enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } });
orderscountRefetch();
refetchOrders();
setCreateloader(false);
// The endpoint answers 200 with an empty assignment when there is
// nothing to route (no created orders in the window). That is a normal
// outcome, not an error — surface it here rather than pushing an empty
// preview the operator would have to back out of.
if (!data?.details?.length) {
enqueueSnackbar(data?.message || 'No created orders to assign', {
variant: 'info',
autoHideDuration: 3000,
anchorOrigin: { vertical: 'top', horizontal: 'right' }
});
return;
}
enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } });
navigate('/nearle/dispatch/preview', {
state: {
dispatchPreviewData: data,
@@ -420,12 +434,10 @@ const Orders = () => {
deliverylocation: val.deliverysuburb
}));
createDeliveryMutation.mutate({
deliveries: deliveryData,
selectedMode: { value: 1 },
hypertuning_params: 'balanced',
absent_riders: []
});
// Scope the run to the rows the operator is looking at. The endpoint also
// accepts an empty body and pulls the created orders itself — passing the
// list explicitly keeps the assignment matched to the button's count.
createDeliveryMutation.mutate({ deliveries: deliveryData });
};
// KPI tile definitions.