feat(tenancies-p5): sidebar entry + 404 top-level page + API module gate
- Dashboard layout resolves tenanciesModuleByPort server-side (one isTenanciesModuleEnabled call per port the user has access to) and passes the map through AppShell → Sidebar. Atomic SSR — no flicker of the nav entry in/out after hydration. - Sidebar gains NavItemGated.requiresTenanciesModule. The Tenancies entry (KeyRound icon, immediately below Berths) only renders when the currently-active port has the flag flipped on. Per-port live switch fires when the rep toggles ports without reload. - /[portSlug]/tenancies + /[portSlug]/tenancies/[id] both call isTenanciesModuleEnabled and notFound() when disabled — guards against direct URL access even when the sidebar is hidden. - API routes (/api/v1/tenancies, /[id], /berths/[id]/tenancies) prepended with assertTenanciesModuleEnabled — matches design § "All routes ... return 404 when off". NotFoundError maps to 404. - Existing tenancy API tests get a makePortWithTenancies() helper (calls enableTenanciesModule after makePort) so the gate is satisfied. Affects 2 test files (16 tests retargeted). Verified: tsc clean, 1493/1493 vitest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import { RealtimeToasts } from '@/components/shared/realtime-toasts';
|
||||
import { WebVitalsReporter } from '@/components/shared/web-vitals-reporter';
|
||||
import { classifyFormFactor } from '@/lib/form-factor';
|
||||
import { getPortBrandingConfig } from '@/lib/services/port-config';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
|
||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const headerList = await headers();
|
||||
@@ -73,6 +74,21 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
);
|
||||
const portLogoUrls: Record<string, string | null> = Object.fromEntries(portBrandingEntries);
|
||||
|
||||
// Per-port tenancies-module gate. Hidden by default; flips on either by
|
||||
// the admin switch (Operations) OR the lazy auto-enable on first row.
|
||||
// Resolved server-side so the sidebar nav SSRs in/out atomically with
|
||||
// the layout instead of flickering after a client-side fetch.
|
||||
const tenanciesModuleEntries = await Promise.all(
|
||||
ports.map(async (p) => {
|
||||
try {
|
||||
return [p.id, await isTenanciesModuleEnabled(p.id)] as const;
|
||||
} catch {
|
||||
return [p.id, false] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const tenanciesModuleByPort: Record<string, boolean> = Object.fromEntries(tenanciesModuleEntries);
|
||||
|
||||
return (
|
||||
<QueryProvider>
|
||||
<PortProvider ports={ports} defaultPortId={ports[0]?.id ?? null}>
|
||||
@@ -95,6 +111,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
user={user}
|
||||
ports={ports}
|
||||
portLogoUrls={portLogoUrls}
|
||||
tenanciesModuleByPort={tenanciesModuleByPort}
|
||||
initialFormFactor={initialFormFactor}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user