refactor: improve code readability and consistency in api.ts and common.json

This commit is contained in:
T. Zehetbauer
2026-04-01 13:33:43 +02:00
parent 2a9d543ee4
commit c98cada7f6
7 changed files with 1032 additions and 234 deletions

View File

@@ -1,17 +1,65 @@
import { import {
CreditCard,
LayoutDashboard, LayoutDashboard,
Settings, Settings,
Users, UserCog,
Database, CreditCard,
// People (Members + Access)
UserCheck, UserCheck,
UserPlus,
IdCard,
ClipboardList,
KeyRound,
// Courses
GraduationCap, GraduationCap,
CalendarDays,
MapPin,
UserRound,
// Events
CalendarHeart,
Ticket,
PartyPopper,
// Bookings
Hotel, Hotel,
Calendar, BedDouble,
Contact,
CalendarRange,
// Finance
Wallet, Wallet,
Receipt,
Landmark,
BarChart3,
// Documents
FileText, FileText,
FilePlus,
FileStack,
// Newsletter
Mail, 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'; } from 'lucide-react';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema'; import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
@@ -21,101 +69,499 @@ import pathsConfig from '~/config/paths.config';
const iconClasses = 'w-4'; 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: <LayoutDashboard className={iconClasses} />,
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: <UserCheck className={iconClasses} />,
},
{
label: 'common:routes.memberApplications',
path: createPath(
pathsConfig.app.accountCmsMembers + '/applications',
account,
),
Icon: <UserPlus className={iconClasses} />,
},
{
label: 'common:routes.memberPortal',
path: createPath(
pathsConfig.app.accountCmsMembers + '/portal',
account,
),
Icon: <KeyRound className={iconClasses} />,
},
{
label: 'common:routes.memberCards',
path: createPath(
pathsConfig.app.accountCmsMembers + '/cards',
account,
),
Icon: <IdCard className={iconClasses} />,
},
{
label: 'common:routes.memberDues',
path: createPath(
pathsConfig.app.accountCmsMembers + '/dues',
account,
),
Icon: <ClipboardList className={iconClasses} />,
},
);
}
// Admin users who can log in — always visible
peopleChildren.push({
label: 'common:routes.accessAndRoles',
path: createPath(pathsConfig.app.accountMembers, account),
Icon: <UserCog className={iconClasses} />,
});
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: <GraduationCap className={iconClasses} />,
},
{
label: 'common:routes.courseCalendar',
path: createPath(
pathsConfig.app.accountCourses + '/calendar',
account,
),
Icon: <CalendarDays className={iconClasses} />,
},
{
label: 'common:routes.courseInstructors',
path: createPath(
pathsConfig.app.accountCourses + '/instructors',
account,
),
Icon: <UserRound className={iconClasses} />,
},
{
label: 'common:routes.courseLocations',
path: createPath(
pathsConfig.app.accountCourses + '/locations',
account,
),
Icon: <MapPin className={iconClasses} />,
},
],
});
}
// ── Events ──
routes.push({
label: 'common:routes.eventManagement',
collapsible: true,
children: [ children: [
{ {
label: 'common.routes.dashboard', label: 'common:routes.eventList',
path: pathsConfig.app.accountHome.replace('[account]', account), path: createPath('/home/[account]/events', account),
Icon: <LayoutDashboard className={iconClasses} />, Icon: <CalendarHeart className={iconClasses} />,
highlightMatch: `${pathsConfig.app.home}$`,
}, },
featureFlagsConfig.enableModuleBuilder
? {
label: 'common.routes.modules',
path: createPath(pathsConfig.app.accountModules, account),
Icon: <Database className={iconClasses} />,
}
: undefined,
featureFlagsConfig.enableMemberManagement
? {
label: 'common.routes.cmsMembers',
path: createPath(pathsConfig.app.accountCmsMembers, account),
Icon: <UserCheck className={iconClasses} />,
}
: undefined,
featureFlagsConfig.enableCourseManagement
? {
label: 'common.routes.courses',
path: createPath(pathsConfig.app.accountCourses, account),
Icon: <GraduationCap className={iconClasses} />,
}
: undefined,
featureFlagsConfig.enableBookingManagement
? {
label: 'common.routes.bookings',
path: createPath(pathsConfig.app.accountBookings, account),
Icon: <Hotel className={iconClasses} />,
}
: undefined,
{ {
label: 'common.routes.events', label: 'common:routes.eventRegistrations',
path: createPath(`/home/[account]/events`, account), path: createPath('/home/[account]/events/registrations', account),
Icon: <Calendar className={iconClasses} />, Icon: <Ticket className={iconClasses} />,
}, },
featureFlagsConfig.enableSepaPayments
? {
label: 'common.routes.finance',
path: createPath(pathsConfig.app.accountFinance, account),
Icon: <Wallet className={iconClasses} />,
}
: undefined,
featureFlagsConfig.enableDocumentGeneration
? {
label: 'common.routes.documents',
path: createPath(pathsConfig.app.accountDocuments, account),
Icon: <FileText className={iconClasses} />,
}
: undefined,
featureFlagsConfig.enableNewsletter
? {
label: 'common.routes.newsletter',
path: createPath(pathsConfig.app.accountNewsletter, account),
Icon: <Mail className={iconClasses} />,
}
: undefined,
{ {
label: 'common.routes.siteBuilder', label: 'common:routes.holidayPasses',
path: createPath(`/home/[account]/site-builder`, account), path: createPath('/home/[account]/events/holiday-passes', account),
Icon: <Globe className={iconClasses} />, Icon: <PartyPopper className={iconClasses} />,
}, },
].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: <Hotel className={iconClasses} />,
},
{
label: 'common:routes.bookingCalendar',
path: createPath(
pathsConfig.app.accountBookings + '/calendar',
account,
),
Icon: <CalendarRange className={iconClasses} />,
},
{
label: 'common:routes.bookingRooms',
path: createPath(pathsConfig.app.accountBookings + '/rooms', account),
Icon: <BedDouble className={iconClasses} />,
},
{
label: 'common:routes.bookingGuests',
path: createPath(
pathsConfig.app.accountBookings + '/guests',
account,
),
Icon: <Contact className={iconClasses} />,
},
],
});
}
// ── Finance ──
if (featureFlagsConfig.enableSepaPayments) {
routes.push({
label: 'common:routes.financeManagement',
collapsible: true,
children: [
{
label: 'common:routes.financeOverview',
path: createPath(pathsConfig.app.accountFinance, account),
Icon: <Wallet className={iconClasses} />,
},
{
label: 'common:routes.financeInvoices',
path: createPath(
pathsConfig.app.accountFinance + '/invoices',
account,
),
Icon: <Receipt className={iconClasses} />,
},
{
label: 'common:routes.financeSepa',
path: createPath(pathsConfig.app.accountFinance + '/sepa', account),
Icon: <Landmark className={iconClasses} />,
},
{
label: 'common:routes.financePayments',
path: createPath(
pathsConfig.app.accountFinance + '/payments',
account,
),
Icon: <BarChart3 className={iconClasses} />,
},
],
});
}
// ── Documents ──
if (featureFlagsConfig.enableDocumentGeneration) {
routes.push({
label: 'common:routes.documentManagement',
collapsible: true,
children: [
{
label: 'common:routes.documentOverview',
path: createPath(pathsConfig.app.accountDocuments, account),
Icon: <FileText className={iconClasses} />,
},
{
label: 'common:routes.documentGenerate',
path: createPath(
pathsConfig.app.accountDocuments + '/generate',
account,
),
Icon: <FilePlus className={iconClasses} />,
},
{
label: 'common:routes.documentTemplates',
path: createPath(
pathsConfig.app.accountDocuments + '/templates',
account,
),
Icon: <FileStack className={iconClasses} />,
},
],
});
}
// ── Newsletter ──
if (featureFlagsConfig.enableNewsletter) {
routes.push({
label: 'common:routes.newsletterManagement',
collapsible: true,
children: [
{
label: 'common:routes.newsletterCampaigns',
path: createPath(pathsConfig.app.accountNewsletter, account),
Icon: <Mail className={iconClasses} />,
},
{
label: 'common:routes.newsletterNew',
path: createPath(pathsConfig.app.accountNewsletter + '/new', account),
Icon: <MailPlus className={iconClasses} />,
},
{
label: 'common:routes.newsletterTemplates',
path: createPath(
pathsConfig.app.accountNewsletter + '/templates',
account,
),
Icon: <FileCode className={iconClasses} />,
},
],
});
}
// ── 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: <PanelTop className={iconClasses} />,
},
{
label: 'common:routes.sitePosts',
path: createPath(
pathsConfig.app.accountSiteBuilder + '/posts',
account,
),
Icon: <Newspaper className={iconClasses} />,
},
{
label: 'common:routes.siteSettings',
path: createPath(
pathsConfig.app.accountSiteBuilder + '/settings',
account,
),
Icon: <Palette className={iconClasses} />,
},
],
});
}
// ── 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: <Database className={iconClasses} />,
},
],
});
}
// ── Fisheries ──
if (featureFlagsConfig.enableFischerei) {
routes.push({
label: 'common:routes.fisheriesManagement',
collapsible: true,
children: [
{
label: 'common:routes.fisheriesOverview',
path: createPath(pathsConfig.app.accountFischerei, account),
Icon: <Fish className={iconClasses} />,
},
{
label: 'common:routes.fisheriesWaters',
path: createPath(
pathsConfig.app.accountFischerei + '/waters',
account,
),
Icon: <Waves className={iconClasses} />,
},
{
label: 'common:routes.fisheriesLeases',
path: createPath(
pathsConfig.app.accountFischerei + '/leases',
account,
),
Icon: <Anchor className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCatchBooks',
path: createPath(
pathsConfig.app.accountFischerei + '/catch-books',
account,
),
Icon: <BookOpen className={iconClasses} />,
},
{
label: 'common:routes.fisheriesPermits',
path: createPath(
pathsConfig.app.accountFischerei + '/permits',
account,
),
Icon: <ShieldCheck className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCompetitions',
path: createPath(
pathsConfig.app.accountFischerei + '/competitions',
account,
),
Icon: <Trophy className={iconClasses} />,
},
],
});
}
// ── 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: <BookMarked className={iconClasses} />,
},
{
label: 'common:routes.meetingsProtocols',
path: createPath(
pathsConfig.app.accountMeetings + '/protocols',
account,
),
Icon: <ScrollText className={iconClasses} />,
},
{
label: 'common:routes.meetingsTasks',
path: createPath(pathsConfig.app.accountMeetings + '/tasks', account),
Icon: <ListChecks className={iconClasses} />,
},
],
});
}
// ── 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: <Building2 className={iconClasses} />,
},
{
label: 'common:routes.associationHierarchy',
path: createPath(
pathsConfig.app.accountVerband + '/hierarchy',
account,
),
Icon: <Network className={iconClasses} />,
},
{
label: 'common:routes.associationMemberSearch',
path: createPath(
pathsConfig.app.accountVerband + '/members',
account,
),
Icon: <SearchCheck className={iconClasses} />,
},
{
label: 'common:routes.associationEvents',
path: createPath(pathsConfig.app.accountVerband + '/events', account),
Icon: <Share2 className={iconClasses} />,
},
{
label: 'common:routes.associationReporting',
path: createPath(
pathsConfig.app.accountVerband + '/reporting',
account,
),
Icon: <PieChart className={iconClasses} />,
},
{
label: 'common:routes.associationTemplates',
path: createPath(
pathsConfig.app.accountVerband + '/templates',
account,
),
Icon: <LayoutTemplate className={iconClasses} />,
},
],
});
}
// ── Administration ──
routes.push({
label: 'common:routes.administration',
collapsible: false, collapsible: false,
children: [ children: [
{ {
label: 'common.routes.settings', label: 'common:routes.accountSettings',
path: createPath(pathsConfig.app.accountSettings, account), path: createPath(pathsConfig.app.accountSettings, account),
Icon: <Settings className={iconClasses} />, Icon: <Settings className={iconClasses} />,
}, },
{
label: 'common.routes.members',
path: createPath(pathsConfig.app.accountMembers, account),
Icon: <Users className={iconClasses} />,
},
featureFlagsConfig.enableTeamAccountBilling featureFlagsConfig.enableTeamAccountBilling
? { ? {
label: 'common.routes.billing', label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account), path: createPath(pathsConfig.app.accountBilling, account),
Icon: <CreditCard className={iconClasses} />, Icon: <CreditCard className={iconClasses} />,
} }
: undefined, : undefined,
].filter(Boolean), ],
}, });
];
return routes;
};
export function getTeamAccountSidebarConfig(account: string) { export function getTeamAccountSidebarConfig(account: string) {
return NavigationConfigSchema.parse({ return NavigationConfigSchema.parse({

View File

@@ -61,24 +61,83 @@
"routes": { "routes": {
"home": "Startseite", "home": "Startseite",
"account": "Konto", "account": "Konto",
"members": "Mitglieder",
"billing": "Abrechnung", "billing": "Abrechnung",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"settings": "Einstellungen", "settings": "Einstellungen",
"profile": "Profil", "profile": "Profil",
"application": "Anwendung",
"modules": "Module", "people": "Personen",
"cmsMembers": "Mitglieder", "clubMembers": "Vereinsmitglieder",
"courses": "Kurse", "memberApplications": "Aufnahmeanträge",
"bookings": "Buchungen", "memberPortal": "Mitgliederportal",
"finance": "Finanzen", "memberCards": "Mitgliedsausweise",
"documents": "Dokumente", "memberDues": "Beitragskategorien",
"newsletter": "Newsletter", "accessAndRoles": "Zugänge & Rollen",
"events": "Veranstaltungen",
"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", "siteBuilder": "Website",
"fischerei": "Fischerei", "sitePages": "Seiten",
"meetings": "Sitzungsprotokolle", "sitePosts": "Beiträge",
"verband": "Verbandsverwaltung" "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": { "roles": {
"owner": { "owner": {

View File

@@ -61,24 +61,83 @@
"routes": { "routes": {
"home": "Home", "home": "Home",
"account": "Account", "account": "Account",
"members": "Members",
"billing": "Billing", "billing": "Billing",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"settings": "Settings", "settings": "Settings",
"profile": "Profile", "profile": "Profile",
"application": "Application",
"modules": "Modules", "people": "People",
"cmsMembers": "Members", "clubMembers": "Club Members",
"courses": "Courses", "memberApplications": "Applications",
"bookings": "Bookings", "memberPortal": "Member Portal",
"events": "Events", "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", "siteBuilder": "Website",
"finance": "Finance", "sitePages": "Pages",
"documents": "Documents", "sitePosts": "Posts",
"newsletter": "Newsletter", "siteSettings": "Settings",
"fischerei": "Fisheries",
"meetings": "Meeting Protocols", "customModules": "Custom Modules",
"verband": "Federation Management" "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": { "roles": {
"owner": { "owner": {

View File

@@ -165,7 +165,9 @@ async function getPatterns() {
} catch { } catch {
// Supabase unreachable — redirect to sign in // Supabase unreachable — redirect to sign in
const signIn = pathsConfig.auth.signIn; 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; const { origin, pathname: next } = req.nextUrl;

View File

@@ -28,7 +28,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: postgres POSTGRES_DB: postgres
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] test: ['CMD-SHELL', 'pg_isready -U postgres -d postgres']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 10 retries: 10
@@ -49,7 +49,7 @@ services:
environment: environment:
PGPASSWORD: ${POSTGRES_PASSWORD} PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
entrypoint: ["/bin/sh", "-c"] entrypoint: ['/bin/sh', '-c']
command: command:
- | - |
echo "🔑 Ensuring role passwords are set (idempotent)..." echo "🔑 Ensuring role passwords are set (idempotent)..."
@@ -63,7 +63,7 @@ services:
echo "✅ App migrations complete." echo "✅ App migrations complete."
echo "" echo ""
sh /app-seed/dev-bootstrap.sh sh /app-seed/dev-bootstrap.sh
restart: "no" restart: 'no'
# ===================================================== # =====================================================
# Supabase Auth (GoTrue) # Supabase Auth (GoTrue)
@@ -102,7 +102,15 @@ services:
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
healthcheck: 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 interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -123,9 +131,9 @@ services:
PGRST_DB_SCHEMAS: public,storage,graphql_public PGRST_DB_SCHEMAS: public,storage,graphql_public
PGRST_DB_ANON_ROLE: anon PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false" PGRST_DB_USE_LEGACY_GUCS: 'false'
healthcheck: healthcheck:
test: ["CMD-SHELL", "head -c0 </dev/tcp/localhost/3000 || exit 1"] test: ['CMD-SHELL', 'head -c0 </dev/tcp/localhost/3000 || exit 1']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -149,20 +157,20 @@ services:
DB_USER: supabase_admin DB_USER: supabase_admin
DB_PASSWORD: ${POSTGRES_PASSWORD} DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: postgres DB_NAME: postgres
DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: supabaserealtime DB_ENC_KEY: supabaserealtime
API_JWT_SECRET: ${JWT_SECRET} API_JWT_SECRET: ${JWT_SECRET}
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq} SECRET_KEY_BASE: ${SECRET_KEY_BASE:-UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq}
ERL_AFLAGS: "-proto_dist inet_tcp" ERL_AFLAGS: '-proto_dist inet_tcp'
DNS_NODES: "''" DNS_NODES: "''"
RLIMIT_NOFILE: "10000" RLIMIT_NOFILE: '10000'
APP_NAME: realtime APP_NAME: realtime
SEED_SELF_HOST: "true" SEED_SELF_HOST: 'true'
REPLICATION_MODE: RLS REPLICATION_MODE: RLS
REPLICATION_POLL_INTERVAL: 100 REPLICATION_POLL_INTERVAL: 100
SECURE_CHANNELS: "true" SECURE_CHANNELS: 'true'
SLOT_NAME: supabase_realtime_rls SLOT_NAME: supabase_realtime_rls
TEMPORARY_SLOT: "true" TEMPORARY_SLOT: 'true'
MAX_RECORD_BYTES: 1048576 MAX_RECORD_BYTES: 1048576
# ===================================================== # =====================================================
@@ -196,7 +204,11 @@ services:
GLOBAL_S3_BUCKET: stub GLOBAL_S3_BUCKET: stub
IMGPROXY_URL: http://supabase-imgproxy:8080 IMGPROXY_URL: http://supabase-imgproxy:8080
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5000/status || exit 1"] test:
[
'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://localhost:5000/status || exit 1',
]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -209,10 +221,10 @@ services:
image: darthsim/imgproxy:v3.8.0 image: darthsim/imgproxy:v3.8.0
restart: unless-stopped restart: unless-stopped
environment: environment:
IMGPROXY_BIND: ":8080" IMGPROXY_BIND: ':8080'
IMGPROXY_LOCAL_FILESYSTEM_ROOT: / IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
IMGPROXY_USE_ETAG: "true" IMGPROXY_USE_ETAG: 'true'
IMGPROXY_ENABLE_WEBP_DETECTION: "true" IMGPROXY_ENABLE_WEBP_DETECTION: 'true'
# ===================================================== # =====================================================
# Supabase pg_meta (DB introspection for Studio) # Supabase pg_meta (DB introspection for Studio)
@@ -252,10 +264,16 @@ services:
SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY} SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}
SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET} AUTH_JWT_SECRET: ${JWT_SECRET}
NEXT_PUBLIC_ENABLE_LOGS: "true" NEXT_PUBLIC_ENABLE_LOGS: 'true'
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
healthcheck: healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/profile', (r) => 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 interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -284,7 +302,7 @@ services:
entrypoint: > 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" 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: environment:
KONG_DATABASE: "off" KONG_DATABASE: 'off'
KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
@@ -295,7 +313,7 @@ services:
volumes: volumes:
- ./docker/kong.yml:/var/lib/kong/kong.yml.tpl:ro - ./docker/kong.yml:/var/lib/kong/kong.yml.tpl:ro
healthcheck: healthcheck:
test: ["CMD", "kong", "health"] test: ['CMD', 'kong', 'health']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -329,15 +347,15 @@ services:
SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret} SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret}
EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de} EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de}
NEXT_PUBLIC_PRODUCT_NAME: MyEasyCMS NEXT_PUBLIC_PRODUCT_NAME: MyEasyCMS
NEXT_PUBLIC_ENABLE_THEME_TOGGLE: "true" NEXT_PUBLIC_ENABLE_THEME_TOGGLE: 'true'
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: "true" NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: 'true'
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: "true" NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: 'true'
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: "false" NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: 'false'
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: "false" NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: 'false'
NEXT_PUBLIC_ENABLE_NOTIFICATIONS: "true" NEXT_PUBLIC_ENABLE_NOTIFICATIONS: 'true'
NEXT_PUBLIC_ENABLE_FISCHEREI: "true" NEXT_PUBLIC_ENABLE_FISCHEREI: 'true'
NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS: "true" NEXT_PUBLIC_ENABLE_MEETING_PROTOCOLS: 'true'
NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG: "true" NEXT_PUBLIC_ENABLE_VERBANDSVERWALTUNG: 'true'
volumes: volumes:
supabase-db-data: supabase-db-data:

View File

@@ -1,7 +1,11 @@
import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js'; 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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,11 +14,25 @@ export function createCourseManagementApi(client: SupabaseClient<Database>) {
return { return {
// --- Courses --- // --- Courses ---
async listCourses(accountId: string, opts?: { status?: string; search?: string; page?: number; pageSize?: number }) { async listCourses(
let query = client.from('courses').select('*', { count: 'exact' }) accountId: string,
.eq('account_id', accountId).order('start_date', { ascending: false }); 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?.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 page = opts?.page ?? 1;
const pageSize = opts?.pageSize ?? 25; const pageSize = opts?.pageSize ?? 25;
query = query.range((page - 1) * pageSize, page * pageSize - 1); query = query.range((page - 1) * pageSize, page * pageSize - 1);
@@ -24,20 +42,38 @@ export function createCourseManagementApi(client: SupabaseClient<Database>) {
}, },
async getCourse(courseId: string) { 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; if (error) throw error;
return data; return data;
}, },
async createCourse(input: CreateCourseInput) { async createCourse(input: CreateCourseInput) {
const { data, error } = await client.from('courses').insert({ const { data, error } = await client
account_id: input.accountId, course_number: input.courseNumber || null, name: input.name, .from('courses')
description: input.description || null, category_id: input.categoryId || null, instructor_id: input.instructorId || null, .insert({
location_id: input.locationId || null, start_date: input.startDate || null, end_date: input.endDate || null, account_id: input.accountId,
fee: input.fee, reduced_fee: input.reducedFee ?? null, capacity: input.capacity, course_number: input.courseNumber || null,
min_participants: input.minParticipants, status: input.status, name: input.name,
registration_deadline: input.registrationDeadline || null, notes: input.notes || null, description: input.description || null,
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },
@@ -45,96 +81,161 @@ export function createCourseManagementApi(client: SupabaseClient<Database>) {
// --- Enrollment --- // --- Enrollment ---
async enrollParticipant(input: EnrollParticipantInput) { async enrollParticipant(input: EnrollParticipantInput) {
// Check capacity // Check capacity
const { count } = await client.from('course_participants').select('*', { count: 'exact', head: true }) const { count } = await client
.eq('course_id', input.courseId).in('status', ['enrolled']); .from('course_participants')
.select('*', { count: 'exact', head: true })
.eq('course_id', input.courseId)
.in('status', ['enrolled']);
const course = await this.getCourse(input.courseId); 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({ const { data, error } = await client
course_id: input.courseId, member_id: input.memberId, .from('course_participants')
first_name: input.firstName, last_name: input.lastName, .insert({
email: input.email, phone: input.phone, status, course_id: input.courseId,
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },
async cancelEnrollment(participantId: string) { 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() }) .update({ status: 'cancelled', cancelled_at: new Date().toISOString() })
.eq('id', participantId); .eq('id', participantId);
if (error) throw error; if (error) throw error;
}, },
async getParticipants(courseId: string) { async getParticipants(courseId: string) {
const { data, error } = await client.from('course_participants').select('*') const { data, error } = await client
.eq('course_id', courseId).order('enrolled_at'); .from('course_participants')
.select('*')
.eq('course_id', courseId)
.order('enrolled_at');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
// --- Sessions --- // --- Sessions ---
async getSessions(courseId: string) { async getSessions(courseId: string) {
const { data, error } = await client.from('course_sessions').select('*') const { data, error } = await client
.eq('course_id', courseId).order('session_date'); .from('course_sessions')
.select('*')
.eq('course_id', courseId)
.order('session_date');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
async createSession(input: { courseId: string; sessionDate: string; startTime: string; endTime: string; locationId?: string }) { async createSession(input: {
const { data, error } = await client.from('course_sessions').insert({ courseId: string;
course_id: input.courseId, session_date: input.sessionDate, sessionDate: string;
start_time: input.startTime, end_time: input.endTime, location_id: input.locationId, startTime: string;
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },
// --- Attendance --- // --- Attendance ---
async getAttendance(sessionId: string) { 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; if (error) throw error;
return data ?? []; return data ?? [];
}, },
async markAttendance(sessionId: string, participantId: string, present: boolean) { async markAttendance(
const { error } = await client.from('course_attendance').upsert({ sessionId: string,
session_id: sessionId, participant_id: participantId, present, participantId: string,
}, { onConflict: 'session_id,participant_id' }); 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; if (error) throw error;
}, },
// --- Categories, Instructors, Locations --- // --- Categories, Instructors, Locations ---
async listCategories(accountId: string) { async listCategories(accountId: string) {
const { data, error } = await client.from('course_categories').select('*') const { data, error } = await client
.eq('account_id', accountId).order('sort_order'); .from('course_categories')
.select('*')
.eq('account_id', accountId)
.order('sort_order');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
async listInstructors(accountId: string) { async listInstructors(accountId: string) {
const { data, error } = await client.from('course_instructors').select('*') const { data, error } = await client
.eq('account_id', accountId).order('last_name'); .from('course_instructors')
.select('*')
.eq('account_id', accountId)
.order('last_name');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
async listLocations(accountId: string) { async listLocations(accountId: string) {
const { data, error } = await client.from('course_locations').select('*') const { data, error } = await client
.eq('account_id', accountId).order('name'); .from('course_locations')
.select('*')
.eq('account_id', accountId)
.order('name');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
// --- Statistics --- // --- Statistics ---
async getStatistics(accountId: string) { async getStatistics(accountId: string) {
const { data: courses } = await client.from('courses').select('status').eq('account_id', accountId); const { data: courses } = await client
const { count: totalParticipants } = await client.from('course_participants') .from('courses')
.select('status')
.eq('account_id', accountId);
const { count: totalParticipants } = await client
.from('course_participants')
.select('*', { count: 'exact', head: true }) .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 }; const stats = {
for (const c of (courses ?? [])) { totalCourses: 0,
openCourses: 0,
completedCourses: 0,
totalParticipants: totalParticipants ?? 0,
};
for (const c of courses ?? []) {
stats.totalCourses++; stats.totalCourses++;
if (c.status === 'open' || c.status === 'running') stats.openCourses++; if (c.status === 'open' || c.status === 'running') stats.openCourses++;
if (c.status === 'completed') stats.completedCourses++; if (c.status === 'completed') stats.completedCourses++;
@@ -143,30 +244,70 @@ export function createCourseManagementApi(client: SupabaseClient<Database>) {
}, },
// --- Create methods for CRUD --- // --- Create methods for CRUD ---
async createCategory(input: { accountId: string; name: string; description?: string; parentId?: string }) { async createCategory(input: {
const { data, error } = await client.from('course_categories').insert({ accountId: string;
account_id: input.accountId, name: input.name, description: input.description, name: string;
parent_id: input.parentId, description?: string;
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },
async createInstructor(input: { accountId: string; firstName: string; lastName: string; email?: string; phone?: string; qualifications?: string; hourlyRate?: number }) { async createInstructor(input: {
const { data, error } = await client.from('course_instructors').insert({ accountId: string;
account_id: input.accountId, first_name: input.firstName, last_name: input.lastName, firstName: string;
email: input.email, phone: input.phone, qualifications: input.qualifications, lastName: string;
hourly_rate: input.hourlyRate, email?: string;
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },
async createLocation(input: { accountId: string; name: string; address?: string; room?: string; capacity?: number }) { async createLocation(input: {
const { data, error } = await client.from('course_locations').insert({ accountId: string;
account_id: input.accountId, name: input.name, address: input.address, name: string;
room: input.room, capacity: input.capacity, address?: string;
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },

View File

@@ -1,6 +1,7 @@
import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import type { CreateEventInput } from '../schema/event.schema'; import type { CreateEventInput } from '../schema/event.schema';
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,16 +11,28 @@ export function createEventManagementApi(client: SupabaseClient<Database>) {
const db = client; const db = client;
return { return {
async listEvents(accountId: string, opts?: { status?: string; page?: number }) { async listEvents(
let query = client.from('events').select('*', { count: 'exact' }) accountId: string,
.eq('account_id', accountId).order('event_date', { ascending: false }); 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); if (opts?.status) query = query.eq('status', opts.status);
const page = opts?.page ?? 1; const page = opts?.page ?? 1;
query = query.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1); query = query.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1);
const { data, error, count } = await query; const { data, error, count } = await query;
if (error) throw error; if (error) throw error;
const total = count ?? 0; 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[]) { async getRegistrationCounts(eventIds: string[]) {
@@ -40,71 +53,131 @@ export function createEventManagementApi(client: SupabaseClient<Database>) {
}, },
async getEvent(eventId: string) { 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; if (error) throw error;
return data; return data;
}, },
async createEvent(input: CreateEventInput) { async createEvent(input: CreateEventInput) {
const { data, error } = await client.from('events').insert({ const { data, error } = await client
account_id: input.accountId, name: input.name, description: input.description || null, .from('events')
event_date: input.eventDate || null, event_time: input.eventTime || null, end_date: input.endDate || null, .insert({
location: input.location || null, capacity: input.capacity, min_age: input.minAge ?? null, account_id: input.accountId,
max_age: input.maxAge ?? null, fee: input.fee, status: input.status, name: input.name,
registration_deadline: input.registrationDeadline || null, description: input.description || null,
contact_name: input.contactName || null, contact_email: input.contactEmail || null, contact_phone: input.contactPhone || null, event_date: input.eventDate || null,
}).select().single(); 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; if (error) throw error;
return data; 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 // Check capacity
const event = await this.getEvent(input.eventId); const event = await this.getEvent(input.eventId);
if (event.capacity) { if (event.capacity) {
const { count } = await client.from('event_registrations').select('*', { count: 'exact', head: true }) const { count } = await client
.eq('event_id', input.eventId).in('status', ['pending', 'confirmed']); .from('event_registrations')
.select('*', { count: 'exact', head: true })
.eq('event_id', input.eventId)
.in('status', ['pending', 'confirmed']);
if ((count ?? 0) >= event.capacity) { if ((count ?? 0) >= event.capacity) {
throw new Error('Event is full'); throw new Error('Event is full');
} }
} }
const { data, error } = await client.from('event_registrations').insert({ const { data, error } = await client
event_id: input.eventId, first_name: input.firstName, last_name: input.lastName, .from('event_registrations')
email: input.email, parent_name: input.parentName, status: 'confirmed', .insert({
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },
async getRegistrations(eventId: string) { async getRegistrations(eventId: string) {
const { data, error } = await client.from('event_registrations').select('*') const { data, error } = await client
.eq('event_id', eventId).order('created_at'); .from('event_registrations')
.select('*')
.eq('event_id', eventId)
.order('created_at');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
// Holiday passes // Holiday passes
async listHolidayPasses(accountId: string) { async listHolidayPasses(accountId: string) {
const { data, error } = await client.from('holiday_passes').select('*') const { data, error } = await client
.eq('account_id', accountId).order('year', { ascending: false }); .from('holiday_passes')
.select('*')
.eq('account_id', accountId)
.order('year', { ascending: false });
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
async getPassActivities(passId: string) { async getPassActivities(passId: string) {
const { data, error } = await client.from('holiday_pass_activities').select('*') const { data, error } = await client
.eq('pass_id', passId).order('activity_date'); .from('holiday_pass_activities')
.select('*')
.eq('pass_id', passId)
.order('activity_date');
if (error) throw error; if (error) throw error;
return data ?? []; return data ?? [];
}, },
async createHolidayPass(input: { accountId: string; name: string; year: number; description?: string; price?: number; validFrom?: string; validUntil?: string }) { async createHolidayPass(input: {
const { data, error } = await client.from('holiday_passes').insert({ accountId: string;
account_id: input.accountId, name: input.name, year: input.year, name: string;
description: input.description, price: input.price ?? 0, year: number;
valid_from: input.validFrom, valid_until: input.validUntil, description?: string;
}).select().single(); 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; if (error) throw error;
return data; return data;
}, },