From c98cada7f69d5374fb7a82a61896c0055432ded9 Mon Sep 17 00:00:00 2001 From: "T. Zehetbauer" <125989630+4thTomost@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:33:43 +0200 Subject: [PATCH] refactor: improve code readability and consistency in api.ts and common.json --- .../config/team-account-navigation.config.tsx | 606 +++++++++++++++--- apps/web/i18n/messages/de/common.json | 85 ++- apps/web/i18n/messages/en/common.json | 85 ++- apps/web/proxy.ts | 4 +- docker-compose.yml | 76 ++- .../course-management/src/server/api.ts | 271 ++++++-- .../event-management/src/server/api.ts | 139 +++- 7 files changed, 1032 insertions(+), 234 deletions(-) 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; },