---
phase: 14-pax8-ui-surface
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - components/admin/DetailModal.tsx
autonomous: true
requirements: [PAX8-13]
must_haves:
  truths:
    - "Passing kind='pax8_company' to DetailModal renders a PAX8 identity/system field layout instead of the flat unstyled key/value fallback, per D-02's formatted/raw drill-down pattern"
    - "When the data object carries a subscriptions array, the Formatted tab renders a subscriptions & cost-breakdown table (product, qty, billing term, amount) plus a summed total, above the field groups, grouped by subscription/product per D-03"
    - "Every existing DetailModal caller (tickets, companies) renders exactly as before — the change is purely additive"
  artifacts:
    - path: "components/admin/DetailModal.tsx"
      provides: "Additive kind prop + PAX8_COMPANY_GROUPS + unconditional subscriptions cost-breakdown section"
      contains: "PAX8_COMPANY_GROUPS"
  key_links:
    - from: "DetailModal detectGroups"
      to: "PAX8_COMPANY_GROUPS"
      via: "kind === 'pax8_company' branch"
      pattern: "kind === 'pax8_company'"
    - from: "DetailModal Formatted tab"
      to: "data.subscriptions"
      via: "Array.isArray guard rendering a cost table"
      pattern: "Array.isArray\\(data.subscriptions\\)"
---

<objective>
Extend `components/admin/DetailModal.tsx` additively so it can render a PAX8 company drill-down: a `kind` prop that selects a new `PAX8_COMPANY_GROUPS` field set (fixing the fact that a PAX8 company has `name`, not `company_name`, and would otherwise fall into the flat unstyled fallback), plus a new unconditional Formatted-tab section that renders the subscriptions/cost-breakdown array as a compact table.

Purpose: PAX8-13's cost breakdown (D-02/D-03) is displayed through this shared modal. RESEARCH.md Pitfall 1 is explicit: DetailModal cannot render an array of subscription rows today — no `FieldType` renders a list. Without this extension the drill-down would show an unstyled key dump with no cost data. The extension must NOT touch `TICKET_GROUPS`/`COMPANY_GROUPS` or their detection branches — it is a pure addition, so every other detail view in the app is unaffected.
Output: extended `DetailModal.tsx`.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14-pax8-ui-surface/14-RESEARCH.md
@.planning/phases/14-pax8-ui-surface/14-PATTERNS.md
@.planning/phases/14-pax8-ui-surface/14-UI-SPEC.md

<interfaces>
<!-- Shape the page (Plan 04) will pass into the extended DetailModal. -->
DetailModal will be called by /pax8 as:
  <DetailModal open={...} onOpenChange={...} kind="pax8_company"
    title={`PAX8: ${company.name}`}
    data={{ ...companyHeaderFields, subscriptions: [...], costTotal }} />

company header fields (camelCase already flattened by the page from the /api/pax8/companies/[id] response):
  id, name, status, city, stateOrProvince, country, website,
  autotaskCompanyId, matchedCompanyName, matchMethod, matchConfidence, syncedAt, isDeleted
subscriptions[] element shape:
  { subscriptionId, productName, sku, quantity, billingTerm, status, currency, latestBilledAmount, startPeriod }
costTotal: number

Current DetailModal structure (verified — components/admin/DetailModal.tsx):
  - FieldGroup type: { label, fields: Array<{ key, label, type?: FieldType }>, paired? }
  - TICKET_GROUPS / COMPANY_GROUPS constants (DO NOT MODIFY)
  - detectGroups(data): sniffs 'ticket_number' in data → TICKET_GROUPS, 'company_name' in data → COMPANY_GROUPS, else flat fallback
  - DetailModalProps: { open, onOpenChange, title, data, fields? }
  - Header block (~lines 348-375) branches on 'ticket_number' in data; the else-branch already renders DialogTitle={title} + "Record ID: {data.id}" + optional activeBadge when 'is_active' in data — this else-branch works for a pax8 company as-is (pax8 company has no is_active, so no badge shows)
  - Formatted tab renders groups, then an unconditional "Description block for tickets" (~lines 543-551) — copy THAT block's shape for the new subscriptions section
  - FieldType includes 'date','bool','url','id' — reuse these, no new scalar types needed
  - Amounts must use tabular-nums font-mono (UI-SPEC Typography: numerics in IBM Plex Mono)
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add kind prop + PAX8_COMPANY_GROUPS field set</name>
  <read_first>
    - components/admin/DetailModal.tsx (full file — you must see detectGroups, DetailModalProps, TICKET_GROUPS/COMPANY_GROUPS, and the header ternary before editing)
    - .planning/phases/14-pax8-ui-surface/14-UI-SPEC.md (Component Inventory item 1-2: kind prop + PAX8_COMPANY_GROUPS Identity/System groups)
    - .planning/phases/14-pax8-ui-surface/14-RESEARCH.md (Pitfall 1 — do not touch existing detection branches)
  </read_first>
  <files>components/admin/DetailModal.tsx</files>
  <action>
    Add an optional `kind?: 'ticket' | 'company' | 'pax8_company'` field to `DetailModalProps` and destructure it in the component signature (default undefined). Define a new `PAX8_COMPANY_GROUPS: FieldGroup[]` constant next to `COMPANY_GROUPS` (do not edit the existing constants): an `Identity` group with fields `name` (label 'Name'), `status` (label 'Status'), `city` (label 'City'), `stateOrProvince` (label 'State/Province'), `country` (label 'Country'); and a `System` group `paired: 'Identity'`... — actually pair `System` with `Identity` is fine, or leave System unpaired — with fields `id` (label 'Record ID', type 'id'), `syncedAt` (label 'Synced At', type 'date'), `isDeleted` (label 'Deleted', type 'bool'), and `website` (label 'Website', type 'url') in the Identity group. Change the `detectGroups` signature to `detectGroups(data, kind?)` and add, as the FIRST check inside it, `if (kind === 'pax8_company') return PAX8_COMPANY_GROUPS;` BEFORE the existing `'ticket_number' in data` / `'company_name' in data` sniff branches (which remain untouched as the fallback when `kind` is absent). Update the single call site `const groups = detectGroups(data);` to `const groups = detectGroups(data, kind);`. The header else-branch already renders `title` + record id correctly for a pax8 company — do not modify the header ternary. Note the field keys are camelCase (`stateOrProvince`, `syncedAt`, `isDeleted`) because the page passes an already-transformed object.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `DetailModalProps` includes `kind?: 'ticket' | 'company' | 'pax8_company'` and the component destructures it
    - `PAX8_COMPANY_GROUPS` constant exists (grep confirms) with Identity + System groups using camelCase keys
    - `detectGroups` returns `PAX8_COMPANY_GROUPS` when `kind === 'pax8_company'`, checked before the existing sniff branches (grep: `kind === 'pax8_company'` present)
    - `TICKET_GROUPS` and `COMPANY_GROUPS` and their `'ticket_number' in data` / `'company_name' in data` branches are unchanged (grep: both still present verbatim)
    - `npx tsc --noEmit --pretty` passes
  </acceptance_criteria>
  <done>DetailModal accepts kind='pax8_company' and renders PAX8 identity/system fields; all existing callers unaffected.</done>
</task>

<task type="auto">
  <name>Task 2: Add subscriptions & cost-breakdown array section to the Formatted tab</name>
  <read_first>
    - components/admin/DetailModal.tsx (the "Description block for tickets" at ~lines 543-551 — copy its conditional-wrapper + h3 + rounded-lg border shape)
    - .planning/phases/14-pax8-ui-surface/14-UI-SPEC.md (Visual Hierarchy: cost table is the focal point, renders above field groups; Copywriting; Typography numerics in font-mono tabular-nums)
    - .planning/phases/14-pax8-ui-surface/14-RESEARCH.md (Pitfall 3 use line_total/latestBilledAmount, Pitfall 4 fallback label)
  </read_first>
  <files>components/admin/DetailModal.tsx</files>
  <action>
    In the Formatted `TabsContent`, add a new section rendered when `Array.isArray(data.subscriptions)`. Per UI-SPEC's Visual Hierarchy, render it as the FIRST child of the formatted `space-y-6` container (above the field groups) so it answers "what am I paying for" first. Structure mirrors the ticket Description block: an `<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-3">` reading "Subscriptions & Cost", then a `rounded-lg border overflow-hidden` container holding a compact table. Each subscription row shows: product label = `sub.productName || sub.sku || 'Unknown item'` (Pitfall 4 fallback), quantity, billing term (or '—' when null), and the amount `sub.latestBilledAmount` — formatted as currency (e.g. `sub.currency ?? 'USD'` + `Number(latestBilledAmount).toFixed(2)`) in `font-mono tabular-nums` (UI-SPEC numerics rule); NEVER recompute from unit_price×quantity (Pitfall 3). Render a header row (Product / Qty / Term / Amount) and a final total row summing `latestBilledAmount` across the array, also font-mono tabular-nums. Use the same `text-xs`/`text-muted-foreground`/`Separator`-or-border-row conventions already used in this file — do not introduce a new visual language (UI-SPEC: match rounded-border card look). Guard the empty array (`data.subscriptions.length === 0`) with a muted "No subscriptions" line inside the same bordered container. The Raw tab needs no change — `data.subscriptions` already serializes as JSON through the existing object branch of `renderRaw`.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - A Formatted-tab section guarded by `Array.isArray(data.subscriptions)` exists (grep confirms) and renders before the field-group map
    - Row amounts render `latestBilledAmount` in `font-mono` with `tabular-nums` and there is a summed total row (grep: `tabular-nums` present; no `unit_price` recomputation in the render)
    - Product label uses the `productName || sku || 'Unknown item'` fallback chain
    - Empty subscriptions array shows a "No subscriptions" state, not a crash
    - The ticket "Description block" and all other Formatted-tab logic remain present and unchanged
    - `npx tsc --noEmit --pretty` passes
  </acceptance_criteria>
  <done>The Formatted tab renders a styled subscriptions/cost-breakdown table with a total whenever data.subscriptions is an array; additive-only.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| API JSON → React render | DetailModal renders DB-sourced company/product/subscription strings client-side |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-14-06 | Tampering (stored XSS) | DetailModal rendering product/company names from Postgres | mitigate | Values rendered as React text children (JSX `{value}`), which auto-escapes — no `dangerouslySetInnerHTML`, no raw HTML injection path introduced |
| T-14-07 | Denial of Service | subscriptions array size in the modal | accept | Per-company subscription counts are small (max seen ~tens); order-item history is server-scoped in Plan 01, not passed to the modal — no unbounded client render |
| T-14-SC | Tampering | npm/pip/cargo installs | accept | Zero new packages; only existing shadcn/lucide primitives used |
</threat_model>

<verification>
- `npx tsc --noEmit --pretty` passes
- Manual (Plan 06): open a PAX8 company drill-down and confirm the cost table renders above the identity fields with a correct total, and that an existing ticket/company DetailModal still renders identically
</verification>

<success_criteria>
- `kind` prop + `PAX8_COMPANY_GROUPS` added; existing constants and detection branches untouched
- Subscriptions cost-breakdown table + total renders when `data.subscriptions` is an array, using `latestBilledAmount` (never recomputed)
- Type check green; change is additive
</success_criteria>

<output>
Create `.planning/phases/14-pax8-ui-surface/14-03-SUMMARY.md` when done
</output>
