import "server-only"; import { cache } from "react" import { and, asc, eq, inArray, type SQL } from "drizzle-orm" import { db } from "@/shared/db" import { classes, classEnrollments, classSchedule, } from "@/shared/db/schema" import { insertClassScheduleItem, updateClassScheduleItemById, deleteClassScheduleItemById, } from "@/modules/scheduling/data-access" import type { ClassScheduleItem, CreateClassScheduleItemInput, StudentScheduleItem, UpdateClassScheduleItemInput, } from "./types" import { getAccessibleClassIdsForTeacher, getSessionTeacherId, getTeacherIdForMutations, } from "./data-access" export const getStudentSchedule = cache(async (studentId: string): Promise => { const id = studentId.trim() if (!id) return [] const rows = await db .select({ id: classSchedule.id, classId: classSchedule.classId, className: classes.name, weekday: classSchedule.weekday, startTime: classSchedule.startTime, endTime: classSchedule.endTime, course: classSchedule.course, location: classSchedule.location, }) .from(classEnrollments) .innerJoin(classes, eq(classes.id, classEnrollments.classId)) .innerJoin(classSchedule, eq(classSchedule.classId, classes.id)) .where(and(eq(classEnrollments.studentId, id), eq(classEnrollments.status, "active"))) .orderBy(asc(classSchedule.weekday), asc(classSchedule.startTime)) return rows.map((r) => ({ id: r.id, classId: r.classId, className: r.className, weekday: r.weekday as StudentScheduleItem["weekday"], startTime: r.startTime, endTime: r.endTime, course: r.course, location: r.location, })) }) export const getClassSchedule = cache( async (params?: { classId?: string; teacherId?: string }): Promise => { const teacherId = params?.teacherId ?? (await getSessionTeacherId()) if (!teacherId) return [] const classId = params?.classId?.trim() const accessibleIds = await getAccessibleClassIdsForTeacher(teacherId) if (accessibleIds.length === 0) return [] const conditions: SQL[] = [inArray(classes.id, accessibleIds)] if (classId) conditions.push(eq(classSchedule.classId, classId)) const rows = await db .select({ id: classSchedule.id, classId: classSchedule.classId, weekday: classSchedule.weekday, startTime: classSchedule.startTime, endTime: classSchedule.endTime, course: classSchedule.course, location: classSchedule.location, }) .from(classSchedule) .innerJoin(classes, eq(classes.id, classSchedule.classId)) .where(and(...conditions)) .orderBy(asc(classSchedule.weekday), asc(classSchedule.startTime)) return rows.map((r) => ({ id: r.id, classId: r.classId, weekday: r.weekday as ClassScheduleItem["weekday"], startTime: r.startTime, endTime: r.endTime, course: r.course, location: r.location, })) } ) const isTimeHHMM = (v: string) => /^\d{2}:\d{2}$/.test(v) export async function createClassScheduleItem(data: CreateClassScheduleItemInput): Promise { const teacherId = await getTeacherIdForMutations() const classId = data.classId.trim() const course = data.course.trim() const startTime = data.startTime.trim() const endTime = data.endTime.trim() const location = data.location?.trim() || null const weekday = data.weekday if (!classId) throw new Error("Class is required") if (!course) throw new Error("Course is required") if (!isTimeHHMM(startTime) || !isTimeHHMM(endTime)) throw new Error("Invalid time format") if (startTime >= endTime) throw new Error("Start time must be earlier than end time") if (weekday < 1 || weekday > 7) throw new Error("Invalid weekday") const [owned] = await db .select({ id: classes.id }) .from(classes) .where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId))) .limit(1) if (!owned) throw new Error("Class not found") // Delegate DB write to scheduling module (unified write entry point) return insertClassScheduleItem({ classId, weekday, startTime, endTime, course, location, }) } export async function updateClassScheduleItem(scheduleId: string, data: UpdateClassScheduleItemInput): Promise { const teacherId = await getTeacherIdForMutations() const id = scheduleId.trim() if (!id) throw new Error("Missing schedule id") const [existing] = await db .select({ id: classSchedule.id, classId: classSchedule.classId, startTime: classSchedule.startTime, endTime: classSchedule.endTime, }) .from(classSchedule) .innerJoin(classes, eq(classes.id, classSchedule.classId)) .where(and(eq(classSchedule.id, id), eq(classes.teacherId, teacherId))) .limit(1) if (!existing) throw new Error("Schedule item not found") const update: Partial = {} if (typeof data.classId === "string") { const nextClassId = data.classId.trim() if (!nextClassId) throw new Error("Class is required") const [ownedNext] = await db .select({ id: classes.id }) .from(classes) .where(and(eq(classes.id, nextClassId), eq(classes.teacherId, teacherId))) .limit(1) if (!ownedNext) throw new Error("Class not found") update.classId = nextClassId } if (typeof data.weekday === "number") { if (data.weekday < 1 || data.weekday > 7) throw new Error("Invalid weekday") update.weekday = data.weekday } if (typeof data.course === "string") { const course = data.course.trim() if (!course) throw new Error("Course is required") update.course = course } const nextStart = typeof data.startTime === "string" ? data.startTime.trim() : undefined const nextEnd = typeof data.endTime === "string" ? data.endTime.trim() : undefined if (nextStart !== undefined) { if (!isTimeHHMM(nextStart)) throw new Error("Invalid time format") update.startTime = nextStart } if (nextEnd !== undefined) { if (!isTimeHHMM(nextEnd)) throw new Error("Invalid time format") update.endTime = nextEnd } if (update.startTime !== undefined || update.endTime !== undefined) { const mergedStart = update.startTime ?? existing.startTime const mergedEnd = update.endTime ?? existing.endTime if (typeof mergedStart === "string" && typeof mergedEnd === "string" && mergedStart >= mergedEnd) { throw new Error("Start time must be earlier than end time") } } if (data.location !== undefined) { update.location = data.location?.trim() || null } if (Object.keys(update).length === 0) return // Delegate DB write to scheduling module (unified write entry point) await updateClassScheduleItemById(id, update) } export async function deleteClassScheduleItem(scheduleId: string): Promise { const teacherId = await getTeacherIdForMutations() const id = scheduleId.trim() if (!id) throw new Error("Missing schedule id") const [owned] = await db .select({ id: classSchedule.id }) .from(classSchedule) .innerJoin(classes, eq(classes.id, classSchedule.classId)) .where(and(eq(classSchedule.id, id), eq(classes.teacherId, teacherId))) .limit(1) if (!owned) throw new Error("Schedule item not found") // Delegate DB write to scheduling module (unified write entry point) await deleteClassScheduleItemById(id) }