diff --git a/docs/superpowers/specs/2026-07-29-buyer-tasks-design.md b/docs/superpowers/specs/2026-07-29-buyer-tasks-design.md new file mode 100644 index 0000000..e8cb8b5 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-buyer-tasks-design.md @@ -0,0 +1,148 @@ +# Buyer Tasks Page Design + +> **Date:** 2026-07-29 +> **Status:** Draft — pending user review + +## Problem + +The buyer's purchase-order workflow is a multi-step process that today is scattered across three pages: + +1. **Order processing** (`/orders/[id]/process`) — buyer adds cost, supplier, and PO description per item. +2. **Order detail** (`/orders/[id]`) — buyer sees the generated draft POs and clicks "Send" to send the PO email. +3. **Collection pool** (`/collection-pool`) — buyer confirms stock is ready with the supplier before items move to delivery. + +The buyer has no single view of "what's waiting for me right now". They have to mentally track which orders have draft POs, which POs are sent, and which need a stock-confirmation call. This is the page that fixes that. + +## Goal + +A dedicated `/orders/tasks` page that groups all the items the buyer is responsible for, organised by their current workflow step, with one-click action buttons that move each group forward. + +## User flow + +1. **Open** `/orders/tasks`. Buyer sees three tabs with counts: `Needs PO (12)`, `PO Draft (5)`, `PO Sent (3)`. +2. **Needs PO tab** — items where `item_status = "processed"` and `purchase_order = ""`. Each card shows order + supplier + item count + total cost. **Blue "Generate PO" button** on the right. +3. **Click "Generate PO"** — server creates the PO, sets `purchase_order` on the items, sets `item_status = "po_generated"`. The group moves to the **PO Draft** tab. +4. **PO Draft tab** — items with a draft PO (`purchase_order != ""` and `purchase_orders.status = "draft"`). Same card layout. **Yellow/amber "Send PO" button**. +5. **Click "Send PO"** — opens the shared send-PO modal (extracted from the order detail page). On send, `purchase_orders.status = "sent"`. The group moves to the **PO Sent** tab. +6. **PO Sent tab** — items where `purchase_orders.status IN ("sent", "sending")` and `item_status != "ready_delivery"`. Same card layout. **Purple "Mark as ready" button**. +7. **Click "Mark as ready"** — opens a confirmation modal: "I spoke to this person and the stock is ready for collection." On confirm, `item_status = "ready_delivery"`. The group disappears from the list. + +## Step-by-step decision table + +| Step | Tab | Item filter | PO status | Action button | On click | +|---|---|---|---|---|---| +| (step 1) | — | (on `/orders/[id]/process`) | — | (save item) | Buyer adds cost + supplier + PO description | +| 2 | **Needs PO** | `item_status = "processed"` AND `purchase_order = ""` | n/a | **Generate PO** (blue) | Create PO, set `purchase_order`, `item_status = "po_generated"` | +| 3 | **PO Draft** | `purchase_order != ""` AND `purchase_orders.status = "draft"` | draft | **Send PO** (yellow/amber) | Open send modal, on send set `purchase_orders.status = "sent"` | +| 4 | **PO Sent** | `purchase_orders.status IN ("sent", "sending")` AND `item_status != "ready_delivery"` | sent | **Mark as ready** (purple) | Open confirm modal, on confirm set `item_status = "ready_delivery"` | + +After step 4, the group disappears from the list. Items live in the collection-pool page from then on. + +## Data shape + +```ts +// Per (order, supplier) group, returned by getBuyerTasks(tab) +type BuyerTaskGroup = { + // Group identity + groupId: string; // `${orderId}::${supplierId}` + orderId: string; + orderNumber: string; + supplierId: string; + supplierName: string; + // Customer / company context (for the header) + customerName: string; + companyName: string; + // Items rolled up + items: Array<{ + id: string; + description: string; + quantity: number; + costPrice: number; + sellingPrice: number; + }>; + itemCount: number; + totalCost: number; + // PO state (null if no PO yet) + purchaseOrderId: string | null; + poStatus: "draft" | "sent" | "sending" | "received" | "mail_failed" | "cancelled" | null; + // Current step (denormalised for the UI) + step: "needs_po" | "po_draft" | "po_sent"; +}; +``` + +The `groupId` is the stable key for the row — derived from the `(orderId, supplierId)` pair so it survives re-fetches. + +## Architecture + +Per the data-layer discipline in AGENTS.md: + +- **Service layer** — `src/lib/server/buyer-tasks-service.ts` + - `listBuyerTasks(tab: "needs_po" | "po_draft" | "po_sent")` — returns groups + items + - `generatePoForGroup(orderId, supplierId)` — creates the PO + updates items + - `markGroupReady(orderId, supplierId)` — sets `item_status = "ready_delivery"` + - Uses `getAdminPocketBase()` for writes, PB client for reads (admin reads ok if user-scoped doesn't work) +- **API routes** — thin handlers under `src/app/api/v1/buyer-tasks/` + - `GET /api/v1/buyer-tasks?tab=needs_po` — list + - `POST /api/v1/buyer-tasks/generate-po` — body `{ orderId, supplierId }`, returns updated group + - `POST /api/v1/buyer-tasks/mark-ready` — body `{ orderId, supplierId }`, returns 204 +- **Page** — `src/app/(app)/orders/tasks/page.tsx` +- **Components** — separate, focused files: + - `src/components/buyer-tasks/buyer-tasks-page.tsx` — page body + - `src/components/buyer-tasks/buyer-task-group-card.tsx` — single card + - `src/components/buyer-tasks/buyer-task-items-list.tsx` — collapsible items list + - `src/components/buyer-tasks/mark-ready-modal.tsx` — confirmation modal + - `src/components/buyer-tasks/buyer-task-tabs.tsx` — tab strip with counts +- **Reuse** — the existing send-PO modal is rich (document history, supplier-aware cc, attachment picker). To avoid duplicating its UI on the new page, **extract the send-PO modal into a shared component** at `src/components/purchase-orders/send-po-modal.tsx`. Both the order detail page and the buyer-tasks page mount it. Same underlying endpoint: `/api/v1/purchase-orders/[id]/send-email`. + +## Edge cases + +- **No purchase order yet** — `purchaseOrderId` and `poStatus` are `null`. The "Generate PO" button is the only action. +- **PO mail failed** — `po_status = "mail_failed"` keeps the group in the **PO Draft** tab so the buyer can retry. (No explicit "Retry" button in this iteration — the existing send-email route handles failures.) +- **Multiple suppliers per order** — each (order, supplier) pair is its own group. The same order can appear in multiple tabs (one group per supplier). +- **Group with zero items** — impossible in practice (a group is defined by items), but the service layer returns `[]` for empty groups so the UI doesn't render ghost rows. +- **Race conditions** — Tab counts and the list are fetched together. The action button is disabled while a request is in flight to prevent double-clicks. +- **Partial failure in generate-po** — if some items have invalid data, the existing per-row partial-failure pattern from the orders review modal applies. The other items in the group still get the PO; the failed items stay on the current tab. + +## Out of scope (this iteration) + +- Bulk actions across multiple groups (e.g., "Generate PO for all 12 Needs PO groups") — YAGNI. +- Filtering / sorting / search within a tab — YAGNI. +- A "Done" tab that shows completed groups — buyer explicitly said groups disappear. +- Removing items from a group before generating the PO — buyer edits via the order detail page. +- Editing supplier per group from this page — buyer edits via the order detail page. + +## Files + +- **New:** + - `src/lib/server/buyer-tasks-service.ts` + - `src/lib/server/buyer-tasks-service.test.ts` + - `src/app/api/v1/buyer-tasks/route.ts` + - `src/app/api/v1/buyer-tasks/route.test.ts` + - `src/app/api/v1/buyer-tasks/generate-po/route.ts` + - `src/app/api/v1/buyer-tasks/mark-ready/route.ts` + - `src/app/api/v1/buyer-tasks/generate-po/route.test.ts` + - `src/app/api/v1/buyer-tasks/mark-ready/route.test.ts` + - `src/app/(app)/orders/tasks/page.tsx` + - `src/components/buyer-tasks/buyer-tasks-page.tsx` + - `src/components/buyer-tasks/buyer-task-group-card.tsx` + - `src/components/buyer-tasks/buyer-task-items-list.tsx` + - `src/components/buyer-tasks/mark-ready-modal.tsx` + - `src/components/buyer-tasks/buyer-task-tabs.tsx` + - `src/components/buyer-tasks/buyer-tasks-page.test.tsx` + - `src/components/buyer-tasks/buyer-task-group-card.test.tsx` + - `src/components/buyer-tasks/mark-ready-modal.test.tsx` +- **Modified:** + - `src/components/sidebar.tsx` — add "Buyer Tasks" entry under Orders section + - `src/app/(app)/orders/[id]/page.tsx` — replace the inline send-PO modal with the shared `` component + - `src/components/purchase-orders/send-po-modal.tsx` — new shared component (extracted from order detail) + - `src/components/purchase-orders/send-po-modal.test.tsx` — extracted modal tests + +## Test coverage + +- **Service:** 6-8 tests covering each tab's filter, `generatePoForGroup` happy path + missing-supplier + missing-items, `markGroupReady` happy path + partial success. +- **Routes:** 4-5 tests per route (200, 400, 404, 500 paths). +- **Components:** 8-10 tests covering tab switching, action button enabled/disabled state, modal open/close, send-PO modal mounting. + +## Open questions + +None — the design is settled after the user's answers to the clarifying questions.