From 20533bcddf33dda65210605db06e6099a6a330a6 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 29 Jul 2026 13:53:36 +0200 Subject: [PATCH] docs: design for orders review modal (amber icon + inline edit + attachment preview) Co-Authored-By: Claude Opus 4.8 Co-authored-by: Orca --- .../2026-07-29-orders-review-modal-design.md | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-29-orders-review-modal-design.md 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 new file mode 100644 index 0000000..4da8006 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-orders-review-modal-design.md @@ -0,0 +1,222 @@ +# Orders Review Modal Design + +Date: 2026-07-29 +Area: `src/app/(app)/orders/page.tsx`, `src/components/orders-review-modal.tsx` (new), `src/lib/server/orders-service.ts`, `src/app/api/v1/orders/[id]/review-save/route.ts` (new) +Status: Drafted for review + +## Goal + +Add a per-order "review against source" workflow so the operator can verify AI-populated order contents (line items, prices, customer) against the original email and attachments in one screen, and correct any AI mistakes inline — without leaving the orders list. + +## User Need + +Today the AI populates orders from inbound emails via the n8n pipeline. When the AI makes a mistake (wrong order number, wrong customer, mis-extracted qty/price, missed line item), the operator has no way to see the AI's output next to the source-of-truth RFQ email/attachment. They have to remember what the email said, open the order detail page, and fix things one field at a time. + +The operator needs: + +- A visible affordance on every order card that opens a review surface. +- The order's line items side-by-side with the source email and attachment(s). +- Inline editing of order_number, customer, and line-item prices/quantities. +- A preview of the actual attachment (PDF/image) inline — not a separate download — so they only need to spot-check the PDF/picture against the system, not flip windows. + +## Approved Direction + +Add a new amber-tinted icon button on every order card that opens a review modal. The modal lays out the order's editable fields on the left and an inline preview of the source attachment (PDF/image) — or, if no attachment, the email body — on the right. Edits save atomically via a new `/api/v1/orders/[id]/review-save` endpoint; the card refreshes in place via the existing PB realtime subscription. + +## Scope + +In scope: + +- A new `OrdersReviewModal` component rendered from `OrdersPage` whenever a card's review button is clicked. +- A new warn-amber icon button on each `OrderCard`, placed to the LEFT of the existing `ExternalLink` "open" button. +- A new `updateOrderReviewFields` + `updateOrderItemsPrices` pair in `src/lib/server/orders-service.ts`. +- A new `POST /api/v1/orders/[id]/review-save` route handler. +- Inline preview of the first matching attachment (PDF via iframe, image via ``, both via the existing `/api/v1/files/{collectionId}/{recordId}/{token}` proxy); tab strip when there are multiple. +- Fallback to the email body (subject + body) when the order has zero source attachments. +- Free-text `order_number` override with uniqueness validation against the `orders` collection (excluding self) on save. +- `CustomerSearch` combobox for the `customer` field. + +Out of scope: + +- Migrating the existing `getOrdersWithItems` / `searchOrdersWithItems` calls in `OrdersPage` from `pb-helpers.ts` to the service layer (one resource at a time, no big-bang rewrites per AGENTS.md). +- Renaming the order-number generator or changing the `order_counters` collection. The override is a free-text PATCH against `orders.order_number`; the counter is untouched. +- New PocketBase collections or schema changes. +- A dedicated `/orders/[id]/review` route. +- Anything beyond header fields + line-item prices/qty in the editable region (description edits, supplier changes, status transitions, etc. stay on the order detail page). + +## Data Source + +Reads from existing collections: + +- `orders` (id, order_number, company, customer, rep, quote, ai_insights) +- `order_items` (order, description, po_description, quantity, selling_price, cost_price, markup) +- `quotes` (via expand on `orders.quote`) — supplies the source email: `email_subject`, `email_body`, and `email_message_id` for fallback +- `quote_attachments` (via `quote` relation) — supplies the source attachment(s) + +Writes back to existing collections: + +- `orders.order_number`, `orders.customer` +- `order_items.quantity`, `order_items.selling_price`, `order_items.cost_price`, `order_items.markup` + +The modal reads via the existing `pb.collection("orders").getOne(orderId, { expand: "quote,customer,company" })` pattern (mirrors the `OrderDetailPage` fetch) plus a separate `quote_attachments` query keyed on `quote_attachments.quote = `. The proxy URL pattern `/api/v1/files/{collectionId}/{recordId}/{token}` is already used by `SprintItemPricingModal` for quote attachments. + +## UX + +### Card affordance + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ┊ ▸ 🛒 ORD-2026-07-014 [↗ Q-2026-07-022] ✓ Complete · Acme Ltd … │ +│ ┊ 12 items · 5 new · 3 processing · 2 delivered · 2 cancelled │ +│ ┊ ⚠ ◯ │ +└──────────────────────────────────────────────────────────────────────────┘ + ▲ ▲ + │ │ + │ └─ ExternalLink (existing) "open" + └───── NEW warn-amber icon "review" +``` + +Same size + ghost-icon styling as `ExternalLink`. Tinted warn-amber. Tooltip: "Review order against email & attachments." Click opens the modal. + +### Modal layout + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ⚠ Review order [Cancel] [ Save ] │ +│ Verify AI-populated fields against the source email & attachments │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Order number [ ORD-2026-07-014 ] │ +│ Company Acme (Pty) Ltd (read-only) │ +│ Customer [ 🏢 J. Smith — Acme ▾ ] (CustomerSearch) │ +│ Rep Maria (read-only) │ +│ │ +├──────────────────────────────┬───────────────────────────────────────────────┤ +│ ORDER ITEMS (left) │ SOURCE OF TRUTH (right) │ +│ │ │ +│ Description Qty Price │ ── Attachment ────────────────────────────── │ +│ ──────────── ──── ──────── │ ┌─[Acme-RFQ.pdf] [site-photo.jpg]──────┐ │ +│ HSS drill 6mm [2 ] [42.50]│ │ │ │ +│ HSS drill 8mm [1 ] [38.00]│ │ [ PDF rendered via iframe / │ │ +│ HSS drill 10mm[3 ] [44.00]│ │ image rendered via ] │ │ +│ HSS drill 12mm[4 ] [50.00]│ │ │ │ +│ │ └───────────────────────────────────────┘ │ +│ + markup auto-recomputes │ │ +│ cost ↔ selling via lock │ ── (only if 0 attachments) ──────────────── │ +│ │ Subject: Re: RFQ — Drill bits │ +│ │ Hi team, please quote on the following: │ +│ │ • 2× 6mm HSS drills │ +│ │ • 1× 8mm HSS drills │ +│ │ … │ +└──────────────────────────────┴───────────────────────────────────────────────┘ +``` + +### 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). +- **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. +- **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. + +### Attachment preview rules + +The preview region resolves attachments in this priority: + +1. If `quote_attachments` has ≥1 entry with `kind = "pdf"`, prefer the first PDF. Render via `