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

@@ -14,6 +14,7 @@ import {
Building2,
Receipt,
FileText,
FileBarChart,
Inbox,
Camera,
Globe,
@@ -55,6 +56,11 @@ interface SidebarProps {
/** Per-port `tenancies_module_enabled` resolution. Gates the Tenancies
* sidebar entry. Resolved server-side in the dashboard layout. */
tenanciesModuleByPort?: Record<string, boolean>;
/** Per-port `expenses_module_enabled` resolution. Gates the Expenses
* + How-to-upload-receipts sidebar entries. Resolved server-side in
* the dashboard layout. Defaults to true (feature on) per port when
* the map is missing for the active port. */
expensesModuleByPort?: Record<string, boolean>;
}
interface NavItem {
@@ -80,6 +86,12 @@ interface NavItemGated extends NavItem {
/** When true, only render this item if the tenancies module is enabled
* for the current port. Resolved against `tenanciesModuleByPort`. */
requiresTenanciesModule?: boolean;
/** When true, only render this item if the expenses module is enabled
* for the current port. Resolved against `expensesModuleByPort`. */
requiresExpensesModule?: boolean;
/** When true, only render this item if Umami analytics is wired up
* for the port. */
umamiRequired?: boolean;
}
function buildNavSections(portSlug: string | undefined): NavSection[] {
@@ -123,17 +135,26 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
{
title: 'Insights',
marinaRequired: true,
umamiRequired: true,
items: [
// Reports surface (dashboard / clients / berths / interests
// builders, plus templates / schedules / runs). Routes existed
// since the report-builder ship but the sidebar entry was never
// wired - reps had to land here via direct link.
{
href: `${base}/reports`,
label: 'Reports',
icon: FileBarChart,
},
// Marketing / Umami integration. Distinct from the main dashboard
// (which is sales-focused) so the audience and the metrics don't
// compete for visual real estate. Whole section is hidden when
// Umami isn't wired up - see SidebarContent.
// compete for visual real estate. Hidden when Umami isn't wired
// up via the per-item umamiRequired flag below.
{
href: `${base}/website-analytics`,
label: 'Website analytics',
icon: Globe,
},
umamiRequired: true,
} as NavItemGated,
],
},
{
@@ -145,7 +166,12 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
title: 'Financial',
marinaRequired: true,
items: [
{ href: `${base}/expenses`, label: 'Expenses', icon: Receipt },
{
href: `${base}/expenses`,
label: 'Expenses',
icon: Receipt,
requiresExpensesModule: true,
} as NavItemGated,
// Invoices nav entry removed - the expense-to-PDF flow is the
// only invoicing surface now (employee expense reports). The
// standalone /invoices route still exists for any back-compat
@@ -157,7 +183,8 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
href: `${base}/invoices/upload-receipts`,
label: 'How to upload receipts',
icon: Camera,
},
requiresExpensesModule: true,
} as NavItemGated,
],
},
{
@@ -252,6 +279,7 @@ function SidebarContent({
hasMarinaAccess,
hasResidentialAccess,
tenanciesModuleEnabled,
expensesModuleEnabled,
user,
ports,
currentPort,
@@ -266,6 +294,7 @@ function SidebarContent({
hasMarinaAccess: boolean;
hasResidentialAccess: boolean;
tenanciesModuleEnabled: boolean;
expensesModuleEnabled: boolean;
user?: SidebarProps['user'];
ports?: Port[];
currentPort: Port | null;
@@ -389,6 +418,8 @@ function SidebarContent({
const gated = item as NavItemGated;
if (gated.requiresTenanciesModule && !tenanciesModuleEnabled)
return false;
if (gated.requiresExpensesModule && !expensesModuleEnabled) return false;
if (gated.umamiRequired && !umamiConfigured) return false;
return true;
})
.map((item) => (
@@ -482,6 +513,7 @@ export function Sidebar({
ports,
portLogoUrls,
tenanciesModuleByPort,
expensesModuleByPort,
}: SidebarProps) {
// Sidebar collapse removed - design preference is the always-expanded
// form. Forcibly false; the store flag stays for backwards-compat with
@@ -494,6 +526,12 @@ export function Sidebar({
const tenanciesModuleEnabled = currentPortId
? (tenanciesModuleByPort?.[currentPortId] ?? false)
: false;
// Expenses defaults to enabled when the port's entry is missing - the
// registry default is `true`, so a port that's never explicitly
// toggled the feature should keep it visible.
const expensesModuleEnabled = currentPortId
? (expensesModuleByPort?.[currentPortId] ?? true)
: true;
// Super admins see every section regardless of role rows.
const hasAdminAccess =
@@ -526,6 +564,7 @@ export function Sidebar({
hasMarinaAccess={hasMarinaAccess}
hasResidentialAccess={hasResidentialAccess}
tenanciesModuleEnabled={tenanciesModuleEnabled}
expensesModuleEnabled={expensesModuleEnabled}
user={user}
ports={ports}
currentPort={currentPort}