UI Patterns Guide
RoundTrip Dispatcher UI — Full Feature Set
Version: 1.0 | Date: March 2026 | Status: Approved
Overview
M8 builds three feature areas on top of the M7 foundation: client management, inventory management, and billing. Every pattern needed already exists in the codebase — M8 is about applying them consistently rather than inventing new ones.
Component Reuse Inventory
Every M8 screen should use these shared components rather than building inline equivalents:
| Component | Location | Used in M8 for |
|---|---|---|
PageHeader | components/common/ | Every page title + subtitle + action button |
SurfacePanel | components/common/ | Every card/panel wrapper |
SectionHeader | components/common/ | Every panel header row |
CountBadge | components/common/ | Counts in section headers |
StatusBadge | components/common/ | Ticket status in service history |
PriorityBadge | components/common/ | Ticket priority in service history |
FieldLabel | components/common/ | All form field labels |
MetaRow | components/common/ | Client detail sidebar rows |
Established Patterns — Apply Consistently
List Page Pattern (from TicketListPage)
PageHeaderwith title, subtitle (total count), and primary action button- Filter bar in a
SurfacePanel— search input + select dropdowns + clear button - Data table in a
SurfacePanel— header row + skeleton rows + empty state + data rows - Priority left-border coloring on rows where priority is relevant
- Pagination below the table when
totalPages > 1 - Every row navigates to detail on click
Detail Page Pattern (from TicketDetailPage)
- Back button (
navigate(-1)) with ArrowLeft icon - Page title from primary identifier (client name, item name, invoice number)
- Two-column grid: main content left (variable width), metadata sidebar right (320px fixed)
SurfacePanel+SectionHeaderfor every content sectionMetaRowfor sidebar metadata fields- Empty states within sections (no notes, no attachments etc.)
Slide-Over Modal Pattern (from CreateTicketModal)
- Dark navy header with Barlow Condensed title
- Scrollable body with
SectionDividergroupings FieldLabelfor every field- Footer with Cancel + primary action button (loading/success states)
- Closes and resets state on success, navigates to created resource
Hook Pattern (from features/tickets/hooks/)
- One hook file per major operation:
useClientList,useClientDetail,useCreateClientetc. - All hooks live in
features/<name>/hooks/ - TanStack Query for all server state
useDebouncefor search inputs (300ms)- Mutation hooks accept an
onSuccesscallback for navigation
API Module Pattern (from lib/api/)
- One file per feature domain:
clientApi.ts,inventoryApi.ts,billingApi.ts - All types co-located in the same file
- Named export object:
export const clientApi = { ... }
M8 Feature Breakdown
TRA-94 — Client Management
Pages/components:
ClientListPage— list with search, commercial/archived filters, "New Client" buttonClientDetailPage— two-column detail with service addresses, contact info, service history tabCreateClientModal— slide-over form (name, contact, primary address)EditClientModal— slide-over form (contact info only — address managed separately)AddAddressModal— slide-over form (add service address to existing client)
New patterns introduced:
- Tab navigation within a detail page — service history is a separate API call loaded on tab switch, not on page load. Prevents loading all history on every detail view.
- Archived state — archived clients show a muted visual treatment with an "Archived" badge. The ArchiveClient action is TenantAdmin only.
API endpoints used:
GET /v1/clients— list with filtersGET /v1/clients/{id}— detail with addressesGET /v1/clients/{id}/service-history— paginated history (loaded on tab switch)POST /v1/clients— createPATCH /v1/clients/{id}— update contact infoPOST /v1/clients/{id}/addresses— add service addressPATCH /v1/clients/{id}/archive— archive (TenantAdmin only)
TRA-95 — Inventory Management
Pages/components:
InventoryListPage— list with search, category filter, low-stock filter toggleInventoryDetailPage— two-column detail with stock history movementsCreateInventoryItemModal— slide-over formAdjustStockModal— slide-over form (positive delta = receive, negative = consume, reason required)
New patterns introduced:
- Low stock indicator — items at or below reorder threshold get a red/amber visual treatment in the list. A "Low Stock" filter toggle shows only those items.
- Stock adjustment — the adjustment form handles both receiving stock (positive number) and manual consumption (negative number). The API takes a delta, not an absolute value.
API endpoints used:
GET /v1/inventory— list with filtersGET /v1/inventory/{id}— detail with stock movement historyPOST /v1/inventory— create itemPATCH /v1/inventory/{id}— update itemPOST /v1/inventory/{id}/adjust— stock adjustmentPATCH /v1/inventory/{id}/archive— archive
TRA-96 — Billing
Pages/components:
InvoiceListPage— list with status filter (Draft, Sent, Paid, Void)InvoiceDetailPage— full detail with line items, totals, payment statusGenerateInvoiceModal— select completed ticket, preview line items, confirm generationSendInvoiceModal— confirm send (email/SMS), shows recipient details
New patterns introduced:
- PDF download — invoice detail has a "Download PDF" button calling
GET /v1/invoices/{id}/pdf - Status workflow — invoices move through Draft → Sent → Paid (or Void). Transition buttons appear based on current status.
- Generate from ticket — the generate modal shows a ticket picker (completed tickets only) and previews the auto-populated line items before creating.
API endpoints used:
GET /v1/invoices— list with status filterGET /v1/invoices/{id}— detail with line itemsGET /v1/invoices/{id}/pdf— download PDFPOST /v1/invoices/generate— generate from completed ticketPOST /v1/invoices/{id}/send— send via email/SMSPOST /v1/invoices/{id}/record-payment— mark as paidPOST /v1/invoices/{id}/void— void invoice
Directory Structure for M8
src/features/
├── clients/
│ ├── hooks/
│ │ ├── useClientList.ts
│ │ ├── useClientDetail.ts
│ │ ├── useClientServiceHistory.ts
│ │ ├── useCreateClient.ts
│ │ ├── useUpdateClient.ts
│ │ └── useAddServiceAddress.ts
│ ├── ClientListPage.tsx
│ ├── ClientDetailPage.tsx
│ ├── CreateClientModal.tsx
│ ├── EditClientModal.tsx
│ └── AddAddressModal.tsx
├── inventory/
│ ├── hooks/
│ │ ├── useInventoryList.ts
│ │ ├── useInventoryDetail.ts
│ │ ├── useCreateInventoryItem.ts
│ │ └── useAdjustStock.ts
│ ├── InventoryListPage.tsx
│ ├── InventoryDetailPage.tsx
│ ├── CreateInventoryItemModal.tsx
│ └── AdjustStockModal.tsx
└── billing/
├── hooks/
│ ├── useInvoiceList.ts
│ ├── useInvoiceDetail.ts
│ ├── useGenerateInvoice.ts
│ └── useSendInvoice.ts
├── InvoiceListPage.tsx
├── InvoiceDetailPage.tsx
├── GenerateInvoiceModal.tsx
└── SendInvoiceModal.tsx
Build Order
Start with clients (TRA-94) — most complete API, familiar patterns, no new UI paradigms except the tab. Then inventory (TRA-95) — straightforward, introduces the stock adjustment pattern. Then billing (TRA-96) — most complex, benefits from patterns established in clients and inventory.
Within each feature, always build in this order:
- API module + types (
clientApi.ts) - List hook + list page
- Detail hook + detail page
- Create/edit modal + mutation hook