diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 099c886a..8966a8a0 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -91,7 +91,11 @@ export default async function DashboardLayout({ children }: { children: React.Re return ( - + diff --git a/src/components/berths/berth-detail.tsx b/src/components/berths/berth-detail.tsx index 2ddd0a39..c9ddb422 100644 --- a/src/components/berths/berth-detail.tsx +++ b/src/components/berths/berth-detail.tsx @@ -8,6 +8,7 @@ import { DetailLayout } from '@/components/shared/detail-layout'; import { DetailNotFound } from '@/components/shared/detail-not-found'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; import { apiFetch } from '@/lib/api/client'; +import { useTenanciesModuleEnabled } from '@/providers/port-provider'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { BerthDetailHeader, type BerthDetailData } from './berth-detail-header'; import { BerthForm } from './berth-form'; @@ -20,6 +21,7 @@ interface BerthDetailProps { export function BerthDetail({ berthId }: BerthDetailProps) { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; + const tenanciesModuleEnabled = useTenanciesModuleEnabled(); const { data, isLoading, error } = useQuery({ queryKey: ['berth', berthId], @@ -84,7 +86,7 @@ export function BerthDetail({ berthId }: BerthDetailProps) { : null} - tabs={berth ? buildBerthTabs(berth) : []} + tabs={berth ? buildBerthTabs(berth, { tenanciesModuleEnabled }) : []} defaultTab="overview" /> {berth ? : null} diff --git a/src/components/berths/berth-tabs.tsx b/src/components/berths/berth-tabs.tsx index ccdf3a1c..d8150c20 100644 --- a/src/components/berths/berth-tabs.tsx +++ b/src/components/berths/berth-tabs.tsx @@ -419,8 +419,11 @@ function OverviewTab({ berth }: { berth: BerthData }) { ); } -export function buildBerthTabs(berth: BerthData): DetailTab[] { - return [ +export function buildBerthTabs( + berth: BerthData, + opts: { tenanciesModuleEnabled: boolean } = { tenanciesModuleEnabled: false }, +): DetailTab[] { + const tabs: DetailTab[] = [ { id: 'overview', label: 'Overview', @@ -431,11 +434,20 @@ export function buildBerthTabs(berth: BerthData): DetailTab[] { label: 'Interests', content: , }, - { + ]; + if (opts.tenanciesModuleEnabled) { + tabs.push({ id: 'tenancies', label: 'Tenancies', content: , - }, + }); + } + tabs.push(...buildBerthDetailRemainder(berth)); + return tabs; +} + +function buildBerthDetailRemainder(berth: BerthData): DetailTab[] { + return [ { id: 'spec', label: 'Spec', diff --git a/src/components/clients/client-detail.tsx b/src/components/clients/client-detail.tsx index 50e10e5b..38961a5e 100644 --- a/src/components/clients/client-detail.tsx +++ b/src/components/clients/client-detail.tsx @@ -9,6 +9,7 @@ import { DetailNotFound } from '@/components/shared/detail-not-found'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; import { ClientDetailHeader } from '@/components/clients/client-detail-header'; import { getClientTabs } from '@/components/clients/client-tabs'; +import { useTenanciesModuleEnabled } from '@/providers/port-provider'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useBreadcrumbHint } from '@/hooks/use-breadcrumb-hint'; import { apiFetch } from '@/lib/api/client'; @@ -118,6 +119,8 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) { 'berth_tenancy:cancelled': [['clients', clientId]], }); + const tenanciesModuleEnabled = useTenanciesModuleEnabled(); + if (error && !isLoading) { const status = (error as { status?: number } | null | undefined)?.status; return ( @@ -130,7 +133,9 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) { ); } - const tabs = data ? getClientTabs({ clientId, currentUserId, client: data }) : []; + const tabs = data + ? getClientTabs({ clientId, currentUserId, client: data, tenanciesModuleEnabled }) + : []; return ( , }, - { - id: 'tenancies', - label: 'Tenancies', - badge: client.activeTenancies.length, - content: , - }, { id: 'addresses', label: 'Addresses', @@ -323,4 +322,15 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt ), }, ]; + if (tenanciesModuleEnabled) { + // Insert the Tenancies tab after Companies (index 4) so the entity + // ordering reads: Overview → Interests → Yachts → Companies → Tenancies. + tabs.splice(4, 0, { + id: 'tenancies', + label: 'Tenancies', + badge: client.activeTenancies.length, + content: , + }); + } + return tabs; } diff --git a/src/components/yachts/yacht-detail.tsx b/src/components/yachts/yacht-detail.tsx index 0076c6bd..2f5214b1 100644 --- a/src/components/yachts/yacht-detail.tsx +++ b/src/components/yachts/yacht-detail.tsx @@ -9,6 +9,7 @@ import { DetailNotFound } from '@/components/shared/detail-not-found'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; import { YachtDetailHeader } from '@/components/yachts/yacht-detail-header'; import { getYachtTabs } from '@/components/yachts/yacht-tabs'; +import { useTenanciesModuleEnabled } from '@/providers/port-provider'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useBreadcrumbHint } from '@/hooks/use-breadcrumb-hint'; import { apiFetch } from '@/lib/api/client'; @@ -79,6 +80,8 @@ export function YachtDetail({ yachtId, currentUserId }: YachtDetailProps) { ], }); + const tenanciesModuleEnabled = useTenanciesModuleEnabled(); + if (error && !isLoading) { const status = (error as { status?: number } | null | undefined)?.status; return ( @@ -91,7 +94,9 @@ export function YachtDetail({ yachtId, currentUserId }: YachtDetailProps) { ); } - const tabs = data ? getYachtTabs({ yachtId, currentUserId, yacht: data }) : []; + const tabs = data + ? getYachtTabs({ yachtId, currentUserId, yacht: data, tenanciesModuleEnabled }) + : []; return ( , }, - { - id: 'tenancies', - label: 'Tenancies', - content: , - }, { id: 'notes', label: 'Notes', @@ -401,4 +401,14 @@ export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions ), }, ]; + if (tenanciesModuleEnabled) { + // Insert after Interests (index 3) so the ordering reads: + // Overview → Ownership History → Interests → Tenancies. + tabs.splice(3, 0, { + id: 'tenancies', + label: 'Tenancies', + content: , + }); + } + return tabs; } diff --git a/src/providers/port-provider.tsx b/src/providers/port-provider.tsx index 64e71fe4..6b463926 100644 --- a/src/providers/port-provider.tsx +++ b/src/providers/port-provider.tsx @@ -12,6 +12,10 @@ interface PortContextValue { currentPort: Port | null; currentPortId: string | null; currentPortSlug: string | null; + /** Per-port feature-flag map — currently just the Tenancies module. + * Resolved server-side in the dashboard layout. Consumers read via + * `useTenanciesModuleEnabled()`. */ + tenanciesModuleByPort: Record; } const PortContext = createContext({ @@ -19,15 +23,22 @@ const PortContext = createContext({ currentPort: null, currentPortId: null, currentPortSlug: null, + tenanciesModuleByPort: {}, }); interface PortProviderProps { children: ReactNode; ports: Port[]; defaultPortId: string | null; + tenanciesModuleByPort?: Record; } -export function PortProvider({ children, ports, defaultPortId }: PortProviderProps) { +export function PortProvider({ + children, + ports, + defaultPortId, + tenanciesModuleByPort = {}, +}: PortProviderProps) { const params = useParams(); const portSlugFromUrl = params?.portSlug as string | undefined; @@ -75,6 +86,7 @@ export function PortProvider({ children, ports, defaultPortId }: PortProviderPro currentPort: currentPort ?? null, currentPortId: currentPort?.id ?? null, currentPortSlug: currentPort?.slug ?? null, + tenanciesModuleByPort, }} > {children} @@ -85,3 +97,12 @@ export function PortProvider({ children, ports, defaultPortId }: PortProviderPro export function usePortContext(): PortContextValue { return useContext(PortContext); } + +/** Read the tenancies-module-enabled flag for the currently-active port. + * Server-side resolved in the dashboard layout, so this is a synchronous + * read with no fetch latency or hydration flicker. */ +export function useTenanciesModuleEnabled(): boolean { + const { currentPortId, tenanciesModuleByPort } = useContext(PortContext); + if (!currentPortId) return false; + return tenanciesModuleByPort[currentPortId] ?? false; +}