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:
44
src/app/(dashboard)/[portSlug]/residential/layout.tsx
Normal file
44
src/app/(dashboard)/[portSlug]/residential/layout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { ports as portsTable } from '@/lib/db/schema/ports';
|
||||
import { isResidentialModuleEnabled } from '@/lib/services/residential-module.service';
|
||||
import { ModuleDisabledPage } from '@/components/shared/module-disabled-page';
|
||||
|
||||
interface ResidentialLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ portSlug: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout-level gate for the entire /residential subtree (clients +
|
||||
* interests, list + detail). When the port has
|
||||
* residential_module_enabled = false, every route under /residential
|
||||
* renders the ModuleDisabledPage instead of the real content. This is
|
||||
* the route-level half of the "hybrid hide+block" model (the sidebar
|
||||
* "Residential" section + mobile entry are independently hidden via
|
||||
* residentialModuleByPort on the SSR-resolved sidebar prop).
|
||||
*
|
||||
* Using a layout rather than per-page guards means: (a) one place to
|
||||
* change the gate logic, (b) nested routes ([id]) are covered
|
||||
* automatically, (c) the children subtree never mounts when disabled,
|
||||
* so its data-fetching effects don't fire.
|
||||
*/
|
||||
export default async function ResidentialLayout({ children, params }: ResidentialLayoutProps) {
|
||||
const { portSlug } = await params;
|
||||
const port = await db.query.ports.findFirst({
|
||||
where: eq(portsTable.slug, portSlug),
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!port) return children;
|
||||
const enabled = await isResidentialModuleEnabled(port.id);
|
||||
if (enabled) return children;
|
||||
return (
|
||||
<ModuleDisabledPage
|
||||
moduleName="Residential"
|
||||
description="The Residential clients and interests pipeline is turned off for this port. Existing residential records are preserved and will reappear when the module is re-enabled."
|
||||
settingsHref={`/${portSlug}/admin/settings`}
|
||||
fallbackHref={`/${portSlug}/dashboard`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user