diff --git a/docs/superpowers/plans/2026-07-09-collection-confirm-rewrite.md b/docs/superpowers/plans/2026-07-09-collection-confirm-rewrite.md new file mode 100644 index 0000000..06a8900 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-collection-confirm-rewrite.md @@ -0,0 +1,440 @@ +# Collection Confirm Rewrite — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rewrite the Collection Confirm row card layout (no more 3-column cram) and fix the actor hydration so the "actor id is empty" toast never fires for a signed-in operator. + +**Architecture:** One file (`src/app/(app)/collection-confirm/page.tsx`) gets two changes: a stacked-vertical `ConfirmCard` JSX rewrite, and a cookie-based actor hydration effect. + +**Tech Stack:** React 19, shadcn/ui primitives, existing `getClientIdentity` helper. + +**Spec:** [docs/superpowers/specs/2026-07-09-collection-confirm-rewrite-design.md](file:///Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/docs/superpowers/specs/2026-07-09-collection-confirm-rewrite-design.md) + +--- + +## File Structure + +| File | Responsibility | Touched? | +| --- | --- | --- | +| `src/app/(app)/collection-confirm/page.tsx` | Cookie-based actor hydration, identity pill, rewritten `ConfirmCard` | Modify | + +--- + +## Task 1: Cookie-based actor hydration + +**Files:** +- Modify: `src/app/(app)/collection-confirm/page.tsx` + +- [ ] **Step 1: Update the imports** + +Add `getClientIdentity` to the existing `@/lib/dev-identity` import. Find the import block (around line 1–12). Add the new import. If `@/lib/dev-identity` is not yet imported, add a fresh import line. + +```ts +import { getClientIdentity } from "@/lib/dev-identity"; +``` + +(Keep all existing imports.) + +- [ ] **Step 2: Replace the localStorage hydration effect** + +Find the `useEffect` that reads `localStorage.wfdash.actor` (around lines 151–163). Replace it with a cookie-based hydration: + +```ts +useEffect(() => { + if (typeof window === "undefined") return; + // The dev-identity switcher writes the active email to the + // `wfdash_dev_email` cookie. The earlier localStorage-based + // hydration never matched because nothing wrote to localStorage. + // getClientIdentity() returns the cookie value or the default + // (DEFAULT_DEV_EMAIL) when unset, so the actor is never empty + // for a signed-in operator. + const email = getClientIdentity(); + if (!email) return; + const name = email.split("@")[0] || email; + setActor({ id: email, name }); +}, []); +``` + +- [ ] **Step 3: Lint** + +```bash +npx eslint 'src/app/(app)/collection-confirm/page.tsx' 2>&1 | tail -n 6 +``` + +Expected: exit 0. Pre-existing errors on unrelated lines are fine. + +- [ ] **Step 4: Commit (hydration only)** + +```bash +git add 'src/app/(app)/collection-confirm/page.tsx' +git commit -m "fix(collection-confirm): hydrate actor from dev-identity cookie, not localStorage" +``` + +--- + +## Task 2: Identity pill in the page header + +**Files:** +- Modify: `src/app/(app)/collection-confirm/page.tsx` + +- [ ] **Step 1: Add a `displayName` state for the header pill** + +Right after the existing `actor` state declaration (line 111), add: + +```ts +// Display name for the header pill. Mirrors the dev-identity +// switcher's `shortName` helper. Kept in state (rather than +// recomputed on every render) so the pill is stable across +// re-renders triggered by the row data. +const [displayName, setDisplayName] = useState(""); +``` + +- [ ] **Step 2: Populate the display name in the same effect as Task 1** + +Inside the `useEffect` from Task 1, add the side effect to set the pill. The new effect body: + +```ts +useEffect(() => { + if (typeof window === "undefined") return; + const email = getClientIdentity(); + if (!email) return; + const name = email.split("@")[0] || email; + setActor({ id: email, name }); + setDisplayName(name); +}, []); +``` + +- [ ] **Step 3: Add the pill to the page header** + +Find the page `
` block (around lines 372–385). Update it to add a `displayName` pill next to the existing `Refresh` button. The new header markup: + +```tsx +
+
+

+ + Collection Confirm +

+

+ Review collected order items, adjust quantity, add notes, and confirm for invoicing. +

+
+
+ + + {displayName || "—"} + + +
+
+``` + +Note: `UserRound` is already imported in the page's existing `lucide-react` import block. If you need a different icon (the spec uses `User`; the existing page imports `UserRound`), prefer `UserRound` since it’s already in scope. If `UserRound` is not in the import list, add it. + +- [ ] **Step 4: Lint** + +```bash +npx eslint 'src/app/(app)/collection-confirm/page.tsx' 2>&1 | tail -n 6 +``` + +Expected: exit 0. Pre-existing errors on unrelated lines are fine. + +- [ ] **Step 5: Commit** + +```bash +git add 'src/app/(app)/collection-confirm/page.tsx' +git commit -m "feat(collection-confirm): show signed-in-as pill in the page header" +``` + +--- + +## Task 3: Rewrite `ConfirmCard` JSX (stacked vertical layout) + +**Files:** +- Modify: `src/app/(app)/collection-confirm/page.tsx` + +- [ ] **Step 1: Replace the `ConfirmCard` return value** + +Find the entire `return` block of `ConfirmCard` (starts at the `return (` after the `const entries = ...` line, ends at the `` before the closing brace of the function). Replace it with the new stacked-vertical layout: + +```tsx +return ( +
  • + {/* ── Header: checkbox + supplier + description + links ─── */} +
    +
    + onToggleSelected(row.id)} + disabled={busy} + /> +
    +
    +

    + {row.expand?.purchase_order?.expand?.supplier?.suppliername ?? "Unknown supplier"} +

    +

    {row.description || "(no description)"}

    +
    + {row.expand?.order?.expand?.customer?.customer && ( + + + {row.expand.order.expand.customer.customer} + + )} + {row.expand?.order?.expand?.company?.company && ( + + + {row.expand.order.expand.company.company} + + )} + {row.expand?.purchase_order?.po_number && ( + + + {row.expand.purchase_order.po_number} + + )} + {row.expand?.order && ( + + Order {row.expand.order.order_number ?? row.expand.order.id.slice(0, 6)} + + + )} +
    +
    +
    + + {/* ── Details: qty + problem + invoice + attachments ─────── */} +
    +
    +
    + Ordered + {row.order_qty ?? row.quantity ?? 0} +
    +
    + Collected + {row.qty_collected ?? 0} +
    + {row.problem_flag && ( +
    + + Problem: {row.problem_type ?? "flagged"} +
    + )} + {row.collection_photo && ( + + )} +
    +
    + {attachments.length > 0 && ( +
    + {attachments.map((name) => ( + + + {displayFilename(name)} + + ))} +
    + )} + {invoice && ( +
    + Invoice {invoice.number} · {invoice.date} +
    + )} +
    +
    + + {/* ── Actions: 2-up grid on sm, 3-up on lg, primary spans 2 ── */} +
    + + + + + + {!canConfirm && ( +

    + Add a confirming note before invoicing this row. +

    + )} +
    + + {/* ── Notes feed (unchanged) ─────────────────────────── */} +
    +

    Notes

    + {entries.length === 0 ? ( +

    No notes yet.

    + ) : ( +
      + {entries.map((entry, idx) => ( +
    • +
      + {new Date(entry.ts).toLocaleString()} + · + {entry.actorName} + · + {categoryLabel(entry.category)} +
      +

      {entry.body}

      +
    • + ))} +
    + )} +
    + + {/* ── Dialogs (unchanged) ────────────────────────────── */} + setQuantityOpen(false)} + onSubmit={(qty, reason) => { + setQuantityOpen(false); + return onQuantity(row, qty, reason); + }} + /> + + setNoteOpen(false)} + onSubmit={(body, options) => { + setNoteOpen(false); + return onAddNote(row, body, options); + }} + /> + + + {row.collection_photo ? ( + /* eslint-disable-next-line @next/next/no-img-element */ + Collection photo + ) : ( +

    No photo attached.

    + )} +
    + + setAttachOpen(false)} + onSubmit={async (file) => { + setAttachOpen(false); + await onAttach(row, file); + }} + /> + + { + if (confirmModal?.bulkRows && confirmModal.bulkRows.length > 0) { + await onSubmitBulkConfirm(confirmModal.bulkRows, invoice); + } else { + await onSubmitConfirm(row, invoice); + } + }} + /> +
  • +); +``` + +- [ ] **Step 2: Lint** + +```bash +npx eslint 'src/app/(app)/collection-confirm/page.tsx' 2>&1 | tail -n 12 +``` + +Expected: exit 0. Pre-existing errors on unrelated lines (lines we already saw in the audit — 278 `setBy` type) are fine. + +- [ ] **Step 3: Run the page tests (if any)** + +```bash +npm run test:run -- 'src/app/(app)/collection-confirm' +``` + +Expected: same passing set as before (we have a pre-existing failure on `view-model.test.ts:278` for the `setBy` type — unrelated to this change). No new failures. + +- [ ] **Step 4: Commit** + +```bash +git add 'src/app/(app)/collection-confirm/page.tsx' +git commit -m "feat(collection-confirm): rewrite row card as stacked vertical sections" +``` + +--- + +## Task 4: Final verification + +- [ ] **Step 1: Targeted tests** + +```bash +npm run test:run -- 'src/app/(app)/collection-confirm' +``` + +Expected: same passing set as before, no new failures. + +- [ ] **Step 2: Full suite** + +```bash +npm run test:run +``` + +Expected: same passing set as before. Pre-existing failures are acceptable. + +- [ ] **Step 3: Manual smoke** + +1. Open the Collection Confirm page. +2. Confirm the page header shows a "Signed in as X" pill (the local-part of your dev identity). +3. Confirm every row card reads top-to-bottom: header → details (qty + problem + attachments + invoice) → actions (in a 2-up grid on small screens, 3-up on large) → notes feed. +4. Pick a row with `Confirm for invoicing` enabled. Click it. The Confirm Invoice dialog opens. +5. Enter a date and invoice number, click `Confirm`. The submit succeeds — **no "actor id is empty" toast**. +6. If the row is fresh in dev (cookie unset), the page should still hydrate to `DEFAULT_DEV_EMAIL` (`david.els1@outlook.com`) and the confirm should work. \ No newline at end of file