Yimaru-Admin/src/lib/learnEnglishPracticePublish.ts

165 lines
5.7 KiB
TypeScript

import type { AxiosError } from "axios"
import {
addQuestionToSet,
createExamPrepLessonPractice,
createParentLinkedPractice,
createQuestion,
createQuestionSet,
} from "../api/courses.api"
import type { PracticeParentKind } from "../types/course.types"
import type { QuestionTypeDefinition } from "../types/questionTypeDefinition.types"
import {
buildCreateQuestionFromDefinition,
questionRowHasContent,
validateDefinitionQuestion,
type LearnEnglishDefinitionQuestionInput,
} from "./learnEnglishDefinitionQuestion"
export type { LearnEnglishDefinitionQuestionInput } from "./learnEnglishDefinitionQuestion"
export function learnEnglishPracticeApiErrorMessage(err: unknown): string {
const ax = err as AxiosError<{ message?: string; error?: string }>
const data = ax.response?.data
if (data && typeof data === "object") {
const m = data.message ?? data.error
if (typeof m === "string" && m.trim()) return m.trim()
}
if (err instanceof Error && err.message) return err.message
return "Request failed"
}
export function validateLearnEnglishQuestionsWithDefinitions(
questions: LearnEnglishDefinitionQuestionInput[],
definitions: QuestionTypeDefinition[],
): string | null {
const byId = new Map(definitions.map((d) => [d.id, d]))
const filled = questions.filter((q) => {
const def = byId.get(q.questionTypeDefinitionId)
return def ? questionRowHasContent(q, def) : false
})
if (filled.length === 0) return "Add at least one question with content."
for (let i = 0; i < filled.length; i++) {
const q = filled[i]
if (!Number.isFinite(q.questionTypeDefinitionId) || q.questionTypeDefinitionId <= 0) {
return `Question ${i + 1}: select a question type from the list.`
}
const def = byId.get(q.questionTypeDefinitionId)
if (!def) {
return `Question ${i + 1}: type definition #${q.questionTypeDefinitionId} was not found. Refresh and try again.`
}
const err = validateDefinitionQuestion(def, q, i + 1)
if (err) return err
}
return null
}
/**
* Learn English parent-linked practice: create PRACTICE question set,
* create questions from GET /questions/type-definitions entries, attach them, POST /practices.
*/
export async function executeLearnEnglishPracticeCreation(opts: {
parentKind: PracticeParentKind
parentId: number
status: "DRAFT" | "PUBLISHED"
questionSetTitle: string
questionSetDescription?: string | null
shuffleQuestions: boolean
practiceTitle: string
storyDescription: string
storyImage: string
quickTips: string
personaName?: string | null
/** Selected persona from step 2 — sent as `persona_id` on POST /practices. */
personaId: number
questions: LearnEnglishDefinitionQuestionInput[]
definitions: QuestionTypeDefinition[]
/** When set, links practice via POST /exam-prep/lessons/:id/practices instead of POST /practices. */
examPrepLessonId?: number
}): Promise<{ questionSetId: number; practiceId: number }> {
const err = validateLearnEnglishQuestionsWithDefinitions(
opts.questions,
opts.definitions,
)
if (err) throw new Error(err)
if (!Number.isFinite(opts.personaId) || opts.personaId < 1) {
throw new Error("persona_id is required. Select a persona before saving.")
}
const byId = new Map(opts.definitions.map((d) => [d.id, d]))
const setRes = await createQuestionSet({
title: opts.questionSetTitle.trim() || "Practice question set",
description: opts.questionSetDescription?.trim() || null,
set_type: "PRACTICE",
owner_type: opts.parentKind,
owner_id: opts.parentId,
shuffle_questions: opts.shuffleQuestions,
status: opts.status,
...(opts.personaName?.trim() ? { persona: opts.personaName.trim() } : {}),
})
const setId = setRes.data?.data?.id
if (!setId) {
throw new Error(
(setRes.data as { message?: string } | undefined)?.message ??
"Could not create question set",
)
}
const toCreate = opts.questions.filter((q) => {
const def = byId.get(q.questionTypeDefinitionId)
return def ? questionRowHasContent(q, def) : false
})
let displayOrder = 0
for (const q of toCreate) {
const def = byId.get(q.questionTypeDefinitionId)
if (!def) throw new Error(`Missing definition #${q.questionTypeDefinitionId}`)
displayOrder += 1
const payload = buildCreateQuestionFromDefinition(def, q, opts.status)
const qRes = await createQuestion(payload)
const questionId = qRes.data?.data?.id
if (!questionId) {
throw new Error(
(qRes.data as { message?: string } | undefined)?.message ??
"Could not create question",
)
}
await addQuestionToSet(setId, {
question_id: questionId,
display_order: displayOrder,
})
}
const practiceRes = opts.examPrepLessonId
? await createExamPrepLessonPractice(opts.examPrepLessonId, {
title: opts.practiceTitle.trim(),
story_description: opts.storyDescription.trim(),
story_image: opts.storyImage.trim(),
persona_id: opts.personaId,
question_set_id: setId,
quick_tips: opts.quickTips.trim(),
})
: await createParentLinkedPractice({
parent_kind: opts.parentKind,
parent_id: opts.parentId,
title: opts.practiceTitle.trim(),
story_description: opts.storyDescription.trim(),
story_image: opts.storyImage.trim(),
question_set_id: setId,
quick_tips: opts.quickTips.trim(),
publish_status: opts.status,
persona_id: opts.personaId,
})
const practiceId = practiceRes.data?.data?.id
if (!practiceId) {
throw new Error(
(practiceRes.data as { message?: string } | undefined)?.message ??
"Could not create practice",
)
}
return { questionSetId: setId, practiceId }
}