Files
NextEdu/src/modules/exams/actions.ts
SpecialX 4f0ef217a0 refactor(modules): update existing module implementations across attendance, audit, auth, classes, course-plans, exams, files, homework, layout, proctoring, questions, scheduling, textbooks, users
- Update attendance components and data-access for record management

- Update audit log views, filters, and data-access

- Update auth login and register forms

- Update classes actions, components, and data-access (admin, schedule, stats)

- Update course-plans actions, form, list, progress, and schema

- Update exams actions, AI pipeline, preview components, and hooks

- Update files components (icon, list, preview, upload) and data-access

- Update homework assignment form, review view, auto-save hook, and stats-service

- Update layout sidebar, header, and navigation config

- Update proctoring actions, anti-cheat monitor, and data-access

- Update questions actions, components (dialog, actions, columns, filters), and data-access

- Update scheduling actions, auto-scheduler, components, and schema

- Update textbooks constants and text-selection hook

- Update users class-registration, import-dialog, data-access, and user-service
2026-06-23 17:38:56 +08:00

854 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server"
import { revalidatePath } from "next/cache"
import type { ActionState } from "@/shared/types/action-state"
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { z } from "zod"
import { createId } from "@paralleldrive/cuid2"
import {
handleActionError,
safeJsonParse,
} from "@/shared/lib/action-utils"
import { trackExamEvent } from "@/shared/lib/track-event"
import {
buildExamDescription,
deleteExamById,
duplicateExam,
getExamCreatorId,
getExamGrades,
getExamPreview,
getExamSubjects,
persistAiGeneratedExamDraft,
persistExamDraft,
resolveSubjectGradeNames,
updateExamWithQuestions,
type ExamModeConfig,
} 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),
subject: z.string().min(1),
grade: z.string().min(1),
difficulty: z.coerce.number().int().min(1).max(5),
totalScore: z.coerce.number().int().min(1),
durationMin: z.coerce.number().int().min(1),
scheduledAt: z.string().optional().nullable(),
questions: z
.array(
z.object({
id: z.string(),
score: z.coerce.number().int().min(0),
})
)
.optional(),
})
const getStringValue = (formData: FormData, key: string) => {
const value = formData.get(key)
return typeof value === "string" ? value : undefined
}
const getBoolValue = (formData: FormData, key: string, fallback = false): boolean => {
const value = formData.get(key)
if (typeof value !== "string") return fallback
return value === "true"
}
const parseExamModeConfig = (formData: FormData): ExamModeConfig => {
const rawMode = getStringValue(formData, "examMode")
const examMode: ExamModeConfig["examMode"] =
rawMode === "timed" || rawMode === "proctored" ? rawMode : "homework"
const rawDuration = getStringValue(formData, "durationMinutes")
const durationMinutes = rawDuration && Number.isFinite(Number(rawDuration))
? Number(rawDuration)
: null
const rawGrace = getStringValue(formData, "lateStartGraceMinutes") ?? "0"
const parsedGrace = Number(rawGrace)
const lateStartGraceMinutes = Number.isFinite(parsedGrace) ? parsedGrace : 0
return {
examMode,
durationMinutes,
shuffleQuestions: getBoolValue(formData, "shuffleQuestions", false),
allowLateStart: getBoolValue(formData, "allowLateStart", false),
lateStartGraceMinutes,
antiCheatEnabled: getBoolValue(formData, "antiCheatEnabled", false),
}
}
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: AiGeneratedQuestion[] = validated.data.map((q) => ({
id: q.id,
type: q.type,
difficulty: q.difficulty,
score: q.score,
content: {
text: q.content.text,
...(q.content.options
? {
options: q.content.options.map((opt) => ({
id: opt.id,
text: opt.text,
isCorrect: opt.isCorrect ?? false,
})),
}
: {}),
...(q.content.subQuestions ? { subQuestions: q.content.subQuestions } : {}),
},
}))
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
): Promise<ActionState<string>> {
try {
const ctx = await requirePermission(Permissions.EXAM_CREATE)
const rawQuestionsValue = formData.get("questionsJson")
const rawQuestions = typeof rawQuestionsValue === "string" ? rawQuestionsValue : null
const parsed = ExamCreateSchema.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 ? safeJsonParse(rawQuestions, "题目数据格式无效") : [],
})
if (!parsed.success) {
return invalidFormState<string>(parsed.error, { useFirstMessage: false })
}
const input = parsed.data
const context = await prepareExamCreateContext({
subject: input.subject,
grade: input.grade,
difficulty: input.difficulty,
totalScore: input.totalScore,
durationMin: input.durationMin,
scheduledAt: input.scheduledAt,
})
const description = context.buildDescription()
try {
await persistExamDraft({
examId: context.examId,
title: input.title,
creatorId: ctx.userId,
subjectId: input.subject,
gradeId: input.grade,
scheduledAt: context.scheduled,
description,
examModeConfig: parseExamModeConfig(formData),
})
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<string>("Database error: Failed to create exam")
}
revalidatePath("/teacher/exams/all")
return successState(context.examId, "Exam created successfully.")
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<string>(error.message)
}
return handleActionError(error)
}
}
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>> {
try {
const ctx = await requirePermission(Permissions.EXAM_AI_GENERATE)
const rawQuestionsValue = formData.get("questionsJson")
const rawQuestions = typeof rawQuestionsValue === "string" ? rawQuestionsValue : null
const rawAiQuestionsValue = formData.get("aiQuestionsJson")
const rawAiQuestions = typeof rawAiQuestionsValue === "string" ? rawAiQuestionsValue : null
const rawStructureValue = formData.get("structureJson")
const rawStructure = typeof rawStructureValue === "string" ? rawStructureValue : 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 ? safeJsonParse(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 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: ctx.userId,
subjectId: input.subject,
gradeId: input.grade,
scheduledAt: context.scheduled,
description,
structure,
generated,
examModeConfig: parseExamModeConfig(formData),
})
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<string>("Database error: Failed to create exam")
}
revalidatePath("/teacher/exams/all")
// V3-4: 埋点监控AI 生成考试)
await trackExamEvent("exam.ai_generated", {
userId: ctx.userId,
targetId: context.examId,
properties: {
aiSourceText: input.aiSourceText?.length ?? 0,
aiQuestionCount: input.aiQuestionCount,
},
})
return successState(context.examId, "Exam created successfully.")
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<string>(error.message)
}
return handleActionError(error)
}
}
export async function previewAiExamAction(
prevState: ActionState<AiPreviewData> | null,
formData: FormData
): Promise<ActionState<AiPreviewData>> {
try {
await requirePermission(Permissions.EXAM_AI_GENERATE)
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 })
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<AiPreviewData>(error.message)
}
return handleActionError(error)
}
}
export async function regenerateAiQuestionAction(
prevState: ActionState<AiRewriteQuestionData> | null,
formData: FormData
): Promise<ActionState<AiRewriteQuestionData>> {
try {
await requirePermission(Permissions.EXAM_AI_GENERATE)
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 (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<AiRewriteQuestionData>("AI question format invalid")
}
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<AiRewriteQuestionData>(error.message)
}
return handleActionError(error)
}
}
const ExamUpdateSchema = z.object({
examId: z.string().min(1),
questions: z
.array(
z.object({
id: z.string(),
score: z.coerce.number().int().min(0),
})
)
.optional(),
structure: z.unknown().optional(),
status: z.enum(["draft", "published", "archived"]).optional(),
})
export async function updateExamAction(
prevState: ActionState<string> | null,
formData: FormData
): Promise<ActionState<string>> {
try {
const ctx = await requirePermission(Permissions.EXAM_UPDATE)
const rawQuestions = formData.get("questionsJson")
const rawStructure = formData.get("structureJson")
const rawQuestionsStr = typeof rawQuestions === "string" ? rawQuestions : null
const rawStructureStr = typeof rawStructure === "string" ? rawStructure : null
const parsed = ExamUpdateSchema.safeParse({
examId: formData.get("examId"),
questions: rawQuestionsStr ? safeJsonParse(rawQuestionsStr, "题目数据格式无效") : undefined,
structure: rawStructureStr ? safeJsonParse(rawStructureStr, "试卷结构数据格式无效") : undefined,
status: formData.get("status") ?? undefined,
})
if (!parsed.success) {
return invalidFormState<string>(parsed.error, {
fallbackMessage: "Invalid update data",
useFirstMessage: false,
})
}
const { examId, questions, structure, status } = parsed.data
// Ownership check: non-admin users can only update their own exams
if (ctx.dataScope.type !== "all") {
const creatorId = await getExamCreatorId(examId)
if (!creatorId || creatorId !== ctx.userId) {
return failState<string>("You can only update exams you created")
}
}
try {
await updateExamWithQuestions(examId, {
questions: questions ?? undefined,
structure,
status,
})
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<string>("Database error: Failed to update exam")
}
revalidatePath("/teacher/exams/all")
// V3-4: 埋点监控
await trackExamEvent("exam.updated", {
userId: ctx.userId,
targetId: examId,
properties: { hasQuestions: !!questions, hasStructure: !!structure, status },
})
return successState(examId, "Exam updated")
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<string>(error.message)
}
return handleActionError(error)
}
}
const ExamDeleteSchema = z.object({
examId: z.string().min(1),
})
export async function deleteExamAction(
prevState: ActionState<string> | null,
formData: FormData
): Promise<ActionState<string>> {
try {
const ctx = await requirePermission(Permissions.EXAM_DELETE)
const parsed = ExamDeleteSchema.safeParse({
examId: formData.get("examId"),
})
if (!parsed.success) {
return invalidFormState<string>(parsed.error, {
fallbackMessage: "Invalid delete data",
useFirstMessage: false,
})
}
const { examId } = parsed.data
// Ownership check: non-admin users can only delete their own exams
if (ctx.dataScope.type !== "all") {
const creatorId = await getExamCreatorId(examId)
if (!creatorId || creatorId !== ctx.userId) {
return failState<string>("You can only delete exams you created")
}
}
try {
await deleteExamById(examId)
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<string>("Database error: Failed to delete exam")
}
revalidatePath("/teacher/exams/all")
// V3-4: 埋点监控
await trackExamEvent("exam.deleted", {
userId: ctx.userId,
targetId: examId,
})
return successState(examId, "Exam deleted")
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<string>(error.message)
}
return handleActionError(error)
}
}
const ExamDuplicateSchema = z.object({
examId: z.string().min(1),
})
export async function duplicateExamAction(
prevState: ActionState<string> | null,
formData: FormData
): Promise<ActionState<string>> {
try {
const ctx = await requirePermission(Permissions.EXAM_DUPLICATE)
const parsed = ExamDuplicateSchema.safeParse({
examId: formData.get("examId"),
})
if (!parsed.success) {
return invalidFormState<string>(parsed.error, {
fallbackMessage: "Invalid duplicate data",
useFirstMessage: false,
})
}
const { examId } = parsed.data
let newExamId: string
try {
const duplicatedId = await duplicateExam(examId, ctx.userId)
if (!duplicatedId) {
return failState<string>("Exam not found")
}
newExamId = duplicatedId
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<string>("Database error: Failed to duplicate exam")
}
revalidatePath("/teacher/exams/all")
// V3-4: 埋点监控
await trackExamEvent("exam.duplicated", {
userId: ctx.userId,
targetId: newExamId,
properties: { sourceExamId: examId },
})
return successState(newExamId, "Exam duplicated")
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<string>(error.message)
}
return handleActionError(error)
}
}
export async function getExamPreviewAction(
examId: string
): Promise<ActionState<{ structure: unknown; questions: Array<{ id: string }> }>> {
try {
await requirePermission(Permissions.EXAM_READ)
try {
const exam = await getExamPreview(examId)
if (!exam) {
return failState<{ structure: unknown; questions: Array<{ id: string }> }>("Exam not found")
}
return successState({
structure: exam.structure,
questions: exam.questions,
})
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<{ structure: unknown; questions: Array<{ id: string }> }>("Failed to load exam preview")
}
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<{ structure: unknown; questions: Array<{ id: string }> }>(error.message)
}
return handleActionError(error)
}
}
export async function getSubjectsAction(): Promise<ActionState<{ id: string; name: string }[]>> {
try {
await requirePermission(Permissions.EXAM_READ)
try {
const allSubjects = await getExamSubjects()
return successState(allSubjects)
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<{ id: string; name: string }[]>("Failed to load subjects")
}
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<{ id: string; name: string }[]>(error.message)
}
return handleActionError(error)
}
}
export async function getGradesAction(): Promise<ActionState<{ id: string; name: string }[]>> {
try {
await requirePermission(Permissions.EXAM_READ)
try {
const allGrades = await getExamGrades()
return successState(allGrades)
} catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error))
return failState<{ id: string; name: string }[]>("Failed to load grades")
}
} catch (error) {
if (error instanceof PermissionDeniedError) {
return failState<{ id: string; name: string }[]>(error.message)
}
return handleActionError(error)
}
}