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:
@@ -1,4 +1,10 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { TenancyDetail } from '@/components/tenancies/tenancy-detail';
|
||||
import { db } from '@/lib/db';
|
||||
import { ports as portsTable } from '@/lib/db/schema/ports';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ portSlug: string; id: string }>;
|
||||
@@ -6,5 +12,12 @@ interface PageProps {
|
||||
|
||||
export default async function TenancyDetailPage({ params }: PageProps) {
|
||||
const { portSlug, id } = await params;
|
||||
const port = await db.query.ports.findFirst({
|
||||
where: eq(portsTable.slug, portSlug),
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!port) notFound();
|
||||
if (!(await isTenanciesModuleEnabled(port.id))) notFound();
|
||||
|
||||
return <TenancyDetail tenancyId={id} portSlug={portSlug} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { TenanciesListPage } from '@/components/tenancies/tenancies-list-page';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { TenanciesListPage } from '@/components/tenancies/tenancies-list-page';
|
||||
import { db } from '@/lib/db';
|
||||
import { ports as portsTable } from '@/lib/db/schema/ports';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ portSlug: string }>;
|
||||
}
|
||||
|
||||
export default async function BerthTenanciesPage({ params }: PageProps) {
|
||||
const { portSlug } = await params;
|
||||
// Per docs/tenancies-design.md §"When disabled": top-level page returns
|
||||
// 404 when the module is off. The sidebar entry is already hidden via
|
||||
// tenanciesModuleByPort, so this 404 guards against direct URL access.
|
||||
const port = await db.query.ports.findFirst({
|
||||
where: eq(portsTable.slug, portSlug),
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!port) notFound();
|
||||
if (!(await isTenanciesModuleEnabled(port.id))) notFound();
|
||||
|
||||
export default function BerthTenanciesPage() {
|
||||
return <TenanciesListPage />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user