import { cache } from 'react'; import { use } from 'react'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { Fish, Waves, Anchor, BookOpen, ShieldCheck, Trophy, ScrollText, ListChecks, BookMarked, Building2, Network, SearchCheck, Share2, PieChart, LayoutTemplate, } from 'lucide-react'; import * as z from 'zod'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { TeamAccountWorkspaceContextProvider } from '@kit/team-accounts/components'; import { NavigationConfigSchema } from '@kit/ui/navigation-schema'; import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page'; import { SidebarProvider } from '@kit/ui/sidebar'; import { AppLogo } from '~/components/app-logo'; import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config'; // local imports import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation'; import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar'; import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu'; import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader'; type TeamWorkspaceLayoutProps = React.PropsWithChildren<{ params: Promise<{ account: string }>; }>; function TeamWorkspaceLayout({ children, params }: TeamWorkspaceLayoutProps) { const account = use(params).account; const state = use(getLayoutState(account)); if (state.style === 'sidebar') { return {children}; } return {children}; } /** * Query account_settings.features for a given account slug. * Cached per-request so multiple calls don't hit the DB twice. */ const getAccountFeatures = cache(async (accountSlug: string) => { const client = getSupabaseServerClient(); const { data: accountData } = await client .from('accounts') .select('id') .eq('slug', accountSlug) .single(); if (!accountData) return {}; const { data: settings } = await client .from('account_settings') .select('features') .eq('account_id', accountData.id) .maybeSingle(); return (settings?.features as Record) ?? {}; }); const iconClasses = 'w-4'; /** * Inject per-account feature module route groups (Fischerei, Meetings, Verband) * into the navigation config. These are added as separate collapsible sections * before the Administration section (last group). * * Only modules enabled in the account's settings are shown. */ function injectAccountFeatureRoutes( config: z.output, account: string, features: Record, ): z.output { const featureGroups: z.output['routes'] = []; if (features.fischerei) { featureGroups.push({ label: 'common.routes.fisheriesManagement', collapsible: true, collapsed: true, children: [ { label: 'common.routes.fisheriesOverview', path: `/home/${account}/fischerei`, Icon: , }, { label: 'common.routes.fisheriesWaters', path: `/home/${account}/fischerei/waters`, Icon: , }, { label: 'common.routes.fisheriesLeases', path: `/home/${account}/fischerei/leases`, Icon: , }, { label: 'common.routes.fisheriesCatchBooks', path: `/home/${account}/fischerei/catch-books`, Icon: , }, { label: 'common.routes.fisheriesPermits', path: `/home/${account}/fischerei/permits`, Icon: , }, { label: 'common.routes.fisheriesCompetitions', path: `/home/${account}/fischerei/competitions`, Icon: , }, ], }); } if (features.meetings) { featureGroups.push({ label: 'common.routes.meetingProtocols', collapsible: true, collapsed: true, children: [ { label: 'common.routes.meetingsOverview', path: `/home/${account}/meetings`, Icon: , }, { label: 'common.routes.meetingsProtocols', path: `/home/${account}/meetings/protocols`, Icon: , }, { label: 'common.routes.meetingsTasks', path: `/home/${account}/meetings/tasks`, Icon: , }, ], }); } if (features.verband) { featureGroups.push({ label: 'common.routes.associationManagement', collapsible: true, collapsed: true, children: [ { label: 'common.routes.associationOverview', path: `/home/${account}/verband`, Icon: , }, { label: 'common.routes.associationHierarchy', path: `/home/${account}/verband/hierarchy`, Icon: , }, { label: 'common.routes.associationMemberSearch', path: `/home/${account}/verband/members`, Icon: , }, { label: 'common.routes.associationEvents', path: `/home/${account}/verband/events`, Icon: , }, { label: 'common.routes.associationReporting', path: `/home/${account}/verband/reporting`, Icon: , }, { label: 'common.routes.associationTemplates', path: `/home/${account}/verband/templates`, Icon: , }, ], }); } if (featureGroups.length === 0) return config; // Insert before the last group (Administration) const routes = [...config.routes]; const adminIndex = routes.length - 1; routes.splice(adminIndex, 0, ...featureGroups); return { ...config, routes }; } async function SidebarLayout({ account, children, }: React.PropsWithChildren<{ account: string; }>) { const [data, state, features] = await Promise.all([ loadTeamWorkspace(account), getLayoutState(account), getAccountFeatures(account), ]); if (!data) { redirect('/'); } const baseConfig = getTeamAccountSidebarConfig(account, features); const config = injectAccountFeatureRoutes(baseConfig, account, features); const accounts = data.accounts.map(({ name, slug, picture_url }) => ({ label: name, value: slug, image: picture_url, })); return (
{children}
); } async function HeaderLayout({ account, children, }: React.PropsWithChildren<{ account: string; }>) { const [data, features] = await Promise.all([ loadTeamWorkspace(account), getAccountFeatures(account), ]); const baseConfig = getTeamAccountSidebarConfig(account, features); const config = injectAccountFeatureRoutes(baseConfig, account, features); const accounts = data.accounts.map(({ name, slug, picture_url }) => ({ label: name, value: slug, image: picture_url, })); return (
{children}
); } async function getLayoutState(account: string) { const cookieStore = await cookies(); const config = getTeamAccountSidebarConfig(account); const LayoutStyleSchema = z .enum(['sidebar', 'header', 'custom']) .default(config.style); const sidebarOpenCookie = cookieStore.get('sidebar_state'); const layoutCookie = cookieStore.get('layout-style'); const layoutStyle = LayoutStyleSchema.safeParse(layoutCookie?.value); const sidebarOpenCookieValue = sidebarOpenCookie ? sidebarOpenCookie.value === 'true' : !config.sidebarCollapsed; const style = layoutStyle.success ? layoutStyle.data : config.style; return { open: sidebarOpenCookieValue, style, }; } export default TeamWorkspaceLayout;