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 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. 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 ## Things that look wrong but are not
- **A delivery can read "Active" while the booking says `Converted_To_Consignment`.** A booking's - **`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.
status freezes the moment it is picked up; the lifecycle continues on the consignment. The - **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.
Deliveries table lets the consignment's status win and labels the row so you can tell where the - **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.
value came from. - **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.
- **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.
## Batches, dates and the one rule about time ## Batches, dates and the one rule about time

View File

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

View File

@@ -39,7 +39,11 @@ export const STATUS_MAP = {
/* Consignment states that have no booking equivalent. */ /* Consignment states that have no booking equivalent. */
in_transit: { label: 'In Transit', tone: 'warning' }, 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' }, 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' }, rto: { label: 'RTO', tone: 'destructive' },
returned: { label: 'Returned', tone: 'destructive' }, returned: { label: 'Returned', tone: 'destructive' },
failed: { label: 'Failed', 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 /* `other` is not declared here — it is appended only when rows actually land
in it, so the strip never carries a permanently empty tab. */ in it, so the strip never carries a permanently empty tab. */
const STATUS_TABS = [ const STATUS_TABS = [
{ value: 'all', label: 'All', countKey: 'total' },
{ value: 'pending', label: 'Pending', countKey: 'uncoveredLength' }, { value: 'pending', label: 'Pending', countKey: 'uncoveredLength' },
{ value: 'accepted', label: 'Accepted', countKey: 'assignedLength' }, { value: 'accepted', label: 'Accepted', countKey: 'assignedLength' },
{ value: 'arrived', label: 'Arrived', countKey: 'arrivedLength' }, { value: 'arrived', label: 'Arrived', countKey: 'arrivedLength' },
@@ -60,26 +61,8 @@ const SETTABLE_STATUSES = [
]; ];
export default function Deliveries() { 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 [range, setRange] = useState({ from: '', to: '' });
const [tab, setTab] = useState('pending'); const [tab, setTab] = useState('all');
// '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 [batch, setBatch] = useState('all'); const [batch, setBatch] = useState('all');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const debouncedSearch = useDebouncedValue(search); const debouncedSearch = useDebouncedValue(search);
@@ -144,7 +127,10 @@ export default function Deliveries() {
const base = STATUS_TABS.map((entry) => ({ const base = STATUS_TABS.map((entry) => ({
value: entry.value, value: entry.value,
label: entry.label, 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 /* 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. */ nowhere. It gets its own tab, but only when one actually exists. */
@@ -154,7 +140,9 @@ export default function Deliveries() {
const rows = useMemo(() => { const rows = useMemo(() => {
const scoped = const scoped =
tab === 'other' tab === 'all'
? batchRows
: tab === 'other'
? unmapped ? unmapped
: batchRows.filter((row) => { : batchRows.filter((row) => {
const status = String(row.orderstatus || '').toLowerCase(); const status = String(row.orderstatus || '').toLowerCase();
@@ -164,7 +152,17 @@ export default function Deliveries() {
return scoped.filter((row) => return scoped.filter((row) =>
matchesQuery( matchesQuery(
row, row,
['orderid', 'deliverycustomer', 'ridername', 'pickupaddress', 'deliveryaddress', 'tenantname'], [
'orderid',
'bookingno',
'bookingid',
'consignmentid',
'deliverycustomer',
'ridername',
'pickupaddress',
'deliveryaddress',
'tenantname',
],
debouncedSearch debouncedSearch
) )
); );
@@ -252,7 +250,7 @@ export default function Deliveries() {
sortable: true, sortable: true,
cell: (row) => ( cell: (row) => (
<div className="flex flex-col items-start gap-0.5"> <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" {/* Where the value came from. Without this, a row reading "Active"
while the booking still says Converted_To_Consignment looks while the booking still says Converted_To_Consignment looks
exactly like a bug. */} exactly like a bug. */}
@@ -643,7 +641,7 @@ function OrderDetailDrawer({ row, onClose }) {
<Surface variant="subtle" padding="sm" radius="lg"> <Surface variant="subtle" padding="sm" radius="lg">
<dl className="grid grid-cols-2 gap-3"> <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)], ['Rider', orDash(row?.ridername)],
['Customer', orDash(row?.deliverycustomer)], ['Customer', orDash(row?.deliverycustomer)],
['Contact', orDash(row?.deliverycontactno)], ['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. 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 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)
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). - `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` ### ⛔ 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. 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', header: 'Status',
sortable: true, sortable: true,
cell: (row) => { cell: (row) => {
const rawStatus = String(row.status || '').trim().toLowerCase();
const isArrived = const isArrived =
String(row.status || '').toLowerCase() === 'pickup_scheduled' && rawStatus === 'pickup_scheduled' &&
Boolean(row.reachedat ?? row.reached_at ?? row.reachedAt ?? row.reachedtime ?? row.reached_time); Boolean(row.reachedat ?? row.reached_at ?? row.reachedAt ?? row.reachedtime ?? row.reached_time);
const displayStatus = isArrived ? 'arrived' : row.status; if (isArrived) {
return <StatusBadge status={displayStatus} dot size="sm" />; 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', out_for_delivery: 'active',
in_transit: 'active', in_transit: 'active',
intransit: 'active', intransit: 'active',
inwarded_at_hub: 'active',
inwarded: 'active',
at_hub: 'active',
outwarded_from_hub: 'active',
in_transit_to_hub: 'active',
active: 'active', active: 'active',
completed: 'delivered', completed: 'delivered',
delivered: 'delivered', delivered: 'delivered',
cancel: 'cancelled', cancel: 'cancelled',
canceled: '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 // Fallback for a status the backend invents that nobody has mapped yet. Renders