From 03ca2c8d29d480b4983b279368c1e6fc1a7e265b Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 29 Jul 2026 13:54:21 +0200 Subject: [PATCH] docs: clarify customer non-nullable, full-order scope, collision link, partial-fail UX Co-Authored-By: Claude Opus 4.8 Co-authored-by: Orca --- .../specs/2026-07-29-orders-review-modal-design.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-07-29-orders-review-modal-design.md b/docs/superpowers/specs/2026-07-29-orders-review-modal-design.md index 4da8006..9a76b2b 100644 --- a/docs/superpowers/specs/2026-07-29-orders-review-modal-design.md +++ b/docs/superpowers/specs/2026-07-29-orders-review-modal-design.md @@ -113,10 +113,10 @@ Same size + ghost-icon styling as `ExternalLink`. Tinted warn-amber. Tooltip: "R ### Behavior details -- **Editability** — every numeric cell in the order items table is an `Input`. The `selling_price` ↔ `cost_price` pair uses the lock-pattern from `SprintItemPricingModal` (type into one, the other recomputes via `markup`). Quantity is a plain integer input. `description` and `po_description` are read-only here (edits live on the order detail page). +- **Editability** — every numeric cell in the order items table is an `Input`. The `selling_price` ↔ `cost_price` pair uses the lock-pattern from `SprintItemPricingModal` (type into one, the other recomputes via `markup`). Quantity is a plain integer input. `description` and `po_description` are read-only here (edits live on the order detail page). The modal shows **all line items** for the order regardless of which tab the operator came from — the review is always whole-order, never tab-filtered. - **Dirty state** — the modal tracks a `dirty: Set<"number" | "customer" | "items">` so Save is disabled until at least one field changes. Cancel and Esc discard changes without confirmation if nothing's dirty; if dirty, show a confirm-discard dialog. - **Save flow** — POST to `/api/v1/orders/[id]/review-save` with `{ order_number?, customer?, items?: [{ id, quantity?, selling_price?, cost_price? }] }`. The route returns 200 on success, 409 on number collision (with the colliding order id), or 4xx with a message on validation failure. -- **Number collision error** — rendered inline beneath the order-number input: "ORD-2026-07-014 is already used by another order." Modal stays open, no fields lost. +- **Number collision error** — rendered inline beneath the order-number input: `"ORD-2026-07-014 is already used by another order."` The colliding order id is rendered as a link to `/orders/` so the operator can jump straight to the existing row and decide what to do (merge, re-rename the new one, etc.). Modal stays open, no fields lost. - **Realtime collision** — if PB fires an `orders` or `order_items` write while the modal is open, the modal keeps its own dirty state and refetches on close. The card behind the modal updates via the existing subscription; reopening the modal re-fetches fresh data. - **No source email** (order not from a quote) — right pane shows "No source email — this order wasn't created from a quote." Edits still work; nothing to preview. @@ -139,10 +139,12 @@ Two new functions in `src/lib/server/orders-service.ts` (the existing data-layer ```ts export async function updateOrderReviewFields( orderId: string, - patch: { order_number?: string; customer?: string | null }, + patch: { order_number?: string; customer?: string }, ): Promise ``` +`customer` is non-nullable here on purpose: the `orders.customer` field is a required relation, and the operator is choosing among real PB customer rows in the `CustomerSearch` combobox. Sending `null` would be a 400 from PB; the route handler enforces this so the modal can never accidentally clear the relation. + - If `order_number` is set and differs from the current value, query `pb.collection("orders").getList(1, 1, { filter: \`order_number = "${newNumber}" && id != "${orderId}"\` })`. If any row matches, throw `OrderNumberCollisionError` carrying the colliding order id. - Else PATCH the order with the supplied fields via `pb.collection("orders").update(orderId, patch)`. - Return the updated `Order`. @@ -178,7 +180,7 @@ Request body: ```ts { order_number?: string; - customer?: string | null; + customer?: string; items?: Array<{ id: string; quantity?: number | null; @@ -213,7 +215,7 @@ Use `getAdminPocketBase()` for writes (mirrors the existing `orders-service.ts` |-------------------------------|----------------------|----------------------------------------------------------------------| | Number already used | Route handler | Inline error under order_number input; modal stays open | | PB validation error on PATCH | Service layer | Toast with the PB error message; modal stays open | -| Partial item PATCH failure | Service layer | Toast: "Saved N items, M failed"; modal closes; failed items list | +| Partial item PATCH failure | Service layer | Toast: "Saved N items, M failed"; modal stays open; failed item ids shown beneath the items table so the operator can retry | | Network failure on Save click | Client (try/catch) | Toast: "Save failed"; modal stays open; no fields lost | | Realtime update during modal | Client (refetch) | Modal keeps dirty state; underlying card updates; reopen re-fetches |