feat(launch-readiness-batch): UAT drains, navigation refactor, launch infra, trackers

Bundles the rest of the in-flight work from this UAT round into one
checkpoint. Each sub-area is independent; see the headings below.

UAT polish (drained 11 findings from active-uat.md):
- Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl →
  sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews
  aren't cramped at 1440-1920px.
- Notes tab badge aggregation: new countFor{Client,Yacht,Company}
  Aggregated helpers in notes.service mirror the listFor*Aggregated
  symmetric-reach joins. yacht-tabs + company-tabs render the
  badge; client-tabs already had badge support.
- Supplemental-info form polish bundle: BrandedAuthShell gains a
  `width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of
  fixed inset-0 pin so long forms scroll naturally). Form picks up
  port branding (logoUrl + backgroundUrl + appName) via
  loadByToken. Address fields completed (street + city + region +
  postal + country). Port name eyebrow + success-state copy added.
- new-document-menu Upload-file landing toast: per-file completion
  emits toast.success with action link to the destination entity
  or folder.
- interest-tabs OverviewTab "from client" pill on Email + Phone
  rows via new EditableRow `inheritedFrom` prop.
- create-document-wizard subject picker → segmented button strip
  (5 types visible at once).

Launch infra:
- UTM column wiring (Init 1b step 4): migration
  0089_website_submissions_utm.sql adds utm_source/medium/campaign/
  term/content + composite index (port_id, utm_source, received_at)
  for per-campaign rollups. website-inquiries intake accepts the
  five fields. Residential intake intentionally untouched per audit
  scope.
- Invoicing module gate (Init 1c spike): new
  invoices-module.service + invoices layout guard + registry entry
  invoices_module_enabled (default false). Audit conclusion in
  launch-readiness.md: payments table is canonical money path;
  /invoices flow is parallel infrastructure now hidden by default.

Smart-back navigation refactor:
- Replaced breadcrumb component with history-aware Back button.
  New route-labels.ts + use-smart-back hook +
  navigation-history-tracker so back falls through to the parent
  route when there's no prior page in history.
- Sidebar / topbar / mobile-topbar adopt the new pattern; old
  breadcrumb-store kept for back-compat consumers but the
  breadcrumbs component is gone.
- 6 detail pages (admin/errors per-id + codes, invoices/
  upload-receipts, reports kind, tenancies detail, analytics
  metric, client detail) migrated.

Trackers + docs:
- docs/launch-readiness.md — master pre-launch tracker. Includes
  the reports gap audit (cross-cutting filter set, Marketing +
  Financial blockers, custom builder remaining entities, scheduled
  CSV/XLSX, template scope picker).
- docs/superpowers/audits/active-uat.md — 15 findings flipped
  OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining
  (each blocked on user input or cross-repo).
- CLAUDE.md — minor session notes carried forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 22:42:37 +02:00
parent 3bdf59e917
commit cb8292464c
62 changed files with 2944 additions and 662 deletions

View File

@@ -622,6 +622,46 @@ export const REGISTRY: SettingEntry[] = [
defaultValue: false,
},
// ─── Operations - Expenses module ─────────────────────────────────────────
// Port-scoped gate for the entire Expenses + receipt-upload surface.
// Defaults to enabled so existing ports keep the feature on deploy.
// Disabling hides both sidebar entries (Expenses + How to upload
// receipts) AND swaps the routes for a "Module disabled" placeholder so
// bookmarks land on a meaningful page (not a 404) and direct API hits
// are rejected at the layout boundary.
{
key: 'expenses_module_enabled',
section: 'operations.expenses',
label: 'Expenses module',
description:
'When enabled, reps can record expenses and upload receipts (mobile scanner + manual entry). Turning this off hides Expenses + receipt-upload from the sidebar and blocks the routes with a "module disabled" page. Disabling does not delete previously-recorded expense rows.',
type: 'boolean',
scope: 'port',
defaultValue: true,
},
// ─── Operations - Invoices module ─────────────────────────────────────────
// Port-scoped gate for the standalone `/invoices` flow. Audit conclusion
// (2026-05-27, Initiative 1c): the schema is rich (invoices + invoice_line_items
// + invoice_expenses + send/payment routes + PDF) but the dev DB has zero
// rows. The canonical "money received" path goes via `payments`
// (auto-advances pipeline) and the canonical expense-report path goes via
// `expenses → invoices` only for the employee-expense use case. The sidebar
// nav entry was removed earlier; this toggle hides the route too so
// bookmarks land on a clear "module disabled" page instead of an orphaned
// form. Default OFF for new ports; existing ports keep the surface visible
// until an admin explicitly turns it off.
{
key: 'invoices_module_enabled',
section: 'operations.invoices',
label: 'Standalone invoicing module',
description:
'When enabled, the standalone /invoices flow (create invoice → line items → PDF → send → mark paid) is reachable. The canonical "we received money" path in this CRM goes through the Payments tab on an interest (auto-advances pipeline); the standalone invoicing surface is a separate flow primarily for employee expense reports. Disabling hides /invoices entirely (route renders a "module disabled" page); existing rows are preserved.',
type: 'boolean',
scope: 'port',
defaultValue: false,
},
// ─── Residential - partner forwarding ──────────────────────────────────────
{
key: 'residential_partner_recipients',

View File

@@ -174,7 +174,14 @@ export async function resolveSettings(
const out = new Map<string, ResolvedRaw>();
await Promise.all(
keys.map(async (k) => {
out.set(k, await resolveSettingWithSource(k, portId));
try {
out.set(k, await resolveSettingWithSource(k, portId));
} catch {
// Unknown registry key — common when a feature stores settings via
// its own dedicated route (e.g. branding) and a batch caller asks
// for them by key. Skipping keeps the rest of the batch usable;
// single-key callers via getSetting() still fail loud.
}
}),
);
return out;