refactor: remove obsolete member management API module
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-03 14:08:31 +02:00
parent 124c6a632a
commit 5c5aaabae5
132 changed files with 10107 additions and 3442 deletions

View File

@@ -6,6 +6,7 @@ import { authActionClient } from '@kit/next/safe-action';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { isCourseDomainError } from '../../lib/errors';
import {
CreateCourseSchema,
UpdateCourseSchema,
@@ -25,7 +26,7 @@ export const createCourse = authActionClient
const api = createCourseManagementApi(client);
logger.info({ name: 'course.create' }, 'Creating course...');
const result = await api.createCourse(input);
const result = await api.courses.create(input, ctx.user.id);
logger.info({ name: 'course.create' }, 'Course created');
return { success: true, data: result };
});
@@ -37,39 +38,60 @@ export const updateCourse = authActionClient
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.update' }, 'Updating course...');
const result = await api.updateCourse(input);
logger.info({ name: 'course.update' }, 'Course updated');
return { success: true, data: result };
try {
logger.info({ name: 'course.update' }, 'Updating course...');
const result = await api.courses.update(input, ctx.user.id);
logger.info({ name: 'course.update' }, 'Course updated');
return { success: true, data: result };
} catch (e) {
if (isCourseDomainError(e)) {
return { success: false, error: e.message, code: e.code };
}
throw e;
}
});
export const deleteCourse = authActionClient
.inputSchema(z.object({ courseId: z.string().uuid() }))
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.delete' }, 'Archiving course...');
await api.deleteCourse(input.courseId);
logger.info({ name: 'course.delete' }, 'Course archived');
return { success: true };
try {
logger.info({ name: 'course.delete' }, 'Archiving course...');
await api.courses.softDelete(input.courseId);
logger.info({ name: 'course.delete' }, 'Course archived');
return { success: true };
} catch (e) {
if (isCourseDomainError(e)) {
return { success: false, error: e.message, code: e.code };
}
throw e;
}
});
export const enrollParticipant = authActionClient
.inputSchema(EnrollParticipantSchema)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info(
{ name: 'course.enrollParticipant' },
'Enrolling participant...',
);
const result = await api.enrollParticipant(input);
logger.info({ name: 'course.enrollParticipant' }, 'Participant enrolled');
return { success: true, data: result };
try {
logger.info(
{ name: 'course.enrollParticipant' },
'Enrolling participant...',
);
const result = await api.enrollment.enroll(input);
logger.info({ name: 'course.enrollParticipant' }, 'Participant enrolled');
return { success: true, data: result };
} catch (e) {
if (isCourseDomainError(e)) {
return { success: false, error: e.message, code: e.code };
}
throw e;
}
});
export const cancelEnrollment = authActionClient
@@ -78,7 +100,7 @@ export const cancelEnrollment = authActionClient
participantId: z.string().uuid(),
}),
)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
@@ -87,9 +109,9 @@ export const cancelEnrollment = authActionClient
{ name: 'course.cancelEnrollment' },
'Cancelling enrollment...',
);
const result = await api.cancelEnrollment(input.participantId);
await api.enrollment.cancel(input.participantId);
logger.info({ name: 'course.cancelEnrollment' }, 'Enrollment cancelled');
return { success: true, data: result };
return { success: true };
});
export const markAttendance = authActionClient
@@ -100,69 +122,69 @@ export const markAttendance = authActionClient
present: z.boolean(),
}),
)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.markAttendance' }, 'Marking attendance...');
const result = await api.markAttendance(
await api.attendance.mark(
input.sessionId,
input.participantId,
input.present,
);
logger.info({ name: 'course.markAttendance' }, 'Attendance marked');
return { success: true, data: result };
return { success: true };
});
export const createCategory = authActionClient
.inputSchema(CreateCategorySchema)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.createCategory' }, 'Creating category...');
const result = await api.createCategory(input);
const result = await api.referenceData.createCategory(input);
logger.info({ name: 'course.createCategory' }, 'Category created');
return { success: true, data: result };
});
export const createInstructor = authActionClient
.inputSchema(CreateInstructorSchema)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.createInstructor' }, 'Creating instructor...');
const result = await api.createInstructor(input);
const result = await api.referenceData.createInstructor(input);
logger.info({ name: 'course.createInstructor' }, 'Instructor created');
return { success: true, data: result };
});
export const createLocation = authActionClient
.inputSchema(CreateLocationSchema)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.createLocation' }, 'Creating location...');
const result = await api.createLocation(input);
const result = await api.referenceData.createLocation(input);
logger.info({ name: 'course.createLocation' }, 'Location created');
return { success: true, data: result };
});
export const createSession = authActionClient
.inputSchema(CreateSessionSchema)
.action(async ({ parsedInput: input, ctx }) => {
.action(async ({ parsedInput: input, ctx: _ctx }) => {
const client = getSupabaseServerClient();
const logger = await getLogger();
const api = createCourseManagementApi(client);
logger.info({ name: 'course.createSession' }, 'Creating session...');
const result = await api.createSession(input);
const result = await api.sessions.create(input);
logger.info({ name: 'course.createSession' }, 'Session created');
return { success: true, data: result };
});

View File

@@ -1,361 +1,22 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import type {
CreateCourseInput,
UpdateCourseInput,
EnrollParticipantInput,
} from '../schema/course.schema';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createAttendanceService } from './services/attendance.service';
import { createCourseCrudService } from './services/course-crud.service';
import { createCourseReferenceDataService } from './services/course-reference-data.service';
import { createCourseStatisticsService } from './services/course-statistics.service';
import { createEnrollmentService } from './services/enrollment.service';
import { createSessionService } from './services/session.service';
export function createCourseManagementApi(client: SupabaseClient<Database>) {
const _db = client;
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 });
if (opts?.status) query = query.eq('status', opts.status);
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);
const { data, error, count } = await query;
if (error) throw error;
return { data: data ?? [], total: count ?? 0, page, pageSize };
},
async getCourse(courseId: string) {
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();
if (error) throw error;
return data;
},
async updateCourse(input: UpdateCourseInput) {
const update: Record<string, unknown> = {};
if (input.name !== undefined) update.name = input.name;
if (input.courseNumber !== undefined)
update.course_number = input.courseNumber || null;
if (input.description !== undefined)
update.description = input.description || null;
if (input.categoryId !== undefined)
update.category_id = input.categoryId || null;
if (input.instructorId !== undefined)
update.instructor_id = input.instructorId || null;
if (input.locationId !== undefined)
update.location_id = input.locationId || null;
if (input.startDate !== undefined)
update.start_date = input.startDate || null;
if (input.endDate !== undefined) update.end_date = input.endDate || null;
if (input.fee !== undefined) update.fee = input.fee;
if (input.reducedFee !== undefined)
update.reduced_fee = input.reducedFee ?? null;
if (input.capacity !== undefined) update.capacity = input.capacity;
if (input.minParticipants !== undefined)
update.min_participants = input.minParticipants;
if (input.status !== undefined) update.status = input.status;
if (input.registrationDeadline !== undefined)
update.registration_deadline = input.registrationDeadline || null;
if (input.notes !== undefined) update.notes = input.notes || null;
const { data, error } = await client
.from('courses')
.update(update)
.eq('id', input.courseId)
.select()
.single();
if (error) throw error;
return data;
},
async deleteCourse(courseId: string) {
const { error } = await client
.from('courses')
.update({ status: 'cancelled' })
.eq('id', courseId);
if (error) throw error;
},
// --- 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 course = await this.getCourse(input.courseId);
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();
if (error) throw error;
return data;
},
async cancelEnrollment(participantId: string) {
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');
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');
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();
if (error) throw error;
return data;
},
// --- Attendance ---
async getAttendance(sessionId: string) {
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' },
);
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');
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');
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');
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')
.select('*', { count: 'exact', head: true })
.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 ?? []) {
stats.totalCourses++;
if (c.status === 'open' || c.status === 'running') stats.openCourses++;
if (c.status === 'completed') stats.completedCourses++;
}
return stats;
},
// --- 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();
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();
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();
if (error) throw error;
return data;
},
courses: createCourseCrudService(client),
enrollment: createEnrollmentService(client),
sessions: createSessionService(client),
attendance: createAttendanceService(client),
referenceData: createCourseReferenceDataService(client),
statistics: createCourseStatisticsService(client),
};
}

View File

@@ -0,0 +1,29 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
export function createAttendanceService(client: SupabaseClient<Database>) {
return {
async getBySession(sessionId: string) {
const { data, error } = await client
.from('course_attendance')
.select('*')
.eq('session_id', sessionId);
if (error) throw error;
return data ?? [];
},
async mark(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;
},
};
}

View File

@@ -0,0 +1,227 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import { getLogger } from '@kit/shared/logger';
import type { Database } from '@kit/supabase/database';
import {
canTransition,
validateTransition,
getValidTransitions,
} from '../../lib/course-status-machine';
import {
CourseNotFoundError,
CourseConcurrencyConflictError,
InvalidCourseStatusTransitionError,
} from '../../lib/errors';
import type {
CreateCourseInput,
UpdateCourseInput,
} from '../../schema/course.schema';
const NAMESPACE = 'course-crud';
export function createCourseCrudService(client: SupabaseClient<Database>) {
return {
async list(
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}%`,
);
const page = opts?.page ?? 1;
const pageSize = opts?.pageSize ?? 25;
query = query.range((page - 1) * pageSize, page * pageSize - 1);
const { data, error, count } = await query;
if (error) throw error;
const total = count ?? 0;
return {
data: data ?? [],
total,
page,
pageSize,
totalPages: Math.max(1, Math.ceil(total / pageSize)),
};
},
async getById(courseId: string) {
const { data, error } = await client
.from('courses')
.select('*')
.eq('id', courseId)
.maybeSingle();
if (error) throw error;
if (!data) throw new CourseNotFoundError(courseId);
return data;
},
async create(input: CreateCourseInput, userId?: string) {
const logger = await getLogger();
logger.info({ name: NAMESPACE }, 'Creating course...');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- created_by/updated_by added via migration, not yet in generated types
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,
...(userId ? { created_by: userId, updated_by: userId } : {}),
} as any)
.select()
.single();
if (error) throw error;
return data;
},
async update(input: UpdateCourseInput, userId?: string) {
const logger = await getLogger();
logger.info(
{ name: NAMESPACE, courseId: input.courseId },
'Updating course...',
);
const update: Record<string, unknown> = {};
if (input.name !== undefined) update.name = input.name;
if (input.courseNumber !== undefined)
update.course_number = input.courseNumber || null;
if (input.description !== undefined)
update.description = input.description || null;
if (input.categoryId !== undefined)
update.category_id = input.categoryId || null;
if (input.instructorId !== undefined)
update.instructor_id = input.instructorId || null;
if (input.locationId !== undefined)
update.location_id = input.locationId || null;
if (input.startDate !== undefined)
update.start_date = input.startDate || null;
if (input.endDate !== undefined) update.end_date = input.endDate || null;
if (input.fee !== undefined) update.fee = input.fee;
if (input.reducedFee !== undefined)
update.reduced_fee = input.reducedFee ?? null;
if (input.capacity !== undefined) update.capacity = input.capacity;
if (input.minParticipants !== undefined)
update.min_participants = input.minParticipants;
if (input.registrationDeadline !== undefined)
update.registration_deadline = input.registrationDeadline || null;
if (input.notes !== undefined) update.notes = input.notes || null;
// Status transition validation
if (input.status !== undefined) {
const { data: current, error: fetchError } = await client
.from('courses')
.select('status')
.eq('id', input.courseId)
.single();
if (fetchError) throw fetchError;
const currentStatus = current.status as string;
if (currentStatus !== input.status) {
try {
const sideEffects = validateTransition(
currentStatus as Parameters<typeof validateTransition>[0],
input.status as Parameters<typeof validateTransition>[1],
);
Object.assign(update, sideEffects);
} catch {
throw new InvalidCourseStatusTransitionError(
currentStatus,
input.status,
getValidTransitions(
currentStatus as Parameters<typeof getValidTransitions>[0],
),
);
}
}
update.status = input.status;
}
if (userId) {
update.updated_by = userId;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- version/updated_by columns added via migration, not yet in generated types
let query = client
.from('courses')
.update(update as any)
.eq('id', input.courseId);
// Optimistic locking via version column
if (input.version !== undefined) {
query = (query as any).eq('version', input.version);
}
const { data, error } = await query.select().single();
if (error) {
if (error.code === 'PGRST116' && input.version !== undefined) {
throw new CourseConcurrencyConflictError();
}
throw error;
}
return data;
},
async softDelete(courseId: string) {
const logger = await getLogger();
logger.info({ name: NAMESPACE, courseId }, 'Cancelling course...');
const { data: current, error: fetchError } = await client
.from('courses')
.select('status')
.eq('id', courseId)
.maybeSingle();
if (fetchError) throw fetchError;
if (!current) throw new CourseNotFoundError(courseId);
type CourseStatus = Parameters<typeof canTransition>[0];
if (!canTransition(current.status as CourseStatus, 'cancelled')) {
throw new InvalidCourseStatusTransitionError(
current.status,
'cancelled',
getValidTransitions(current.status as CourseStatus),
);
}
const { error } = await client
.from('courses')
.update({ status: 'cancelled' })
.eq('id', courseId);
if (error) throw error;
},
};
}

View File

@@ -0,0 +1,108 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
export function createCourseReferenceDataService(
client: SupabaseClient<Database>,
) {
return {
async listCategories(accountId: string) {
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');
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');
if (error) throw error;
return data ?? [];
},
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();
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();
if (error) throw error;
return data;
},
};
}

View File

@@ -0,0 +1,42 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
export function createCourseStatisticsService(
client: SupabaseClient<Database>,
) {
return {
async getQuickStats(accountId: string) {
const { data, error } = await (client.rpc as CallableFunction)(
'get_course_statistics',
{ p_account_id: accountId },
);
if (error) throw error;
// RPC returns a single row as an array
const stats = Array.isArray(data) ? data[0] : data;
return (
stats ?? {
total_courses: 0,
open_courses: 0,
running_courses: 0,
completed_courses: 0,
cancelled_courses: 0,
total_participants: 0,
total_waitlisted: 0,
avg_occupancy_rate: 0,
total_revenue: 0,
}
);
},
async getAttendanceSummary(courseId: string) {
const { data, error } = await (client.rpc as CallableFunction)(
'get_course_attendance_summary',
{ p_course_id: courseId },
);
if (error) throw error;
return data ?? [];
},
};
}

View File

@@ -0,0 +1,51 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import type { EnrollParticipantInput } from '../../schema/course.schema';
export function createEnrollmentService(client: SupabaseClient<Database>) {
return {
async enroll(input: EnrollParticipantInput) {
// Uses the enroll_course_participant RPC which handles capacity checks
// and waitlisting atomically, avoiding race conditions.
const { data, error } = await (client.rpc as CallableFunction)(
'enroll_course_participant',
{
p_course_id: input.courseId,
p_member_id: input.memberId ?? null,
p_first_name: input.firstName,
p_last_name: input.lastName,
p_email: input.email || null,
p_phone: input.phone || null,
},
);
if (error) throw error;
return data;
},
async cancel(participantId: string) {
const { data, error } = await (client.rpc as CallableFunction)(
'cancel_course_enrollment',
{ p_participant_id: participantId },
);
if (error) throw error;
return data as {
cancelled_id: string;
promoted_id: string | null;
promoted_name: string | null;
};
},
async listParticipants(courseId: string) {
const { data, error } = await client
.from('course_participants')
.select('*')
.eq('course_id', courseId)
.order('enrolled_at');
if (error) throw error;
return data ?? [];
},
};
}

View File

@@ -0,0 +1,31 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import { createAttendanceService } from './attendance.service';
import { createCourseCrudService } from './course-crud.service';
import { createCourseReferenceDataService } from './course-reference-data.service';
import { createCourseStatisticsService } from './course-statistics.service';
import { createEnrollmentService } from './enrollment.service';
import { createSessionService } from './session.service';
export {
createAttendanceService,
createCourseCrudService,
createCourseReferenceDataService,
createCourseStatisticsService,
createEnrollmentService,
createSessionService,
};
export function createCourseServices(client: SupabaseClient<Database>) {
return {
courses: createCourseCrudService(client),
enrollment: createEnrollmentService(client),
sessions: createSessionService(client),
attendance: createAttendanceService(client),
referenceData: createCourseReferenceDataService(client),
statistics: createCourseStatisticsService(client),
};
}

View File

@@ -0,0 +1,63 @@
import 'server-only';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
export function createSessionService(client: SupabaseClient<Database>) {
return {
async list(courseId: string) {
const { data, error } = await client
.from('course_sessions')
.select('*')
.eq('course_id', courseId)
.order('session_date');
if (error) throw error;
return data ?? [];
},
async create(input: {
courseId: string;
sessionDate: string;
startTime: string;
endTime: string;
locationId?: string;
}) {
// Check instructor availability if course has an instructor
const { data: course, error: courseError } = await client
.from('courses')
.select('instructor_id')
.eq('id', input.courseId)
.single();
if (courseError) throw courseError;
if (course?.instructor_id) {
const { data: isAvailable, error: availError } = await (
client.rpc as CallableFunction
)('check_instructor_availability', {
p_instructor_id: course.instructor_id,
p_session_date: input.sessionDate,
p_start_time: input.startTime,
p_end_time: input.endTime,
});
if (availError) throw availError;
if (!isAvailable) {
throw new Error('Kursleiter ist zu diesem Zeitpunkt nicht verfügbar');
}
}
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;
},
};
}