Add collection pool mark-ready notes spec

This commit is contained in:
SJCoetz 2026-07-10 00:00:40 +02:00
parent 0eef0abd92
commit dbbb3e2b2c

View file

@ -0,0 +1,144 @@
# Collection Pool — required notes on Mark Ready
**Goal:** When an operator clicks `Mark Ready` on a collection-pool row, require a free-text note (saved into `OrderItem.notes`) before the request is sent. The note captures context like "spoke to David", "ETA tomorrow", "no stock — partial".
## Scope
### In Scope
- New modal on the Collection Pool page that opens when the operator clicks `Mark Ready`.
- Modal has a required `Textarea` (with a character counter) and a primary `Save & Mark Ready` button.
- A `Cancel` button closes the modal without persisting.
- On submit: the modal calls the existing `PATCH /api/v1/collection-map/items/{id}/assign` with `{ ready_for_collection: true, ready_notes: "..." }`. The route extends its body schema to accept and persist the new field.
- Server route writes `ready_notes` into `OrderItem.notes` (free text). It does NOT create a new PB field.
- A small `Notes` chip appears on the row whenever the row has any `notes` content, with a tooltip showing the first ~120 characters.
- Unmarking (`Mark Awaiting`) does NOT require a note — the existing flow is unchanged.
### Out of Scope
- Creating a new PB field for "ready notes". The existing `OrderItem.notes` text field is the right place.
- Changes to other collection-pool actions (assign driver, reschedule, mark complete).
- Migrating old `OrderItem.notes` values into the new modal prefill.
## Current State
- `Mark Ready` in the Collection Pool page calls `PATCH /api/v1/collection-map/items/{id}/assign` with `{ ready_for_collection: true }`. No notes.
- The route in `src/app/api/v1/collection-map/items/[id]/assign/route.ts` already accepts a body shape; the body type is `{ driverId?, date?, ready_for_collection? }`. We extend the type with `ready_notes?`.
- The existing `OrderItem.notes` field is a free-text string on the order item. It's already rendered in the Order Detail page.
## User Confirmed Rules
- The note is required when the operator clicks `Mark Ready`.
- Unmarking (`Mark Awaiting`) does not require a note.
- A simple textarea is enough — no structured fields.
- A small `Notes` chip on the row shows when notes exist (tooltip with the first 120 chars).
## UX
### Modal
```
┌─ Mark ready: <PO Number> · <item> ──────────────────────┐
│ Add a note for the audit log. Required. │
│ │
│ [ multi-line textarea, rows=4, maxLength=500 ] │
│ │
│ 0/500 │
│ │
│ [ Cancel ] [ Save & Mark Ready ] │
└───────────────────────────────────────────────────────┘
```
- `Save & Mark Ready` is disabled until the textarea is non-empty.
- `autoFocus` on the textarea.
- Same wide-modal treatment as the Send PO modal: `sm:max-w-md` is enough (single-column, ~28rem); `max-h-[90vh]` so the body scrolls on tiny screens.
- Title: `Mark ready: {PO Number} · {item description}` (truncated).
- Description: short hint about why a note is required.
- On success: close the modal, refresh the row, show toast `Marked ready — note saved.`
- On failure: keep the modal open, show the error in the footer area, allow retry.
### Row chip
- A small uppercase chip `NOTES` on the row (next to the existing `ready`/`awaiting` badge) when `row.notes?.trim()` is non-empty.
- Hover/focus shows a tooltip with the first 120 chars + `…` if longer.
- Empty notes: no chip.
- Click on chip: no action (read-only audit indicator).
## Data Model
No schema change. We reuse the existing `OrderItem.notes` field on the order item record. Operators already see this field on the Order Detail page, so the note is visible everywhere the row shows up.
The modal writes the value via the existing PATCH route. The route's body schema gets one new optional field:
```ts
type BodyInput = {
driverId?: string;
date?: string;
ready_for_collection?: boolean;
ready_notes?: string; // NEW
};
```
`ready_notes` is required (in spirit) when `ready_for_collection === true` and the request is NOT also assigning a driver (the existing `AssignDriverDialog` flow doesn't take notes). The route enforces the requirement:
- If `ready_for_collection === true` AND `ready_notes` is empty/missing → 400 with `{ error: "A note is required when marking ready." }`.
## Behavior
### Operator flow
1. Operator clicks `Mark Ready` on a row.
2. Modal opens with an empty textarea (and `Save & Mark Ready` disabled).
3. Operator types a note; the button enables.
4. Operator clicks `Save & Mark Ready`. The button shows a spinner while the request is in flight.
5. On 2xx, the modal closes, the row refreshes (it now shows the new note in the chip), and a toast confirms.
6. On 4xx/5xx, the modal stays open with the error in the footer.
### Server flow
- PATCH `/api/v1/collection-map/items/{id}/assign` with `{ ready_for_collection: true, ready_notes: "..." }`.
- Route validates: if `ready_for_collection` is true and `ready_notes` is empty/whitespace, return 400.
- Otherwise, write the patch (existing patch logic) AND `notes: ready_notes.trim()` to the order item.
- Return the updated item.
### Audit trail
The note becomes part of the order item record. It's visible on:
- The Collection Pool row chip.
- The Order Detail page's order item view.
- Anywhere else the order item renders.
## Testing
### UI tests (collection-pool/page.test.tsx)
- Clicking `Mark Ready` opens the modal.
- The modal's primary button is disabled when the textarea is empty.
- The modal's primary button is enabled once the textarea has any non-whitespace content.
- Submitting the modal calls the PATCH endpoint with `{ ready_for_collection: true, ready_notes: "<value>" }`.
- Submitting a note that is only whitespace (e.g. " ") is treated as empty.
- The modal closes on a 2xx response.
- The modal stays open on a 4xx/5xx response and shows the error.
- A 400 from the server for missing note is rendered to the user, not just a toast.
### Server tests (route.test.ts)
- PATCH with `{ ready_for_collection: true, ready_notes: "spoke to David" }` writes `notes: "spoke to David"` to the item.
- PATCH with `{ ready_for_collection: true, ready_notes: "" }` returns 400.
- PATCH with `{ ready_for_collection: true, ready_notes: " " }` returns 400 (whitespace treated as empty).
- PATCH with `{ ready_for_collection: false, ready_notes: "..." }` returns 200 and does NOT write notes (only the ready flag is updated; notes is left as-is). This is the unmarking path; no note required.
- PATCH with `{ ready_for_collection: true }` and no `ready_notes` returns 400.
- The note is trimmed before being written (no leading/trailing whitespace).
## Acceptance Criteria
- Marking a row Ready without a note is impossible — both the UI and the server block it.
- The note is persisted in `OrderItem.notes` and visible on the row chip and the Order Detail page.
- Unmarking (`Mark Awaiting`) does not require a note.
- The PATCH route is backward-compatible — old callers that send `{ ready_for_collection: true }` without `ready_notes` now get a 400, which is the intended new behavior.
## Risk / Rollback
- Low risk. No schema change, no new PB field.
- The only compatibility break is the now-required `ready_notes` on the server. The current client always sends nothing in this slot, so existing UI flows that don't have a note will start receiving 400s. The modal change ships the same commit, so they go green together.
- Rollback: revert the modal and the route validation. The `notes` write stays as a no-op write.