Add collection pool mark-ready notes spec
This commit is contained in:
parent
0eef0abd92
commit
dbbb3e2b2c
1 changed files with 144 additions and 0 deletions
|
|
@ -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.
|
||||
Loading…
Reference in a new issue