feat(residential-toggle): port-level module gate for Residential
Adds a `residential_module_enabled` port setting (default ON) that hides/disables the entire Residential surface when an admin turns it off, mirroring the Tenancies / Invoices / Expenses module-toggle pattern. Disabling is a soft hide — residential clients/interests are preserved and reappear on re-enable. Surfaces gated: - Route guard: new residential/layout.tsx renders ModuleDisabledPage (covers all 5 residential pages) - Sidebar "Residential" section + mobile more-sheet tile (SSR-resolved residentialModuleByPort threaded layout → app-shell → sidebar) - Global search: residential client/interest buckets early-return at the shared chokepoint so disabled-port records don't dead-end - Public intake: /api/public/residential-inquiries 404s when off - Admin Switch in settings-manager (writes via settings PUT) Service TDD'd (residential-module.test.ts, 6 tests) plus a disabled-port rejection test on the public endpoint. tsc + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import { classifyFormFactor } from '@/lib/form-factor';
|
||||
import { getPortBrandingConfig } from '@/lib/services/port-config';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
import { isExpensesModuleEnabled } from '@/lib/services/expenses-module.service';
|
||||
import { isResidentialModuleEnabled } from '@/lib/services/residential-module.service';
|
||||
|
||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const headerList = await headers();
|
||||
@@ -108,6 +109,24 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
);
|
||||
const expensesModuleByPort: Record<string, boolean> = Object.fromEntries(expensesModuleEntries);
|
||||
|
||||
// Per-port residential-module gate. Defaults to enabled (the registry's
|
||||
// default) so existing ports keep the feature on deploy. Resolved
|
||||
// server-side so the sidebar "Residential" section SSRs in/out without
|
||||
// flicker when an admin has turned the feature off for a tenant.
|
||||
const residentialModuleEntries = await Promise.all(
|
||||
ports.map(async (p) => {
|
||||
try {
|
||||
return [p.id, await isResidentialModuleEnabled(p.id)] as const;
|
||||
} catch {
|
||||
// Conservative default on lookup failure: keep the feature
|
||||
// visible so a transient DB hiccup doesn't hide the module.
|
||||
return [p.id, true] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const residentialModuleByPort: Record<string, boolean> =
|
||||
Object.fromEntries(residentialModuleEntries);
|
||||
|
||||
return (
|
||||
<QueryProvider>
|
||||
<PortProvider
|
||||
@@ -136,6 +155,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
portLogoUrls={portLogoUrls}
|
||||
tenanciesModuleByPort={tenanciesModuleByPort}
|
||||
expensesModuleByPort={expensesModuleByPort}
|
||||
residentialModuleByPort={residentialModuleByPort}
|
||||
initialFormFactor={initialFormFactor}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user