import "server-only" import { eq } from "drizzle-orm" import { db } from "@/shared/db" import { classSchedule } from "@/shared/db/schema" import { getTeacherIdForMutations, verifyTeacherOwnsClass, } from "@/modules/classes/data-access" import { insertClassScheduleItem, updateClassScheduleItemById, deleteClassScheduleItemById, } from "./data-access" import type { CreateClassScheduleItemInput, UpdateClassScheduleItemInput, } from "./types" const isTimeHHMM = (v: string): boolean => /^\d{2}:\d{2}$/.test(v) /** * Create a single classSchedule item. * Ownership: the caller (teacher) must own the target class. * DB write is delegated to the unified scheduling write entry point. */ 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 verifyTeacherOwnsClass(classId, teacherId) if (!owned) throw new Error("Class not found") return insertClassScheduleItem({ classId, weekday, startTime, endTime, course, location, }) } /** * Update a classSchedule item by id. * Ownership: the teacher must own the class associated with the schedule item * (and the target class when classId is being changed). */ 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) .where(eq(classSchedule.id, id)) .limit(1) if (!existing) throw new Error("Schedule item not found") const ownedExisting = await verifyTeacherOwnsClass(existing.classId, teacherId) if (!ownedExisting) 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 verifyTeacherOwnsClass(nextClassId, teacherId) 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 await updateClassScheduleItemById(id, update) } /** * Delete a classSchedule item by id. * Ownership: the teacher must own the class associated with the schedule item. */ export async function deleteClassScheduleItem(scheduleId: string): 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 }) .from(classSchedule) .where(eq(classSchedule.id, id)) .limit(1) if (!existing) throw new Error("Schedule item not found") const owned = await verifyTeacherOwnsClass(existing.classId, teacherId) if (!owned) throw new Error("Schedule item not found") await deleteClassScheduleItemById(id) }