updates on the deliveries page and status updates as well

This commit is contained in:
2026-08-27 10:34:48 +05:30
parent cf4f4ecd9c
commit 4bcf604539
7 changed files with 256 additions and 158 deletions

View File

@@ -151,21 +151,34 @@ plan against their GPS trail, and what the day earned. Per-stop actual distance
the trail by each stop's share of the plan — the feed carries no per-stop odometer, so it is an
estimate and the panel says so.
## Status Lifecycle & Backend Status Pairs
The platform manages two distinct layers of state: the initial **PickupBooking** (before pickup) and the subsequent **Consignment** (after pickup). `Converted_To_Consignment` acts solely as an internal backend handoff marker and is **never shown literally** in the UI.
### Status Derivation Matrix
| Phase | Backend State / Combination | Frontend Display Status | Deliveries Page Tab | Badge Tone |
| :--- | :--- | :--- | :--- | :--- |
| **Booking** | `status = Pending_Pickup` | **Pending** | `Pending` | `neutral` (grey) |
| **Booking** | `status = Miler_Assigned` | **Pending** (Deliveries) / **Assigned** (Orders) | `Pending` | `info` (blue) |
| **Booking** | `status = Pickup_Scheduled` + `reachedat == null` | **Accepted** | `Accepted` | `info` (blue) |
| **Booking** | `status = Pickup_Scheduled` + `reachedat != null` | **Arrived** | `Arrived` | `info` (cyan) |
| **Handoff** | `status = Converted_To_Consignment` | *(Switches to `consignmentstatus`)* | — | — |
| **Consignment** | `consignmentstatus = Collected_By_Miler` | **Picked** | `Picked` | `accent` (purple) |
| **Consignment** | `consignmentstatus = Out_for_Delivery` | **Active / Out for Delivery** | `Active` | `warning` (teal/yellow) |
| **Consignment** | `consignmentstatus = Inwarded_At_Hub` / `At_Hub` | **Inwarded At Hub** | `Active` | `info` (blue) |
| **Consignment** | `consignmentstatus = Outwarded_From_Hub` | **Outwarded From Hub** | `Active` | `warning` (teal) |
| **Consignment** | `consignmentstatus = In_Transit_To_Hub` | **In Transit To Hub** | `Active` | `warning` (teal) |
| **Consignment** | `consignmentstatus = Delivered` | **Delivered** | `Delivered` | `success` (green) |
| **Consignment** | `consignmentstatus = Cancelled` / `Returned` / `Failed` | **Cancelled** | `Cancelled` | `destructive` (red) |
| **Consignment** | `consignmentstatus = Skipped` / `RTO` | **Skipped** | `Skipped` | `destructive` (red) |
## Things that look wrong but are not
- **A delivery can read "Active" while the booking says `Converted_To_Consignment`.** A booking's
status freezes the moment it is picked up; the lifecycle continues on the consignment. The
Deliveries table lets the consignment's status win and labels the row so you can tell where the
value came from.
- **The Picked tab is empty on hyperlocal traffic.** A matching 3-digit pickup/delivery pincode
prefix goes straight to `Out_for_Delivery` at pickup, so those parcels never rest in "picked".
- **Update Status refuses half its own options.** The write is consignment-scoped, and Pending,
Accepted, Arrived, Picked and Skipped describe states a consignment cannot be in. Those are
refused with the reason rather than guessed at.
- **Assigned means different things on Orders and Deliveries.** Orders tracks the *operator's*
workflow, so assigning a rider counts as Assigned. Deliveries tracks the *rider's* engagement, so
it waits for the rider to accept. This is deliberate, and the two taxonomies live in
`orderStatusGroups.js` and `queries.js` respectively.
- **`Converted_To_Consignment` is a handoff marker, not a display state.** A booking's status freezes the moment it is picked up; the lifecycle continues on the consignment. The frontend automatically reads `consignmentstatus` from that point on.
- **The Picked tab is empty on hyperlocal traffic when the collection flag is off.** A matching 3-digit pickup/delivery pincode prefix goes straight to `Out_for_Delivery` at pickup, so those parcels transition directly from Arrived to Active.
- **Update Status refuses half its own options.** The write is consignment-scoped, and Pending, Accepted, Arrived, Picked and Skipped describe states a consignment cannot be in. Those are refused with the reason rather than guessed at.
- **Assigned means different things on Orders and Deliveries.** Orders tracks the *operator's* workflow, so assigning a rider counts as Assigned. Deliveries tracks the *rider's* engagement, so it waits for the rider to accept. This is deliberate, and the two taxonomies live in `orderStatusGroups.js` and `queries.js` respectively.
## Batches, dates and the one rule about time

View File

@@ -91,12 +91,20 @@ const BOOKING_STATUS_TO_DELIVERY_STATUS = {
out_for_delivery: 'active',
in_transit: 'active',
intransit: 'active',
inwarded_at_hub: 'active',
inwarded: 'active',
at_hub: 'active',
outwarded_from_hub: 'active',
in_transit_to_hub: 'active',
active: 'active',
delivered: 'delivered',
completed: 'delivered',
cancelled: 'cancelled',
canceled: 'cancelled',
skipped: 'skipped'
skipped: 'skipped',
rto: 'skipped',
returned: 'cancelled',
failed: 'cancelled'
};
// Exported (not just module-local) so anything else that needs to classify a
@@ -104,34 +112,69 @@ const BOOKING_STATUS_TO_DELIVERY_STATUS = {
// reuses this instead of growing a second copy of BOOKING_STATUS_TO_DELIVERY_STATUS
// that can drift from it.
// Supports both (status, reachedAt) and a booking/consignment row object.
export const mapBookingStatusToDeliveryStatus = (statusOrRow, maybeReachedAt) => {
if (statusOrRow && typeof statusOrRow === 'object') {
const b = statusOrRow;
const rawConsignmentStatus = b.consignmentstatus ?? b.consignment_status;
if (rawConsignmentStatus) {
return mapBookingStatusToDeliveryStatus(rawConsignmentStatus);
export const deriveDeliveryStatus = (bookingOrStatus, consignmentMapOrReachedAt) => {
if (!bookingOrStatus) return 'pending';
// If passed a raw status string
if (typeof bookingOrStatus === 'string') {
const raw = bookingOrStatus.trim().toLowerCase();
if (raw === 'pickup_scheduled') {
const isReached =
consignmentMapOrReachedAt != null &&
consignmentMapOrReachedAt !== false &&
consignmentMapOrReachedAt !== '';
return isReached ? 'arrived' : 'accepted';
}
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
const rawStatus = b.status ?? b.orderstatus ?? b.bookingstatus;
return mapBookingStatusToDeliveryStatus(rawStatus, reached);
return BOOKING_STATUS_TO_DELIVERY_STATUS[raw] || raw;
}
const raw = String(statusOrRow || '').trim().toLowerCase();
if (raw === 'pickup_scheduled') {
const isReached = maybeReachedAt != null && maybeReachedAt !== false && maybeReachedAt !== '';
// If passed a booking row object
const b = bookingOrStatus;
const rawBookingStatus = String(b.status ?? b.bookingstatus ?? b.orderstatus ?? '').trim().toLowerCase();
const cStatus = consignmentStatusFor(b, typeof consignmentMapOrReachedAt === 'object' && consignmentMapOrReachedAt instanceof Map ? consignmentMapOrReachedAt : undefined);
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
// If booking is Converted_To_Consignment, switch directly to consignmentstatus:
// Converted_To_Consignment is a handoff marker, not a display state.
if (rawBookingStatus === 'converted_to_consignment') {
if (cStatus) {
return mapBookingStatusToDeliveryStatus(cStatus);
}
return 'picked';
}
// If consignment status is present, it wins:
if (cStatus) {
return mapBookingStatusToDeliveryStatus(cStatus);
}
// Before pickup (booking lifecycle):
if (rawBookingStatus === 'pickup_scheduled') {
const isReached = reached != null && reached !== false && reached !== '';
return isReached ? 'arrived' : 'accepted';
}
return BOOKING_STATUS_TO_DELIVERY_STATUS[raw] || raw;
return mapBookingStatusToDeliveryStatus(rawBookingStatus, reached);
};
export const mapBookingStatusToDeliveryStatus = (statusOrRow, maybeReachedAt) => {
return deriveDeliveryStatus(statusOrRow, maybeReachedAt);
};
// 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.
// status. GetAdminBookings attaches consignmentstatus directly to each row,
// and GET /admin/consignments can also be joined.
const consignmentStatusFor = (booking, consignmentMap) => {
if (!booking?.consignmentid || !consignmentMap?.size) return undefined;
const record = consignmentMap.get(String(booking.consignmentid));
// 1. Direct field attached to booking row by GetAdminBookings
const attached = booking?.consignmentstatus ?? booking?.consignment_status;
if (typeof attached === 'string' && attached.trim()) {
return attached.trim();
}
// 2. Lookup in joined consignmentMap if available
const consignmentId = booking?.consignmentid ?? booking?.consignment_id;
if (!consignmentId || !consignmentMap?.size) return undefined;
const record = consignmentMap.get(String(consignmentId));
if (!record) return undefined;
const raw = record.status ?? record.consignmentstatus ?? record.currentstatus ?? record.consignment_status;
return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
@@ -648,97 +691,113 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
const milerMap = new Map((milers || []).map((m) => [m.userid ?? m.milerid, m]));
const tenantMap = new Map((tenants || []).map((t) => [t.tenantid, t]));
const dispatched = (bookings || []).filter((b) => b.assignedmileruserid || b.consignmentid);
const isDispatched = (b) => {
if (!b) return false;
const riderId = b.assignedmileruserid ?? b.assigned_miler_user_id ?? b.mileruserid ?? b.milerid ?? b.assignedmilerid;
const consignmentId = b.consignmentid ?? b.consignment_id;
const rawStatus = String(b.status || b.orderstatus || '').trim().toLowerCase();
if (riderId || consignmentId) return true;
if (rawStatus && !['pending_pickup', 'created', 'new', 'booked', 'order_placed', 'unassigned', ''].includes(rawStatus)) {
return true;
}
return false;
};
const dispatched = (bookings || []).filter(isDispatched);
const rows = dispatched.map((b) => {
const customer = customerMap.get(b.appcustomerid);
const miler = milerMap.get(b.assignedmileruserid);
const riderId = b.assignedmileruserid ?? b.assigned_miler_user_id ?? b.mileruserid ?? b.milerid ?? b.assignedmilerid;
const consignmentId = b.consignmentid ?? b.consignment_id;
const customerId = b.appcustomerid ?? b.customerid ?? b.id;
const customer = customerMap.get(customerId);
const miler = milerMap.get(riderId) || (riderId != null ? milerMap.get(Number(riderId)) : null);
const tenant = tenantMap.get(b.tenantid);
const charge = b.serviceoptions?.[0]?.estimatedprice;
const cStatus = consignmentStatusFor(b, consignmentMap);
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
const effectiveDeliveryStatus = cStatus
? mapBookingStatusToDeliveryStatus(cStatus)
: mapBookingStatusToDeliveryStatus(b.status, reached);
const cStatus = consignmentStatusFor(b, consignmentMap);
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
const effectiveDeliveryStatus = deriveDeliveryStatus(b, consignmentMap);
return {
orderheaderid: b.bookingid,
deliveryid: b.bookingid,
orderid: b.bookingno || `#${b.bookingid}`,
consignmentid: b.consignmentid,
tenantid: b.tenantid,
tenantname: tenant?.tenantname || '',
tenantsuburb: '',
applocation: '',
tenantadress: tenant?.primaryemail || '',
locationname: tenant?.tenantname || '',
locationsuburb: '',
// Was hardcoded '' — Dispatch.js's kitchen markers read this as the
// pickup business name (`o.pickupcustomer || o.kitchen_key || 'Unknown'`,
// Dispatch.js:1660), and no booking on this API carries a `kitchen_key`
// field at all, so every kitchen pin fell through to the literal string
// 'Unknown' — rendered as a "U" marker whose hover/popup then showed
// "Unknown". The tenant IS the kitchen for a B2B booking (same value
// already used for tenantname/locationname above), so reuse it here.
pickupcustomer: tenant?.tenantname || '',
pickupcontactno: '',
Pickupaddress: b.pickupaddress || '',
pickupaddress: b.pickupaddress || '',
pickuplocation: b.pickupaddress || '',
pickupsuburb: '',
deliverycustomer: customer?.firstname || customer?.name || (b.appcustomerid ? `Customer #${b.appcustomerid}` : ''),
deliverycontactno: customer?.phone || customer?.contactno || '',
deliveryaddress: b.deliveryaddress || '',
deliverylocation: b.deliveryaddress || '',
deliverysuburb: '',
ridername: miler?.displayname || (b.assignedmileruserid ? `Rider #${b.assignedmileruserid}` : ''),
userid: b.assignedmileruserid,
// GET /admin/milers/:id/notify (and block/assign-vehicle) key off
// milerprofileid, not the userid stored on the booking — confirmed
// live (a booking's assignedmileruserid matches a miler's `userid`
// field, which 404s against /admin/milers/:id; milerprofileid is the
// real primary key of that resource).
milerprofileid: miler?.milerprofileid,
ridercontact: miler?.phone || '',
expecteddeliverytime: b.serviceoptions?.[0]?.estimateddeliveryat,
// No route-plan data source (step order/transit time/cumulative km were
// computed by the old jupiter backend from the dispatch optimiser's
// output, not stored on a booking/consignment) — left undefined so the
// UI's own "—" fallbacks render instead of a fabricated number.
transitminutes: undefined,
cumulativekms: undefined,
step: undefined,
// No road-distance field on a booking — approximated as a straight
// line between pickup and delivery coordinates.
kms: haversineKm(b.pickuplatitude, b.pickuplongitude, b.deliverylatitude, b.deliverylongitude),
pickuplatitude: b.pickuplatitude,
pickuplongitude: b.pickuplongitude,
deliverylatitude: b.deliverylatitude,
deliverylongitude: b.deliverylongitude,
deliverycharges: charge,
deliveryamt: charge,
deliveryamount: charge,
Quantity: b.parcels?.length || 0,
quantity: b.parcels?.length || 0,
collectionamt: undefined,
notes: b.notes || '',
deliverytype: customer ? 'B' : 'C',
orderdate: b.createdat,
deliverydate: b.serviceoptions?.[0]?.estimateddeliveryat || b.updatedat,
reachedat: reached,
assigntime: b.updatedat,
// The consignment's status WINS when there is one (e.g. Collected_By_Miler -> Picked,
// Out_for_Delivery -> Active, Delivered -> Delivered).
// Otherwise derives from booking status + reachedat:
// Pickup_Scheduled + reachedat present -> Arrived
// Pickup_Scheduled + reachedat absent -> Accepted
orderstatus: effectiveDeliveryStatus,
consignmentstatus: cStatus,
statusfromconsignment: cStatus != null,
droplat: b.deliverylatitude,
droplon: b.deliverylongitude
};
});
return {
orderheaderid: b.bookingid ?? b.id,
deliveryid: b.bookingid ?? b.id,
bookingid: b.bookingid ?? b.id,
bookingno: b.bookingno,
orderid: b.bookingno || (b.bookingid ? `#${b.bookingid}` : (b.id ? `#${b.id}` : '')),
consignmentid: consignmentId,
tenantid: b.tenantid,
tenantname: tenant?.tenantname || '',
tenantsuburb: '',
applocation: '',
tenantadress: tenant?.primaryemail || '',
locationname: tenant?.tenantname || '',
locationsuburb: '',
// Was hardcoded '' — Dispatch.js's kitchen markers read this as the
// pickup business name (`o.pickupcustomer || o.kitchen_key || 'Unknown'`,
// Dispatch.js:1660), and no booking on this API carries a `kitchen_key`
// field at all, so every kitchen pin fell through to the literal string
// 'Unknown' — rendered as a "U" marker whose hover/popup then showed
// "Unknown". The tenant IS the kitchen for a B2B booking (same value
// already used for tenantname/locationname above), so reuse it here.
pickupcustomer: tenant?.tenantname || '',
pickupcontactno: '',
Pickupaddress: b.pickupaddress || '',
pickupaddress: b.pickupaddress || '',
pickuplocation: b.pickupaddress || '',
pickupsuburb: '',
deliverycustomer: customer?.firstname || customer?.name || (customerId ? `Customer #${customerId}` : ''),
deliverycontactno: customer?.phone || customer?.contactno || '',
deliveryaddress: b.deliveryaddress || '',
deliverylocation: b.deliveryaddress || '',
deliverysuburb: '',
ridername: miler?.displayname || miler?.name || (riderId ? `Rider #${riderId}` : ''),
userid: riderId,
// GET /admin/milers/:id/notify (and block/assign-vehicle) key off
// milerprofileid, not the userid stored on the booking — confirmed
// live (a booking's assignedmileruserid matches a miler's `userid`
// field, which 404s against /admin/milers/:id; milerprofileid is the
// real primary key of that resource).
milerprofileid: miler?.milerprofileid ?? miler?.id,
ridercontact: miler?.phone || miler?.contactno || '',
expecteddeliverytime: b.serviceoptions?.[0]?.estimateddeliveryat,
// No route-plan data source (step order/transit time/cumulative km were
// computed by the old jupiter backend from the dispatch optimiser's
// output, not stored on a booking/consignment) — left undefined so the
// UI's own "—" fallbacks render instead of a fabricated number.
transitminutes: undefined,
cumulativekms: undefined,
step: undefined,
// No road-distance field on a booking — approximated as a straight
// line between pickup and delivery coordinates.
kms: haversineKm(b.pickuplatitude, b.pickuplongitude, b.deliverylatitude, b.deliverylongitude),
pickuplatitude: b.pickuplatitude,
pickuplongitude: b.pickuplongitude,
deliverylatitude: b.deliverylatitude,
deliverylongitude: b.deliverylongitude,
deliverycharges: charge,
deliveryamt: charge,
deliveryamount: charge,
Quantity: b.parcels?.length || 0,
quantity: b.parcels?.length || 0,
collectionamt: undefined,
notes: b.notes || '',
deliverytype: customer ? 'B' : 'C',
orderdate: b.createdat,
deliverydate: b.serviceoptions?.[0]?.estimateddeliveryat || b.updatedat,
reachedat: reached,
assigntime: b.updatedat,
// The consignment's status WINS when there is one (e.g. Collected_By_Miler -> Picked,
// Out_for_Delivery -> Active, Delivered -> Delivered).
// If status == Converted_To_Consignment, switches to consignmentstatus.
// Otherwise derives from booking status + reachedat:
// Pickup_Scheduled + reachedat present -> Arrived
// Pickup_Scheduled + reachedat absent -> Accepted
orderstatus: effectiveDeliveryStatus,
consignmentstatus: cStatus,
statusfromconsignment: cStatus != null || String(b.status || '').toLowerCase() === 'converted_to_consignment',
droplat: b.deliverylatitude,
droplon: b.deliverylongitude
};
});
// Apply the requested date range to the booking's CREATION day. This has to
// agree with what the batch bucketing reads (Dispatch.js's
@@ -783,14 +842,21 @@ const getDeliveryStatusCounts = async () => {
const id = c?.consignmentid ?? c?.id;
if (id != null) consignmentMap.set(String(id), c);
});
const dispatched = (bookings || []).filter((b) => b.assignedmileruserid || b.consignmentid);
const isDispatched = (b) => {
if (!b) return false;
const riderId = b.assignedmileruserid ?? b.assigned_miler_user_id ?? b.mileruserid ?? b.milerid ?? b.assignedmilerid;
const consignmentId = b.consignmentid ?? b.consignment_id;
const rawStatus = String(b.status || b.orderstatus || '').trim().toLowerCase();
if (riderId || consignmentId) return true;
if (rawStatus && !['pending_pickup', 'created', 'new', 'booked', 'order_placed', 'unassigned', ''].includes(rawStatus)) {
return true;
}
return false;
};
const dispatched = (bookings || []).filter(isDispatched);
const counts = { total: dispatched.length };
dispatched.forEach((b) => {
const cStatus = consignmentStatusFor(b, consignmentMap);
const reached = b.reachedat ?? b.reached_at ?? b.reachedAt ?? b.reachedtime ?? b.reached_time;
const status = cStatus
? mapBookingStatusToDeliveryStatus(cStatus)
: mapBookingStatusToDeliveryStatus(b.status, reached);
const status = deriveDeliveryStatus(b, consignmentMap);
counts[status] = (counts[status] || 0) + 1;
});
return counts;

View File

@@ -39,7 +39,11 @@ export const STATUS_MAP = {
/* Consignment states that have no booking equivalent. */
in_transit: { label: 'In Transit', tone: 'warning' },
inwarded_at_hub: { label: 'Inwarded At Hub', tone: 'info' },
inwarded: { label: 'Inwarded At Hub', tone: 'info' },
at_hub: { label: 'At Hub', tone: 'info' },
outwarded_from_hub: { label: 'Outwarded From Hub', tone: 'warning' },
in_transit_to_hub: { label: 'In Transit To Hub', tone: 'warning' },
rto: { label: 'RTO', tone: 'destructive' },
returned: { label: 'Returned', tone: 'destructive' },
failed: { label: 'Failed', tone: 'destructive' },

View File

@@ -33,6 +33,7 @@ import { currency, exportRows, km as formatKm, matchesQuery, orDash, useDebounce
/* `other` is not declared here — it is appended only when rows actually land
in it, so the strip never carries a permanently empty tab. */
const STATUS_TABS = [
{ value: 'all', label: 'All', countKey: 'total' },
{ value: 'pending', label: 'Pending', countKey: 'uncoveredLength' },
{ value: 'accepted', label: 'Accepted', countKey: 'assignedLength' },
{ value: 'arrived', label: 'Arrived', countKey: 'arrivedLength' },
@@ -60,26 +61,8 @@ const SETTABLE_STATUSES = [
];
export default function Deliveries() {
// Unscoped by default. `fetchDeliveries` treats a blank from/to as "no
// bound" (see queries.js — `if (!startdate && !enddate) return true`), and
// it filters on `orderdate` (the booking's creation day), never on when a
// row was last touched. Defaulting this range to today meant the Pending/
// Accepted/Arrived/Picked/Active tabs — an outstanding-work QUEUE, not a
// day's log — only showed a booking that was also *created* today.
// Reassign an order that came in yesterday and it's still correctly
// "Pending" work, but it dropped off the board the moment you did it,
// reading as "I just assigned it and it's not there." Same "state vs
// flow" distinction this app already draws elsewhere for orders. Delivered/
// Cancelled stay meaningful to scope by date — the operator narrows via
// the date inputs same as before.
const [range, setRange] = useState({ from: '', to: '' });
const [tab, setTab] = useState('pending');
// 'all' rather than the wave matching the current clock hour: a batch is a
// slice of ONE day, so "jump to the slot matching right now" only means
// something once the date range above is scoped to a single day. With the
// range unscoped by default, defaulting the batch too would silently hide
// almost everything — a booking created three days ago at 11am has no
// relationship to "the slot matching right now" on a different day.
const [tab, setTab] = useState('all');
const [batch, setBatch] = useState('all');
const [search, setSearch] = useState('');
const debouncedSearch = useDebouncedValue(search);
@@ -144,7 +127,10 @@ export default function Deliveries() {
const base = STATUS_TABS.map((entry) => ({
value: entry.value,
label: entry.label,
count: batchRows.filter((row) => String(row.orderstatus || '').toLowerCase() === entry.value).length,
count:
entry.value === 'all'
? batchRows.length
: batchRows.filter((row) => String(row.orderstatus || '').toLowerCase() === entry.value).length,
}));
/* A row whose status matches no tab used to be counted nowhere and shown
nowhere. It gets its own tab, but only when one actually exists. */
@@ -154,7 +140,9 @@ export default function Deliveries() {
const rows = useMemo(() => {
const scoped =
tab === 'other'
tab === 'all'
? batchRows
: tab === 'other'
? unmapped
: batchRows.filter((row) => {
const status = String(row.orderstatus || '').toLowerCase();
@@ -164,7 +152,17 @@ export default function Deliveries() {
return scoped.filter((row) =>
matchesQuery(
row,
['orderid', 'deliverycustomer', 'ridername', 'pickupaddress', 'deliveryaddress', 'tenantname'],
[
'orderid',
'bookingno',
'bookingid',
'consignmentid',
'deliverycustomer',
'ridername',
'pickupaddress',
'deliveryaddress',
'tenantname',
],
debouncedSearch
)
);
@@ -252,7 +250,7 @@ export default function Deliveries() {
sortable: true,
cell: (row) => (
<div className="flex flex-col items-start gap-0.5">
<StatusBadge status={row.orderstatus} dot size="sm" />
<StatusBadge status={row.consignmentstatus || row.orderstatus} dot size="sm" />
{/* Where the value came from. Without this, a row reading "Active"
while the booking still says Converted_To_Consignment looks
exactly like a bug. */}
@@ -643,7 +641,7 @@ function OrderDetailDrawer({ row, onClose }) {
<Surface variant="subtle" padding="sm" radius="lg">
<dl className="grid grid-cols-2 gap-3">
{[
['Status', row?.orderstatus ? <StatusBadge status={row.orderstatus} size="sm" /> : '—'],
['Status', row ? <StatusBadge status={row.consignmentstatus || row.orderstatus} size="sm" /> : '—'],
['Rider', orDash(row?.ridername)],
['Customer', orDash(row?.deliverycustomer)],
['Contact', orDash(row?.deliverycontactno)],

View File

@@ -54,13 +54,15 @@ An opt-in "activity" date basis was added so an order booked yesterday and still
The filter field and the bucket field have to be the same field. If carried-over work needs to be visible on the board, it needs its own bucket ("Carried over") — not a time-of-day wave it does not belong to. Decide that before reaching for the date filter again.
### Picked is empty for hyperlocal traffic, and that is correct
### Picked vs Out for Delivery & Converted_To_Consignment
`doormile-flow.md` §5: at `pickup-complete` the booking becomes a consignment, and **matching 3-digit pickup/delivery pincode prefixes go straight to `Out_for_Delivery`** (hyperlocal); everything else routes via a hub.
`doormile-flow.md` §5: at `pickup-complete` the booking becomes a consignment, and **matching 3-digit pickup/delivery pincode prefixes go straight to `Out_for_Delivery`** (hyperlocal) when the pickup collection flag is off; everything else sits at `Collected_By_Miler` (Picked) or routes via a hub (`Inwarded_At_Hub`).
So a Coimbatore-to-Coimbatore order (641xxx → 641xxx) is picked up and is *immediately* out for delivery. It never rests in a "picked" state. The Picked tab reading 0 while orders are plainly moving is expected behaviour on hyperlocal traffic, not a missing mapping.
The confusing part is downstream of that: the booking's own `status` **freezes at `Converted_To_Consignment`** the moment it is picked up. The Deliveries page joins `GET /admin/consignments` and lets the consignment's status win, so a row correctly reads **Active** while `GET /admin/bookings` still says `Converted_To_Consignment`. That looks like a bug on inspection, which is why the status badge carries a tooltip naming the consignment and its status whenever the value came from there (`statusfromconsignment` on the row).
The booking's own `status` **freezes at `Converted_To_Consignment`** the moment pickup is complete. `Converted_To_Consignment` is a handoff marker, not a display state, and is never displayed literally to the user. The Deliveries page joins `GET /admin/consignments` (and reads `consignmentstatus` attached to the booking row) to derive the live state:
- `Collected_By_Miler` → **Picked** (under Picked tab)
- `Out_for_Delivery` → **Active / Out for Delivery** (under Active tab)
- `Inwarded_At_Hub` / `At_Hub` / `Outwarded_From_Hub` → **Active** (with dedicated Inwarded/Hub badge)
- `Delivered` → **Delivered** (under Delivered tab)
### ⛔ Never bucket on `assigntime`
It is **not** an assignment time. The Doormile bookings feed has no assignment timestamp, so `api.js` maps `assigntime` to the booking's `updatedat` — its last-modified column. Any status change, parcel scan, payment or pickup-complete re-stamps it.

View File

@@ -275,11 +275,18 @@ export default function Orders() {
header: 'Status',
sortable: true,
cell: (row) => {
const rawStatus = String(row.status || '').trim().toLowerCase();
const isArrived =
String(row.status || '').toLowerCase() === 'pickup_scheduled' &&
rawStatus === 'pickup_scheduled' &&
Boolean(row.reachedat ?? row.reached_at ?? row.reachedAt ?? row.reachedtime ?? row.reached_time);
const displayStatus = isArrived ? 'arrived' : row.status;
return <StatusBadge status={displayStatus} dot size="sm" />;
if (isArrived) {
return <StatusBadge status="arrived" dot size="sm" />;
}
if (rawStatus === 'converted_to_consignment') {
const cStatus = row.consignmentstatus ?? row.consignment_status;
return <StatusBadge status={cStatus || 'picked'} dot size="sm" />;
}
return <StatusBadge status={row.status} dot size="sm" />;
},
},
],

View File

@@ -85,12 +85,20 @@ export const STATUS_ALIASES = {
out_for_delivery: 'active',
in_transit: 'active',
intransit: 'active',
inwarded_at_hub: 'active',
inwarded: 'active',
at_hub: 'active',
outwarded_from_hub: 'active',
in_transit_to_hub: 'active',
active: 'active',
completed: 'delivered',
delivered: 'delivered',
cancel: 'cancelled',
canceled: 'cancelled',
skipped: 'skipped'
skipped: 'skipped',
rto: 'skipped',
returned: 'cancelled',
failed: 'cancelled'
};
// Fallback for a status the backend invents that nobody has mapped yet. Renders