updates on the ai bot and assigned by chatbot

This commit is contained in:
2026-08-20 12:00:12 +05:30
parent 37ca2352e2
commit e1cc874218
17 changed files with 794 additions and 248 deletions

View File

@@ -14,6 +14,7 @@ import {
getHubs,
getBookings,
getBooking,
getConsignments,
assignMilerToBooking,
cancelBooking,
updateConsignmentStatus,
@@ -94,6 +95,19 @@ export const mapBookingStatusToDeliveryStatus = (status) => {
return BOOKING_STATUS_TO_DELIVERY_STATUS[key] || key;
};
// A consignment's own status, when this booking has one and it looks like a
// status. `GET /admin/consignments` has no documented response schema, so this
// reads defensively: only a non-empty string on one of the plausible field
// names counts, and anything else returns undefined so the caller keeps using
// the booking's status.
const consignmentStatusFor = (booking, consignmentMap) => {
if (!booking?.consignmentid || !consignmentMap?.size) return undefined;
const record = consignmentMap.get(String(booking.consignmentid));
if (!record) return undefined;
const raw = record.status ?? record.consignmentstatus ?? record.currentstatus;
return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
};
// Haversine straight-line distance in km — used wherever a resource carries
// lat/lng but no road-distance field (bookings, and by extension deliveries).
const haversineKm = (lat1, lon1, lat2, lon2) => {
@@ -565,11 +579,7 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
// parameter is documented in express-console-api.md and guessing one risks a
// silent 400 or, worse, a silently-ignored filter), so the range is applied
// client-side below, after the rows are normalised.
// queryKey[11] is an OPT-IN date basis. Four pages share this function
// (deliveries, Dispatch, reports/ordersDetails, reports/profitability) and
// three of them genuinely want "created in this window", so the default is
// unchanged and only the Deliveries page passes 'activity'.
const [, , , , startdate, enddate, rowsPerPage, , , , , dateBasis] = queryKey;
const [, , , , startdate, enddate, rowsPerPage] = queryKey;
// Unlike the 3 joins below (customers/milers/tenants — each individually
// guarded so a failed join just degrades a display field, not the whole
// page), a failed bookings call is the one thing this function can't
@@ -582,11 +592,29 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
OpenToast(err.response?.data?.message || err.message || 'Failed to load deliveries', 'error', 2000);
return { rows: [], nextPage: undefined };
}
const [customers, milers, tenants] = await Promise.all([
// Consignments are joined for ONE reason: once a booking becomes a
// consignment, its lifecycle continues on the CONSIGNMENT record and the
// booking's own `status` stops moving. `PUT /admin/consignments/:id/status`
// — the Update Status dialog — writes there, so the write succeeded, the
// toast said so, and this page went on showing the booking's stale
// `Converted_To_Consignment` because that is the only field it read.
//
// Guarded like the other three joins: if the call fails, or the response
// carries no recognisable status, the row falls back to the booking's status
// and behaviour is exactly what it was before this join existed.
const [customers, milers, tenants, consignments] = await Promise.all([
getAdminCustomers().catch(() => []),
getMilers().catch(() => []),
getAdminTenants().catch(() => [])
getAdminTenants().catch(() => []),
getConsignments().catch(() => [])
]);
// The id field on a consignment record has never been captured, so both
// plausible names are indexed rather than guessing one.
const consignmentMap = new Map();
(consignments || []).forEach((c) => {
const id = c?.consignmentid ?? c?.id;
if (id != null) consignmentMap.set(String(id), c);
});
const customerMap = new Map((customers || []).map((c) => [c.appcustomerid ?? c.customerid ?? c.id, c]));
const milerMap = new Map((milers || []).map((m) => [m.userid ?? m.milerid, m]));
const tenantMap = new Map((tenants || []).map((t) => [t.tenantid, t]));
@@ -680,7 +708,12 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
// remains fine to DISPLAY assigntime as a "last updated" stamp, which is
// all the reports use it for.
assigntime: b.updatedat,
orderstatus: mapBookingStatusToDeliveryStatus(b.status),
// The consignment's status WINS when there is one. That is the record
// the rider app and the Update Status dialog both advance; the booking's
// status is frozen at Converted_To_Consignment from pickup onwards.
// Falls back to the booking whenever the consignment is absent or carries
// nothing status-shaped — never invents a state.
orderstatus: mapBookingStatusToDeliveryStatus(consignmentStatusFor(b, consignmentMap) ?? b.status),
droplat: b.deliverylatitude,
droplon: b.deliverylongitude
};
@@ -699,22 +732,23 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
// A missing/blank bound means "unbounded on that side", which preserves the
// old behaviour for any caller that doesn't pass real dates.
//
// 'activity' additionally keeps a row whose LAST UPDATE falls in the window.
// Scoping the Deliveries page purely by creation date meant an order created
// yesterday and picked or delivered today was invisible today — which is
// exactly why every tab past Accepted read 0 while the day's fresh orders
// filled Pending and Accepted. A delivery board has to show what is moving
// now, not only what was booked now.
const dayOf = (value) => {
const t = parseDoormileTimestamp(value);
return t.isValid() ? t.format('YYYY-MM-DD') : null;
};
// ⛔ An "activity" basis — also admitting a row whose `assigntime`
// (== `updatedat`) falls in the window — was tried and REVERTED. It let an
// order created yesterday evening and merely touched today onto today's
// board, but batch bucketing reads `orderdate`, so that row landed in
// Evening Batch. The live result was "Evening 6" at 10:41 in the morning on a
// day with no orders created at all. Admitting a row on one timestamp while
// bucketing it on another cannot produce an honest batch count; if
// carried-over work needs to be visible it needs its own bucket, not a
// time-of-day wave it does not belong to.
const inRange = (row) => {
if (!startdate && !enddate) return true;
const days = [dayOf(row.orderdate), dateBasis === 'activity' ? dayOf(row.assigntime) : null].filter(Boolean);
if (!days.length) return false;
return days.some((day) => (!startdate || day >= String(startdate)) && (!enddate || day <= String(enddate)));
const t = parseDoormileTimestamp(row.orderdate);
if (!t.isValid()) return false;
const day = t.format('YYYY-MM-DD');
if (startdate && day < String(startdate)) return false;
if (enddate && day > String(enddate)) return false;
return true;
};
return {
@@ -803,7 +837,64 @@ export const changeRiderAPI = async (selectedRider, selectedRow) =>
// truthy, so `deliveryid ?? consignmentid` never actually fell through to
// consignmentid even when it was present, silently calling the endpoint
// with the wrong kind of id on every Update Status submit.
export const updateDeliveryAPI = async (orderData) => updateConsignmentStatus(orderData.consignmentid ?? orderData.deliveryid, orderData);
//
// ---- The body ---------------------------------------------------------------
//
// This used to forward the dialog's WHOLE state object as the request body —
// the old jupiter shape (`orderstatus`, `deliveryid`, `orderheaderid`,
// `deliveryamt`, `cumulativekms`, `userid`). The endpoint wants one field
// called `status`, so every submit came back:
//
// PUT /admin/consignments/40/status → 400 {"status is required"}
//
// The status was in the payload the whole time, under the wrong name.
//
// The VALUE has to be translated too. The dialog's options are this page's own
// lifecycle keys (`delivered`, `cancelled`, …); the API speaks the booking enum
// (`Delivered`, `Cancelled`, …). Sending `delivered` where `Delivered` is
// expected is the same class of bug one layer down.
//
// Reverse of BOOKING_STATUS_TO_DELIVERY_STATUS, and deliberately NOT derived
// from it by inversion: that map is many-to-one (`pending_pickup` and
// `miler_assigned` both mean `pending`), so an automatic inversion would pick
// whichever happened to be last and silently write the wrong one.
const DELIVERY_STATUS_TO_BOOKING_STATUS = {
pending: 'Pending_Pickup',
accepted: 'Pickup_Scheduled',
picked: 'Converted_To_Consignment',
// The dialog offers "started", which this API has no separate state for — a
// consignment that has started IS out for delivery.
started: 'Out_for_Delivery',
active: 'Out_for_Delivery',
delivered: 'Delivered',
cancelled: 'Cancelled',
canceled: 'Cancelled'
};
export const updateDeliveryAPI = async (orderData) => {
const id = orderData.consignmentid ?? orderData.deliveryid;
const chosen = String(orderData.orderstatus || '').toLowerCase();
const status = DELIVERY_STATUS_TO_BOOKING_STATUS[chosen];
// `arrived` and `skipped` have no booking-status equivalent at all (the rider
// actions behind them — /miler/bookings/:id/reached and
// /miler/consignments/:id/skip — write no booking status). Refusing here with
// the reason is honest; guessing a near-enough status would set the wrong one
// on a real delivery.
if (!status) {
return {
success: false,
message: chosen
? `"${orderData.orderstatus}" has no equivalent on the consignment API, so it can't be set from here.`
: 'Choose a status first.'
};
}
// Only `status` is sent. The dialog's kms / amount / notes have no field on
// this endpoint (see the note above), and this request 400s on validation —
// so posting the rest is at best ignored and at worst another rejection.
return updateConsignmentStatus(id, { status });
};
// ==============================|| getalltenants (tenants) ||============================== //