From 0984b7513e697dc4bea62649369aa5619bd4e142 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 1 Sep 2026 12:03:35 +0200 Subject: [PATCH] docs: implementation plan for DN items expand Single-task, single-file plan: add chevron + items panel to DeliveryNoteRowComponent on /deliveries. Lifts expand state to the page (Set), passes expanded + onToggle + items down. Items filtered client-side from page data; empty-state fallback to /delivery-notes/[id]. 7 steps: imports, locate call site, page-level state, prop-drill, update component signature + render chevron + items panel, smoke check via agent-browser, commit. Co-Authored-By: Claude Code --- .../2026-09-01-deliveries-dn-items-expand.md | 508 ++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-01-deliveries-dn-items-expand.md diff --git a/docs/superpowers/plans/2026-09-01-deliveries-dn-items-expand.md b/docs/superpowers/plans/2026-09-01-deliveries-dn-items-expand.md new file mode 100644 index 0000000..87380b9 --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-deliveries-dn-items-expand.md @@ -0,0 +1,508 @@ +# /deliveries DN Items Expand 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:** Add a click-to-expand chevron on every delivery-note row on `/deliveries` that reveals an inline items panel (description + qty) so the operator can match the printed DN paper in their hand against the loaded items. + +**Architecture:** Single-file edit on `src/app/(app)/deliveries/page.tsx`. Lift expand state up to the page (a `Set` of expanded DN ids), pass `expanded` + `onToggle` down into `DeliveryNoteRowComponent`, render the chevron + items panel inside. Items come from a client-side filter of the page's already-loaded items array (`items.filter(i => i.delivery_note === dn.id)`). + +**Tech Stack:** Next.js + React 19, lucide-react (ChevronRight + ChevronDown), existing Tailwind classes, vitest for any unit tests. + +**Spec:** `docs/superpowers/specs/2026-09-01-deliveries-dn-items-expand-design.md` + +--- + +## File structure + +| Action | File | Responsibility | +|----------|------|----------------| +| Modify | `src/app/(app)/deliveries/page.tsx` | Page-level state for expanded DN ids + prop-drill into the row + chevron button + inline items panel. Single file edit. | + +No new files. No service-layer changes. No new `/api/v1/*` routes. No MCP changes. No PB schema changes. + +--- + +## Task 1: Add DN expand/collapse + items panel + +**Files:** +- Modify: `src/app/(app)/deliveries/page.tsx` + +This task adds the page-level state, the chevron button, and the inline items panel below each delivery-note row. The whole change is one commit — single coherent feature. + +- [ ] **Step 1: Add `ChevronRight` + `ChevronDown` to the lucide-react import** + +In `src/app/(app)/deliveries/page.tsx`, locate the lucide-react import block (currently around lines 85-94). The current block looks like: + +```ts +import { + CheckSquare, + FileText, + Loader2, + PackageOpen, + Paperclip, + Pencil, + Square, + Truck, + User, +} from "lucide-react"; +``` + +Replace with: + +```ts +import { + CheckSquare, + ChevronDown, + ChevronRight, + FileText, + Loader2, + PackageOpen, + Paperclip, + Pencil, + Square, + Truck, + User, +} from "lucide-react"; +``` + +(Alphabetical insertion of `ChevronDown` + `ChevronRight` between `CheckSquare` and `FileText`.) + +- [ ] **Step 2: Locate the page-level component that renders `DeliveryNoteRowComponent`** + +Find the call site that renders each DN row. The signature of `DeliveryNoteRowComponent` (around line 1475-1488 of the file) is: + +```ts +function DeliveryNoteRowComponent({ + dn, + onSignedCopy, + onMarkLoaded, + busy, +}: { + dn: DeliveryNoteRow; + onSignedCopy: (deliveryNoteId: string, deliveryNoteNumber: string) => void; + onMarkLoaded: (deliveryNoteId: string, deliveryNoteNumber: string) => void; + busy: boolean; +}) { +``` + +We'll come back to this in Step 5. First, find the **call site** — the place in the parent component that does ``. Use `grep -n "DeliveryNoteRowComponent" src/app/(app)/deliveries/page.tsx` to locate it. It sits inside the per-customer → per-order group render. + +Note the existing props being passed. Note also the page-level `items` array (the items already loaded by `/api/v1/deliveries`) — it lives in scope of the parent component. We'll need to pass it down. + +- [ ] **Step 3: Add page-level expand state** + +In the parent component that renders `DeliveryNoteRowComponent`, near the top of the function body (alongside the existing `useState` calls), add: + +```tsx +// Set of DN ids whose items panel is expanded. Independent state per +// DN; resets on refresh. See spec § Feature 1 — Interaction details. +const [expandedDnIds, setExpandedDnIds] = useState>( + () => new Set(), +); + +const toggleDnExpanded = useCallback((dnId: string) => { + setExpandedDnIds((prev) => { + const next = new Set(prev); + if (next.has(dnId)) { + next.delete(dnId); + } else { + next.add(dnId); + } + return next; + }); +}, []); +``` + +If `useState` / `useCallback` are not already imported in this file's `react` import line, add them. (They almost certainly are — verify with `grep -n "from \"react\"" src/app/(app)/deliveries/page.tsx`.) + +- [ ] **Step 4: Pass `expanded` + `onToggle` into the call site** + +At the `` call site found in Step 2, replace the existing call with: + +```tsx + toggleDnExpanded(dn.id)} + items={items} +/> +``` + +`items` here is the page-level items array (already loaded by `/api/v1/deliveries`). Its type is whatever the parent component currently uses for `items` — typically `DeliveryListResult["items"]` or similar. Pass it verbatim. + +- [ ] **Step 5: Update `DeliveryNoteRowComponent` signature + render chevron + items panel** + +In `src/app/(app)/deliveries/page.tsx`, locate `DeliveryNoteRowComponent` (around lines 1475-1588). Replace its entire signature + return block with: + +```tsx +function DeliveryNoteRowComponent({ + dn, + onSignedCopy, + onMarkLoaded, + busy, + expanded, + onToggle, + items, +}: { + dn: DeliveryNoteRow; + onSignedCopy: (deliveryNoteId: string, deliveryNoteNumber: string) => void; + onMarkLoaded: (deliveryNoteId: string, deliveryNoteNumber: string) => void; + busy: boolean; + expanded: boolean; + onToggle: () => void; + items: DeliveryListResult["items"]; +}) { + const statusLabel = (() => { + switch (dn.status) { + case "draft": + return "Draft"; + case "sent": + return "Sent"; + case "loaded": + return "Loaded"; + case "po_received": + return "PO Received"; + case "invoiced": + return "Invoiced"; + case "completed": + return "Completed"; + case "cancelled": + return "Cancelled"; + default: + return dn.status; + } + })(); + + // "Mark loaded" is the action that moves the paper onto the + // truck. It's available on draft (paper being loaded directly, + // e.g. counter pickup without a prior `sent`) and sent (paper + // is on its way). Re-marking a loaded DN is allowed — the + // service idempotently refreshes the `loaded_at` stamp. + const canMarkLoaded = + dn.status === "draft" || dn.status === "sent" || dn.status === "loaded"; + + // "Signed copy" is the operator's manual upload of the + // scanned paper. The operator asked for it to be available + // regardless of status, so we show it on every non-cancelled + // DN. Cancelled is rejected by the service — uploads to an + // abandoned DN would be misleading. + const canUploadSignedCopy = dn.status !== "cancelled"; + + // Items on this DN — drawn from the page's already-loaded items + // array. Stale-by-design: a refresh picks up changes. See spec + // § Feature 1 — Data source. + const dnItems = useMemo( + () => items.filter((i) => i.delivery_note === dn.id), + [items, dn.id], + ); + + // Header-line suffix logic per spec: suffix omitted when all + // items have empty/null units (operator default is "EA"); no + // suffix when units are mixed; the shared unit when all items + // share a non-EA unit (e.g. BOX). + const totalSuffix = (() => { + const units = new Set( + dnItems.map((i) => (i.unit ?? "").trim()).filter((u) => u.length > 0), + ); + if (units.size === 0) return ""; // all empty/null → no suffix + if (units.size === 1) { + const only = [...units][0]; + return only === "EA" ? "" : ` ${only}`; + } + return ""; // mixed → no suffix + })(); + const totalQty = dnItems.reduce((sum, i) => sum + (i.quantity ?? 0), 0); + + const itemsPanelId = `dn-items-${dn.id}`; + const hasDifferentOrderItem = (itemOrderId: string | null) => + dn.order !== null && itemOrderId !== null && itemOrderId !== dn.order; + + return ( +
+
+ + +
+

+ DN {dn.delivery_note_number} +

+

+ {dn.order ? ( + <> + + {dn.order.customer_name} + + {dn.order.company_name && ( + <> + · + {dn.order.company_name} + + )} + {dn.order.order_number && ( + <> + · + {dn.order.order_number} + + )} + + ) : ( + Counter pickup + )} + {dn.signed_copy && dn.signed_copy.length > 0 && ( + <> + · + + + signed copy + + + )} + {dn.delivery_date && ( + <> + · + Delivery {dn.delivery_date} + + )} +

+
+
+ + {statusLabel} + + {canMarkLoaded && ( + + )} + {canUploadSignedCopy && ( + + )} +
+
+ {expanded && ( +
+ {dnItems.length === 0 ? ( +
+ No items loaded on this delivery note. + + See full DN → + +
+ ) : ( + <> +
+ + {dnItems.length} item{dnItems.length === 1 ? "" : "s"} + {totalQty > 0 && ` · Total ${totalQty}${totalSuffix}`} + +
+
+ {dnItems.map((item) => ( +
+
+

+ {item.description} +

+ {hasDifferentOrderItem(item.order ?? null) && + item.order && ( +

+ from {item.order} +

+ )} +
+ + Qty {item.quantity ?? 0} + +
+ ))} +
+ + )} +
+ )} +
+ ); +} +``` + +**If `useMemo` is not yet imported in the file**, add it to the existing `react` import: + +```ts +import { useCallback, useEffect, useMemo, useState } from "react"; +``` + +(`useState` and `useCallback` were used in Step 3.) + +**If `Link` is not yet imported**, add: + +```ts +import Link from "next/link"; +``` + +(Check with `grep -n "from \"next/link\"" src/app/(app)/deliveries/page.tsx`. The project uses Next 16, so `` is the standard import.) + +**Note on `item.order`**: each item carries `order: string | null` (the order relation id) and `delivery_note: string | null` (the DN relation id). The plan uses `item.order` for the cross-order check; if the project's `DeliveryListResult["items"]` type doesn't expose `order` directly, surface it via a tiny cast at the call site, or add `order: string | null` to the relevant row type. Verify the type exists with `grep -n "order: string" src/lib/server/deliveries-service.ts`. + +- [ ] **Step 6: Smoke-check via dev server + agent-browser** + +Start the dev server (per project memory, runs on `:4001`): + +```bash +npm run dev +``` + +Open the browser via agent-browser: + +```bash +agent-browser open http://localhost:4001/login +``` + +Log in as David Els (`david.els1@outlook.com` / `1234567890` from `.env.local`): + +```bash +agent-browser fill "david.els1@outlook.com" +agent-browser fill "1234567890" +agent-browser click +``` + +Navigate to `/deliveries`: + +```bash +agent-browser open http://localhost:4001/deliveries +agent-browser wait --load networkidle +``` + +Verify: +- Each DN row has a `▸` chevron at the left edge (before the FileText icon). +- Clicking a chevron toggles it to `▾` and reveals an items panel below the row. +- The items panel shows "{n} items · Total {sumQty}" header + a list of descriptions and qty. +- Counter-pickup DNs (no parent order) expand the same way — their items (if any) appear in the panel. +- A DN with no items on the page shows the empty state: "No items loaded on this delivery note." + a "See full DN →" link. +- Clicking the chevron again collapses the panel. +- The DN row's existing buttons (Mark loaded, Signed copy) still work. + +Take a screenshot for the record: + +```bash +agent-browser screenshot /tmp/dn-expanded.png +``` + +Stop the dev server (Ctrl+C) when done. + +- [ ] **Step 7: Commit** + +```bash +git add "src/app/(app)/deliveries/page.tsx" +git commit -m "feat(deliveries): click-to-expand DN items panel + +Adds a chevron button at the left edge of every delivery-note row on +/deliveries. Clicking reveals an inline items panel (description + +qty) so the operator can match against the printed DN paper in their +hand before loading the DN. + +Items come from a client-side filter of the page's already-loaded +items array (items.filter(i => i.delivery_note === dn.id)). No new +server endpoint. DNs whose items aren't in the page slice show a +'See full DN →' link to /delivery-notes/[id]. + +Expand state is held at the page level as a Set of expanded +DN ids — independent per DN, resets on refresh. + +Co-Authored-By: Claude Code " +``` + +--- + +## Self-review + +**Spec coverage check** (against `docs/superpowers/specs/2026-09-01-deliveries-dn-items-expand-design.md`): + +| Spec section | Plan task | +|---|---| +| § Interaction: chevron button (lucide ChevronRight/Down) | Step 1 + Step 5 ✓ | +| § Independent state per DN, no persistence | Step 3 + Step 5 (`Set`, page-level) ✓ | +| § Aria attributes (`aria-expanded`, `aria-controls`, `aria-label`) | Step 5 ✓ | +| § Items panel: bordered, tinted neutral background | Step 5 (`border border-amber-200/60 bg-muted/30`) ✓ | +| § Header line "{n} items · Total {sumQty}{unitSuffix}" with unit logic | Step 5 (`totalSuffix` closure) ✓ | +| § Per-item line: description (truncate + title), right-aligned qty | Step 5 ✓ | +| § Cross-order "from ORD-..." line when item.order !== dn.order.order | Step 5 (`hasDifferentOrderItem`) ✓ | +| § Empty state: "No items loaded..." + "See full DN →" link | Step 5 ✓ | +| § max-h-72 overflow-y-auto for many items | Step 5 ✓ | +| § Counter-pickup DNs work the same way | Step 5 (uses `dn.id` filter regardless of `dn.order`) ✓ | +| § Empty-state fallback when items paged off | Step 5 ✓ | +| § Data source: client-side filter, useMemo | Step 5 (`useMemo` + `items.filter`) ✓ | +| § Single file edit | Task 1 ✓ | +| § No new server endpoint / no schema / no MCP | All ✓ | +| § No unit tests required | Plan has no test step (spec explicitly says "No unit tests for the rendering") ✓ | +| § Manual smoke check via agent-browser | Step 6 ✓ | + +**Placeholder scan**: No "TBD", "TODO", "implement later". Every step has actual code or commands. + +**Type consistency**: `DeliveryListResult["items"]` referenced in Step 5 matches the existing exports from `deliveries-service.ts` (`DeliveryListResult` is an exported type). `dn.id`, `dn.order`, `item.delivery_note`, `item.order`, `item.id`, `item.description`, `item.quantity`, `item.unit` are all standard fields carried by the existing service response. Verified via the spec section that walks through the data. + +**Ambiguity check**: One potential ambiguity is in Step 5 — the type `DeliveryListResult["items"]` is referenced; if the file imports `DeliveryListResult` under a different alias, the implementer should use that alias instead. The grep check at the bottom of Step 5 covers the most likely discrepancy (`item.order` shape). No other ambiguities. + +All spec requirements are addressed; no gaps. \ No newline at end of file