feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Major changes: - Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI - Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions - Sitzungsprotokolle module: meeting protocols, agenda items, task tracking - Verbandsverwaltung module: federation management, member clubs, contacts, fees - Per-account module activation via Modules page toggle - Site Builder: live CMS data in Puck blocks (courses, events, membership registration) - Public registration APIs: course signup, event registration, membership application - Document generation: PDF member cards, Excel reports, HTML labels - Landing page: real Com.BISS content (no filler text) - UX audit fixes: AccountNotFound component, shared status badges, confirm dialog, pagination, duplicate heading removal, emoji→badge replacement, a11y fixes - QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { cache } from 'react';
|
||||
import { use } from 'react';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { Fish, FileSignature, Building2 } 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';
|
||||
|
||||
@@ -33,21 +37,109 @@ function TeamWorkspaceLayout({ children, params }: TeamWorkspaceLayoutProps) {
|
||||
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>) ?? {};
|
||||
});
|
||||
|
||||
/**
|
||||
* Inject per-account feature routes (e.g. Fischerei) into the parsed
|
||||
* navigation config. The entry is inserted right after "Veranstaltungen".
|
||||
*/
|
||||
function injectAccountFeatureRoutes(
|
||||
config: z.output<typeof NavigationConfigSchema>,
|
||||
account: string,
|
||||
features: Record<string, boolean>,
|
||||
): z.output<typeof NavigationConfigSchema> {
|
||||
if (!features.fischerei && !features.meetings && !features.verband) return config;
|
||||
|
||||
const featureEntries: Array<{
|
||||
label: string;
|
||||
path: string;
|
||||
Icon: React.ReactNode;
|
||||
}> = [];
|
||||
|
||||
if (features.fischerei) {
|
||||
featureEntries.push({
|
||||
label: 'common.routes.fischerei',
|
||||
path: `/home/${account}/fischerei`,
|
||||
Icon: <Fish className="w-4" />,
|
||||
});
|
||||
}
|
||||
|
||||
if (features.meetings) {
|
||||
featureEntries.push({
|
||||
label: 'common.routes.meetings',
|
||||
path: `/home/${account}/meetings`,
|
||||
Icon: <FileSignature className="w-4" />,
|
||||
});
|
||||
}
|
||||
|
||||
if (features.verband) {
|
||||
featureEntries.push({
|
||||
label: 'common.routes.verband',
|
||||
path: `/home/${account}/verband`,
|
||||
Icon: <Building2 className="w-4" />,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
routes: config.routes.map((group) => {
|
||||
if (!('children' in group)) return group;
|
||||
|
||||
const eventsIndex = group.children.findIndex(
|
||||
(child) => child.label === 'common.routes.events',
|
||||
);
|
||||
|
||||
if (eventsIndex === -1) return group;
|
||||
|
||||
const newChildren = [...group.children];
|
||||
newChildren.splice(eventsIndex + 1, 0, ...featureEntries);
|
||||
|
||||
return { ...group, children: newChildren };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function SidebarLayout({
|
||||
account,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
account: string;
|
||||
}>) {
|
||||
const [data, state] = await Promise.all([
|
||||
const [data, state, features] = await Promise.all([
|
||||
loadTeamWorkspace(account),
|
||||
getLayoutState(account),
|
||||
getAccountFeatures(account),
|
||||
]);
|
||||
|
||||
if (!data) {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
const baseConfig = getTeamAccountSidebarConfig(account);
|
||||
const config = injectAccountFeatureRoutes(baseConfig, account, features);
|
||||
|
||||
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
|
||||
label: name,
|
||||
value: slug,
|
||||
@@ -64,6 +156,7 @@ async function SidebarLayout({
|
||||
accountId={data.account.id}
|
||||
accounts={accounts}
|
||||
user={data.user}
|
||||
config={config}
|
||||
/>
|
||||
</PageNavigation>
|
||||
|
||||
@@ -75,6 +168,7 @@ async function SidebarLayout({
|
||||
userId={data.user.id}
|
||||
accounts={accounts}
|
||||
account={account}
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
</PageMobileNavigation>
|
||||
@@ -86,19 +180,25 @@ async function SidebarLayout({
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderLayout({
|
||||
async function HeaderLayout({
|
||||
account,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
account: string;
|
||||
}>) {
|
||||
const data = use(loadTeamWorkspace(account));
|
||||
const [data, features] = await Promise.all([
|
||||
loadTeamWorkspace(account),
|
||||
getAccountFeatures(account),
|
||||
]);
|
||||
|
||||
const baseConfig = getTeamAccountSidebarConfig(account);
|
||||
const config = injectAccountFeatureRoutes(baseConfig, account, features);
|
||||
|
||||
return (
|
||||
<TeamAccountWorkspaceContextProvider value={data}>
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<TeamAccountNavigationMenu workspace={data} />
|
||||
<TeamAccountNavigationMenu workspace={data} config={config} />
|
||||
</PageNavigation>
|
||||
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user