Skip to content

Staff Entity Detail Page Redesign

Overview

Replace the current mixed edit/display detail pages across every entity under apps/web/src/app/(app)/staff/ with detail views that separate display from edit, in a Stripe-inspired two-column layout. Values render as labels-and-text only — never as inline form inputs mixed with display fields. Mutations route through Vaul drawer sheets and an actions menu. Create flows also use sheets, eliminating /new routes.

This spec describes the contract — invariants, component shapes, per-entity content maps, backend data requirements. Design rationale, wireframes, header examples, and visual layout patterns live in design.md.

Scope

In scope: all detail-bearing routes under apps/web/src/app/(app)/staff/. The redesign applies uniformly — there is no "old pattern" tail.

Originally specified (canonical content maps below): Lockout, User, Location, Resource, Asset, Organization.

Newly in scope (content maps to be added per-entity in plan.md):

  • Mutable: Access Code, Access Gate, Credit Package, Credit, Inquiry, Location Group, Migration, Referral Code, Resource Group, Waitlist
  • Read-only / financial: Payment, Charge, Ledger entry, Lock, Studio Reference (treated per Read-only invariant below)

Reservation as a single shell

Reservations are not split per type. A single Reservation detail page uses one DetailLayout shell and branches its content map on reservation.type. The ReservationType enum (from apps/api/prisma/schema.prisma) is:

Type Main column Sidebar additions
MONTHLY (Lockout) Payments · Transfers · Access Codes (multiple) Subscription section
HOURLY Payment · Access Code (single) Booking window
TOUR Tour notes · Outcome Scheduled at, Tour guide

"Lockout" is shorthand for Reservation.type === 'MONTHLY'. Note this is distinct from ResourceType.STUDIO_MONTHLY (a resource attribute). A Lockout always involves a STUDIO_MONTHLY resource, but the branching driver in this redesign is the reservation type.

Header status chip names the variant (Lockout / Hourly / Tour). The lockout subroute (reservations/lockout/) collapses into the unified reservations/[id]/ route.

Read-only / financial entities

Payments, charges, ledger entries, locks, and studio references render through the same DetailLayout + DetailHeader + sidebar primitives, but:

  • No [Edit X] button, no on sidebar sections.
  • The actions menu may still appear for record-acting operations (Refund, Void, Resend receipt, View in Stripe →) — these act on the immutable record, they do not edit it.
  • Sidebar fields are all none editable.

Single-column fallback for sparse entities

DetailLayout may render single-column when the main column has nothing to show (e.g., a referral code with no usage history). The sidebar promotes to full width. This is part of the same component contract — no separate SingleColumnDetail shape.

Invariants

  • Display is separated from edit. Detail surfaces render values as labels-and-text only. No HeroUI Input / Select / Textarea / Autocomplete is mixed with display fields on a detail page. Mutations route through sheets or the actions menu — never through form inputs embedded inside the display.
  • All edits and creates use a single EditSheet (Vaul drawer, already installed via vaul: ^1.1.2).
  • Two-column desktop, single-column mobile. Main column ≈ 65% (activity / related entities), sidebar ≈ 35% (entity properties). Sidebar stacks below main on mobile. When the main column has no content (sparse entities), the layout collapses to a single full-width sidebar — see Single-column fallback.
  • Status badge is inline with the title, not on the right side competing with header buttons. Each entity's content map declares its status field source (e.g., resource.status); the badge reads the enum and applies a deterministic color via a single shared statusColor(entity, status) helper. Adding a new enum value does not require a spec edit.
  • Three edit affordances per page:
  • [Edit X] button in header → primary sheet with core editable fields.
  • button on sidebar sections → focused metadata sheet for that section.
  • [...] actions menu → rare or destructive operations (cancel, delete, pause, impersonate, "View in Stripe →").
  • Create = sheet, not route. List pages have [+ Add X] opening the same sheet component in create mode. On save: POST → close sheet → redirect to new entity detail page. Existing /new routes are removed.
  • MG ID surfaces in the breadcrumb for every entity. (User MG ID is added as part of this redesign — see MG ID System.)
  • Insurance is a billing concern, not a resource property. Insurance fields appear on the Lockout (subscription) sidebar, never on the underlying Resource.
  • Type fields are immutable post-create: LocationType, ResourceType, AssetCategory. Section sheets must not expose them.
  • Read-only System section present on every entity sidebar: ID, MG ID (where applicable), Created, Updated.

MG ID System

Existing service. Surfaces in breadcrumbs and the sidebar System section for every entity that has one. Formats verified against the integration-test regex (apps/api/src/services/shared/MgIdPresence.integration.test.ts, MgIdAllocation.integration.test.ts).

Entity Format Example Source
Reservation RSV{seq} RSV22 Global sequence
Transaction TXN{seq} TXN89 Global sequence
Waitlist Entry WL{seq} WL15 Global sequence
Asset AST{seq} AST42 Global sequence
Location {TypeCode}{seq} MG7 LOCATION_TYPE_CODE + local seq
Resource {locMgId}-{TypeCode}{seq} MG7-SHR248 Location MG ID + RESOURCE_TYPE_CODE + local seq
Resource Group RGP{seq} RGP12 RESOURCE_GROUP_TYPE_CODE + local seq
Location Group LGP{seq} LGP3 Global sequence
Access Gate {resourceMgId}-DOR{seq} MG7-SHR248-DOR1 Resource MG ID + DOR + local seq
User USR{seq} USR42 Global sequence (new — backend prereq)

design.md and the original spec showed RSV-22 (with dash). The actual format is RSV22 (no dash). Same correction for all global-sequence entities.

User MG ID is a new addition. The original spec excluded users from the MG ID system. This redesign adds them so breadcrumbs, sidebar System sections, and verbal/Slack references work uniformly. Backend prerequisites the plan must include before any User-detail UI work:

  • Add User.mgId column (Prisma schema + migration).
  • Add 'USER' entry to MgIdService allocator with prefix USR.
  • Add 'USR' to type-code constants.
  • Backfill existing users with sequential MG IDs in the migration (or via a one-shot script).

References: apps/api/src/services/shared/MgIdService.ts · packages/shared-constants/src/mg-id.ts

Sheet interaction contract

Behavior shared by every EditSheet (create or edit, primary or section).

Save flow

  1. User clicks Save / Update / Create. Submit button enters loading state (spinner, disabled). Other footer buttons disable.
  2. PATCH/POST fires.
  3. On 2xx: close sheet, invalidate the entity's primary query and any affected table queries (e.g., a primary edit on Resource invalidates useResource(id) and useResources({ locationId })), show a success toast ("<Entity> updated" / "<Entity> created").
  4. On 4xx with field errors: keep sheet open, surface field-level errors inline; show a generic form-level error if no field mapping.
  5. On 5xx / network error: keep sheet open, show form-level error toast ("Couldn't save — try again"), preserve user input. Submit re-enabled.

This is invalidate-and-refetch, not optimistic. Optimistic updates are out of scope for sheets; if any future inline-edit affordance needs them, that surface specifies its own pattern.

Validation error rendering

  • Backend returns errors as { errors: [{ path: 'fieldName', message: 'string' }, ...] } (existing shape from createErrorResponse). Sheet maps path → field, sets RHF error via setError.
  • Errors not mapped to a known field render as a form-level error banner at the top of the sheet body.

Sheet anchoring

Breakpoint Vaul direction Size Close affordances
Desktop ≥md right fixed ~480px width X (top-right) · Cancel button (footer) · ESC · overlay click
Mobile <md bottom ~90vh, drag handle top X (top-right) · Cancel button (footer) · swipe-down · overlay tap

Every dismiss vector — including swipe-down on mobile — triggers the dirty-state guard. Sticky footer holds Cancel + Save / Update / Create. No multi-detent snap points; one large detent is enough.

Concurrent edits

Last-write-wins. No version checks, no 409 handling, no polling. To give operators visibility when conflicts do happen, every entity's sidebar System section includes Updated by <user link> at <timestamp> (in addition to Updated). The user link goes to the User detail page.

Backend: primary GET endpoints must include the most recent updater's user ref in their response. If a Postgres-level updated_by column doesn't exist for an entity, the plan must add a migration to populate it (or use audit-log lookup if cheap enough — plan decides).

Dirty-state guard on close

If the form is dirty (RHF formState.isDirty), all three close vectors prompt before closing:

  • Cancel button
  • ESC key
  • Overlay (outside) click

Prompt copy: Discard changes? with Discard (destructive) and Keep editing buttons. Clean forms close immediately without the prompt.

Detail tables

DetailTable shape ({ title, columns, data, onAdd?, emptyMessage, viewAllHref? }) describes the props. The behavior contract:

Pagination

  • Default: truncated-with-link. Each table shows up to 10 rows; if more exist, the table footer shows View all <count> → linking to the entity's filtered list page (e.g., /staff/reservations?userId=<id>). Per-table override of the row cap is allowed.
  • No inline pagination, no infinite scroll on regular detail-page tables. "I need to see all of them" belongs on the list page.
  • Exception: the Activity table uses a Load more button instead, since audit history grows unbounded and staff often dig chronologically.

Sort and filter

  • Default sort: newest first on every detail-page table (newest createdAt or table-appropriate equivalent — e.g., upcoming first for future-dated tables like scheduled tours).
  • No search or filter UI inside detail-page tables. They're a glance, not a tool. If a user needs to slice the data, "View all →" goes to the list page where filtering already exists.
  • Per-table sort override is allowed when "newest first" doesn't fit the column shape (e.g., Resource Groups → alphabetical).

Loading and error states

Three states per table, standardized:

  • Loading: skeleton rows (3 placeholders matching column count) using Skeleton from apps/web/src/components/atoms/feedback/Skeleton.tsx.
  • Error: error message in the table body with a Retry button. A failed table fetch never blanks the whole detail page — tables fetch independently.
  • Empty: emptyMessage prop renders as muted text in the body.

Same triplet for the primary entity fetch:

  • Loading: full-page skeleton (header + sidebar + main column placeholders).
  • Error: full-page error with Retry.
  • Not found: 404 page (existing convention).

Per-row actions

Every row table — on a detail page or a list page — exposes a per-row ... menu sourced from the row entity's canonical actions function.

  • One definition per entity type. Each entity owns a single rowActions(entity, viewerRoles) → MenuItem[] function (e.g., apps/web/src/lib/entities/payment/rowActions.ts). Both DetailTable and list-page tables consume the same function for their entity type. No surface-specific overrides — a Payment row's menu on Customer → Payments is identical to its menu on /staff/payments.
  • Role gating lives inside the function. It filters its returned items by the role booleans passed in. UI hides items it didn't return (no disabled state).
  • Click behavior: the row itself navigates to the entity's detail page; the ... menu is a separate hit target.
  • Destructive actions (delete, cancel) are placed last with a divider above them.
  • Spec does not enumerate menus per (entity, table). Each entity's rowActions file is the single source.

DetailTable props gain a required rowActions: (row) => MenuItem[] prop.

Activity table

Every detail page renders an Activity section as the last block of the main column, populated from the existing AuditLog model.

  • Source: GET /admin/audit-logs?tableName=<table>&recordId=<id> (route lives at /admin/audit-logs but is gated by isStaffOrAbove, so visible to any staff). Plan must verify or add the tableName + recordId filter params.
  • Row format: <actor link> <action> <field summary> · <relative time>. Impersonated actions surface as <impersonator> as <actor>. Click row → /admin/audit-logs/[id].
  • Pagination: Load more button (the only place this pattern is allowed). Initial 10, +10 per click.
  • Visibility: same as the backend — any staff.

Permissions

The redesign defers entirely to the existing role system — see roles.md and apps/api/src/utils/auth/role-check.ts. There is no new permission catalog or per-action gating layer.

Conventions detail pages must follow:

  • Server pages pass role booleans down. Detail pages compute role flags from sessionData and pass isAdmin, canMutate (or similar entity-specific flags) into the client component — same pattern as apps/web/src/app/(app)/staff/credits/[id]/page.tsx.
  • Affordances hide, not disable, when the user lacks the role. A [Edit X] button, a section button, or an actions-menu item the user can't use is omitted from the rendered tree.
  • Read-all, edit-scoped. Matches the existing backend pattern: read endpoints gate on isStaffOrAbove (scope-blind), so any staff member can view any detail page or cross-location data. Mutation endpoints gate on isStaffOrAboveAt(locationId) (or isAdmin), so location-scoped staff can only edit entities at their scoped locations. The UI mirrors this: detail pages render fully for any staff, but [Edit X] / / mutating actions-menu items are hidden when the viewer's role doesn't satisfy the entity's location scope.
  • Mutation gating is enforced server-side. Sheet save handlers do their own role check inside the route handler; the UI gate is for affordance, not security.

Per-entity action visibility (which actions appear in which actions menu, who can see them) is derived from the existing role helpers, not enumerated here.

Frontend contract

Component shapes

File locations and the proposed directory tree live in design.md. The contract is the props and responsibility per component:

Component Props Responsibility
DetailLayout children (main), sidebar Two-column responsive grid; collapses to single column on mobile (main first, sidebar after).
DetailHeader breadcrumbs, title, subtitle, status, primaryAction, actionsMenu 3-row header: breadcrumb + buttons / title + status / subtitle.
DetailSection title, onEdit?, children Sidebar card with optional button. Calls onEdit to open the section's sheet.
DetailTable title, columns, data, onAdd?, emptyMessage, viewAllHref? Main column table. Optional [+] add, optional "View all →" link, row actions via ....
DisplayField label, value, href?, copyable?, description? Label + value sidebar field. Link styling for entity refs, copy button for IDs.
DisplayGrid children Layout container for DisplayField groups.
EditSheet title, isOpen, onClose, onSave, isLoading, mode: 'create' \| 'edit', children Vaul drawer from right. Sticky footer (Cancel + Save / Update / Create). Same component for both modes.

Per-entity content map

Each detail page renders the sections below. Editable column: primary = via header [Edit X] sheet · section = via sidebar · none = read-only.

Reservation (single shell, content branches on type)

ReservationType enum: MONTHLY | HOURLY | TOUR (source: apps/api/prisma/schema.prisma). One detail page handles all three; the lockout subroute (reservations/lockout/) collapses into the unified reservations/[id]/.

Common to all types

Sidebar always includes Details (Customer, Resource, Location) and System (ID, MG ID RSV-{globalSeq}, Created, Updated). Header status chip names the variant.

Type-specific content

Type Main column Type-specific sidebar sections Primary sheet fields Actions menu
MONTHLY (Lockout) Payments · Transfers · Access Codes (multiple) Subscription (Stripe Sub link, Billing Anchor, Monthly Total + insurance breakdown, Insurance, Promo); Notes (section) Location, Resource, Card Price, ACH Price, Insurance, Timing (immediate | next_cycle). Proration preview via POST /lockouts/[id]/preview-transfer. View in Stripe → · Cancel subscription
HOURLY Payment (single) · Access Code (single) Booking (Start, End, Duration, Price); Notes (section) Resource, Start, End, Notes Mark no-show · Cancel reservation · View in Stripe →
TOUR Tour Notes · Outcome Tour (Scheduled at, Tour guide, Source/referrer); Notes (section) Scheduled at, Tour guide, Notes Mark no-show · Cancel tour

The original spec referenced STUDIO_MONTHLY — corrected here. The actual enum value is MONTHLY. Lockout-vs-not is determined by type === 'MONTHLY'.

User

Main: Reservations · Transactions · Credit Balances · Access Codes · Waitlist.

Sidebar:

Section Fields Editable
Profile Name, Email (read-only), Phone, Bio section
Status Approved, Organization (link) section
Billing Stripe Customer (link), Stripe Balance section
Notes Admin notes section
System ID, MG ID, Auth ID, Created, Last Login none

Primary sheet fields: Name, Email (read-only), Phone, Bio, Approved status.

Actions menu: Send password reset · Impersonate · View in Stripe → · Delete user.

Location

Main: Resources · Access Gates · Images (grid) · Directions (list).

Sidebar:

Section Fields Editable
Details Type (immutable), Opened, Slug, Description section
Address Street, City, State, ZIP, Timezone, Coordinates, Parking section
Contact Phone, Email, Website, Manager (link) section
Pricing Monthly Rate, Hourly Rate, Peak Multiplier, Insurance settings section
Integrations Stripe Account (link), UniFi Group section
System ID, MG ID, Created, Updated none

Primary sheet fields: Name, Status, Description, Notes, Slug, Opened date.

Actions menu: View in Stripe → · Deactivate.

Resource

Main: Reservations · Asset Placements · Resource Groups (membership list) · Images (grid).

Sidebar:

Section Fields Editable
Details Type (immutable), Status, Availability, Description, Featured primary
Location Location (link), Acuity Calendar primary
Physical Dimensions, Area, Max Occupancy, Floor, Wing section
Pricing Card Price, ACH Price, Rate per Sq Ft, Peak settings section
Notes Admin notes section
System ID, MG ID, Created, Updated none

Primary sheet fields: Name, Status, Description, Availability, Featured, Acuity Calendar ID.

Actions menu: Delete resource.

Asset

Main: Placement History · Component Installations (when applicable).

Sidebar:

Section Fields Editable
Details Category (immutable), Subcategory, Status, Condition, Consumable, Description primary
Equipment Manufacturer, Model, Serial Number section
Purchase Date, Price, Vendor, Order Number section
Warranty Expiration, Notes section
System ID, MG ID, Created, Updated none

Primary sheet fields: Name, Status, Condition, Description, Consumable flag.

Actions menu: Delete asset.

Organization

Main: Members (users matching email domain) · Credit Balances.

Sidebar:

Section Fields Editable
Details Name, Email Domain primary
Owner Owner (link) section
System ID, Created, Updated none

Primary sheet fields: Name, Email Domain.

Actions menu: Delete organization.

Pending content maps

The entities below are in scope but their content maps are deferred to plan.md (or per-entity sub-specs). Each map must follow the template above (sections + editable column + primary sheet fields + actions menu).

Entity Route Mutability Notes
Access Code staff/access-codes/[id] mutable Per-reservation (already on Reservation main column); standalone view too
Access Gate staff/access-gates/[id] mutable UniFi device binding
Credit Package staff/credit-packages/[id] mutable Pricing + active flag
Credit staff/credits/[id] mutable Balance ledger; consider read-only treatment
Inquiry staff/inquiries/[id] mutable Status transitions (new → contacted → converted)
Location Group staff/location-groups/[id] mutable Sets of locations for grouping
Migration staff/migrations/[id] mutable Tenant migration submissions
Referral Code staff/referral-codes/[id] mutable Code, partner, payout config
Resource Group staff/resource-groups/[id] mutable Sets of resources for floor/wing
Waitlist Entry staff/waitlist/[id] and staff/waitlist/invitations/[id] mutable Two related sub-entities
Payment staff/payments/[id] read-only Per Read-only invariant
Charge staff/charges/[id] read-only Per Read-only invariant
Ledger entry staff/ledger/[id] read-only Per Read-only invariant
Lock staff/locks/[id] read-only Per Read-only invariant
Studio Reference staff/studio-reference/[id] read-only Reference data; verify route shape

Reused field components

These existing molecules are used inside sheets. Do not rewrite — sheets reuse Zod schemas and selectors as-is:

LocationSelector · ResourceSelector · UserSelector · InsuranceSelector · CouponSelector · AccessCodeSelector · RecipientInput · AddressAutocomplete · AssetSelector · FormFieldGroup · PeakHours

Backend contract

Each detail page hydrates from a single primary endpoint plus per-table secondary endpoints. Tables load separately so they paginate independently.

Primary endpoints — required relations

Entity Endpoint Must include
Lockout GET /reservations/[id] reservation + customer + resource + location + payments[] + stripeSubscriptionId (string only — live Stripe data comes from subscription-metadata, see Live Stripe data is a separate fetch)
User GET /users/[id] user + organization (if any) + roles[]
Location GET /locations/[id] location + manager + Stripe account ref
Resource GET /resources/[id] resource + location + resource group memberships[]
Asset GET /assets/[id] asset + current placement (if any)
Organization GET /organizations/[id] organization + owner

View in Stripe URL helper

View in Stripe → items across every actions menu use a single shared helper. Avoids 15+ slightly-wrong implementations.

  • Location: packages/shared-utils/src/stripe/dashboardUrl.ts (or co-located with StripeService — plan decides).
  • Signature: stripeDashboardUrl(resource: 'customer' | 'subscription' | 'payment' | 'invoice' | 'price' | ..., id: string): string
  • Output: https://dashboard.stripe.com/<test/?><resource-path>/<id> — the test/ segment appears iff the env is non-prod (env source: same flag the existing StripeService uses).
  • All transfers are on a single platform account; no Connect account routing in the URL. Live transfers to other Connect accounts read through that platform.

The actions menu wires View in Stripe → to window.open(stripeDashboardUrl(...), '_blank'). No external resolution needed in the backend.

Action endpoints policy

Every action listed in any actions menu must have a working backend endpoint at ship time. Items without an endpoint are stripped from the UI; no "Coming soon" tooltips, no disabled placeholders. Backend gaps stay in Future actions backlog until their endpoints land in a follow-up spec.

Future actions backlog

Stripped from launch menus pending backend work. Each gets re-added once its endpoint exists.

Action Entity Notes
Pause billing Reservation (MONTHLY) No /lockouts/[id]/pause endpoint today
Send payment link Reservation (MONTHLY) No payment-link generator endpoint
Refund Payment / Reservation No refund endpoint
Resend receipt Payment No resend-receipt endpoint
Convert tour to lockout Reservation (TOUR) No conversion endpoint
Duplicate location Location No duplicate endpoint
Duplicate resource Resource No duplicate endpoint
Duplicate asset Asset No duplicate endpoint
Export resources Location No export endpoint
Mark tour completed Reservation (TOUR) Probably PATCH status; verify in plan

Live Stripe data is a separate fetch

Primary entity GETs return DB-stored columns only — they do not call Stripe. Display values that require a live Stripe API call (insurance breakdown, promo code display, current period dates, default payment method) live behind dedicated endpoints (e.g., GET /reservations/[id]/subscription-metadata, GET /users/[id]/stripe-balance). Detail pages render the DB-known parts immediately and lazy-fetch the Stripe-dependent parts. A failed Stripe fetch does not blank the page — it surfaces an inline error in the affected sidebar section, per the Loading and error states contract.

Confirmed table endpoints (existing)

Endpoint Used by
GET /reservations/[id]/subscription-metadata Lockout subscription card
GET /reservations/[id]/access-codes Lockout access codes table
POST /lockouts/[id]/preview-transfer Lockout primary edit sheet preview
GET /resources?locationId=X Location resources table
GET /access-gates?locationId=X Location access gates table
GET /asset-placements?resourceId=X Resource asset placements table
GET /users/[id]/stripe-balance User billing card
GET /locations/[id]/direction-steps Location directions list
GET /locations/[id]/images Location images grid

Endpoints to verify or add

The frontend content map references the following queries that may not exist in the needed shape today. The plan must enumerate which exist, which need parameter changes, and which are new:

  • Transfers per reservation — Lockout main column needs a transfers table. Confirm shape: GET /reservations/[id]/transfers or surface from existing payment/transaction endpoints.
  • User reservations / transactions / credit balances / access codes / waitlist — User detail needs five tables filtered by userId. Some hooks exist (usePayments, useReservations, useCredits); confirm each accepts userId and returns the staff-needed fields.
  • Cross-reservation access codes for a user — Distinct from per-reservation access codes; aggregation may not exist.
  • Resource reservations — Verify GET /reservations?resourceId=X returns the staff-needed fields.
  • Asset placement historyasset-placements?assetId=X may need an includeRemoved flag and removed-date in response.
  • Asset component installations — Shape unclear; confirm before relying on it.
  • Organization members — Users matching email domain; may need GET /organizations/[id]/members.
  • Organization credit balancesGET /organizations/[id]/credit-balances or similar; verify.

Mutation endpoints

Sheets PATCH to existing entity endpoints (PATCH /resources/[id], etc.) with partial payloads scoped to the section. Plan must enumerate any handlers that don't accept the full field set the section sheet exposes (notably Location section sheets, which currently route through one large form).

Create sheets POST to existing collection endpoints (POST /resources, etc.) and on success redirect to the new detail page.

Testing strategy

Per-entity acceptance criteria the plan must verify:

  1. Render. Detail page renders two-column on desktop, single column on mobile. MG ID present in breadcrumb (where applicable).
  2. Primary edit. [Edit X] opens sheet pre-filled with current values. Save mutates and refreshes view.
  3. Section edit. Each opens scoped sheet. Save mutates only the section's fields.
  4. Actions menu. Each item routes correctly (modal confirm, external link, mutation).
  5. Create flow. [+ Add X] on list opens sheet in create mode. Save POSTs and redirects to new detail page.
  6. Empty states. Tables show empty messages; sidebar fields show for null values.
  7. Cross-entity links. Sidebar entity links navigate correctly (customer → user, resource → resource).
  8. Regressions. Existing test suites continue to pass.

Known gaps

Spec leads code. Tracked in plan.md.

Open items the plan must resolve:

  • Author per-entity content maps for the newly in-scope entities (see Scope). Each follows the same template as the original 6.
  • Confirm or design the endpoints flagged in Endpoints to verify or add.
  • Decide whether to split implementation into separate frontend / backend plans, or combine.
  • Confirm entity rollout ordering (design.md proposes Resource as the prototype because it has variety without subscription complexity). Reservation's three-type branching is a candidate second prototype.
  • StaffFormLayout / FormSection / LabeledValue are removed once all staff entities migrate. Until then they remain available to in-flight migrations.