From ce32465a6f90ec2a497f927f5db7fd40001b2888 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 20 Aug 2026 13:12:56 +0530 Subject: [PATCH] updates on the ui changes and the bot changes --- src/pages/api/CLAUDE.md | 1 + src/pages/api/api.js | 59 ++++- src/pages/nearle/assistant/CLAUDE.md | 26 +- .../nearle/assistant/DoormileAI/AIMessage.js | 14 +- .../nearle/assistant/DoormileAI/AIPanel.js | 148 ++++++++++- .../nearle/assistant/bulkOrderActions.js | 5 +- src/pages/nearle/assistant/intents.js | 14 + src/pages/nearle/assistant/repeatFlow.js | 30 +++ src/pages/nearle/assistant/repeatRuns.js | 239 ++++++++++++++++++ src/pages/nearle/deliveries/deliveries.js | 18 +- src/pages/nearle/dispatch/CLAUDE.md | 8 + src/pages/nearle/dispatch/Dispatch.css | 30 ++- 12 files changed, 567 insertions(+), 25 deletions(-) create mode 100644 src/pages/nearle/assistant/repeatFlow.js create mode 100644 src/pages/nearle/assistant/repeatRuns.js diff --git a/src/pages/api/CLAUDE.md b/src/pages/api/CLAUDE.md index fdd78bb..52bb918 100644 --- a/src/pages/api/CLAUDE.md +++ b/src/pages/api/CLAUDE.md @@ -98,4 +98,5 @@ export const someFetch = async () => { - Several `api.js` functions ignore most of their destructured `queryKey`/arguments — the new endpoint doesn't accept those filters. This is deliberate degradation, not a bug; see §3. - A handful of pages (`createorder1.js`, `multipleOrders.js`) synthesize fallback data client-side (e.g. a fixed 09:00–21:00 delivery-slot window) because the endpoint that used to supply it no longer exists. Look for the comment explaining why before "fixing" it. +- **`updateDeliveryAPI` refuses half the Deliveries dialog's options, on purpose.** It writes to `PUT /admin/consignments/:id/status`, which speaks the **consignment** enum — not the booking enum they look like. `Picked` maps to `Converted_To_Consignment`, which names the *moment a booking becomes a consignment*; a consignment cannot be set to it, so choosing Picked did nothing and the row kept showing the consignment's real state. `Pending` and `Accepted` describe a booking *before* pickup, which a consignment can never return to. Only `Out_for_Delivery`, `Delivered` and `Cancelled` are sent; everything else is refused with a reason and **nothing goes on the wire**. If the real consignment enum is ever captured, widen the map — don't guess a value. - `notifyRider` / `notifyMiler` now take a **miler profile ID**, not an FCM token — the new API's `/admin/milers/:id/notify` looks the device token up server-side. Don't pass a token here. diff --git a/src/pages/api/api.js b/src/pages/api/api.js index c478bd8..c7a4965 100644 --- a/src/pages/api/api.js +++ b/src/pages/api/api.js @@ -714,6 +714,13 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { // Falls back to the booking whenever the consignment is absent or carries // nothing status-shaped — never invents a state. orderstatus: mapBookingStatusToDeliveryStatus(consignmentStatusFor(b, consignmentMap) ?? b.status), + // Which record the status above came from, and what it said. A booking + // freezes at Converted_To_Consignment the moment it is picked up, so a + // row can legitimately read "Active" while GET /admin/bookings still says + // Converted_To_Consignment — which looks exactly like a bug unless the UI + // can say where the value came from. + consignmentstatus: consignmentStatusFor(b, consignmentMap), + statusfromconsignment: consignmentStatusFor(b, consignmentMap) != null, droplat: b.deliverylatitude, droplon: b.deliverylongitude }; @@ -858,12 +865,30 @@ export const changeRiderAPI = async (selectedRider, selectedRow) => // from it by inversion: that map is many-to-one (`pending_pickup` and // `miler_assigned` both mean `pending`), so an automatic inversion would pick // whichever happened to be last and silently write the wrong one. -const DELIVERY_STATUS_TO_BOOKING_STATUS = { - pending: 'Pending_Pickup', - accepted: 'Pickup_Scheduled', - picked: 'Converted_To_Consignment', - // The dialog offers "started", which this API has no separate state for — a - // consignment that has started IS out for delivery. +// ⚠ These are CONSIGNMENT statuses, not booking statuses. The endpoint is +// `PUT /admin/consignments/:id/status`, and the two enums are not the same +// vocabulary even though they overlap on Delivered and Cancelled. +// +// This map used to include the booking's pre-pickup states, and they were all +// nonsense to send here: +// +// picked → 'Converted_To_Consignment' — that names the MOMENT a booking +// becomes a consignment. A consignment cannot be set to it; it +// already is one. Choosing "Picked" in the dialog therefore did +// nothing and the row kept showing whatever the consignment +// really was, which is what "I set it to picked and it still says +// Active" was. +// pending → 'Pending_Pickup' +// accepted → 'Pickup_Scheduled' — both describe a booking BEFORE +// pickup. A consignment only exists after it, so it can never go +// back to either. +// +// What a consignment can actually be set to, per doormile-flow.md §5–6: it is +// created at pickup-complete already Out_for_Delivery (hyperlocal) or routed +// via a hub, then delivered, skipped, or cancelled. +const DELIVERY_STATUS_TO_CONSIGNMENT_STATUS = { + // The dialog offers "started"; a consignment that has started IS out for + // delivery — this API has no separate state for it. started: 'Out_for_Delivery', active: 'Out_for_Delivery', delivered: 'Delivered', @@ -871,10 +896,24 @@ const DELIVERY_STATUS_TO_BOOKING_STATUS = { canceled: 'Cancelled' }; +// Why a status can't be set, when it can't. Specific beats generic: "Picked +// can't be set" is useless next to "it's already a consignment, which is what +// picked means". +const UNSETTABLE_REASON = { + picked: 'this order is already a consignment — which is exactly what "picked" means. There is no earlier state to set it back to.', + pending: + 'a consignment only exists after pickup, so it can’t go back to Pending — that describes a booking before the parcel was collected.', + accepted: + 'a consignment only exists after pickup, so it can’t go back to Accepted — that describes a booking before the parcel was collected.', + arrived: 'the rider action behind Arrived (POST /miler/bookings/:id/reached) writes no consignment status, so there is nothing to set.', + skipped: + 'Skipped is recorded by the rider (POST /miler/consignments/:id/skip) as an attempt count, not as a status this endpoint can write.' +}; + export const updateDeliveryAPI = async (orderData) => { const id = orderData.consignmentid ?? orderData.deliveryid; const chosen = String(orderData.orderstatus || '').toLowerCase(); - const status = DELIVERY_STATUS_TO_BOOKING_STATUS[chosen]; + const status = DELIVERY_STATUS_TO_CONSIGNMENT_STATUS[chosen]; // `arrived` and `skipped` have no booking-status equivalent at all (the rider // actions behind them — /miler/bookings/:id/reached and @@ -882,11 +921,11 @@ export const updateDeliveryAPI = async (orderData) => { // the reason is honest; guessing a near-enough status would set the wrong one // on a real delivery. if (!status) { + if (!chosen) return { success: false, message: 'Choose a status first.' }; return { success: false, - message: chosen - ? `"${orderData.orderstatus}" has no equivalent on the consignment API, so it can't be set from here.` - : 'Choose a status first.' + message: + UNSETTABLE_REASON[chosen] || `"${orderData.orderstatus}" has no equivalent on the consignment API, so it can't be set from here.` }; } diff --git a/src/pages/nearle/assistant/CLAUDE.md b/src/pages/nearle/assistant/CLAUDE.md index 2d7d32e..390062d 100644 --- a/src/pages/nearle/assistant/CLAUDE.md +++ b/src/pages/nearle/assistant/CLAUDE.md @@ -224,6 +224,30 @@ A file (`bulkFile.js`) and a paste (`parseBulkRows`) produce the **same row arra Over-cap files chunk into batches of `BULK_MAX` (200) and report per row regardless of batch. Nothing is ever silently truncated. +### Repeat Runs — `repeatRuns.js` / `repeatFlow.js` + +"Same orders as yesterday." One question (which day), then a pass, then the usual gate. + +**It is the cheapest write here, and the reason is structural:** a booking already carries 15 of the 17 fields `buildOrderPayload` needs — including BOTH SETS OF COORDINATES. Only `customer_name` and `customer_phone` are missing, and they come from the `appcustomerid` → `/admin/customers` join. **So a repeat needs no geocoding at all** — the ~1 lookup/second Nominatim throttle that dominates the bulk-file flow simply doesn't apply. + +**A booking is a snapshot, not a template.** The drift check is phase one, not polish. All three of its main rules came from one live page of 36 bookings, not from imagination: + +| Trap | Seen on | +|---|---| +| `pickupaddress` absent entirely | booking 57 — has the pincode and coordinates, no address key | +| `tenantid` is null | every `Customer_App` booking (24–27). `Number(null)` → tenant `0` | +| pickup pincode no longer served | CityGate refuses at the middleware, before the handler | + +Plus: the customer record can be deleted, and a booking can lack delivery coordinates. `driftReason` returns a **reason, never a boolean** — an operator dropping a row deserves to know which field went stale. + +**Duplicate safety is INVERTED here.** Everywhere else near-identical orders are an error (`wasAlreadySubmitted`); a repeat deliberately creates them, so that guard would misfire every time. The question that matters is *has this run already been repeated today?* — answered by fingerprinting today's own bookings on `(phone + delivery address + pickup pincode)` and setting aside anything already present. Without it, a double-click books every customer twice, because the bulk endpoint has no idempotency key. + +**Prices are re-quoted at today's tariff, never copied.** `finalprice` is deliberately left blank so `priceBulkRows` fills it exactly as an unpriced bulk row. Yesterday's number is kept as `previousPrice` purely so a tariff change is *visible* rather than discovered on an invoice. Pricing runs **per row** because a day's run can span tenants, and a tenant's own pricing row decides the number. + +**`__pickup` travels on the ROW, not the shared draft** — which is why `executeCreateBulk` now prefers `r.__pickup ?? shared.__pickup`. A bulk file shares one kitchen; a repeated day does not, and collapsing them would silently re-address half the orders. + +Cancelled orders are never repeated. Lookback is 7 days — beyond that it stops being "the usual round". + ### Assigning a rider — `assignActions.js` / `assignFlow.js` The fourth write. Reached three ways: automatically after a single create, from `"assign a rider to DM-BK-…"`, and offered after a bulk run. @@ -238,7 +262,7 @@ The fourth write. Reached three ways: automatically after a single create, from Notification failure never fails the assignment: the order **is** assigned at that point, and reporting otherwise would be a lie. It is recorded as a failed source call instead. A rider with no `milerprofileid` is stated explicitly rather than letting the operator assume a phone buzzed. -Assertions for both engines live outside the repo (project convention is lint-only) — 56 for `orderFlow`, 44 for `bulkFlow`, 42 for `assignFlow`, 20 for `customerFlow`, covering the branching, the geocode re-ask, the CityGate refusal and the unpriceable path. +Assertions for both engines live outside the repo (project convention is lint-only) — 56 for `orderFlow`, 44 for `bulkFlow`, 42 for `assignFlow`, 30 for `repeatRuns`, 20 for `customerFlow`, covering the branching, the geocode re-ask, the CityGate refusal and the unpriceable path. --- diff --git a/src/pages/nearle/assistant/DoormileAI/AIMessage.js b/src/pages/nearle/assistant/DoormileAI/AIMessage.js index d24973f..798b4b2 100644 --- a/src/pages/nearle/assistant/DoormileAI/AIMessage.js +++ b/src/pages/nearle/assistant/DoormileAI/AIMessage.js @@ -141,7 +141,9 @@ const AssistantMessage = ({ message, onCopy, onAsk, onSubmitForm, onCancelAction