Files
pn-new-crm/src/app/(dashboard)/[portSlug]/expenses/layout.tsx

48 lines
2.0 KiB
TypeScript

import { notFound } from 'next/navigation';
import { eq } from 'drizzle-orm';
import { db } from '@/lib/db';
import { ports as portsTable } from '@/lib/db/schema/ports';
import { isExpensesModuleEnabled } from '@/lib/services/expenses-module.service';
import { ModuleDisabledPage } from '@/components/shared/module-disabled-page';
interface ExpensesLayoutProps {
children: React.ReactNode;
params: Promise<{ portSlug: string }>;
}
/**
* Layout-level gate for the entire /expenses subtree (list, scan,
* detail). When the port has expenses_module_enabled = false, every
* route under /expenses renders the ModuleDisabledPage instead of the
* real content. This is the route-level half of the "hybrid hide+block"
* model (the sidebar entries are independently hidden via
* expensesModuleByPort 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 (scan, [id]) are covered
* automatically, (c) the children subtree never mounts when disabled,
* so its data-fetching effects don't fire.
*/
export default async function ExpensesLayout({ children, params }: ExpensesLayoutProps) {
const { portSlug } = await params;
const port = await db.query.ports.findFirst({
where: eq(portsTable.slug, portSlug),
columns: { id: true },
});
// Fail closed: an unresolved slug means the port doesn't exist (or the
// user mistyped one) — 404 rather than silently rendering the gated
// subtree without a module check.
if (!port) notFound();
const enabled = await isExpensesModuleEnabled(port.id);
if (enabled) return children;
return (
<ModuleDisabledPage
moduleName="Expenses"
description="Expense tracking and receipt upload are turned off for this port. Previously-recorded expense rows are preserved and will reappear when the module is re-enabled."
settingsHref={`/${portSlug}/admin/settings`}
fallbackHref={`/${portSlug}/dashboard`}
/>
);
}