Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/layout.tsx
Zaid Marzguioui a1aa1bee86
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 5m38s
Workflow / ⚫️ Test (push) Has been skipped
fix: rename unused ctx params to _ctx, remove unused imports
- Fix 17 unused ctx params in module-builder, verbandsverwaltung, sitzungsprotokolle actions
- Remove unused CardDescription/CardHeader/CardTitle from pricing calculator
- Remove unused FileSignature import from layout
2026-04-02 19:25:01 +02:00

342 lines
9.6 KiB
TypeScript

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 <SidebarLayout account={account}>{children}</SidebarLayout>;
}
return <HeaderLayout account={account}>{children}</HeaderLayout>;
}
/**
* 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<string, boolean>) ?? {};
});
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<typeof NavigationConfigSchema>,
account: string,
features: Record<string, boolean>,
): z.output<typeof NavigationConfigSchema> {
const featureGroups: z.output<typeof NavigationConfigSchema>['routes'] = [];
if (features.fischerei) {
featureGroups.push({
label: 'common.routes.fisheriesManagement',
collapsible: true,
collapsed: true,
children: [
{
label: 'common.routes.fisheriesOverview',
path: `/home/${account}/fischerei`,
Icon: <Fish className={iconClasses} />,
},
{
label: 'common.routes.fisheriesWaters',
path: `/home/${account}/fischerei/waters`,
Icon: <Waves className={iconClasses} />,
},
{
label: 'common.routes.fisheriesLeases',
path: `/home/${account}/fischerei/leases`,
Icon: <Anchor className={iconClasses} />,
},
{
label: 'common.routes.fisheriesCatchBooks',
path: `/home/${account}/fischerei/catch-books`,
Icon: <BookOpen className={iconClasses} />,
},
{
label: 'common.routes.fisheriesPermits',
path: `/home/${account}/fischerei/permits`,
Icon: <ShieldCheck className={iconClasses} />,
},
{
label: 'common.routes.fisheriesCompetitions',
path: `/home/${account}/fischerei/competitions`,
Icon: <Trophy className={iconClasses} />,
},
],
});
}
if (features.meetings) {
featureGroups.push({
label: 'common.routes.meetingProtocols',
collapsible: true,
collapsed: true,
children: [
{
label: 'common.routes.meetingsOverview',
path: `/home/${account}/meetings`,
Icon: <BookMarked className={iconClasses} />,
},
{
label: 'common.routes.meetingsProtocols',
path: `/home/${account}/meetings/protocols`,
Icon: <ScrollText className={iconClasses} />,
},
{
label: 'common.routes.meetingsTasks',
path: `/home/${account}/meetings/tasks`,
Icon: <ListChecks className={iconClasses} />,
},
],
});
}
if (features.verband) {
featureGroups.push({
label: 'common.routes.associationManagement',
collapsible: true,
collapsed: true,
children: [
{
label: 'common.routes.associationOverview',
path: `/home/${account}/verband`,
Icon: <Building2 className={iconClasses} />,
},
{
label: 'common.routes.associationHierarchy',
path: `/home/${account}/verband/hierarchy`,
Icon: <Network className={iconClasses} />,
},
{
label: 'common.routes.associationMemberSearch',
path: `/home/${account}/verband/members`,
Icon: <SearchCheck className={iconClasses} />,
},
{
label: 'common.routes.associationEvents',
path: `/home/${account}/verband/events`,
Icon: <Share2 className={iconClasses} />,
},
{
label: 'common.routes.associationReporting',
path: `/home/${account}/verband/reporting`,
Icon: <PieChart className={iconClasses} />,
},
{
label: 'common.routes.associationTemplates',
path: `/home/${account}/verband/templates`,
Icon: <LayoutTemplate className={iconClasses} />,
},
],
});
}
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 (
<TeamAccountWorkspaceContextProvider value={data}>
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<TeamAccountLayoutSidebar
account={account}
accountId={data.account.id}
accounts={accounts}
user={data.user}
config={config}
/>
</PageNavigation>
<PageMobileNavigation>
<AppLogo />
<div className={'flex'}>
<TeamAccountLayoutMobileNavigation
userId={data.user.id}
accounts={accounts}
account={account}
/>
</div>
</PageMobileNavigation>
{children}
</Page>
</SidebarProvider>
</TeamAccountWorkspaceContextProvider>
);
}
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 (
<TeamAccountWorkspaceContextProvider value={data}>
<Page style={'header'}>
<PageNavigation>
<TeamAccountNavigationMenu workspace={data} config={config} />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<div>
<AppLogo />
</div>
<div>
<TeamAccountLayoutMobileNavigation
userId={data.user.id}
accounts={accounts}
account={account}
/>
</div>
</PageMobileNavigation>
{children}
</Page>
</TeamAccountWorkspaceContextProvider>
);
}
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;