import "server-only" import { createId } from "@paralleldrive/cuid2" import { and, count, eq } from "drizzle-orm" import { db } from "@/shared/db" import { classes, classEnrollments, classSubjectTeachers, exams, homeworkAnswers, homeworkAssignmentQuestions, homeworkAssignmentTargets, homeworkAssignments, homeworkSubmissions, } from "@/shared/db/schema" import type { DataScope } from "@/shared/types/permissions" // ---- Types ---- export type HomeworkExamQuestionData = { questionId: string score: number | null order: number | null } export type HomeworkExamData = { id: string title: string subjectId: string | null structure: unknown questions: HomeworkExamQuestionData[] } export type HomeworkSubmissionPermissionData = { id: string studentId: string status: string | null assignment: { dueAt: Date | null allowLate: boolean lateDueAt: Date | null } } export type CreateHomeworkAssignmentData = { assignmentId: string sourceExamId: string title: string description: string | null structure: unknown status: string creatorId: string availableAt: Date | null dueAt: Date | null allowLate: boolean lateDueAt: Date | null maxAttempts: number publish: boolean questions: HomeworkExamQuestionData[] targetStudentIds: string[] } // ---- Query helpers (for permission/validation in actions) ---- export const getClassTeacherById = async ( classId: string ): Promise<{ id: string; teacherId: string } | null> => { const [row] = await db .select({ id: classes.id, teacherId: classes.teacherId }) .from(classes) .where(eq(classes.id, classId)) .limit(1) return row ?? null } export const getExamWithQuestionsForHomework = async ( examId: string ): Promise => { const exam = await db.query.exams.findFirst({ where: eq(exams.id, examId), with: { questions: { orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)], }, }, }) if (!exam) return null return { id: exam.id, title: exam.title, subjectId: exam.subjectId, structure: exam.structure, questions: exam.questions.map((q) => ({ questionId: q.questionId, score: q.score ?? null, order: q.order ?? null, })), } } export const getTeacherAssignedSubjectIds = async ( classId: string, teacherId: string ): Promise => { const rows = await db .select({ subjectId: classSubjectTeachers.subjectId }) .from(classSubjectTeachers) .where( and( eq(classSubjectTeachers.classId, classId), eq(classSubjectTeachers.teacherId, teacherId) ) ) return rows.map((r) => r.subjectId) } export const getActiveClassStudentIdsForHomework = async ( classId: string, dataScope: DataScope, userId: string, classTeacherId: string ): Promise => { const classScope = dataScope.type === "all" ? eq(classes.id, classId) : classTeacherId === userId ? eq(classes.teacherId, userId) : eq(classes.id, classId) const rows = await db .select({ studentId: classEnrollments.studentId }) .from(classEnrollments) .innerJoin(classes, eq(classes.id, classEnrollments.classId)) .where( and( eq(classEnrollments.classId, classId), eq(classEnrollments.status, "active"), classScope ) ) return rows.map((r) => r.studentId) } export const getHomeworkSubmissionForPermission = async ( submissionId: string ): Promise => { const submission = await db.query.homeworkSubmissions.findFirst({ where: eq(homeworkSubmissions.id, submissionId), with: { assignment: true }, }) if (!submission) return null return { id: submission.id, studentId: submission.studentId, status: submission.status, assignment: { dueAt: submission.assignment.dueAt, allowLate: submission.assignment.allowLate, lateDueAt: submission.assignment.lateDueAt, }, } } // ---- Write functions ---- export const createHomeworkAssignment = async ( input: CreateHomeworkAssignmentData ): Promise => { await db.transaction(async (tx) => { await tx.insert(homeworkAssignments).values({ id: input.assignmentId, sourceExamId: input.sourceExamId, title: input.title, description: input.description, structure: input.publish ? input.structure : null, status: input.status, creatorId: input.creatorId, availableAt: input.availableAt, dueAt: input.dueAt, allowLate: input.allowLate, lateDueAt: input.lateDueAt, maxAttempts: input.maxAttempts, }) if (input.publish && input.questions.length > 0) { await tx.insert(homeworkAssignmentQuestions).values( input.questions.map((q) => ({ assignmentId: input.assignmentId, questionId: q.questionId, score: q.score ?? 0, order: q.order ?? 0, })) ) } if (input.publish && input.targetStudentIds.length > 0) { await tx.insert(homeworkAssignmentTargets).values( input.targetStudentIds.map((studentId) => ({ assignmentId: input.assignmentId, studentId, })) ) } }) return input.assignmentId } export const startHomeworkSubmission = async ( assignmentId: string, studentId: string ): Promise<{ submissionId: string } | { error: string }> => { const assignment = await db.query.homeworkAssignments.findFirst({ where: eq(homeworkAssignments.id, assignmentId), }) if (!assignment) return { error: "Assignment not found" } if (assignment.status !== "published") return { error: "Assignment not available" } const target = await db.query.homeworkAssignmentTargets.findFirst({ where: and( eq(homeworkAssignmentTargets.assignmentId, assignmentId), eq(homeworkAssignmentTargets.studentId, studentId) ), }) if (!target) return { error: "Not assigned" } if (assignment.availableAt && assignment.availableAt > new Date()) { return { error: "Not available yet" } } const [attemptRow] = await db .select({ c: count() }) .from(homeworkSubmissions) .where( and( eq(homeworkSubmissions.assignmentId, assignmentId), eq(homeworkSubmissions.studentId, studentId) ) ) const attemptNo = (attemptRow?.c ?? 0) + 1 if (attemptNo > assignment.maxAttempts) return { error: "No attempts left" } const submissionId = createId() await db.insert(homeworkSubmissions).values({ id: submissionId, assignmentId, studentId, attemptNo, status: "started", startedAt: new Date(), }) return { submissionId } } export const saveHomeworkAnswer = async ( submissionId: string, questionId: string, answerContent: unknown ): Promise => { await db.transaction(async (tx) => { const existing = await tx.query.homeworkAnswers.findFirst({ where: and( eq(homeworkAnswers.submissionId, submissionId), eq(homeworkAnswers.questionId, questionId) ), }) if (existing) { await tx .update(homeworkAnswers) .set({ answerContent, updatedAt: new Date() }) .where(eq(homeworkAnswers.id, existing.id)) } else { await tx.insert(homeworkAnswers).values({ id: createId(), submissionId, questionId, answerContent, }) } }) } export const markHomeworkSubmitted = async ( submissionId: string, isLate: boolean ): Promise => { const now = new Date() await db .update(homeworkSubmissions) .set({ status: "submitted", submittedAt: now, isLate, updatedAt: now }) .where(eq(homeworkSubmissions.id, submissionId)) } export const gradeHomeworkAnswers = async ( submissionId: string, answers: Array<{ id: string; score: number; feedback: string | null }> ): Promise => { let totalScore = 0 for (const ans of answers) { await db .update(homeworkAnswers) .set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() }) .where(eq(homeworkAnswers.id, ans.id)) totalScore += ans.score } await db .update(homeworkSubmissions) .set({ score: totalScore, status: "graded", updatedAt: new Date() }) .where(eq(homeworkSubmissions.id, submissionId)) }