updates on the deliveries page and status updates as well
This commit is contained in:
39
README.md
39
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" />;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user