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
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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)],
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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" />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user