feat: exam actions and data safety fixes

This commit is contained in:
SpecialX
2025-12-30 17:48:22 +08:00
parent e7c902e8e1
commit f7ff018490
27 changed files with 896 additions and 194 deletions

View File

@@ -101,8 +101,8 @@ const ExamUpdateSchema = z.object({
score: z.coerce.number().int().min(0),
})
)
.default([]),
structure: z.any().optional(), // Accept structure JSON
.optional(),
structure: z.unknown().optional(),
status: z.enum(["draft", "published", "archived"]).optional(),
})
@@ -110,13 +110,15 @@ export async function updateExamAction(
prevState: ActionState<string> | null,
formData: FormData
): Promise<ActionState<string>> {
const rawQuestions = formData.get("questionsJson") as string | null
const rawStructure = formData.get("structureJson") as string | null
const rawQuestions = formData.get("questionsJson")
const rawStructure = formData.get("structureJson")
const hasQuestions = typeof rawQuestions === "string"
const hasStructure = typeof rawStructure === "string"
const parsed = ExamUpdateSchema.safeParse({
examId: formData.get("examId"),
questions: rawQuestions ? JSON.parse(rawQuestions) : [],
structure: rawStructure ? JSON.parse(rawStructure) : undefined,
questions: hasQuestions ? JSON.parse(rawQuestions) : undefined,
structure: hasStructure ? JSON.parse(rawStructure) : undefined,
status: formData.get("status") ?? undefined,
})
@@ -131,22 +133,24 @@ export async function updateExamAction(
const { examId, questions, structure, status } = parsed.data
try {
await db.delete(examQuestions).where(eq(examQuestions.examId, examId))
if (questions.length > 0) {
await db.insert(examQuestions).values(
questions.map((q, idx) => ({
examId,
questionId: q.id,
score: q.score ?? 0,
order: idx,
}))
)
if (questions) {
await db.delete(examQuestions).where(eq(examQuestions.examId, examId))
if (questions.length > 0) {
await db.insert(examQuestions).values(
questions.map((q, idx) => ({
examId,
questionId: q.id,
score: q.score ?? 0,
order: idx,
}))
)
}
}
// Prepare update object
const updateData: any = {}
const updateData: Partial<typeof exams.$inferInsert> = {}
if (status) updateData.status = status
if (structure) updateData.structure = structure
if (structure !== undefined) updateData.structure = structure
if (Object.keys(updateData).length > 0) {
await db.update(exams).set(updateData).where(eq(exams.id, examId))
@@ -169,6 +173,143 @@ export async function updateExamAction(
}
}
const ExamDeleteSchema = z.object({
examId: z.string().min(1),
})
export async function deleteExamAction(
prevState: ActionState<string> | null,
formData: FormData
): Promise<ActionState<string>> {
const parsed = ExamDeleteSchema.safeParse({
examId: formData.get("examId"),
})
if (!parsed.success) {
return {
success: false,
message: "Invalid delete data",
errors: parsed.error.flatten().fieldErrors,
}
}
const { examId } = parsed.data
try {
await db.delete(exams).where(eq(exams.id, examId))
} catch (error) {
console.error("Failed to delete exam:", error)
return {
success: false,
message: "Database error: Failed to delete exam",
}
}
revalidatePath("/teacher/exams/all")
revalidatePath("/teacher/exams/grading")
return {
success: true,
message: "Exam deleted",
data: examId,
}
}
const ExamDuplicateSchema = z.object({
examId: z.string().min(1),
})
const omitScheduledAtFromDescription = (description: string | null) => {
if (!description) return null
try {
const parsed: unknown = JSON.parse(description)
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return description
const meta = parsed as Record<string, unknown>
if ("scheduledAt" in meta) delete meta.scheduledAt
return JSON.stringify(meta)
} catch {
return description
}
}
export async function duplicateExamAction(
prevState: ActionState<string> | null,
formData: FormData
): Promise<ActionState<string>> {
const parsed = ExamDuplicateSchema.safeParse({
examId: formData.get("examId"),
})
if (!parsed.success) {
return {
success: false,
message: "Invalid duplicate data",
errors: parsed.error.flatten().fieldErrors,
}
}
const { examId } = parsed.data
const source = await db.query.exams.findFirst({
where: eq(exams.id, examId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
},
},
})
if (!source) {
return {
success: false,
message: "Exam not found",
}
}
const newExamId = createId()
const user = await getCurrentUser()
try {
await db.transaction(async (tx) => {
await tx.insert(exams).values({
id: newExamId,
title: `${source.title} (Copy)`,
description: omitScheduledAtFromDescription(source.description),
creatorId: user?.id ?? "user_teacher_123",
startTime: null,
endTime: null,
status: "draft",
structure: source.structure,
})
if (source.questions.length > 0) {
await tx.insert(examQuestions).values(
source.questions.map((q) => ({
examId: newExamId,
questionId: q.questionId,
score: q.score ?? 0,
order: q.order ?? 0,
}))
)
}
})
} catch (error) {
console.error("Failed to duplicate exam:", error)
return {
success: false,
message: "Database error: Failed to duplicate exam",
}
}
revalidatePath("/teacher/exams/all")
return {
success: true,
message: "Exam duplicated",
data: newExamId,
}
}
const GradingSchema = z.object({
submissionId: z.string().min(1),
answers: z.array(z.object({