231 lines
7.3 KiB
TypeScript
231 lines
7.3 KiB
TypeScript
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<StudentScheduleItem[]> => {
|
|
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<ClassScheduleItem[]> => {
|
|
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<string> {
|
|
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<void> {
|
|
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<typeof classSchedule.$inferSelect> = {}
|
|
|
|
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<void> {
|
|
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)
|
|
}
|