WFDASH-WEBAPP/docs/superpowers/specs/2026-07-09-collection-pool-mark-ready-notes-design.md
2026-07-10 00:00:40 +02:00

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 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:

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.