Files
NextEdu/src/modules/classes/data-access-schedule.ts

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)
}