diff --git a/apps/web/config/team-account-navigation.config.tsx b/apps/web/config/team-account-navigation.config.tsx
index 911c64408..5944a675d 100644
--- a/apps/web/config/team-account-navigation.config.tsx
+++ b/apps/web/config/team-account-navigation.config.tsx
@@ -1,17 +1,65 @@
import {
- CreditCard,
LayoutDashboard,
Settings,
- Users,
- Database,
+ UserCog,
+ CreditCard,
+ // People (Members + Access)
UserCheck,
+ UserPlus,
+ IdCard,
+ ClipboardList,
+ KeyRound,
+ // Courses
GraduationCap,
+ CalendarDays,
+ MapPin,
+ UserRound,
+ // Events
+ CalendarHeart,
+ Ticket,
+ PartyPopper,
+ // Bookings
Hotel,
- Calendar,
+ BedDouble,
+ Contact,
+ CalendarRange,
+ // Finance
Wallet,
+ Receipt,
+ Landmark,
+ BarChart3,
+ // Documents
FileText,
+ FilePlus,
+ FileStack,
+ // Newsletter
Mail,
- Globe,
+ MailPlus,
+ FileCode,
+ // Site Builder
+ PanelTop,
+ Newspaper,
+ Palette,
+ // Fisheries
+ Fish,
+ Waves,
+ Anchor,
+ BookOpen,
+ ShieldCheck,
+ Trophy,
+ // Meetings
+ BookMarked,
+ ListChecks,
+ ScrollText,
+ // Association (Verband)
+ Building2,
+ Network,
+ SearchCheck,
+ Share2,
+ PieChart,
+ LayoutTemplate,
+ // Modules
+ Database,
} from 'lucide-react';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
@@ -21,101 +69,499 @@ import pathsConfig from '~/config/paths.config';
const iconClasses = 'w-4';
-const getRoutes = (account: string) => [
+const getRoutes = (account: string) => {
+ const routes: Array<
+ | {
+ label: string;
+ collapsible?: boolean;
+ collapsed?: boolean;
+ children: Array<
+ | {
+ label: string;
+ path: string;
+ Icon: React.ReactNode;
+ highlightMatch?: string;
+ }
+ | undefined
+ >;
+ }
+ | { divider: true }
+ > = [
+ // ── Dashboard ──
+ {
+ label: 'common:routes.dashboard',
+ children: [
+ {
+ label: 'common:routes.dashboard',
+ path: pathsConfig.app.accountHome.replace('[account]', account),
+ Icon: ,
+ highlightMatch: `${pathsConfig.app.home}$`,
+ },
+ ],
+ },
+ ];
+
+ // ── People (Members + Access) ──
{
- label: 'common.routes.application',
+ const peopleChildren: Array<{
+ label: string;
+ path: string;
+ Icon: React.ReactNode;
+ }> = [];
+
+ if (featureFlagsConfig.enableMemberManagement) {
+ peopleChildren.push(
+ {
+ label: 'common:routes.clubMembers',
+ path: createPath(pathsConfig.app.accountCmsMembers, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.memberApplications',
+ path: createPath(
+ pathsConfig.app.accountCmsMembers + '/applications',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.memberPortal',
+ path: createPath(
+ pathsConfig.app.accountCmsMembers + '/portal',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.memberCards',
+ path: createPath(
+ pathsConfig.app.accountCmsMembers + '/cards',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.memberDues',
+ path: createPath(
+ pathsConfig.app.accountCmsMembers + '/dues',
+ account,
+ ),
+ Icon: ,
+ },
+ );
+ }
+
+ // Admin users who can log in — always visible
+ peopleChildren.push({
+ label: 'common:routes.accessAndRoles',
+ path: createPath(pathsConfig.app.accountMembers, account),
+ Icon: ,
+ });
+
+ routes.push({
+ label: 'common:routes.people',
+ collapsible: true,
+ children: peopleChildren,
+ });
+ }
+
+ // ── Courses ──
+ if (featureFlagsConfig.enableCourseManagement) {
+ routes.push({
+ label: 'common:routes.courseManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.courseList',
+ path: createPath(pathsConfig.app.accountCourses, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.courseCalendar',
+ path: createPath(
+ pathsConfig.app.accountCourses + '/calendar',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.courseInstructors',
+ path: createPath(
+ pathsConfig.app.accountCourses + '/instructors',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.courseLocations',
+ path: createPath(
+ pathsConfig.app.accountCourses + '/locations',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Events ──
+ routes.push({
+ label: 'common:routes.eventManagement',
+ collapsible: true,
children: [
{
- label: 'common.routes.dashboard',
- path: pathsConfig.app.accountHome.replace('[account]', account),
- Icon: ,
- highlightMatch: `${pathsConfig.app.home}$`,
+ label: 'common:routes.eventList',
+ path: createPath('/home/[account]/events', account),
+ Icon: ,
},
- featureFlagsConfig.enableModuleBuilder
- ? {
- label: 'common.routes.modules',
- path: createPath(pathsConfig.app.accountModules, account),
- Icon: ,
- }
- : undefined,
- featureFlagsConfig.enableMemberManagement
- ? {
- label: 'common.routes.cmsMembers',
- path: createPath(pathsConfig.app.accountCmsMembers, account),
- Icon: ,
- }
- : undefined,
- featureFlagsConfig.enableCourseManagement
- ? {
- label: 'common.routes.courses',
- path: createPath(pathsConfig.app.accountCourses, account),
- Icon: ,
- }
- : undefined,
- featureFlagsConfig.enableBookingManagement
- ? {
- label: 'common.routes.bookings',
- path: createPath(pathsConfig.app.accountBookings, account),
- Icon: ,
- }
- : undefined,
{
- label: 'common.routes.events',
- path: createPath(`/home/[account]/events`, account),
- Icon: ,
+ label: 'common:routes.eventRegistrations',
+ path: createPath('/home/[account]/events/registrations', account),
+ Icon: ,
},
- featureFlagsConfig.enableSepaPayments
- ? {
- label: 'common.routes.finance',
- path: createPath(pathsConfig.app.accountFinance, account),
- Icon: ,
- }
- : undefined,
- featureFlagsConfig.enableDocumentGeneration
- ? {
- label: 'common.routes.documents',
- path: createPath(pathsConfig.app.accountDocuments, account),
- Icon: ,
- }
- : undefined,
- featureFlagsConfig.enableNewsletter
- ? {
- label: 'common.routes.newsletter',
- path: createPath(pathsConfig.app.accountNewsletter, account),
- Icon: ,
- }
- : undefined,
{
- label: 'common.routes.siteBuilder',
- path: createPath(`/home/[account]/site-builder`, account),
- Icon: ,
+ label: 'common:routes.holidayPasses',
+ path: createPath('/home/[account]/events/holiday-passes', account),
+ Icon: ,
},
- ].filter(Boolean),
- },
- {
- label: 'common.routes.settings',
+ ],
+ });
+
+ // ── Bookings ──
+ if (featureFlagsConfig.enableBookingManagement) {
+ routes.push({
+ label: 'common:routes.bookingManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.bookingList',
+ path: createPath(pathsConfig.app.accountBookings, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.bookingCalendar',
+ path: createPath(
+ pathsConfig.app.accountBookings + '/calendar',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.bookingRooms',
+ path: createPath(pathsConfig.app.accountBookings + '/rooms', account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.bookingGuests',
+ path: createPath(
+ pathsConfig.app.accountBookings + '/guests',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Finance ──
+ if (featureFlagsConfig.enableSepaPayments) {
+ routes.push({
+ label: 'common:routes.financeManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.financeOverview',
+ path: createPath(pathsConfig.app.accountFinance, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.financeInvoices',
+ path: createPath(
+ pathsConfig.app.accountFinance + '/invoices',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.financeSepa',
+ path: createPath(pathsConfig.app.accountFinance + '/sepa', account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.financePayments',
+ path: createPath(
+ pathsConfig.app.accountFinance + '/payments',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Documents ──
+ if (featureFlagsConfig.enableDocumentGeneration) {
+ routes.push({
+ label: 'common:routes.documentManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.documentOverview',
+ path: createPath(pathsConfig.app.accountDocuments, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.documentGenerate',
+ path: createPath(
+ pathsConfig.app.accountDocuments + '/generate',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.documentTemplates',
+ path: createPath(
+ pathsConfig.app.accountDocuments + '/templates',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Newsletter ──
+ if (featureFlagsConfig.enableNewsletter) {
+ routes.push({
+ label: 'common:routes.newsletterManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.newsletterCampaigns',
+ path: createPath(pathsConfig.app.accountNewsletter, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.newsletterNew',
+ path: createPath(pathsConfig.app.accountNewsletter + '/new', account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.newsletterTemplates',
+ path: createPath(
+ pathsConfig.app.accountNewsletter + '/templates',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Site Builder ──
+ if (featureFlagsConfig.enableSiteBuilder) {
+ routes.push({
+ label: 'common:routes.siteBuilder',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.sitePages',
+ path: createPath(pathsConfig.app.accountSiteBuilder, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.sitePosts',
+ path: createPath(
+ pathsConfig.app.accountSiteBuilder + '/posts',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.siteSettings',
+ path: createPath(
+ pathsConfig.app.accountSiteBuilder + '/settings',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Custom Modules ──
+ if (featureFlagsConfig.enableModuleBuilder) {
+ routes.push({
+ label: 'common:routes.customModules',
+ collapsible: true,
+ collapsed: true,
+ children: [
+ {
+ label: 'common:routes.moduleList',
+ path: createPath(pathsConfig.app.accountModules, account),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Fisheries ──
+ if (featureFlagsConfig.enableFischerei) {
+ routes.push({
+ label: 'common:routes.fisheriesManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.fisheriesOverview',
+ path: createPath(pathsConfig.app.accountFischerei, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.fisheriesWaters',
+ path: createPath(
+ pathsConfig.app.accountFischerei + '/waters',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.fisheriesLeases',
+ path: createPath(
+ pathsConfig.app.accountFischerei + '/leases',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.fisheriesCatchBooks',
+ path: createPath(
+ pathsConfig.app.accountFischerei + '/catch-books',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.fisheriesPermits',
+ path: createPath(
+ pathsConfig.app.accountFischerei + '/permits',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.fisheriesCompetitions',
+ path: createPath(
+ pathsConfig.app.accountFischerei + '/competitions',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Meeting Protocols ──
+ if (featureFlagsConfig.enableMeetingProtocols) {
+ routes.push({
+ label: 'common:routes.meetingProtocols',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.meetingsOverview',
+ path: createPath(pathsConfig.app.accountMeetings, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.meetingsProtocols',
+ path: createPath(
+ pathsConfig.app.accountMeetings + '/protocols',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.meetingsTasks',
+ path: createPath(pathsConfig.app.accountMeetings + '/tasks', account),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Association Management (Verband) ──
+ if (featureFlagsConfig.enableVerbandsverwaltung) {
+ routes.push({
+ label: 'common:routes.associationManagement',
+ collapsible: true,
+ children: [
+ {
+ label: 'common:routes.associationOverview',
+ path: createPath(pathsConfig.app.accountVerband, account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.associationHierarchy',
+ path: createPath(
+ pathsConfig.app.accountVerband + '/hierarchy',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.associationMemberSearch',
+ path: createPath(
+ pathsConfig.app.accountVerband + '/members',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.associationEvents',
+ path: createPath(pathsConfig.app.accountVerband + '/events', account),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.associationReporting',
+ path: createPath(
+ pathsConfig.app.accountVerband + '/reporting',
+ account,
+ ),
+ Icon: ,
+ },
+ {
+ label: 'common:routes.associationTemplates',
+ path: createPath(
+ pathsConfig.app.accountVerband + '/templates',
+ account,
+ ),
+ Icon: ,
+ },
+ ],
+ });
+ }
+
+ // ── Administration ──
+ routes.push({
+ label: 'common:routes.administration',
collapsible: false,
children: [
{
- label: 'common.routes.settings',
+ label: 'common:routes.accountSettings',
path: createPath(pathsConfig.app.accountSettings, account),
Icon: ,
},
- {
- label: 'common.routes.members',
- path: createPath(pathsConfig.app.accountMembers, account),
- Icon: ,
- },
featureFlagsConfig.enableTeamAccountBilling
? {
- label: 'common.routes.billing',
+ label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account),
Icon: ,
}
: undefined,
- ].filter(Boolean),
- },
-];
+ ],
+ });
+
+ return routes;
+};
export function getTeamAccountSidebarConfig(account: string) {
return NavigationConfigSchema.parse({
diff --git a/apps/web/i18n/messages/de/common.json b/apps/web/i18n/messages/de/common.json
index 99ba5a813..b0f12f3ee 100644
--- a/apps/web/i18n/messages/de/common.json
+++ b/apps/web/i18n/messages/de/common.json
@@ -61,24 +61,83 @@
"routes": {
"home": "Startseite",
"account": "Konto",
- "members": "Mitglieder",
"billing": "Abrechnung",
"dashboard": "Dashboard",
"settings": "Einstellungen",
"profile": "Profil",
- "application": "Anwendung",
- "modules": "Module",
- "cmsMembers": "Mitglieder",
- "courses": "Kurse",
- "bookings": "Buchungen",
- "finance": "Finanzen",
- "documents": "Dokumente",
- "newsletter": "Newsletter",
- "events": "Veranstaltungen",
+
+ "people": "Personen",
+ "clubMembers": "Vereinsmitglieder",
+ "memberApplications": "Aufnahmeanträge",
+ "memberPortal": "Mitgliederportal",
+ "memberCards": "Mitgliedsausweise",
+ "memberDues": "Beitragskategorien",
+ "accessAndRoles": "Zugänge & Rollen",
+
+ "courseManagement": "Kursverwaltung",
+ "courseList": "Alle Kurse",
+ "courseCalendar": "Kurskalender",
+ "courseInstructors": "Kursleiter",
+ "courseLocations": "Standorte",
+
+ "eventManagement": "Veranstaltungen",
+ "eventList": "Alle Veranstaltungen",
+ "eventRegistrations": "Anmeldungen",
+ "holidayPasses": "Ferienpässe",
+
+ "bookingManagement": "Buchungsverwaltung",
+ "bookingList": "Alle Buchungen",
+ "bookingCalendar": "Belegungskalender",
+ "bookingRooms": "Zimmer",
+ "bookingGuests": "Gäste",
+
+ "financeManagement": "Finanzen",
+ "financeOverview": "Übersicht",
+ "financeInvoices": "Rechnungen",
+ "financeSepa": "SEPA-Einzüge",
+ "financePayments": "Zahlungen",
+
+ "documentManagement": "Dokumente",
+ "documentOverview": "Übersicht",
+ "documentGenerate": "Generieren",
+ "documentTemplates": "Vorlagen",
+
+ "newsletterManagement": "Newsletter",
+ "newsletterCampaigns": "Kampagnen",
+ "newsletterNew": "Neuer Newsletter",
+ "newsletterTemplates": "Vorlagen",
+
"siteBuilder": "Website",
- "fischerei": "Fischerei",
- "meetings": "Sitzungsprotokolle",
- "verband": "Verbandsverwaltung"
+ "sitePages": "Seiten",
+ "sitePosts": "Beiträge",
+ "siteSettings": "Einstellungen",
+
+ "customModules": "Benutzerdefinierte Module",
+ "moduleList": "Alle Module",
+
+ "fisheriesManagement": "Fischerei",
+ "fisheriesOverview": "Übersicht",
+ "fisheriesWaters": "Gewässer",
+ "fisheriesLeases": "Pachten",
+ "fisheriesCatchBooks": "Fangbücher",
+ "fisheriesPermits": "Erlaubnisscheine",
+ "fisheriesCompetitions": "Wettbewerbe",
+
+ "meetingProtocols": "Sitzungsprotokolle",
+ "meetingsOverview": "Übersicht",
+ "meetingsProtocols": "Protokolle",
+ "meetingsTasks": "Offene Aufgaben",
+
+ "associationManagement": "Verbandsverwaltung",
+ "associationOverview": "Übersicht",
+ "associationHierarchy": "Organisationsstruktur",
+ "associationMemberSearch": "Verbandsweite Suche",
+ "associationEvents": "Geteilte Veranstaltungen",
+ "associationReporting": "Berichte",
+ "associationTemplates": "Geteilte Vorlagen",
+
+ "administration": "Administration",
+ "accountSettings": "Kontoeinstellungen"
},
"roles": {
"owner": {
diff --git a/apps/web/i18n/messages/en/common.json b/apps/web/i18n/messages/en/common.json
index 1212393ba..33a2ddaea 100644
--- a/apps/web/i18n/messages/en/common.json
+++ b/apps/web/i18n/messages/en/common.json
@@ -61,24 +61,83 @@
"routes": {
"home": "Home",
"account": "Account",
- "members": "Members",
"billing": "Billing",
"dashboard": "Dashboard",
"settings": "Settings",
"profile": "Profile",
- "application": "Application",
- "modules": "Modules",
- "cmsMembers": "Members",
- "courses": "Courses",
- "bookings": "Bookings",
- "events": "Events",
+
+ "people": "People",
+ "clubMembers": "Club Members",
+ "memberApplications": "Applications",
+ "memberPortal": "Member Portal",
+ "memberCards": "Member Cards",
+ "memberDues": "Dues Categories",
+ "accessAndRoles": "Access & Roles",
+
+ "courseManagement": "Courses",
+ "courseList": "All Courses",
+ "courseCalendar": "Calendar",
+ "courseInstructors": "Instructors",
+ "courseLocations": "Locations",
+
+ "eventManagement": "Events",
+ "eventList": "All Events",
+ "eventRegistrations": "Registrations",
+ "holidayPasses": "Holiday Passes",
+
+ "bookingManagement": "Bookings",
+ "bookingList": "All Bookings",
+ "bookingCalendar": "Availability Calendar",
+ "bookingRooms": "Rooms",
+ "bookingGuests": "Guests",
+
+ "financeManagement": "Finance",
+ "financeOverview": "Overview",
+ "financeInvoices": "Invoices",
+ "financeSepa": "SEPA Batches",
+ "financePayments": "Payments",
+
+ "documentManagement": "Documents",
+ "documentOverview": "Overview",
+ "documentGenerate": "Generate",
+ "documentTemplates": "Templates",
+
+ "newsletterManagement": "Newsletter",
+ "newsletterCampaigns": "Campaigns",
+ "newsletterNew": "New Newsletter",
+ "newsletterTemplates": "Templates",
+
"siteBuilder": "Website",
- "finance": "Finance",
- "documents": "Documents",
- "newsletter": "Newsletter",
- "fischerei": "Fisheries",
- "meetings": "Meeting Protocols",
- "verband": "Federation Management"
+ "sitePages": "Pages",
+ "sitePosts": "Posts",
+ "siteSettings": "Settings",
+
+ "customModules": "Custom Modules",
+ "moduleList": "All Modules",
+
+ "fisheriesManagement": "Fisheries",
+ "fisheriesOverview": "Overview",
+ "fisheriesWaters": "Waters",
+ "fisheriesLeases": "Leases",
+ "fisheriesCatchBooks": "Catch Books",
+ "fisheriesPermits": "Permits",
+ "fisheriesCompetitions": "Competitions",
+
+ "meetingProtocols": "Meeting Protocols",
+ "meetingsOverview": "Overview",
+ "meetingsProtocols": "Protocols",
+ "meetingsTasks": "Open Tasks",
+
+ "associationManagement": "Association Management",
+ "associationOverview": "Overview",
+ "associationHierarchy": "Organization Structure",
+ "associationMemberSearch": "Association-wide Search",
+ "associationEvents": "Shared Events",
+ "associationReporting": "Reports",
+ "associationTemplates": "Shared Templates",
+
+ "administration": "Administration",
+ "accountSettings": "Account Settings"
},
"roles": {
"owner": {
diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts
index d8a78993a..308811d16 100644
--- a/apps/web/proxy.ts
+++ b/apps/web/proxy.ts
@@ -165,7 +165,9 @@ async function getPatterns() {
} catch {
// Supabase unreachable — redirect to sign in
const signIn = pathsConfig.auth.signIn;
- return NextResponse.redirect(new URL(signIn, req.nextUrl.origin).href);
+ return NextResponse.redirect(
+ new URL(signIn, req.nextUrl.origin).href,
+ );
}
const { origin, pathname: next } = req.nextUrl;
diff --git a/docker-compose.yml b/docker-compose.yml
index 310a601ff..a00901a62 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,7 +28,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: postgres
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
+ test: ['CMD-SHELL', 'pg_isready -U postgres -d postgres']
interval: 10s
timeout: 5s
retries: 10
@@ -49,7 +49,7 @@ services:
environment:
PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
- entrypoint: ["/bin/sh", "-c"]
+ entrypoint: ['/bin/sh', '-c']
command:
- |
echo "🔑 Ensuring role passwords are set (idempotent)..."
@@ -63,7 +63,7 @@ services:
echo "✅ App migrations complete."
echo ""
sh /app-seed/dev-bootstrap.sh
- restart: "no"
+ restart: 'no'
# =====================================================
# Supabase Auth (GoTrue)
@@ -102,7 +102,15 @@ services:
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
+ test:
+ [
+ 'CMD',
+ 'wget',
+ '--no-verbose',
+ '--tries=1',
+ '--spider',
+ 'http://localhost:9999/health',
+ ]
interval: 10s
timeout: 5s
retries: 5
@@ -123,9 +131,9 @@ services:
PGRST_DB_SCHEMAS: public,storage,graphql_public
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
- PGRST_DB_USE_LEGACY_GUCS: "false"
+ PGRST_DB_USE_LEGACY_GUCS: 'false'
healthcheck:
- test: ["CMD-SHELL", "head -c0 r.statusCode === 200 ? process.exit(0) : process.exit(1))"]
+ test:
+ [
+ 'CMD',
+ 'node',
+ '-e',
+ "require('http').get('http://localhost:3000/api/profile', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))",
+ ]
interval: 10s
timeout: 5s
retries: 5
@@ -284,7 +302,7 @@ services:
entrypoint: >
sh -c "sed 's|\$${SUPABASE_ANON_KEY}|'\"$$SUPABASE_ANON_KEY\"'|g; s|\$${SUPABASE_SERVICE_KEY}|'\"$$SUPABASE_SERVICE_KEY\"'|g' /var/lib/kong/kong.yml.tpl > /tmp/kong.yml && KONG_DECLARATIVE_CONFIG=/tmp/kong.yml /docker-entrypoint.sh kong docker-start"
environment:
- KONG_DATABASE: "off"
+ KONG_DATABASE: 'off'
KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
@@ -295,7 +313,7 @@ services:
volumes:
- ./docker/kong.yml:/var/lib/kong/kong.yml.tpl:ro
healthcheck:
- test: ["CMD", "kong", "health"]
+ test: ['CMD', 'kong', 'health']
interval: 10s
timeout: 5s
retries: 5
@@ -329,15 +347,15 @@ services:
SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret}
EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de}
NEXT_PUBLIC_PRODUCT_NAME: MyEasyCMS
- NEXT_PUBLIC_ENABLE_THEME_TOGGLE: "true"
- NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: "true"
- NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: "true"
- NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: "false"
- NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: "false"
- NEXT_PUBLIC_ENABLE_NOTIFICATIONS: "true"
- NEXT_PUBLIC_ENABLE_FISCHEREI: "true"
- NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS: "true"
- NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG: "true"
+ NEXT_PUBLIC_ENABLE_THEME_TOGGLE: 'true'
+ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: 'true'
+ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: 'true'
+ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: 'false'
+ NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: 'false'
+ NEXT_PUBLIC_ENABLE_NOTIFICATIONS: 'true'
+ NEXT_PUBLIC_ENABLE_FISCHEREI: 'true'
+ NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS: 'true'
+ NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG: 'true'
volumes:
supabase-db-data:
diff --git a/packages/features/course-management/src/server/api.ts b/packages/features/course-management/src/server/api.ts
index 83501fb15..3894fb97a 100644
--- a/packages/features/course-management/src/server/api.ts
+++ b/packages/features/course-management/src/server/api.ts
@@ -1,7 +1,11 @@
-import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js';
-import type { CreateCourseInput, EnrollParticipantInput } from '../schema/course.schema';
+import type { Database } from '@kit/supabase/database';
+
+import type {
+ CreateCourseInput,
+ EnrollParticipantInput,
+} from '../schema/course.schema';
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,11 +14,25 @@ export function createCourseManagementApi(client: SupabaseClient) {
return {
// --- Courses ---
- async listCourses(accountId: string, opts?: { status?: string; search?: string; page?: number; pageSize?: number }) {
- let query = client.from('courses').select('*', { count: 'exact' })
- .eq('account_id', accountId).order('start_date', { ascending: false });
+ async listCourses(
+ accountId: string,
+ opts?: {
+ status?: string;
+ search?: string;
+ page?: number;
+ pageSize?: number;
+ },
+ ) {
+ let query = client
+ .from('courses')
+ .select('*', { count: 'exact' })
+ .eq('account_id', accountId)
+ .order('start_date', { ascending: false });
if (opts?.status) query = query.eq('status', opts.status);
- if (opts?.search) query = query.or(`name.ilike.%${opts.search}%,course_number.ilike.%${opts.search}%`);
+ if (opts?.search)
+ query = query.or(
+ `name.ilike.%${opts.search}%,course_number.ilike.%${opts.search}%`,
+ );
const page = opts?.page ?? 1;
const pageSize = opts?.pageSize ?? 25;
query = query.range((page - 1) * pageSize, page * pageSize - 1);
@@ -24,20 +42,38 @@ export function createCourseManagementApi(client: SupabaseClient) {
},
async getCourse(courseId: string) {
- const { data, error } = await client.from('courses').select('*').eq('id', courseId).single();
+ const { data, error } = await client
+ .from('courses')
+ .select('*')
+ .eq('id', courseId)
+ .single();
if (error) throw error;
return data;
},
async createCourse(input: CreateCourseInput) {
- const { data, error } = await client.from('courses').insert({
- account_id: input.accountId, course_number: input.courseNumber || null, name: input.name,
- description: input.description || null, category_id: input.categoryId || null, instructor_id: input.instructorId || null,
- location_id: input.locationId || null, start_date: input.startDate || null, end_date: input.endDate || null,
- fee: input.fee, reduced_fee: input.reducedFee ?? null, capacity: input.capacity,
- min_participants: input.minParticipants, status: input.status,
- registration_deadline: input.registrationDeadline || null, notes: input.notes || null,
- }).select().single();
+ const { data, error } = await client
+ .from('courses')
+ .insert({
+ account_id: input.accountId,
+ course_number: input.courseNumber || null,
+ name: input.name,
+ description: input.description || null,
+ category_id: input.categoryId || null,
+ instructor_id: input.instructorId || null,
+ location_id: input.locationId || null,
+ start_date: input.startDate || null,
+ end_date: input.endDate || null,
+ fee: input.fee,
+ reduced_fee: input.reducedFee ?? null,
+ capacity: input.capacity,
+ min_participants: input.minParticipants,
+ status: input.status,
+ registration_deadline: input.registrationDeadline || null,
+ notes: input.notes || null,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
@@ -45,96 +81,161 @@ export function createCourseManagementApi(client: SupabaseClient) {
// --- Enrollment ---
async enrollParticipant(input: EnrollParticipantInput) {
// Check capacity
- const { count } = await client.from('course_participants').select('*', { count: 'exact', head: true })
- .eq('course_id', input.courseId).in('status', ['enrolled']);
+ const { count } = await client
+ .from('course_participants')
+ .select('*', { count: 'exact', head: true })
+ .eq('course_id', input.courseId)
+ .in('status', ['enrolled']);
const course = await this.getCourse(input.courseId);
- const status = (count ?? 0) >= course.capacity ? 'waitlisted' : 'enrolled';
+ const status =
+ (count ?? 0) >= course.capacity ? 'waitlisted' : 'enrolled';
- const { data, error } = await client.from('course_participants').insert({
- course_id: input.courseId, member_id: input.memberId,
- first_name: input.firstName, last_name: input.lastName,
- email: input.email, phone: input.phone, status,
- }).select().single();
+ const { data, error } = await client
+ .from('course_participants')
+ .insert({
+ course_id: input.courseId,
+ member_id: input.memberId,
+ first_name: input.firstName,
+ last_name: input.lastName,
+ email: input.email,
+ phone: input.phone,
+ status,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
async cancelEnrollment(participantId: string) {
- const { error } = await client.from('course_participants')
+ const { error } = await client
+ .from('course_participants')
.update({ status: 'cancelled', cancelled_at: new Date().toISOString() })
.eq('id', participantId);
if (error) throw error;
},
async getParticipants(courseId: string) {
- const { data, error } = await client.from('course_participants').select('*')
- .eq('course_id', courseId).order('enrolled_at');
+ const { data, error } = await client
+ .from('course_participants')
+ .select('*')
+ .eq('course_id', courseId)
+ .order('enrolled_at');
if (error) throw error;
return data ?? [];
},
// --- Sessions ---
async getSessions(courseId: string) {
- const { data, error } = await client.from('course_sessions').select('*')
- .eq('course_id', courseId).order('session_date');
+ const { data, error } = await client
+ .from('course_sessions')
+ .select('*')
+ .eq('course_id', courseId)
+ .order('session_date');
if (error) throw error;
return data ?? [];
},
- async createSession(input: { courseId: string; sessionDate: string; startTime: string; endTime: string; locationId?: string }) {
- const { data, error } = await client.from('course_sessions').insert({
- course_id: input.courseId, session_date: input.sessionDate,
- start_time: input.startTime, end_time: input.endTime, location_id: input.locationId,
- }).select().single();
+ async createSession(input: {
+ courseId: string;
+ sessionDate: string;
+ startTime: string;
+ endTime: string;
+ locationId?: string;
+ }) {
+ const { data, error } = await client
+ .from('course_sessions')
+ .insert({
+ course_id: input.courseId,
+ session_date: input.sessionDate,
+ start_time: input.startTime,
+ end_time: input.endTime,
+ location_id: input.locationId,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
// --- Attendance ---
async getAttendance(sessionId: string) {
- const { data, error } = await client.from('course_attendance').select('*').eq('session_id', sessionId);
+ const { data, error } = await client
+ .from('course_attendance')
+ .select('*')
+ .eq('session_id', sessionId);
if (error) throw error;
return data ?? [];
},
- async markAttendance(sessionId: string, participantId: string, present: boolean) {
- const { error } = await client.from('course_attendance').upsert({
- session_id: sessionId, participant_id: participantId, present,
- }, { onConflict: 'session_id,participant_id' });
+ async markAttendance(
+ sessionId: string,
+ participantId: string,
+ present: boolean,
+ ) {
+ const { error } = await client.from('course_attendance').upsert(
+ {
+ session_id: sessionId,
+ participant_id: participantId,
+ present,
+ },
+ { onConflict: 'session_id,participant_id' },
+ );
if (error) throw error;
},
// --- Categories, Instructors, Locations ---
async listCategories(accountId: string) {
- const { data, error } = await client.from('course_categories').select('*')
- .eq('account_id', accountId).order('sort_order');
+ const { data, error } = await client
+ .from('course_categories')
+ .select('*')
+ .eq('account_id', accountId)
+ .order('sort_order');
if (error) throw error;
return data ?? [];
},
async listInstructors(accountId: string) {
- const { data, error } = await client.from('course_instructors').select('*')
- .eq('account_id', accountId).order('last_name');
+ const { data, error } = await client
+ .from('course_instructors')
+ .select('*')
+ .eq('account_id', accountId)
+ .order('last_name');
if (error) throw error;
return data ?? [];
},
async listLocations(accountId: string) {
- const { data, error } = await client.from('course_locations').select('*')
- .eq('account_id', accountId).order('name');
+ const { data, error } = await client
+ .from('course_locations')
+ .select('*')
+ .eq('account_id', accountId)
+ .order('name');
if (error) throw error;
return data ?? [];
},
// --- Statistics ---
async getStatistics(accountId: string) {
- const { data: courses } = await client.from('courses').select('status').eq('account_id', accountId);
- const { count: totalParticipants } = await client.from('course_participants')
+ const { data: courses } = await client
+ .from('courses')
+ .select('status')
+ .eq('account_id', accountId);
+ const { count: totalParticipants } = await client
+ .from('course_participants')
.select('*', { count: 'exact', head: true })
- .in('course_id', (courses ?? []).map((c: any) => c.id));
+ .in(
+ 'course_id',
+ (courses ?? []).map((c: any) => c.id),
+ );
- const stats = { totalCourses: 0, openCourses: 0, completedCourses: 0, totalParticipants: totalParticipants ?? 0 };
- for (const c of (courses ?? [])) {
+ const stats = {
+ totalCourses: 0,
+ openCourses: 0,
+ completedCourses: 0,
+ totalParticipants: totalParticipants ?? 0,
+ };
+ for (const c of courses ?? []) {
stats.totalCourses++;
if (c.status === 'open' || c.status === 'running') stats.openCourses++;
if (c.status === 'completed') stats.completedCourses++;
@@ -143,30 +244,70 @@ export function createCourseManagementApi(client: SupabaseClient) {
},
// --- Create methods for CRUD ---
- async createCategory(input: { accountId: string; name: string; description?: string; parentId?: string }) {
- const { data, error } = await client.from('course_categories').insert({
- account_id: input.accountId, name: input.name, description: input.description,
- parent_id: input.parentId,
- }).select().single();
+ async createCategory(input: {
+ accountId: string;
+ name: string;
+ description?: string;
+ parentId?: string;
+ }) {
+ const { data, error } = await client
+ .from('course_categories')
+ .insert({
+ account_id: input.accountId,
+ name: input.name,
+ description: input.description,
+ parent_id: input.parentId,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
- async createInstructor(input: { accountId: string; firstName: string; lastName: string; email?: string; phone?: string; qualifications?: string; hourlyRate?: number }) {
- const { data, error } = await client.from('course_instructors').insert({
- account_id: input.accountId, first_name: input.firstName, last_name: input.lastName,
- email: input.email, phone: input.phone, qualifications: input.qualifications,
- hourly_rate: input.hourlyRate,
- }).select().single();
+ async createInstructor(input: {
+ accountId: string;
+ firstName: string;
+ lastName: string;
+ email?: string;
+ phone?: string;
+ qualifications?: string;
+ hourlyRate?: number;
+ }) {
+ const { data, error } = await client
+ .from('course_instructors')
+ .insert({
+ account_id: input.accountId,
+ first_name: input.firstName,
+ last_name: input.lastName,
+ email: input.email,
+ phone: input.phone,
+ qualifications: input.qualifications,
+ hourly_rate: input.hourlyRate,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
- async createLocation(input: { accountId: string; name: string; address?: string; room?: string; capacity?: number }) {
- const { data, error } = await client.from('course_locations').insert({
- account_id: input.accountId, name: input.name, address: input.address,
- room: input.room, capacity: input.capacity,
- }).select().single();
+ async createLocation(input: {
+ accountId: string;
+ name: string;
+ address?: string;
+ room?: string;
+ capacity?: number;
+ }) {
+ const { data, error } = await client
+ .from('course_locations')
+ .insert({
+ account_id: input.accountId,
+ name: input.name,
+ address: input.address,
+ room: input.room,
+ capacity: input.capacity,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
diff --git a/packages/features/event-management/src/server/api.ts b/packages/features/event-management/src/server/api.ts
index b6a80c963..c13037599 100644
--- a/packages/features/event-management/src/server/api.ts
+++ b/packages/features/event-management/src/server/api.ts
@@ -1,6 +1,7 @@
-import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js';
+import type { Database } from '@kit/supabase/database';
+
import type { CreateEventInput } from '../schema/event.schema';
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,16 +11,28 @@ export function createEventManagementApi(client: SupabaseClient) {
const db = client;
return {
- async listEvents(accountId: string, opts?: { status?: string; page?: number }) {
- let query = client.from('events').select('*', { count: 'exact' })
- .eq('account_id', accountId).order('event_date', { ascending: false });
+ async listEvents(
+ accountId: string,
+ opts?: { status?: string; page?: number },
+ ) {
+ let query = client
+ .from('events')
+ .select('*', { count: 'exact' })
+ .eq('account_id', accountId)
+ .order('event_date', { ascending: false });
if (opts?.status) query = query.eq('status', opts.status);
const page = opts?.page ?? 1;
query = query.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1);
const { data, error, count } = await query;
if (error) throw error;
const total = count ?? 0;
- return { data: data ?? [], total, page, pageSize: PAGE_SIZE, totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)) };
+ return {
+ data: data ?? [],
+ total,
+ page,
+ pageSize: PAGE_SIZE,
+ totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
+ };
},
async getRegistrationCounts(eventIds: string[]) {
@@ -40,71 +53,131 @@ export function createEventManagementApi(client: SupabaseClient) {
},
async getEvent(eventId: string) {
- const { data, error } = await client.from('events').select('*').eq('id', eventId).single();
+ const { data, error } = await client
+ .from('events')
+ .select('*')
+ .eq('id', eventId)
+ .single();
if (error) throw error;
return data;
},
async createEvent(input: CreateEventInput) {
- const { data, error } = await client.from('events').insert({
- account_id: input.accountId, name: input.name, description: input.description || null,
- event_date: input.eventDate || null, event_time: input.eventTime || null, end_date: input.endDate || null,
- location: input.location || null, capacity: input.capacity, min_age: input.minAge ?? null,
- max_age: input.maxAge ?? null, fee: input.fee, status: input.status,
- registration_deadline: input.registrationDeadline || null,
- contact_name: input.contactName || null, contact_email: input.contactEmail || null, contact_phone: input.contactPhone || null,
- }).select().single();
+ const { data, error } = await client
+ .from('events')
+ .insert({
+ account_id: input.accountId,
+ name: input.name,
+ description: input.description || null,
+ event_date: input.eventDate || null,
+ event_time: input.eventTime || null,
+ end_date: input.endDate || null,
+ location: input.location || null,
+ capacity: input.capacity,
+ min_age: input.minAge ?? null,
+ max_age: input.maxAge ?? null,
+ fee: input.fee,
+ status: input.status,
+ registration_deadline: input.registrationDeadline || null,
+ contact_name: input.contactName || null,
+ contact_email: input.contactEmail || null,
+ contact_phone: input.contactPhone || null,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
- async registerForEvent(input: { eventId: string; firstName: string; lastName: string; email?: string; parentName?: string }) {
+ async registerForEvent(input: {
+ eventId: string;
+ firstName: string;
+ lastName: string;
+ email?: string;
+ parentName?: string;
+ }) {
// Check capacity
const event = await this.getEvent(input.eventId);
if (event.capacity) {
- const { count } = await client.from('event_registrations').select('*', { count: 'exact', head: true })
- .eq('event_id', input.eventId).in('status', ['pending', 'confirmed']);
+ const { count } = await client
+ .from('event_registrations')
+ .select('*', { count: 'exact', head: true })
+ .eq('event_id', input.eventId)
+ .in('status', ['pending', 'confirmed']);
if ((count ?? 0) >= event.capacity) {
throw new Error('Event is full');
}
}
- const { data, error } = await client.from('event_registrations').insert({
- event_id: input.eventId, first_name: input.firstName, last_name: input.lastName,
- email: input.email, parent_name: input.parentName, status: 'confirmed',
- }).select().single();
+ const { data, error } = await client
+ .from('event_registrations')
+ .insert({
+ event_id: input.eventId,
+ first_name: input.firstName,
+ last_name: input.lastName,
+ email: input.email,
+ parent_name: input.parentName,
+ status: 'confirmed',
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},
async getRegistrations(eventId: string) {
- const { data, error } = await client.from('event_registrations').select('*')
- .eq('event_id', eventId).order('created_at');
+ const { data, error } = await client
+ .from('event_registrations')
+ .select('*')
+ .eq('event_id', eventId)
+ .order('created_at');
if (error) throw error;
return data ?? [];
},
// Holiday passes
async listHolidayPasses(accountId: string) {
- const { data, error } = await client.from('holiday_passes').select('*')
- .eq('account_id', accountId).order('year', { ascending: false });
+ const { data, error } = await client
+ .from('holiday_passes')
+ .select('*')
+ .eq('account_id', accountId)
+ .order('year', { ascending: false });
if (error) throw error;
return data ?? [];
},
async getPassActivities(passId: string) {
- const { data, error } = await client.from('holiday_pass_activities').select('*')
- .eq('pass_id', passId).order('activity_date');
+ const { data, error } = await client
+ .from('holiday_pass_activities')
+ .select('*')
+ .eq('pass_id', passId)
+ .order('activity_date');
if (error) throw error;
return data ?? [];
},
- async createHolidayPass(input: { accountId: string; name: string; year: number; description?: string; price?: number; validFrom?: string; validUntil?: string }) {
- const { data, error } = await client.from('holiday_passes').insert({
- account_id: input.accountId, name: input.name, year: input.year,
- description: input.description, price: input.price ?? 0,
- valid_from: input.validFrom, valid_until: input.validUntil,
- }).select().single();
+ async createHolidayPass(input: {
+ accountId: string;
+ name: string;
+ year: number;
+ description?: string;
+ price?: number;
+ validFrom?: string;
+ validUntil?: string;
+ }) {
+ const { data, error } = await client
+ .from('holiday_passes')
+ .insert({
+ account_id: input.accountId,
+ name: input.name,
+ year: input.year,
+ description: input.description,
+ price: input.price ?? 0,
+ valid_from: input.validFrom,
+ valid_until: input.validUntil,
+ })
+ .select()
+ .single();
if (error) throw error;
return data;
},