=test_update_homework_tests_and_work_log
Some checks failed
CI / build-deploy (push) Has been cancelled
Some checks failed
CI / build-deploy (push) Has been cancelled
This commit is contained in:
@@ -5,9 +5,24 @@ import { ActionState } from "@/shared/types/action-state"
|
||||
import { z } from "zod"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { db } from "@/shared/db"
|
||||
import { exams, examQuestions, subjects, grades } from "@/shared/db/schema"
|
||||
import { exams, examQuestions } from "@/shared/db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { omitScheduledAtFromDescription } from "./data-access"
|
||||
import { buildExamDescription, omitScheduledAtFromDescription, persistAiGeneratedExamDraft, persistExamDraft, resolveSubjectGradeNames } from "./data-access"
|
||||
import {
|
||||
AiGeneratedStructureSchema,
|
||||
AiInsertQuestionSchema,
|
||||
AiQuestionSchema,
|
||||
generateAiCreateDraftFromSource,
|
||||
generateAiPreviewData,
|
||||
regenerateAiQuestionByInstruction,
|
||||
} from "./ai-pipeline"
|
||||
import type {
|
||||
AiGeneratedQuestion,
|
||||
AiGeneratedStructureNode,
|
||||
AiPreviewData,
|
||||
AiRewriteQuestionData,
|
||||
} from "./ai-pipeline"
|
||||
export type { AiPreviewData, AiRewriteQuestionData } from "./ai-pipeline"
|
||||
|
||||
const ExamCreateSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
@@ -27,6 +42,213 @@ const ExamCreateSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const getStringValue = (formData: FormData, key: string) => {
|
||||
const value = formData.get(key)
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
const failState = <T>(message: string, errors?: Record<string, string[]>): ActionState<T> => ({
|
||||
success: false,
|
||||
message,
|
||||
errors,
|
||||
})
|
||||
|
||||
const successState = <T>(data: T, message?: string): ActionState<T> => ({
|
||||
success: true,
|
||||
message,
|
||||
data,
|
||||
})
|
||||
|
||||
const invalidFormState = <T>(
|
||||
error: z.ZodError,
|
||||
options?: { fallbackMessage?: string; useFirstMessage?: boolean }
|
||||
): ActionState<T> => {
|
||||
const errors = error.flatten().fieldErrors
|
||||
const fallbackMessage = options?.fallbackMessage ?? "Invalid form data"
|
||||
const useFirstMessage = options?.useFirstMessage ?? true
|
||||
const messages = Object.values(errors).flatMap((items) => items ?? [])
|
||||
const firstMessage = messages.find((msg): msg is string => typeof msg === "string" && msg.length > 0)
|
||||
return failState<T>(useFirstMessage ? (firstMessage ?? fallbackMessage) : fallbackMessage, errors)
|
||||
}
|
||||
|
||||
const prepareExamCreateContext = async (input: {
|
||||
subject: string
|
||||
grade: string
|
||||
difficulty: number
|
||||
totalScore: number
|
||||
durationMin: number
|
||||
scheduledAt?: string | null
|
||||
}) => {
|
||||
const examId = createId()
|
||||
const scheduled = input.scheduledAt || undefined
|
||||
const resolvedNames = await resolveSubjectGradeNames({
|
||||
subjectId: input.subject,
|
||||
gradeId: input.grade,
|
||||
})
|
||||
const subjectName = resolvedNames.subjectName ?? input.subject
|
||||
const gradeName = resolvedNames.gradeName ?? input.grade
|
||||
const buildDescription = (options?: { questionCount?: number }) => buildExamDescription({
|
||||
subject: subjectName,
|
||||
grade: gradeName,
|
||||
difficulty: input.difficulty,
|
||||
totalScore: input.totalScore,
|
||||
durationMin: input.durationMin,
|
||||
scheduledAt: scheduled,
|
||||
questionCount: options?.questionCount,
|
||||
})
|
||||
return { examId, scheduled, subjectName, gradeName, buildDescription }
|
||||
}
|
||||
|
||||
const loadAiDraftQuestionsAndStructure = async (input: {
|
||||
rawAiQuestions: string | null
|
||||
rawStructure: string | null
|
||||
title: string
|
||||
subject: string
|
||||
grade: string
|
||||
difficulty: number
|
||||
totalScore: number
|
||||
durationMin: number
|
||||
aiSourceText?: string
|
||||
aiQuestionCount?: number
|
||||
aiProviderId?: string
|
||||
}): Promise<
|
||||
| { ok: true; generated: AiGeneratedQuestion[]; structure: AiGeneratedStructureNode[] }
|
||||
| { ok: false; message: string }
|
||||
> => {
|
||||
if (input.rawAiQuestions) {
|
||||
let parsedQuestions: unknown = null
|
||||
try {
|
||||
parsedQuestions = JSON.parse(input.rawAiQuestions)
|
||||
} catch {
|
||||
return { ok: false, message: "Invalid AI preview payload" }
|
||||
}
|
||||
const validated = z.array(AiInsertQuestionSchema).safeParse(parsedQuestions)
|
||||
if (!validated.success || validated.data.length === 0) {
|
||||
return { ok: false, message: "Invalid AI preview payload" }
|
||||
}
|
||||
const generated = validated.data.map((q) => ({
|
||||
id: q.id,
|
||||
type: q.type,
|
||||
difficulty: q.difficulty,
|
||||
content: q.content,
|
||||
score: q.score,
|
||||
}))
|
||||
let structure: AiGeneratedStructureNode[] = []
|
||||
if (input.rawStructure) {
|
||||
try {
|
||||
const parsedStructure = JSON.parse(input.rawStructure)
|
||||
const validatedStructure = AiGeneratedStructureSchema.safeParse(parsedStructure)
|
||||
if (validatedStructure.success) {
|
||||
structure = validatedStructure.data
|
||||
} else {
|
||||
return { ok: false, message: "Invalid preview structure" }
|
||||
}
|
||||
} catch {
|
||||
return { ok: false, message: "Invalid preview structure" }
|
||||
}
|
||||
}
|
||||
if (structure.length === 0) {
|
||||
structure = generated.map((q) => ({
|
||||
id: createId(),
|
||||
type: "question",
|
||||
questionId: q.id,
|
||||
score: q.score,
|
||||
}))
|
||||
}
|
||||
return { ok: true, generated, structure }
|
||||
}
|
||||
|
||||
const sourceText = input.aiSourceText?.trim()
|
||||
if (!sourceText) {
|
||||
return { ok: false, message: "Please analyze and preview before creating" }
|
||||
}
|
||||
const aiDraft = await generateAiCreateDraftFromSource({
|
||||
title: input.title,
|
||||
subject: input.subject,
|
||||
grade: input.grade,
|
||||
difficulty: input.difficulty,
|
||||
totalScore: input.totalScore,
|
||||
durationMin: input.durationMin,
|
||||
questionCount: input.aiQuestionCount,
|
||||
sourceText,
|
||||
aiProviderId: input.aiProviderId,
|
||||
})
|
||||
if (!aiDraft.ok) {
|
||||
return { ok: false, message: aiDraft.message }
|
||||
}
|
||||
return { ok: true, generated: aiDraft.generated, structure: aiDraft.structure }
|
||||
}
|
||||
|
||||
const prepareAiPreviewRequest = async (input: {
|
||||
title?: string
|
||||
subject?: string
|
||||
grade?: string
|
||||
difficulty?: number
|
||||
totalScore?: number
|
||||
durationMin?: number
|
||||
aiSourceText: string
|
||||
aiQuestionCount?: number
|
||||
aiProviderId?: string
|
||||
}) => {
|
||||
const resolvedNames = await resolveSubjectGradeNames({
|
||||
subjectId: input.subject,
|
||||
gradeId: input.grade,
|
||||
})
|
||||
const title = input.title && input.title.trim().length > 0 ? input.title : "AI Exam"
|
||||
const subjectName = input.subject ? resolvedNames.subjectName ?? input.subject : undefined
|
||||
const gradeName = input.grade ? resolvedNames.gradeName ?? input.grade : undefined
|
||||
return {
|
||||
title,
|
||||
subject: subjectName,
|
||||
grade: gradeName,
|
||||
difficulty: input.difficulty ?? 3,
|
||||
totalScore: input.totalScore ?? 100,
|
||||
durationMin: input.durationMin ?? 90,
|
||||
questionCount: input.aiQuestionCount,
|
||||
sourceText: input.aiSourceText,
|
||||
aiProviderId: input.aiProviderId,
|
||||
}
|
||||
}
|
||||
|
||||
const parseRegenerateAiQuestionInput = (
|
||||
formData: FormData
|
||||
):
|
||||
| {
|
||||
ok: true
|
||||
instruction: string
|
||||
aiProviderId?: string
|
||||
sourceText?: string
|
||||
originalQuestion: z.infer<typeof AiQuestionSchema>
|
||||
}
|
||||
| { ok: false; state: ActionState<AiRewriteQuestionData> } => {
|
||||
const instruction = getStringValue(formData, "instruction")?.trim()
|
||||
const aiProviderId = getStringValue(formData, "aiProviderId")?.trim()
|
||||
const sourceText = getStringValue(formData, "sourceText")?.trim()
|
||||
const questionJson = getStringValue(formData, "questionJson")
|
||||
if (!instruction) {
|
||||
return { ok: false, state: failState<AiRewriteQuestionData>("Please enter rewrite instruction") }
|
||||
}
|
||||
if (!questionJson) {
|
||||
return { ok: false, state: failState<AiRewriteQuestionData>("No selected question data") }
|
||||
}
|
||||
try {
|
||||
const parsedQuestion = JSON.parse(questionJson) as unknown
|
||||
const validatedQuestion = AiQuestionSchema.safeParse(parsedQuestion)
|
||||
if (!validatedQuestion.success) {
|
||||
return { ok: false, state: failState<AiRewriteQuestionData>("Selected question format invalid") }
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
instruction,
|
||||
aiProviderId,
|
||||
sourceText,
|
||||
originalQuestion: validatedQuestion.data,
|
||||
}
|
||||
} catch {
|
||||
return { ok: false, state: failState<AiRewriteQuestionData>("Selected question format invalid") }
|
||||
}
|
||||
}
|
||||
|
||||
export async function createExamAction(
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
@@ -34,72 +256,235 @@ export async function createExamAction(
|
||||
const rawQuestions = formData.get("questionsJson") as string | null
|
||||
|
||||
const parsed = ExamCreateSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
subject: formData.get("subject"),
|
||||
grade: formData.get("grade"),
|
||||
difficulty: formData.get("difficulty"),
|
||||
totalScore: formData.get("totalScore"),
|
||||
durationMin: formData.get("durationMin"),
|
||||
scheduledAt: formData.get("scheduledAt"),
|
||||
title: getStringValue(formData, "title"),
|
||||
subject: getStringValue(formData, "subject"),
|
||||
grade: getStringValue(formData, "grade"),
|
||||
difficulty: getStringValue(formData, "difficulty"),
|
||||
totalScore: getStringValue(formData, "totalScore"),
|
||||
durationMin: getStringValue(formData, "durationMin"),
|
||||
scheduledAt: getStringValue(formData, "scheduledAt") ?? null,
|
||||
questions: rawQuestions ? JSON.parse(rawQuestions) : [],
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid form data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
return invalidFormState<string>(parsed.error, { useFirstMessage: false })
|
||||
}
|
||||
|
||||
const input = parsed.data
|
||||
|
||||
const examId = createId()
|
||||
const scheduled = input.scheduledAt || undefined
|
||||
|
||||
// Retrieve names for JSON description (to maintain compatibility)
|
||||
const subjectRecord = await db.query.subjects.findFirst({
|
||||
where: eq(subjects.id, input.subject),
|
||||
})
|
||||
const gradeRecord = await db.query.grades.findFirst({
|
||||
where: eq(grades.id, input.grade),
|
||||
})
|
||||
|
||||
const meta = {
|
||||
subject: subjectRecord?.name ?? input.subject,
|
||||
grade: gradeRecord?.name ?? input.grade,
|
||||
const context = await prepareExamCreateContext({
|
||||
subject: input.subject,
|
||||
grade: input.grade,
|
||||
difficulty: input.difficulty,
|
||||
totalScore: input.totalScore,
|
||||
durationMin: input.durationMin,
|
||||
scheduledAt: scheduled ?? undefined,
|
||||
}
|
||||
scheduledAt: input.scheduledAt,
|
||||
})
|
||||
const description = context.buildDescription()
|
||||
|
||||
try {
|
||||
const user = await getCurrentUser()
|
||||
await db.insert(exams).values({
|
||||
id: examId,
|
||||
await persistExamDraft({
|
||||
examId: context.examId,
|
||||
title: input.title,
|
||||
description: JSON.stringify(meta),
|
||||
creatorId: user?.id ?? "user_teacher_math",
|
||||
subjectId: input.subject,
|
||||
gradeId: input.grade,
|
||||
startTime: scheduled ? new Date(scheduled) : null,
|
||||
status: "draft",
|
||||
scheduledAt: context.scheduled,
|
||||
description,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to create exam:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Database error: Failed to create exam",
|
||||
}
|
||||
return failState<string>("Database error: Failed to create exam")
|
||||
}
|
||||
|
||||
revalidatePath("/teacher/exams/all")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Exam created successfully.",
|
||||
data: examId,
|
||||
return successState(context.examId, "Exam created successfully.")
|
||||
}
|
||||
|
||||
const AiExamCreateSchema = ExamCreateSchema.extend({
|
||||
aiSourceText: z.string().optional(),
|
||||
aiQuestionCount: z.coerce.number().int().min(1).max(200).optional(),
|
||||
aiProviderId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
const AiExamPreviewSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
grade: z.string().optional(),
|
||||
difficulty: z.coerce.number().int().min(1).max(5).optional(),
|
||||
totalScore: z.coerce.number().int().min(1).optional(),
|
||||
durationMin: z.coerce.number().int().min(1).optional(),
|
||||
aiSourceText: z.string().min(1),
|
||||
aiQuestionCount: z.coerce.number().int().min(1).max(200).optional(),
|
||||
aiProviderId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export async function createAiExamAction(
|
||||
prevState: ActionState<string> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
const rawQuestions = formData.get("questionsJson") as string | null
|
||||
const rawAiQuestions = formData.get("aiQuestionsJson") as string | null
|
||||
const rawStructure = formData.get("structureJson") as string | null
|
||||
const aiSourceTextRaw = formData.get("aiSourceText")
|
||||
const aiQuestionCountRaw = formData.get("aiQuestionCount")
|
||||
const aiProviderIdRaw = formData.get("aiProviderId")
|
||||
|
||||
const parsed = AiExamCreateSchema.safeParse({
|
||||
title: getStringValue(formData, "title"),
|
||||
subject: getStringValue(formData, "subject"),
|
||||
grade: getStringValue(formData, "grade"),
|
||||
difficulty: getStringValue(formData, "difficulty"),
|
||||
totalScore: getStringValue(formData, "totalScore"),
|
||||
durationMin: getStringValue(formData, "durationMin"),
|
||||
scheduledAt: getStringValue(formData, "scheduledAt") ?? null,
|
||||
questions: rawQuestions ? JSON.parse(rawQuestions) : [],
|
||||
aiSourceText: typeof aiSourceTextRaw === "string" ? aiSourceTextRaw.trim() : undefined,
|
||||
aiQuestionCount: typeof aiQuestionCountRaw === "string" && aiQuestionCountRaw.trim().length > 0
|
||||
? aiQuestionCountRaw
|
||||
: undefined,
|
||||
aiProviderId: typeof aiProviderIdRaw === "string" && aiProviderIdRaw.trim().length > 0
|
||||
? aiProviderIdRaw
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return invalidFormState<string>(parsed.error)
|
||||
}
|
||||
|
||||
const input = parsed.data
|
||||
if (!rawAiQuestions && !input.aiSourceText) {
|
||||
return failState<string>("Please analyze and preview before creating")
|
||||
}
|
||||
const context = await prepareExamCreateContext({
|
||||
subject: input.subject,
|
||||
grade: input.grade,
|
||||
difficulty: input.difficulty,
|
||||
totalScore: input.totalScore,
|
||||
durationMin: input.durationMin,
|
||||
scheduledAt: input.scheduledAt,
|
||||
})
|
||||
|
||||
const user = await getCurrentUser()
|
||||
const aiDraftResult = await loadAiDraftQuestionsAndStructure({
|
||||
rawAiQuestions,
|
||||
rawStructure,
|
||||
title: input.title,
|
||||
subject: context.subjectName,
|
||||
grade: context.gradeName,
|
||||
difficulty: input.difficulty,
|
||||
totalScore: input.totalScore,
|
||||
durationMin: input.durationMin,
|
||||
aiSourceText: input.aiSourceText,
|
||||
aiQuestionCount: input.aiQuestionCount,
|
||||
aiProviderId: input.aiProviderId,
|
||||
})
|
||||
if (!aiDraftResult.ok) {
|
||||
return failState<string>(aiDraftResult.message)
|
||||
}
|
||||
const { generated, structure } = aiDraftResult
|
||||
|
||||
const questionCount = generated.length
|
||||
const description = context.buildDescription({ questionCount })
|
||||
|
||||
try {
|
||||
await persistAiGeneratedExamDraft({
|
||||
examId: context.examId,
|
||||
title: input.title,
|
||||
creatorId: user?.id ?? "user_teacher_math",
|
||||
subjectId: input.subject,
|
||||
gradeId: input.grade,
|
||||
scheduledAt: context.scheduled,
|
||||
description,
|
||||
structure,
|
||||
generated,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to create exam:", error)
|
||||
return failState<string>("Database error: Failed to create exam")
|
||||
}
|
||||
|
||||
revalidatePath("/teacher/exams/all")
|
||||
|
||||
return successState(context.examId, "Exam created successfully.")
|
||||
}
|
||||
|
||||
export async function previewAiExamAction(
|
||||
prevState: ActionState<AiPreviewData> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<AiPreviewData>> {
|
||||
const aiSourceTextRaw = formData.get("aiSourceText")
|
||||
const aiQuestionCountRaw = formData.get("aiQuestionCount")
|
||||
const aiProviderIdRaw = formData.get("aiProviderId")
|
||||
|
||||
const sourceText = typeof aiSourceTextRaw === "string" ? aiSourceTextRaw.trim() : ""
|
||||
if (!sourceText) {
|
||||
return failState<AiPreviewData>("Please paste the full exam text first", {
|
||||
aiSourceText: ["Please paste the full exam text first"],
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = AiExamPreviewSchema.safeParse({
|
||||
title: getStringValue(formData, "title"),
|
||||
subject: getStringValue(formData, "subject"),
|
||||
grade: getStringValue(formData, "grade"),
|
||||
difficulty: getStringValue(formData, "difficulty"),
|
||||
totalScore: getStringValue(formData, "totalScore"),
|
||||
durationMin: getStringValue(formData, "durationMin"),
|
||||
aiSourceText: sourceText,
|
||||
aiQuestionCount: typeof aiQuestionCountRaw === "string" && aiQuestionCountRaw.trim().length > 0
|
||||
? aiQuestionCountRaw
|
||||
: undefined,
|
||||
aiProviderId: typeof aiProviderIdRaw === "string" && aiProviderIdRaw.trim().length > 0
|
||||
? aiProviderIdRaw
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return invalidFormState<AiPreviewData>(parsed.error)
|
||||
}
|
||||
|
||||
const input = parsed.data
|
||||
const previewRequest = await prepareAiPreviewRequest(input)
|
||||
const aiDraft = await generateAiPreviewData(previewRequest)
|
||||
if (!aiDraft.ok) {
|
||||
return failState<AiPreviewData>(aiDraft.message)
|
||||
}
|
||||
return successState({ ...aiDraft.data, rawOutput: aiDraft.rawOutput })
|
||||
}
|
||||
|
||||
export async function regenerateAiQuestionAction(
|
||||
prevState: ActionState<AiRewriteQuestionData> | null,
|
||||
formData: FormData
|
||||
): Promise<ActionState<AiRewriteQuestionData>> {
|
||||
const parsedInput = parseRegenerateAiQuestionInput(formData)
|
||||
if (!parsedInput.ok) {
|
||||
return parsedInput.state
|
||||
}
|
||||
const { instruction, aiProviderId, sourceText, originalQuestion } = parsedInput
|
||||
|
||||
const originalDifficulty = originalQuestion.difficulty ?? 3
|
||||
const originalScore = originalQuestion.score ?? 0
|
||||
|
||||
try {
|
||||
const result = await regenerateAiQuestionByInstruction({
|
||||
instruction,
|
||||
originalQuestion,
|
||||
sourceText,
|
||||
aiProviderId,
|
||||
})
|
||||
if (!result.ok) {
|
||||
return failState<AiRewriteQuestionData>(result.message)
|
||||
}
|
||||
return successState({
|
||||
type: result.data.type,
|
||||
difficulty: result.data.difficulty ?? originalDifficulty,
|
||||
score: result.data.score ?? originalScore,
|
||||
content: result.data.content,
|
||||
})
|
||||
} catch {
|
||||
return failState<AiRewriteQuestionData>("AI question format invalid")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +519,10 @@ export async function updateExamAction(
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid update data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
return invalidFormState<string>(parsed.error, {
|
||||
fallbackMessage: "Invalid update data",
|
||||
useFirstMessage: false,
|
||||
})
|
||||
}
|
||||
|
||||
const { examId, questions, structure, status } = parsed.data
|
||||
@@ -168,19 +552,12 @@ export async function updateExamAction(
|
||||
}
|
||||
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
message: "Database error: Failed to update exam",
|
||||
}
|
||||
return failState<string>("Database error: Failed to update exam")
|
||||
}
|
||||
|
||||
revalidatePath("/teacher/exams/all")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Exam updated",
|
||||
data: examId,
|
||||
}
|
||||
return successState(examId, "Exam updated")
|
||||
}
|
||||
|
||||
const ExamDeleteSchema = z.object({
|
||||
@@ -196,11 +573,10 @@ export async function deleteExamAction(
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid delete data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
return invalidFormState<string>(parsed.error, {
|
||||
fallbackMessage: "Invalid delete data",
|
||||
useFirstMessage: false,
|
||||
})
|
||||
}
|
||||
|
||||
const { examId } = parsed.data
|
||||
@@ -208,19 +584,12 @@ export async function deleteExamAction(
|
||||
try {
|
||||
await db.delete(exams).where(eq(exams.id, examId))
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
message: "Database error: Failed to delete exam",
|
||||
}
|
||||
return failState<string>("Database error: Failed to delete exam")
|
||||
}
|
||||
|
||||
revalidatePath("/teacher/exams/all")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Exam deleted",
|
||||
data: examId,
|
||||
}
|
||||
return successState(examId, "Exam deleted")
|
||||
}
|
||||
|
||||
const ExamDuplicateSchema = z.object({
|
||||
@@ -236,11 +605,10 @@ export async function duplicateExamAction(
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid duplicate data",
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
return invalidFormState<string>(parsed.error, {
|
||||
fallbackMessage: "Invalid duplicate data",
|
||||
useFirstMessage: false,
|
||||
})
|
||||
}
|
||||
|
||||
const { examId } = parsed.data
|
||||
@@ -255,10 +623,7 @@ export async function duplicateExamAction(
|
||||
})
|
||||
|
||||
if (!source) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Exam not found",
|
||||
}
|
||||
return failState<string>("Exam not found")
|
||||
}
|
||||
|
||||
const newExamId = createId()
|
||||
@@ -289,22 +654,17 @@ export async function duplicateExamAction(
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
message: "Database error: Failed to duplicate exam",
|
||||
}
|
||||
return failState<string>("Database error: Failed to duplicate exam")
|
||||
}
|
||||
|
||||
revalidatePath("/teacher/exams/all")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Exam duplicated",
|
||||
data: newExamId,
|
||||
}
|
||||
return successState(newExamId, "Exam duplicated")
|
||||
}
|
||||
|
||||
export async function getExamPreviewAction(examId: string) {
|
||||
export async function getExamPreviewAction(
|
||||
examId: string
|
||||
): Promise<ActionState<{ structure: unknown; questions: Array<{ id: string }> }>> {
|
||||
try {
|
||||
const exam = await db.query.exams.findFirst({
|
||||
where: eq(exams.id, examId),
|
||||
@@ -319,23 +679,17 @@ export async function getExamPreviewAction(examId: string) {
|
||||
})
|
||||
|
||||
if (!exam) {
|
||||
return { success: false, message: "Exam not found" }
|
||||
return failState<{ structure: unknown; questions: Array<{ id: string }> }>("Exam not found")
|
||||
}
|
||||
|
||||
// Extract questions from the relation
|
||||
const questions = exam.questions.map(eq => eq.question)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
const questions = exam.questions.map((eq) => eq.question)
|
||||
return successState({
|
||||
structure: exam.structure,
|
||||
questions: questions
|
||||
}
|
||||
questions,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return failState<{ structure: unknown; questions: Array<{ id: string }> }>("Failed to load exam preview")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { success: false, message: "Failed to load exam preview" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSubjectsAction(): Promise<ActionState<{ id: string; name: string }[]>> {
|
||||
@@ -344,16 +698,10 @@ export async function getSubjectsAction(): Promise<ActionState<{ id: string; nam
|
||||
orderBy: (subjects, { asc }) => [asc(subjects.order), asc(subjects.name)],
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: allSubjects.map((s) => ({ id: s.id, name: s.name })),
|
||||
}
|
||||
return successState(allSubjects.map((s) => ({ id: s.id, name: s.name })))
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch subjects:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to load subjects",
|
||||
}
|
||||
return failState<{ id: string; name: string }[]>("Failed to load subjects")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,16 +711,10 @@ export async function getGradesAction(): Promise<ActionState<{ id: string; name:
|
||||
orderBy: (grades, { asc }) => [asc(grades.order), asc(grades.name)],
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: allGrades.map((g) => ({ id: g.id, name: g.name })),
|
||||
}
|
||||
return successState(allGrades.map((g) => ({ id: g.id, name: g.name })))
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch grades:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to load grades",
|
||||
}
|
||||
return failState<{ id: string; name: string }[]>("Failed to load grades")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user