diff --git a/README.md b/README.md index fe216c9..f14da73 100644 --- a/README.md +++ b/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 diff --git a/src/api/doormile/queries.js b/src/api/doormile/queries.js index 19ade80..8c12922 100644 --- a/src/api/doormile/queries.js +++ b/src/api/doormile/queries.js @@ -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; diff --git a/src/components/ds/StatusBadge.jsx b/src/components/ds/StatusBadge.jsx index 68ee745..85a449f 100644 --- a/src/components/ds/StatusBadge.jsx +++ b/src/components/ds/StatusBadge.jsx @@ -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' }, diff --git a/src/pages/doormile/deliveries/Deliveries.jsx b/src/pages/doormile/deliveries/Deliveries.jsx index e36b31e..df075fb 100644 --- a/src/pages/doormile/deliveries/Deliveries.jsx +++ b/src/pages/doormile/deliveries/Deliveries.jsx @@ -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) => (
- + {/* 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 }) {
{[ - ['Status', row?.orderstatus ? : '—'], + ['Status', row ? : '—'], ['Rider', orDash(row?.ridername)], ['Customer', orDash(row?.deliverycustomer)], ['Contact', orDash(row?.deliverycontactno)], diff --git a/src/pages/doormile/dispatch/CLAUDE.md b/src/pages/doormile/dispatch/CLAUDE.md index 450e13c..b62b9a4 100644 --- a/src/pages/doormile/dispatch/CLAUDE.md +++ b/src/pages/doormile/dispatch/CLAUDE.md @@ -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. diff --git a/src/pages/doormile/orders/Orders.jsx b/src/pages/doormile/orders/Orders.jsx index 5a892ae..c5a693f 100644 --- a/src/pages/doormile/orders/Orders.jsx +++ b/src/pages/doormile/orders/Orders.jsx @@ -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 ; + if (isArrived) { + return ; + } + if (rawStatus === 'converted_to_consignment') { + const cStatus = row.consignmentstatus ?? row.consignment_status; + return ; + } + return ; }, }, ], diff --git a/src/themes/dt/status.jsx b/src/themes/dt/status.jsx index 48b476e..f0e1625 100644 --- a/src/themes/dt/status.jsx +++ b/src/themes/dt/status.jsx @@ -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