# /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.