docs: buyer tasks page design spec

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-29 19:35:44 +02:00
parent d307e8d400
commit fe1af217e0

View file

@ -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 `<SendPoModal />` 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.