7.7 KiB
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 primarySave & Mark Readybutton. - A
Cancelbutton closes the modal without persisting. - On submit: the modal calls the existing
PATCH /api/v1/collection-map/items/{id}/assignwith{ ready_for_collection: true, ready_notes: "..." }. The route extends its body schema to accept and persist the new field. - Server route writes
ready_notesintoOrderItem.notes(free text). It does NOT create a new PB field. - A small
Noteschip appears on the row whenever the row has anynotescontent, 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.notestext field is the right place. - Changes to other collection-pool actions (assign driver, reschedule, mark complete).
- Migrating old
OrderItem.notesvalues into the new modal prefill.
Current State
Mark Readyin the Collection Pool page callsPATCH /api/v1/collection-map/items/{id}/assignwith{ ready_for_collection: true }. No notes.- The route in
src/app/api/v1/collection-map/items/[id]/assign/route.tsalready accepts a body shape; the body type is{ driverId?, date?, ready_for_collection? }. We extend the type withready_notes?. - The existing
OrderItem.notesfield 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
Noteschip 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 Readyis disabled until the textarea is non-empty.autoFocuson the textarea.- Same wide-modal treatment as the Send PO modal:
sm:max-w-mdis 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
NOTESon the row (next to the existingready/awaitingbadge) whenrow.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:
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 === trueANDready_notesis empty/missing → 400 with{ error: "A note is required when marking ready." }.
Behavior
Operator flow
- Operator clicks
Mark Readyon a row. - Modal opens with an empty textarea (and
Save & Mark Readydisabled). - Operator types a note; the button enables.
- Operator clicks
Save & Mark Ready. The button shows a spinner while the request is in flight. - On 2xx, the modal closes, the row refreshes (it now shows the new note in the chip), and a toast confirms.
- On 4xx/5xx, the modal stays open with the error in the footer.
Server flow
- PATCH
/api/v1/collection-map/items/{id}/assignwith{ ready_for_collection: true, ready_notes: "..." }. - Route validates: if
ready_for_collectionis true andready_notesis 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 Readyopens 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" }writesnotes: "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 noready_notesreturns 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.notesand 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 }withoutready_notesnow 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_noteson 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
noteswrite stays as a no-op write.