- 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
342 lines
9.6 KiB
TypeScript
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;
|