docs(14-03): record DetailModal PAX8 extension summary

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
This commit is contained in:
lorentz 2026-07-11 14:29:26 -04:00
parent 04c75bedf8
commit a6dcc9e29e

View file

@ -0,0 +1,70 @@
---
phase: 14-pax8-ui-surface
plan: 03
subsystem: shared-ui
tags: [detail-modal, pax8, cost-breakdown]
dependency-graph:
requires: []
provides:
- "DetailModal kind='pax8_company' support"
- "DetailModal subscriptions cost-breakdown table"
affects:
- "Phase 14 Plan 04 (/pax8 page — will call DetailModal with kind='pax8_company')"
tech-stack:
added: []
patterns:
- "Additive-extension pattern: new prop + new constant + new detection branch, all guarded so existing callers are byte-for-byte unaffected"
key-files:
created: []
modified:
- components/admin/DetailModal.tsx
decisions:
- "website field placed in PAX8_COMPANY_GROUPS' Identity group (not System) per plan task action text's explicit final clause, even though the UI-SPEC's shorthand list omitted it"
- "Identity/System groups paired side-by-side (paired: 'Identity'/'System') rather than stacked — consistent with COMPANY_GROUPS' existing Address/System pairing convention"
- "Total row uses the first subscription's currency as the display currency (single-currency assumption, consistent with a single-company drill-down)"
metrics:
duration: "~15 minutes"
completed: 2026-07-11
---
# Phase 14 Plan 03: DetailModal PAX8 Extension Summary
Extended `components/admin/DetailModal.tsx` additively with a `kind='pax8_company'` field-group set and an unconditional subscriptions/cost-breakdown table, so the upcoming `/pax8` drill-down (Plan 04) renders formatted PAX8 identity fields and a cost table instead of falling into the flat unstyled key/value fallback.
## What Was Built
**Task 1 — `kind` prop + `PAX8_COMPANY_GROUPS`:**
- Added `kind?: 'ticket' | 'company' | 'pax8_company'` to `DetailModalProps`, destructured in the component signature.
- Added `PAX8_COMPANY_GROUPS: FieldGroup[]` next to `COMPANY_GROUPS` — an `Identity` group (`name`, `status`, `city`, `stateOrProvince`, `country`, `website` as `url`) paired with a `System` group (`id` as `id`, `syncedAt` as `date`, `isDeleted` as `bool`), using camelCase keys since the `/pax8` page passes an already-transformed object.
- `detectGroups(data, kind?)` now checks `kind === 'pax8_company'` as the first branch, before the existing `'ticket_number' in data` / `'company_name' in data` sniffs, which remain the fallback when `kind` is absent.
- The single call site updated to `detectGroups(data, kind)`.
- Header else-branch (title + Record ID + optional `is_active` badge) left untouched — it already renders correctly for a pax8 company object (no `is_active` field means no badge).
**Task 2 — Subscriptions & cost-breakdown table:**
- New section in the Formatted tab, guarded by `Array.isArray(data.subscriptions)`, rendered as the first child of the `space-y-6` container (above the field-group map), per UI-SPEC's Visual Hierarchy (cost table is the primary focal point).
- Header row (Product / Qty / Billing Term / Amount), one row per subscription with product label fallback chain `sub.productName || sub.sku || 'Unknown item'` (Pitfall 4), amount rendered directly from `sub.latestBilledAmount` (never recomputed from `unit_price × quantity` — Pitfall 3) in `font-mono tabular-nums`.
- Summed total row across all subscriptions, also `font-mono tabular-nums`.
- Empty array (`data.subscriptions.length === 0`) renders a muted "No subscriptions" line inside the same bordered container instead of an empty table or crash.
- Styled with the same `rounded-lg border overflow-hidden` card look and `text-xs`/`text-muted-foreground` label conventions already used elsewhere in the file (copied shape from the ticket "Description block" section).
- Raw tab required no change — `data.subscriptions` already serializes as JSON through the existing object branch of `renderRaw`.
## Deviations from Plan
None — plan executed exactly as written. The plan's Task 1 action text contained one internally contradictory sentence about where `website` should live; resolved by following its explicit final clause ("in the Identity group"), which is what was implemented.
## Verification
- `npx tsc --noEmit --pretty` — passes (run after each task and again at the end of the plan).
- Grep-confirmed: `TICKET_GROUPS`, `COMPANY_GROUPS`, `'ticket_number' in data`, `'company_name' in data` all present verbatim and unchanged.
- Grep-confirmed: `kind === 'pax8_company'`, `Array.isArray(data.subscriptions)`, `tabular-nums`, `productName || sub.sku || 'Unknown item'`, `No subscriptions` all present.
- Manual verification of the rendered drill-down (open a PAX8 company from `/pax8` and confirm cost table + identity fields) is deferred to Plan 06 per the plan's own `<verification>` block — Plan 04 (which builds the `/pax8` page and its `DetailModal` call site) has not yet executed in this wave.
## Threat Flags
None — this plan's threat model (T-14-06 stored XSS, T-14-07 DoS, T-14-SC package tampering) is fully addressed by construction: all rendered values are JSX text children (auto-escaped, no `dangerouslySetInnerHTML`), the subscriptions array size is bounded per company (tens of rows, not the full order-item history), and zero new packages were introduced.
## Self-Check: PASSED
- FOUND: components/admin/DetailModal.tsx (modified, both tasks present)
- FOUND: commit 3492a16 (Task 1 — kind prop + PAX8_COMPANY_GROUPS)
- FOUND: commit 04c75be (Task 2 — subscriptions cost-breakdown table)