657 lines
22 KiB
TypeScript
657 lines
22 KiB
TypeScript
import "server-only";
|
|
|
|
import { randomInt } from "node:crypto"
|
|
import { cache } from "react"
|
|
import { and, asc, eq, inArray, isNull, sql } from "drizzle-orm"
|
|
import { createId } from "@paralleldrive/cuid2"
|
|
|
|
import { db } from "@/shared/db"
|
|
import {
|
|
classes,
|
|
classEnrollments,
|
|
classSubjectTeachers,
|
|
subjects,
|
|
roles,
|
|
users,
|
|
usersToRoles,
|
|
} from "@/shared/db/schema"
|
|
import { DEFAULT_CLASS_SUBJECTS } from "./types"
|
|
import type {
|
|
ClassSubject,
|
|
CreateTeacherClassInput,
|
|
TeacherOption,
|
|
TeacherClass,
|
|
UpdateTeacherClassInput,
|
|
} from "./types"
|
|
import { getClassHomeworkInsights } from "./data-access-stats"
|
|
import { getClassSchedule } from "./data-access-schedule"
|
|
|
|
export const getSessionTeacherId = async (): Promise<string | null> => {
|
|
const { auth } = await import("@/auth")
|
|
const session = await auth()
|
|
const userId = String(session?.user?.id ?? "").trim()
|
|
if (!userId) return null
|
|
|
|
const [teacher] = await db
|
|
.select({ id: users.id })
|
|
.from(users)
|
|
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
|
.where(and(eq(users.id, userId), eq(roles.name, "teacher")))
|
|
.limit(1)
|
|
return teacher?.id ?? null
|
|
}
|
|
|
|
// Strict subjectId-based mapping: no aliasing
|
|
|
|
export const isDuplicateInvitationCodeError = (err: unknown) => {
|
|
if (!err) return false
|
|
const msg = err instanceof Error ? err.message : String(err)
|
|
const m = msg.toLowerCase()
|
|
return m.includes("duplicate") && (m.includes("invitation") || m.includes("invitation_code"))
|
|
}
|
|
|
|
const generateInvitationCode = () => {
|
|
const n = randomInt(0, 1_000_000)
|
|
return String(n).padStart(6, "0")
|
|
}
|
|
|
|
export const generateUniqueInvitationCode = async (): Promise<string> => {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
const code = generateInvitationCode()
|
|
const [existing] = await db
|
|
.select({ id: classes.id })
|
|
.from(classes)
|
|
.where(eq(classes.invitationCode, code))
|
|
.limit(1)
|
|
if (!existing) return code
|
|
}
|
|
throw new Error("Failed to generate invitation code")
|
|
}
|
|
|
|
export const getTeacherIdForMutations = async (): Promise<string> => {
|
|
const teacherId = await getSessionTeacherId()
|
|
if (!teacherId) throw new Error("Teacher not found")
|
|
return teacherId
|
|
}
|
|
|
|
export const getClassSubjects = async (): Promise<string[]> => {
|
|
const rows = await db.query.subjects.findMany({
|
|
orderBy: (subjects, { asc }) => [asc(subjects.order), asc(subjects.name)],
|
|
})
|
|
|
|
const names = rows.map((r) => r.name.trim()).filter((n) => n.length > 0)
|
|
return Array.from(new Set(names))
|
|
}
|
|
|
|
const normalizeSortText = (v: string | null | undefined) => (typeof v === "string" ? v.trim().toLowerCase() : "")
|
|
|
|
const parseFirstInt = (v: string) => {
|
|
const m = v.match(/\d+/)
|
|
return m ? Number(m[0]) : null
|
|
}
|
|
|
|
const compareGradeLabel = (a: string, b: string) => {
|
|
const aNum = parseFirstInt(a)
|
|
const bNum = parseFirstInt(b)
|
|
if (typeof aNum === "number" && typeof bNum === "number" && aNum !== bNum) return aNum - bNum
|
|
return a.localeCompare(b)
|
|
}
|
|
|
|
export const compareClassLike = (
|
|
a: { schoolName?: string | null; grade: string; name: string; homeroom?: string | null; room?: string | null },
|
|
b: { schoolName?: string | null; grade: string; name: string; homeroom?: string | null; room?: string | null }
|
|
) => {
|
|
const schoolCmp = normalizeSortText(a.schoolName).localeCompare(normalizeSortText(b.schoolName))
|
|
if (schoolCmp !== 0) return schoolCmp
|
|
|
|
const gradeCmp = compareGradeLabel(a.grade, b.grade)
|
|
if (gradeCmp !== 0) return gradeCmp
|
|
|
|
const nameCmp = normalizeSortText(a.name).localeCompare(normalizeSortText(b.name))
|
|
if (nameCmp !== 0) return nameCmp
|
|
|
|
const hrCmp = normalizeSortText(a.homeroom).localeCompare(normalizeSortText(b.homeroom))
|
|
if (hrCmp !== 0) return hrCmp
|
|
|
|
return normalizeSortText(a.room).localeCompare(normalizeSortText(b.room))
|
|
}
|
|
|
|
export const getAccessibleClassIdsForTeacher = async (teacherId: string): Promise<string[]> => {
|
|
const ownedIds = await db.select({ id: classes.id }).from(classes).where(eq(classes.teacherId, teacherId))
|
|
const assignedIds = await db
|
|
.select({ id: classSubjectTeachers.classId })
|
|
.from(classSubjectTeachers)
|
|
.where(eq(classSubjectTeachers.teacherId, teacherId))
|
|
return Array.from(new Set([...ownedIds.map((x) => x.id), ...assignedIds.map((x) => x.id)]))
|
|
}
|
|
|
|
export const getTeacherSubjectIdsForClass = async (teacherId: string, classId: string): Promise<string[]> => {
|
|
const rows = await db
|
|
.select({ subjectId: classSubjectTeachers.subjectId })
|
|
.from(classSubjectTeachers)
|
|
.where(and(eq(classSubjectTeachers.teacherId, teacherId), eq(classSubjectTeachers.classId, classId)))
|
|
return Array.from(new Set(rows.map((r) => String(r.subjectId))))
|
|
}
|
|
|
|
export const getTeacherClasses = cache(async (params?: { teacherId?: string }): Promise<TeacherClass[]> => {
|
|
const teacherId = params?.teacherId ?? (await getSessionTeacherId())
|
|
if (!teacherId) return []
|
|
|
|
const rows = await (async () => {
|
|
try {
|
|
const allIds = await getAccessibleClassIdsForTeacher(teacherId)
|
|
|
|
if (allIds.length === 0) return []
|
|
|
|
return await db
|
|
.select({
|
|
id: classes.id,
|
|
schoolName: classes.schoolName,
|
|
name: classes.name,
|
|
grade: classes.grade,
|
|
homeroom: classes.homeroom,
|
|
room: classes.room,
|
|
invitationCode: classes.invitationCode,
|
|
studentCount: sql<number>`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`,
|
|
})
|
|
.from(classes)
|
|
.leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id))
|
|
.where(inArray(classes.id, allIds))
|
|
.groupBy(classes.id, classes.schoolName, classes.name, classes.grade, classes.homeroom, classes.room, classes.invitationCode)
|
|
.orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
|
|
} catch {
|
|
return []
|
|
}
|
|
})()
|
|
|
|
const list = rows.map((r) => ({
|
|
id: r.id,
|
|
schoolName: r.schoolName,
|
|
name: r.name,
|
|
grade: r.grade,
|
|
homeroom: r.homeroom,
|
|
room: r.room,
|
|
invitationCode: r.invitationCode ?? null,
|
|
studentCount: Number(r.studentCount ?? 0),
|
|
}))
|
|
|
|
list.sort(compareClassLike)
|
|
|
|
// Fetch recent assignments for trends and schedule
|
|
const listWithTrends = await Promise.all(
|
|
list.map(async (c) => {
|
|
const [insights, schedule] = await Promise.all([
|
|
getClassHomeworkInsights({ classId: c.id, teacherId, limit: 7 }),
|
|
getClassSchedule({ classId: c.id, teacherId }),
|
|
])
|
|
|
|
const recentAssignments = insights
|
|
? insights.assignments.map((a) => ({
|
|
id: a.assignmentId,
|
|
title: a.title,
|
|
status: a.status,
|
|
subject: a.subject,
|
|
isActive: a.isActive,
|
|
isOverdue: a.isOverdue,
|
|
dueAt: a.dueAt ? new Date(a.dueAt) : null,
|
|
submittedCount: a.submittedCount,
|
|
targetCount: a.targetCount,
|
|
avgScore: a.scoreStats.avg,
|
|
medianScore: a.scoreStats.median,
|
|
}))
|
|
: []
|
|
return { ...c, recentAssignments, schedule }
|
|
})
|
|
)
|
|
|
|
return listWithTrends
|
|
})
|
|
|
|
export const getTeacherOptions = cache(async (): Promise<TeacherOption[]> => {
|
|
const rows = await db
|
|
.select({ id: users.id, name: users.name, email: users.email })
|
|
.from(users)
|
|
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
|
.where(eq(roles.name, "teacher"))
|
|
.orderBy(asc(users.createdAt))
|
|
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
name: r.name ?? "Unnamed",
|
|
email: r.email,
|
|
}))
|
|
})
|
|
|
|
export const getTeacherTeachingSubjects = cache(async (): Promise<ClassSubject[]> => {
|
|
const teacherId = await getSessionTeacherId()
|
|
if (!teacherId) return []
|
|
|
|
const rows = await db
|
|
.select({ subject: subjects.name })
|
|
.from(classSubjectTeachers)
|
|
.innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
|
|
.where(eq(classSubjectTeachers.teacherId, teacherId))
|
|
.groupBy(subjects.name)
|
|
.orderBy(asc(subjects.name))
|
|
|
|
return rows
|
|
.map((r) => r.subject as ClassSubject)
|
|
.filter((s) => DEFAULT_CLASS_SUBJECTS.includes(s))
|
|
})
|
|
|
|
export async function createTeacherClass(data: CreateTeacherClassInput): Promise<string> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
const id = createId()
|
|
|
|
const schoolName = data.schoolName?.trim() || null
|
|
const schoolId = data.schoolId?.trim() || null
|
|
const name = data.name.trim()
|
|
const grade = data.grade.trim()
|
|
const gradeId = data.gradeId?.trim() || null
|
|
const homeroom = data.homeroom?.trim() || null
|
|
const room = data.room?.trim() || null
|
|
|
|
if (!name) throw new Error("Name is required")
|
|
if (!grade) throw new Error("Grade is required")
|
|
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
const invitationCode = await generateUniqueInvitationCode()
|
|
try {
|
|
const subjectRows = await db
|
|
.select({ id: subjects.id, name: subjects.name })
|
|
.from(subjects)
|
|
.where(inArray(subjects.name, DEFAULT_CLASS_SUBJECTS))
|
|
const idByName = new Map(subjectRows.map((r) => [r.name as ClassSubject, r.id]))
|
|
|
|
await db.transaction(async (tx) => {
|
|
await tx.insert(classes).values({
|
|
id,
|
|
schoolName,
|
|
schoolId,
|
|
name,
|
|
grade,
|
|
gradeId,
|
|
homeroom,
|
|
room,
|
|
invitationCode,
|
|
teacherId,
|
|
})
|
|
|
|
const values = DEFAULT_CLASS_SUBJECTS
|
|
.filter((name) => idByName.has(name))
|
|
.map((name) => ({
|
|
classId: id,
|
|
subjectId: idByName.get(name)!,
|
|
teacherId: null,
|
|
}))
|
|
await tx.insert(classSubjectTeachers).values(values)
|
|
})
|
|
return id
|
|
} catch (err) {
|
|
if (isDuplicateInvitationCodeError(err)) continue
|
|
throw err
|
|
}
|
|
}
|
|
throw new Error("Failed to create class")
|
|
|
|
return id
|
|
}
|
|
|
|
export async function ensureClassInvitationCode(classId: string): Promise<string> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
const id = classId.trim()
|
|
if (!id) throw new Error("Missing class id")
|
|
|
|
const [owned] = await db
|
|
.select({ id: classes.id, invitationCode: classes.invitationCode })
|
|
.from(classes)
|
|
.where(and(eq(classes.id, id), eq(classes.teacherId, teacherId)))
|
|
.limit(1)
|
|
|
|
if (!owned) throw new Error("Class not found")
|
|
|
|
const existing = owned.invitationCode
|
|
if (typeof existing === "string" && /^\d{6}$/.test(existing)) return existing
|
|
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
const code = await generateUniqueInvitationCode()
|
|
try {
|
|
await db.update(classes).set({ invitationCode: code }).where(eq(classes.id, id))
|
|
return code
|
|
} catch (err) {
|
|
if (isDuplicateInvitationCodeError(err)) continue
|
|
throw err
|
|
}
|
|
}
|
|
|
|
throw new Error("Failed to generate invitation code")
|
|
}
|
|
|
|
export async function regenerateClassInvitationCode(classId: string): Promise<string> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
const id = classId.trim()
|
|
if (!id) throw new Error("Missing class id")
|
|
|
|
const [owned] = await db
|
|
.select({ id: classes.id })
|
|
.from(classes)
|
|
.where(and(eq(classes.id, id), eq(classes.teacherId, teacherId)))
|
|
.limit(1)
|
|
|
|
if (!owned) throw new Error("Class not found")
|
|
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
const code = await generateUniqueInvitationCode()
|
|
try {
|
|
await db.update(classes).set({ invitationCode: code }).where(eq(classes.id, id))
|
|
return code
|
|
} catch (err) {
|
|
if (isDuplicateInvitationCodeError(err)) continue
|
|
throw err
|
|
}
|
|
}
|
|
|
|
throw new Error("Failed to generate invitation code")
|
|
}
|
|
|
|
export async function enrollStudentByInvitationCode(studentId: string, invitationCode: string): Promise<string> {
|
|
const sid = studentId.trim()
|
|
const code = invitationCode.trim()
|
|
if (!sid) throw new Error("Missing student id")
|
|
if (!/^\d{6}$/.test(code)) throw new Error("Invalid invitation code")
|
|
|
|
const [cls] = await db
|
|
.select({ id: classes.id })
|
|
.from(classes)
|
|
.where(eq(classes.invitationCode, code))
|
|
.limit(1)
|
|
|
|
if (!cls) throw new Error("Invalid invitation code")
|
|
|
|
await db
|
|
.insert(classEnrollments)
|
|
.values({ classId: cls.id, studentId: sid, status: "active" })
|
|
.onDuplicateKeyUpdate({ set: { status: "active" } })
|
|
|
|
return cls.id
|
|
}
|
|
|
|
export async function enrollTeacherByInvitationCode(
|
|
teacherId: string,
|
|
invitationCode: string,
|
|
subject: string | null
|
|
): Promise<string> {
|
|
const tid = teacherId.trim()
|
|
const code = invitationCode.trim()
|
|
if (!tid) throw new Error("Missing teacher id")
|
|
if (!/^\d{6}$/.test(code)) throw new Error("Invalid invitation code")
|
|
|
|
const [teacher] = await db
|
|
.select({ id: users.id })
|
|
.from(users)
|
|
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
|
.where(and(eq(users.id, tid), eq(roles.name, "teacher")))
|
|
.limit(1)
|
|
|
|
if (!teacher) throw new Error("Teacher not found")
|
|
|
|
const [cls] = await db
|
|
.select({ id: classes.id, teacherId: classes.teacherId })
|
|
.from(classes)
|
|
.where(eq(classes.invitationCode, code))
|
|
.limit(1)
|
|
|
|
if (!cls) throw new Error("Invalid invitation code")
|
|
if (cls.teacherId === tid) return cls.id
|
|
|
|
const subjectValue = typeof subject === "string" ? subject.trim() : ""
|
|
const [existing] = await db
|
|
.select({ id: classSubjectTeachers.classId })
|
|
.from(classSubjectTeachers)
|
|
.where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.teacherId, tid)))
|
|
.limit(1)
|
|
|
|
if (existing && !subjectValue) return cls.id
|
|
if (subjectValue) {
|
|
const [subRow] = await db.select({ id: subjects.id }).from(subjects).where(eq(subjects.name, subjectValue)).limit(1)
|
|
if (!subRow) throw new Error("Subject not found")
|
|
const sid = subRow.id
|
|
|
|
const [mapping] = await db
|
|
.select({ teacherId: classSubjectTeachers.teacherId })
|
|
.from(classSubjectTeachers)
|
|
.where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid)))
|
|
.limit(1)
|
|
|
|
if (mapping?.teacherId && mapping.teacherId !== tid) throw new Error("Subject already assigned")
|
|
if (mapping?.teacherId === tid) return cls.id
|
|
if (!mapping) {
|
|
await db
|
|
.insert(classSubjectTeachers)
|
|
.values({ classId: cls.id, subjectId: sid, teacherId: null })
|
|
.onDuplicateKeyUpdate({ set: { teacherId: sql`${classSubjectTeachers.teacherId}` } })
|
|
}
|
|
|
|
const [existingSubject] = await db
|
|
.select({ id: classSubjectTeachers.classId })
|
|
.from(classSubjectTeachers)
|
|
.where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid), eq(classSubjectTeachers.teacherId, tid)))
|
|
.limit(1)
|
|
|
|
if (existingSubject) return cls.id
|
|
|
|
await db
|
|
.update(classSubjectTeachers)
|
|
.set({ teacherId: tid })
|
|
.where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid), isNull(classSubjectTeachers.teacherId)))
|
|
|
|
const [assigned] = await db
|
|
.select({ id: classSubjectTeachers.classId })
|
|
.from(classSubjectTeachers)
|
|
.where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid), eq(classSubjectTeachers.teacherId, tid)))
|
|
.limit(1)
|
|
|
|
if (!assigned) throw new Error("Subject already assigned")
|
|
} else {
|
|
const subjectRows = await db
|
|
.select({ id: classSubjectTeachers.subjectId, name: subjects.name })
|
|
.from(classSubjectTeachers)
|
|
.innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
|
|
.where(and(eq(classSubjectTeachers.classId, cls.id), isNull(classSubjectTeachers.teacherId)))
|
|
|
|
const preferred = DEFAULT_CLASS_SUBJECTS.find((s) => subjectRows.some((r) => r.name === s))
|
|
if (!preferred) throw new Error("Class already has assigned teachers")
|
|
const sid = subjectRows.find((r) => r.name === preferred)!.id
|
|
|
|
await db
|
|
.update(classSubjectTeachers)
|
|
.set({ teacherId: tid })
|
|
.where(
|
|
and(
|
|
eq(classSubjectTeachers.classId, cls.id),
|
|
eq(classSubjectTeachers.subjectId, sid),
|
|
isNull(classSubjectTeachers.teacherId)
|
|
)
|
|
)
|
|
|
|
const [assigned] = await db
|
|
.select({ id: classSubjectTeachers.classId })
|
|
.from(classSubjectTeachers)
|
|
.where(
|
|
and(
|
|
eq(classSubjectTeachers.classId, cls.id),
|
|
eq(classSubjectTeachers.subjectId, sid),
|
|
eq(classSubjectTeachers.teacherId, tid)
|
|
)
|
|
)
|
|
.limit(1)
|
|
|
|
if (!assigned) throw new Error("Class already has assigned teachers")
|
|
}
|
|
|
|
return cls.id
|
|
}
|
|
|
|
export async function updateTeacherClass(classId: string, data: UpdateTeacherClassInput): Promise<void> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
|
|
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")
|
|
|
|
const update: Partial<typeof classes.$inferSelect> = {}
|
|
if (data.schoolName !== undefined) update.schoolName = data.schoolName?.trim() || null
|
|
if (data.schoolId !== undefined) update.schoolId = data.schoolId?.trim() || null
|
|
if (typeof data.name === "string") update.name = data.name.trim()
|
|
if (typeof data.grade === "string") update.grade = data.grade.trim()
|
|
if (data.gradeId !== undefined) update.gradeId = data.gradeId?.trim() || null
|
|
if (data.homeroom !== undefined) update.homeroom = data.homeroom?.trim() || null
|
|
if (data.room !== undefined) update.room = data.room?.trim() || null
|
|
|
|
if (Object.keys(update).length === 0) return
|
|
|
|
await db
|
|
.update(classes)
|
|
.set(update)
|
|
.where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
|
|
}
|
|
|
|
export async function setClassSubjectTeachers(params: {
|
|
classId: string
|
|
assignments: Array<{ subject: ClassSubject; teacherId: string | null }>
|
|
}): Promise<void> {
|
|
const classId = params.classId.trim()
|
|
if (!classId) throw new Error("Missing class id")
|
|
|
|
const [existing] = await db.select({ id: classes.id }).from(classes).where(eq(classes.id, classId)).limit(1)
|
|
if (!existing) throw new Error("Class not found")
|
|
|
|
const teacherIds = params.assignments
|
|
.map((a) => a.teacherId)
|
|
.filter((v): v is string => typeof v === "string" && v.trim().length > 0)
|
|
|
|
if (teacherIds.length > 0) {
|
|
const rows = await db
|
|
.select({ id: users.id })
|
|
.from(users)
|
|
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
|
.where(and(eq(roles.name, "teacher"), inArray(users.id, teacherIds)))
|
|
if (rows.length !== new Set(teacherIds).size) throw new Error("Teacher not found")
|
|
}
|
|
|
|
const teacherBySubject = new Map<ClassSubject, string | null>()
|
|
for (const a of params.assignments) {
|
|
if (!DEFAULT_CLASS_SUBJECTS.includes(a.subject)) continue
|
|
teacherBySubject.set(a.subject, typeof a.teacherId === "string" && a.teacherId.trim().length > 0 ? a.teacherId.trim() : null)
|
|
}
|
|
|
|
// Map subject names to ids
|
|
const subjectRows = await db
|
|
.select({ id: subjects.id, name: subjects.name })
|
|
.from(subjects)
|
|
.where(inArray(subjects.name, DEFAULT_CLASS_SUBJECTS))
|
|
const idByName = new Map(subjectRows.map((r) => [r.name as ClassSubject, r.id]))
|
|
|
|
const values = DEFAULT_CLASS_SUBJECTS
|
|
.filter((name) => idByName.has(name))
|
|
.map((name) => ({
|
|
classId,
|
|
subjectId: idByName.get(name)!,
|
|
teacherId: teacherBySubject.get(name) ?? null,
|
|
}))
|
|
|
|
await db
|
|
.insert(classSubjectTeachers)
|
|
.values(values)
|
|
.onDuplicateKeyUpdate({ set: { teacherId: sql`VALUES(${classSubjectTeachers.teacherId})` } })
|
|
}
|
|
|
|
export async function deleteTeacherClass(classId: string): Promise<void> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
|
|
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")
|
|
|
|
await db
|
|
.delete(classes)
|
|
.where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
|
|
}
|
|
|
|
export async function enrollStudentByEmail(classId: string, email: string): Promise<void> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
const normalized = email.trim().toLowerCase()
|
|
if (!normalized) throw new Error("Student email is required")
|
|
|
|
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")
|
|
|
|
const [student] = await db
|
|
.select({ id: users.id })
|
|
.from(users)
|
|
.where(eq(users.email, normalized))
|
|
.limit(1)
|
|
|
|
if (!student) throw new Error("Student not found")
|
|
const [studentRole] = await db
|
|
.select({ id: usersToRoles.userId })
|
|
.from(usersToRoles)
|
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
|
.where(and(eq(usersToRoles.userId, student.id), eq(roles.name, "student")))
|
|
.limit(1)
|
|
if (!studentRole) throw new Error("User is not a student")
|
|
|
|
await db
|
|
.insert(classEnrollments)
|
|
.values({ classId, studentId: student.id, status: "active" })
|
|
.onDuplicateKeyUpdate({ set: { status: "active" } })
|
|
}
|
|
|
|
export async function setStudentEnrollmentStatus(classId: string, studentId: string, status: "active" | "inactive"): Promise<void> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
|
|
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")
|
|
|
|
const [existing] = await db
|
|
.select({ classId: classEnrollments.classId })
|
|
.from(classEnrollments)
|
|
.where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.studentId, studentId)))
|
|
.limit(1)
|
|
|
|
if (!existing) throw new Error("Enrollment not found")
|
|
|
|
await db
|
|
.update(classEnrollments)
|
|
.set({ status })
|
|
.where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.studentId, studentId)))
|
|
}
|
|
|
|
// Re-export from split files for backward compatibility
|
|
export * from "./data-access-stats"
|
|
export * from "./data-access-schedule"
|
|
export * from "./data-access-students"
|
|
export * from "./data-access-admin"
|