refactor: improve code readability and consistency in api.ts and common.json
This commit is contained in:
@@ -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: <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: [
|
||||
{
|
||||
label: 'common.routes.dashboard',
|
||||
path: pathsConfig.app.accountHome.replace('[account]', account),
|
||||
Icon: <LayoutDashboard className={iconClasses} />,
|
||||
highlightMatch: `${pathsConfig.app.home}$`,
|
||||
label: 'common:routes.eventList',
|
||||
path: createPath('/home/[account]/events', account),
|
||||
Icon: <CalendarHeart className={iconClasses} />,
|
||||
},
|
||||
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',
|
||||
path: createPath(`/home/[account]/events`, account),
|
||||
Icon: <Calendar className={iconClasses} />,
|
||||
label: 'common:routes.eventRegistrations',
|
||||
path: createPath('/home/[account]/events/registrations', account),
|
||||
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',
|
||||
path: createPath(`/home/[account]/site-builder`, account),
|
||||
Icon: <Globe className={iconClasses} />,
|
||||
label: 'common:routes.holidayPasses',
|
||||
path: createPath('/home/[account]/events/holiday-passes', account),
|
||||
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,
|
||||
children: [
|
||||
{
|
||||
label: 'common.routes.settings',
|
||||
label: 'common:routes.accountSettings',
|
||||
path: createPath(pathsConfig.app.accountSettings, account),
|
||||
Icon: <Settings className={iconClasses} />,
|
||||
},
|
||||
{
|
||||
label: 'common.routes.members',
|
||||
path: createPath(pathsConfig.app.accountMembers, account),
|
||||
Icon: <Users className={iconClasses} />,
|
||||
},
|
||||
featureFlagsConfig.enableTeamAccountBilling
|
||||
? {
|
||||
label: 'common.routes.billing',
|
||||
label: 'common:routes.billing',
|
||||
path: createPath(pathsConfig.app.accountBilling, account),
|
||||
Icon: <CreditCard className={iconClasses} />,
|
||||
}
|
||||
: undefined,
|
||||
].filter(Boolean),
|
||||
},
|
||||
];
|
||||
],
|
||||
});
|
||||
|
||||
return routes;
|
||||
};
|
||||
|
||||
export function getTeamAccountSidebarConfig(account: string) {
|
||||
return NavigationConfigSchema.parse({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 </dev/tcp/localhost/3000 || exit 1"]
|
||||
test: ['CMD-SHELL', 'head -c0 </dev/tcp/localhost/3000 || exit 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -149,20 +157,20 @@ services:
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
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
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq}
|
||||
ERL_AFLAGS: "-proto_dist inet_tcp"
|
||||
ERL_AFLAGS: '-proto_dist inet_tcp'
|
||||
DNS_NODES: "''"
|
||||
RLIMIT_NOFILE: "10000"
|
||||
RLIMIT_NOFILE: '10000'
|
||||
APP_NAME: realtime
|
||||
SEED_SELF_HOST: "true"
|
||||
SEED_SELF_HOST: 'true'
|
||||
REPLICATION_MODE: RLS
|
||||
REPLICATION_POLL_INTERVAL: 100
|
||||
SECURE_CHANNELS: "true"
|
||||
SECURE_CHANNELS: 'true'
|
||||
SLOT_NAME: supabase_realtime_rls
|
||||
TEMPORARY_SLOT: "true"
|
||||
TEMPORARY_SLOT: 'true'
|
||||
MAX_RECORD_BYTES: 1048576
|
||||
|
||||
# =====================================================
|
||||
@@ -196,7 +204,11 @@ services:
|
||||
GLOBAL_S3_BUCKET: stub
|
||||
IMGPROXY_URL: http://supabase-imgproxy:8080
|
||||
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
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -209,10 +221,10 @@ services:
|
||||
image: darthsim/imgproxy:v3.8.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
IMGPROXY_BIND: ":8080"
|
||||
IMGPROXY_BIND: ':8080'
|
||||
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||
IMGPROXY_USE_ETAG: "true"
|
||||
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
|
||||
IMGPROXY_USE_ETAG: 'true'
|
||||
IMGPROXY_ENABLE_WEBP_DETECTION: 'true'
|
||||
|
||||
# =====================================================
|
||||
# Supabase pg_meta (DB introspection for Studio)
|
||||
@@ -252,10 +264,16 @@ services:
|
||||
SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
NEXT_PUBLIC_ENABLE_LOGS: "true"
|
||||
NEXT_PUBLIC_ENABLE_LOGS: 'true'
|
||||
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -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<Database>) {
|
||||
|
||||
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<Database>) {
|
||||
},
|
||||
|
||||
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<Database>) {
|
||||
// --- 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<Database>) {
|
||||
},
|
||||
|
||||
// --- 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;
|
||||
},
|
||||
|
||||
@@ -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<Database>) {
|
||||
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<Database>) {
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user