docs: buyer tasks page design spec
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
d307e8d400
commit
fe1af217e0
1 changed files with 148 additions and 0 deletions
148
docs/superpowers/specs/2026-07-29-buyer-tasks-design.md
Normal file
148
docs/superpowers/specs/2026-07-29-buyer-tasks-design.md
Normal 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.
|
||||
Loading…
Reference in a new issue