WFDASH-WEBAPP/docs/superpowers/specs/2026-09-01-deliveries-dn-items-expand-design.md
Dave 66e2fb9856 docs: clarify unit suffix handling in DN items panel header
The 'Total {sumQty}{unitSuffix}' header line now explicitly documents
how mixed / missing units are handled: suffix is omitted when all
items have empty/null units (operator defaults to 'EA'); when units
are mixed, no suffix is shown; when all items share a non-EA unit
(e.g. BOX), the suffix is shown.

Self-review pass; no other changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 12:01:42 +02:00

9.5 KiB

/deliveries DN Items Expand — Design

Purpose

On /deliveries, each delivery-note row currently shows only header metadata (DN number, status badge, customer / company / order / signed-copy indicator). The operator can't see which items are on that DN without leaving to /delivery-notes/[id], which breaks the at-a-glance matching workflow against the printed paper in their hand.

This spec adds a click-to-expand chevron on each DN row that reveals a small inline list of that DN's line items (description + qty) so the operator can match against the paper before loading the DN.

Scope decisions (confirmed during brainstorming)

  • Interaction: click-to-expand via a chevron at the left edge of the DN row. Independent state per DN (no "expand all"). No persistence across refresh.
  • Item content: description + qty only (no SKU, no price, no invoice number — operator matches against the printed paper, which shows description and qty).
  • Data source: client-side filter of the items already loaded by /api/v1/deliveries. No new server endpoint. Items not in the current page slice (200-item cap) get a "see full DN →" link to /delivery-notes/[id].
  • No PB schema changes. Pure rendering work.

Architecture

Modified files (1):

src/app/(app)/deliveries/page.tsx

No service-layer changes. No new /api/v1/* routes. No new MCP tools. No new PB schema.

State management

The existing DeliveryNoteRowComponent is a stateless component that takes a single dn row. To add expand state without changing every caller, lift state up to the page-level:

  • The page keeps const [expandedDnIds, setExpandedDnIds] = useState<Set<string>>(new Set()).
  • Pass expanded: boolean, onToggle: () => void into DeliveryNoteRowComponent.
  • When toggled, page updates the set (setExpandedDnIds(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; })).

The page already groups items + DNs by customer → order, and we already pass the expanded state per-DN. State hoisting to the page component keeps the data ownership clear (page has the items array, so it knows which DN items belong to).


Feature 1 — DN row expand/collapse

Current DN row (post the just-shipped compact subline change)

[DN DN26082701]                [Draft]
Benjamin · Caliber Machine... · ORD-2026-08-013 · [📎 signed copy]
                                              [Mark loaded] [Signed copy]

Target

The chevron sits at the left edge of the row, just inside the existing <FileText /> icon column:

▸ [DN DN26082701]                [Draft]
  Benjamin · Caliber Machine... · ORD-2026-08-013 · [📎 signed copy]
                                              [Mark loaded] [Signed copy]

(when expanded:)
▾ [DN DN26082701]                [Draft]
  Benjamin · Caliber Machine... · ORD-2026-08-013 · [📎 signed copy]
                                              [Mark loaded] [Signed copy]
┌──────────────────────────────────────────────────────┐
│ 3 items · Total 10 EA                                │
│ ─────────────────────────────────────────────────── │
│ JEI HSS CUTTER LONG 24mm ...           Qty 2        │
│ JEI HSS CUTTER LONG 22mm ...           Qty 2        │
│ JEI HSS CUTTER LONG 18mm ...           Qty 6        │
└──────────────────────────────────────────────────────┘

The items panel sits inside the same <div> as the DN row but below the existing <div className="flex shrink-0 flex-col..."> button column. Visual hierarchy: existing row keeps its cream/amber background; the items panel uses a slightly tinted neutral (bg-muted/30) so the operator's eye reads "DN row + its items" as one unit.

Interaction details

  • Chevron button (lucide ChevronRight / ChevronDown):
    • Position: left edge of the row, before <FileText />
    • Size: h-4 w-4 (matches the existing icon size used elsewhere on the page)
    • aria-expanded={expanded}, aria-controls={items-panel-id}, aria-label="Show items on this delivery note"
    • Hover background: subtle (hover:bg-amber-100/40 to match the amber row tint)
  • Independent state per DN. Each DN toggles its own expand. The page-level Set<string> of expanded DN ids survives re-renders but resets on refresh.
  • No "expand all" / "collapse all" — out of scope. YAGNI.
  • Keyboard: chevron is a <button>, so Enter/Space toggle it natively.

Items panel — rendering

A bordered panel immediately below the existing <div className="flex shrink-0 flex-col items-end gap-1.5"> button column, inside the same outer <div className="flex items-start gap-3 ...">:

  • Header line: "{n} items · Total {sumQty}{unitSuffix}" — right-aligned, text-xs, muted foreground. sumQty is the sum of item.quantity across the panel's items. unitSuffix is empty when the items' units are mixed or empty/null; otherwise it's a single shared unit string (e.g. " EA" or " BOX"). When all items have unit "EA" or empty, the suffix is omitted entirely (the operator's mental default is "EA"). When n === 0, the header line is omitted; the empty-state takes its place.
  • Per item line (<div className="flex items-baseline gap-2">):
    • Description: truncate + title for full text (truncate, text-xs)
    • Right-aligned qty: Qty {n} in text-xs tabular-nums
    • If item.order !== dn.order?.order (DN spans multiple orders — rare): a tiny from ORD-... line under the description in text-[10px] text-muted-foreground
  • Empty state when items.filter(i => i.delivery_note === dn.id).length === 0:
    No items loaded on this delivery note.
    See full DN →
    
    The link <Link href={/delivery-notes/${dn.id}}> is styled as a small primary-coloured link, opens in the same tab. Per-row message — keeps the operator oriented (some DNs legitimately have zero items for ad-hoc counter pickups; this is the fallback for "we don't have the data loaded").
  • Many items: max-h-72 overflow-y-auto on the panel so a DN with 20+ items doesn't push everything else off screen.

Data source — client-side filter

The page already calls listDeliveryItems() which returns up to 200 items per tab with their delivery_note relation id and delivery_note_number text. On render, the items panel computes:

const dnItems = useMemo(
  () => items.filter((i) => i.delivery_note === dn.id),
  [items, dn.id],
);

useMemo keeps the filter from running on every keystroke. The page already has the full items array in scope, so no new fetch.

Edge cases

  • Multi-DN under one order: DNs in the same group expand independently. Items belong to exactly one DN at a time, so the lists are disjoint.
  • Counter-pickup DNs (dn.order === null): items can still be on them via the delivery_note relation on order_items. The items show under the DN regardless. The "from ORD-..." line is suppressed in this case (no parent order to reference).
  • Items paged off: if the DN's items aren't in the page slice (e.g. >200 items loaded, this DN's items at position 201+), the empty-state with /delivery-notes/[id] link is the fallback. Acceptable for v1 — the operator can drill into the DN.
  • Stale data: client-side filter is stale-by-design — if an item is added/removed from a DN after the page loaded, the panel doesn't reflect it until refresh. Acceptable for this at-a-glance workflow.
  • Loading the page fresh: all DNs start collapsed.

Files touched

  • src/app/(app)/deliveries/page.tsx — single-file edit
    • Add page-level useState<Set<string>> for expanded DN ids
    • Pass expanded + onToggle into DeliveryNoteRowComponent
    • Inside DeliveryNoteRowComponent: add the chevron button at the left of the row + an inline items panel below the button column
    • Filter items from the page-level items array

No service / API / schema changes.


What's NOT in scope

  • No new server endpoint.
  • No PB schema changes.
  • No select / checkbox on the items — operator is matching, not selecting.
  • No SKU / price / invoice column per the brainstorming decision.
  • No persistent expand state — refresh collapses everything.
  • No "expand all" / "collapse all" controls.
  • No item count badge on the DN row — YAGNI; can add later.
  • No changes to the existing item rows in the PO groups below the DNs.

Testing

  • Manual smoke check via agent-browser: log in, navigate to /deliveries, expand a DN with items, verify the items panel renders with correct descriptions + qtys. Collapse and re-expand to verify state. Trigger the empty state by expanding a counter-pickup DN with no items.
  • No unit tests for the rendering — the filter is a one-liner (items.filter(i => i.delivery_note === dn.id)) and the panel is presentation. If the page already has a render test, verify it still passes.

Risk assessment

Low. Single-file edit, additive rendering. The expand state is local to the page and resets on refresh. No data layer touched, no schema touched, no API touched. Worst case: a layout glitch on the DN row is easily reverted by removing the new onClick and the inline panel.