Initial state for GitNexus analysis
This commit is contained in:
34
packages/features/course-management/package.json
Normal file
34
packages/features/course-management/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@kit/course-management",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
"./api": "./src/server/api.ts",
|
||||
"./schema/*": "./src/schema/*.ts",
|
||||
"./components": "./src/components/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/next": "workspace:*",
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-safe-action": "catalog:",
|
||||
"react": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const EnrollmentStatusEnum = z.enum(['enrolled', 'waitlisted', 'cancelled', 'completed']);
|
||||
export type EnrollmentStatus = z.infer<typeof EnrollmentStatusEnum>;
|
||||
|
||||
export const CourseStatusEnum = z.enum(['planned', 'open', 'running', 'completed', 'cancelled']);
|
||||
|
||||
export const CreateCourseSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
courseNumber: z.string().optional(),
|
||||
name: z.string().min(1).max(256),
|
||||
description: z.string().optional(),
|
||||
categoryId: z.string().uuid().optional(),
|
||||
instructorId: z.string().uuid().optional(),
|
||||
locationId: z.string().uuid().optional(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
fee: z.number().min(0).default(0),
|
||||
reducedFee: z.number().min(0).optional(),
|
||||
capacity: z.number().int().min(1).default(20),
|
||||
minParticipants: z.number().int().min(0).default(5),
|
||||
status: CourseStatusEnum.default('planned'),
|
||||
registrationDeadline: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
export type CreateCourseInput = z.infer<typeof CreateCourseSchema>;
|
||||
|
||||
export const UpdateCourseSchema = CreateCourseSchema.partial().extend({
|
||||
courseId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const EnrollParticipantSchema = z.object({
|
||||
courseId: z.string().uuid(),
|
||||
memberId: z.string().uuid().optional(),
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email().optional().or(z.literal('')),
|
||||
phone: z.string().optional(),
|
||||
});
|
||||
export type EnrollParticipantInput = z.infer<typeof EnrollParticipantSchema>;
|
||||
|
||||
export const CreateSessionSchema = z.object({
|
||||
courseId: z.string().uuid(),
|
||||
sessionDate: z.string(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
locationId: z.string().uuid().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CreateCategorySchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
parentId: z.string().uuid().optional(),
|
||||
name: z.string().min(1).max(128),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CreateInstructorSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email().optional().or(z.literal('')),
|
||||
phone: z.string().optional(),
|
||||
qualifications: z.string().optional(),
|
||||
hourlyRate: z.number().min(0).optional(),
|
||||
});
|
||||
|
||||
export const CreateLocationSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
name: z.string().min(1),
|
||||
address: z.string().optional(),
|
||||
room: z.string().optional(),
|
||||
capacity: z.number().int().optional(),
|
||||
});
|
||||
145
packages/features/course-management/src/server/api.ts
Normal file
145
packages/features/course-management/src/server/api.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import type { CreateCourseInput, EnrollParticipantInput } from '../schema/course.schema';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
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, name: input.name,
|
||||
description: input.description, category_id: input.categoryId, instructor_id: input.instructorId,
|
||||
location_id: input.locationId, start_date: input.startDate, end_date: input.endDate,
|
||||
fee: input.fee, reduced_fee: input.reducedFee, capacity: input.capacity,
|
||||
min_participants: input.minParticipants, status: input.status,
|
||||
registration_deadline: input.registrationDeadline, notes: input.notes,
|
||||
}).select().single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
// --- 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
6
packages/features/course-management/tsconfig.json
Normal file
6
packages/features/course-management/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" },
|
||||
"include": ["*.ts", "*.tsx", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user