sync-docs-and-fixes

This commit is contained in:
SpecialX
2026-03-03 17:32:26 +08:00
parent 538805bad0
commit eb08c0ab68
73 changed files with 2218 additions and 422 deletions

View File

@@ -1,64 +1,64 @@
"use server"
import { revalidatePath } from "next/cache"
import { headers } from "next/headers"
import { createId } from "@paralleldrive/cuid2"
import { and, count, eq } from "drizzle-orm"
import { and, count, eq, inArray } from "drizzle-orm"
import { auth } from "@/auth"
import { db } from "@/shared/db"
import {
classes,
classEnrollments,
classSubjectTeachers,
exams,
homeworkAnswers,
homeworkAssignmentQuestions,
homeworkAssignmentTargets,
homeworkAssignments,
homeworkSubmissions,
roles,
users,
usersToRoles,
} from "@/shared/db/schema"
import type { ActionState } from "@/shared/types/action-state"
import { CreateHomeworkAssignmentSchema, GradeHomeworkSchema } from "./schema"
type CurrentUser = { id: string; role: "admin" | "teacher" | "student" }
type TeacherRole = "admin" | "teacher"
type StudentRole = "student"
async function getCurrentUser() {
const ref = (await headers()).get("referer") || ""
const roleHint: CurrentUser["role"] = ref.includes("/admin/")
? "admin"
: ref.includes("/student/")
? "student"
: ref.includes("/teacher/")
? "teacher"
: "teacher"
const byRole = await db.query.users.findFirst({
where: eq(users.role, roleHint),
orderBy: (u, { asc }) => [asc(u.createdAt)],
})
if (byRole) return { id: byRole.id, role: roleHint }
const anyUser = await db.query.users.findFirst({
orderBy: (u, { asc }) => [asc(u.createdAt)],
})
if (anyUser) return { id: anyUser.id, role: roleHint }
return { id: "user_teacher_math", role: roleHint }
const getSessionUserId = async (): Promise<string | null> => {
const session = await auth()
const userId = String(session?.user?.id ?? "").trim()
return userId.length > 0 ? userId : null
}
async function ensureTeacher() {
const user = await getCurrentUser()
if (!user || (user.role !== "teacher" && user.role !== "admin")) throw new Error("Unauthorized")
return user
async function ensureTeacher(): Promise<{ id: string; role: TeacherRole }> {
const userId = await getSessionUserId()
if (!userId) throw new Error("Unauthorized")
const [row] = await db
.select({ id: users.id, role: roles.name })
.from(users)
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
.where(and(eq(users.id, userId), inArray(roles.name, ["teacher", "admin"])))
.limit(1)
if (!row) throw new Error("Unauthorized")
return { id: row.id, role: row.role as TeacherRole }
}
async function ensureStudent() {
const user = await getCurrentUser()
if (!user || user.role !== "student") throw new Error("Unauthorized")
return user
async function ensureStudent(): Promise<{ id: string; role: StudentRole }> {
const userId = await getSessionUserId()
if (!userId) throw new Error("Unauthorized")
const [row] = 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, "student")))
.limit(1)
if (!row) throw new Error("Unauthorized")
return { id: row.id, role: "student" }
}
const parseStudentIds = (raw: string): string[] => {
@@ -108,12 +108,12 @@ export async function createHomeworkAssignmentAction(
const input = parsed.data
const publish = input.publish ?? true
const [ownedClass] = await db
.select({ id: classes.id })
const [classRow] = await db
.select({ id: classes.id, teacherId: classes.teacherId })
.from(classes)
.where(user.role === "admin" ? eq(classes.id, input.classId) : and(eq(classes.id, input.classId), eq(classes.teacherId, user.id)))
.where(eq(classes.id, input.classId))
.limit(1)
if (!ownedClass) return { success: false, message: "Class not found" }
if (!classRow) return { success: false, message: "Class not found" }
const exam = await db.query.exams.findFirst({
where: eq(exams.id, input.sourceExamId),
@@ -126,23 +126,43 @@ export async function createHomeworkAssignmentAction(
if (!exam) return { success: false, message: "Exam not found" }
if (user.role !== "admin" && classRow.teacherId !== user.id) {
const assignedSubjectRows = await db
.select({ subjectId: classSubjectTeachers.subjectId })
.from(classSubjectTeachers)
.where(and(eq(classSubjectTeachers.classId, input.classId), eq(classSubjectTeachers.teacherId, user.id)))
if (assignedSubjectRows.length === 0) {
return { success: false, message: "Not assigned to this class" }
}
const assignedSubjectIds = new Set(assignedSubjectRows.map((r) => r.subjectId))
if (!exam.subjectId) {
return { success: false, message: "Exam subject not set" }
}
if (!assignedSubjectIds.has(exam.subjectId)) {
return { success: false, message: "Not assigned to this subject" }
}
}
const assignmentId = createId()
const availableAt = input.availableAt ? new Date(input.availableAt) : null
const dueAt = input.dueAt ? new Date(input.dueAt) : null
const lateDueAt = input.lateDueAt ? new Date(input.lateDueAt) : null
const classScope =
user.role === "admin"
? eq(classes.id, input.classId)
: classRow.teacherId === user.id
? eq(classes.teacherId, user.id)
: eq(classes.id, input.classId)
const classStudentIds = (
await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.where(
and(
eq(classEnrollments.classId, input.classId),
eq(classEnrollments.status, "active"),
user.role === "admin" ? eq(classes.id, input.classId) : eq(classes.teacherId, user.id)
)
and(eq(classEnrollments.classId, input.classId), eq(classEnrollments.status, "active"), classScope)
)
).map((r) => r.studentId)

View File

@@ -12,7 +12,6 @@ import { Checkbox } from "@/shared/components/ui/checkbox"
import { Label } from "@/shared/components/ui/label"
import { Textarea } from "@/shared/components/ui/textarea"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import { Separator } from "@/shared/components/ui/separator"
import { Clock, CheckCircle2, Save, FileText } from "lucide-react"
import type { StudentHomeworkTakeData } from "../types"

View File

@@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/sha
import { Checkbox } from "@/shared/components/ui/checkbox"
import { Label } from "@/shared/components/ui/label"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import { CheckCircle2, FileText, ChevronLeft } from "lucide-react"
import { FileText, ChevronLeft } from "lucide-react"
import Link from "next/link"
import type { StudentHomeworkTakeData } from "../types"
@@ -57,7 +57,6 @@ type HomeworkReviewViewProps = {
export function HomeworkReviewView({ initialData }: HomeworkReviewViewProps) {
const submissionStatus = initialData.submission?.status ?? "not_started"
const isGraded = submissionStatus === "graded"
const isSubmitted = submissionStatus === "submitted"
const answersByQuestionId = useMemo(() => {
const map = new Map<string, { answer: unknown }>()

View File

@@ -3,6 +3,7 @@ import "server-only"
import { cache } from "react"
import { and, count, desc, eq, inArray, isNull, lte, or, sql } from "drizzle-orm"
import { auth } from "@/auth"
import { db } from "@/shared/db"
import {
classEnrollments,
@@ -11,7 +12,9 @@ import {
homeworkAssignmentTargets,
homeworkAssignments,
homeworkSubmissions,
roles,
users,
usersToRoles,
} from "@/shared/db/schema"
import type {
@@ -550,17 +553,20 @@ export const getHomeworkSubmissionDetails = cache(async (submissionId: string):
})
export const getDemoStudentUser = cache(async (): Promise<{ id: string; name: string } | null> => {
const student = await db.query.users.findFirst({
where: eq(users.role, "student"),
orderBy: (u, { asc }) => [asc(u.createdAt)],
})
if (student) return { id: student.id, name: student.name || "Student" }
const session = await auth()
const userId = String(session?.user?.id ?? "").trim()
if (!userId) return null
const anyUser = await db.query.users.findFirst({
orderBy: (u, { asc }) => [asc(u.createdAt)],
})
if (!anyUser) return null
return { id: anyUser.id, name: anyUser.name || "User" }
const [student] = await db
.select({ id: users.id, name: users.name })
.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, "student")))
.limit(1)
if (!student) return null
return { id: student.id, name: student.name || "Student" }
})
const toStudentProgressStatus = (v: string | null | undefined): StudentHomeworkProgressStatus => {
@@ -592,19 +598,23 @@ export const getStudentHomeworkAssignments = cache(async (studentId: string): Pr
const assignmentIds = assignments.map((a) => a.id)
const submissions = await db.query.homeworkSubmissions.findMany({
where: and(eq(homeworkSubmissions.studentId, studentId), inArray(homeworkSubmissions.assignmentId, assignmentIds)),
orderBy: [desc(homeworkSubmissions.createdAt)],
orderBy: [desc(homeworkSubmissions.updatedAt)],
})
const attemptsByAssignmentId = new Map<string, number>()
const latestByAssignmentId = new Map<string, (typeof submissions)[number]>()
const latestSubmittedByAssignmentId = new Map<string, (typeof submissions)[number]>()
for (const s of submissions) {
attemptsByAssignmentId.set(s.assignmentId, (attemptsByAssignmentId.get(s.assignmentId) ?? 0) + 1)
if (!latestByAssignmentId.has(s.assignmentId)) latestByAssignmentId.set(s.assignmentId, s)
if (s.status === "submitted" || s.status === "graded") {
if (!latestSubmittedByAssignmentId.has(s.assignmentId)) latestSubmittedByAssignmentId.set(s.assignmentId, s)
}
}
return assignments.map((a) => {
const latest = latestByAssignmentId.get(a.id) ?? null
const latest = latestSubmittedByAssignmentId.get(a.id) ?? latestByAssignmentId.get(a.id) ?? null
const attemptsUsed = attemptsByAssignmentId.get(a.id) ?? 0
const item: StudentHomeworkAssignmentListItem = {