import { redirect } from 'next/navigation'; import { cookies, headers } from 'next/headers'; import { eq } from 'drizzle-orm'; import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { ports as portsTable } from '@/lib/db/schema/ports'; import { userPortRoles, userProfiles } from '@/lib/db/schema/users'; import { QueryProvider } from '@/providers/query-provider'; import { SocketProvider } from '@/providers/socket-provider'; import { PortProvider } from '@/providers/port-provider'; import { PermissionsProvider } from '@/providers/permissions-provider'; import { AppShell } from '@/components/layout/app-shell'; import { OnboardingBanner } from '@/components/admin/onboarding-banner'; import { DevModeBanner } from '@/components/shared/dev-mode-banner'; import { RealtimeToasts } from '@/components/shared/realtime-toasts'; import { WebVitalsReporter } from '@/components/shared/web-vitals-reporter'; 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'; export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const headerList = await headers(); const session = await auth.api.getSession({ headers: headerList }); if (!session?.user) redirect('/login'); // Super admins have implicit access to every port; everyone else only sees // ports they have an explicit user_port_roles row for. const profile = await db.query.userProfiles.findFirst({ where: eq(userProfiles.userId, session.user.id), }); const portRoles = await db.query.userPortRoles.findMany({ where: eq(userPortRoles.userId, session.user.id), with: { port: true, role: true }, }); const ports = profile?.isSuperAdmin ? await db.query.ports.findMany({ orderBy: portsTable.name }) : portRoles.map((pr) => pr.port); // Prefer a previously-resolved tier from the client's cookie so the // server renders the matching shell on first paint - eliminates the // mobile↔desktop chrome flicker that happens when UA-based classification // disagrees with the actual viewport (e.g. macOS Safari with the // window dragged below 1024). AppShell writes the cookie after the // first matchMedia evaluation; subsequent reloads use the hint. const cookieStore = await cookies(); const tierCookie = cookieStore.get('pn-crm.viewport-tier')?.value; const initialFormFactor: 'mobile' | 'desktop' = tierCookie === 'mobile' ? 'mobile' : tierCookie === 'tablet' || tierCookie === 'desktop' ? 'desktop' : classifyFormFactor(headerList.get('user-agent')); const user = { name: profile?.displayName ?? session.user.name ?? session.user.email, email: session.user.email, }; // Per-port logo map for the sidebar. Resolved server-side so the // sidebar can swap brand on port switch without an extra round-trip. // Falls back to null per port when no logo is configured - the // sidebar surfaces nothing rather than leaking a generic placeholder. const portBrandingEntries = await Promise.all( ports.map(async (p) => { try { const cfg = await getPortBrandingConfig(p.id); return [p.id, cfg.logoUrl] as const; } catch { return [p.id, null] as const; } }), ); const portLogoUrls: Record = Object.fromEntries(portBrandingEntries); // Per-port tenancies-module gate. Hidden by default; flips on either by // the admin switch (Operations) OR the lazy auto-enable on first row. // Resolved server-side so the sidebar nav SSRs in/out atomically with // the layout instead of flickering after a client-side fetch. const tenanciesModuleEntries = await Promise.all( ports.map(async (p) => { try { return [p.id, await isTenanciesModuleEnabled(p.id)] as const; } catch { return [p.id, false] as const; } }), ); const tenanciesModuleByPort: Record = Object.fromEntries(tenanciesModuleEntries); // Per-port expenses-module gate. Defaults to enabled (the registry's // default) so existing ports keep the feature on deploy. Resolved // server-side so the sidebar SSRs without flicker when an admin has // turned the feature off for a tenant. const expensesModuleEntries = await Promise.all( ports.map(async (p) => { try { return [p.id, await isExpensesModuleEnabled(p.id)] as const; } catch { // Conservative default on lookup failure: keep the feature // visible so a transient DB hiccup doesn't hide the module // for a port that actually has it enabled. return [p.id, true] as const; } }), ); const expensesModuleByPort: Record = Object.fromEntries(expensesModuleEntries); return ( {/* Sticky banner across the app whenever EMAIL_REDIRECT_TO is set so reps + admins always know outbound mail is being rerouted. Production hides itself (env.ts forbids the flag in prod) so the banner is dev/staging-only. */} {/* #26: AppShell mounts ONE responsive tree (desktop OR * mobile) per render - never both - so pages don't pay the * double-state, double-fetch, double-Tabs-provider tax. */} {children} ); }