import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams, } from "react-router-dom"; import { ArrowLeft, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../../components/ui/button"; import { Stepper } from "../../components/ui/stepper"; import successIcon from "../../assets/success.svg"; import type { QuestionTypeDefinition } from "../../types/questionTypeDefinition.types"; import { getExamPrepPracticeFull, getLearnEnglishPracticeFull, } from "../../api/courses.api"; import { getQuestionTypeDefinitions } from "../../api/questionTypeDefinitions.api"; import { learnEnglishPracticeApiErrorMessage, validateLearnEnglishQuestionsWithDefinitions, } from "../../lib/learnEnglishPracticePublish"; import { executePracticeUpdate } from "../../lib/practiceEditOrchestrator"; import { mapPracticeFullToFormState, unwrapPracticeFullData, type PreservedQuestionSetFields, } from "../../lib/practiceFullMapper"; import { ContextStep } from "./components/practice-steps/ContextStep"; import { ScenarioStep } from "./components/practice-steps/ScenarioStep"; import { PersonaStep } from "./components/practice-steps/PersonaStep"; import { QuestionsStep } from "./components/practice-steps/QuestionsStep"; import { ReviewStep } from "./components/practice-steps/ReviewStep"; import { personaIdNumber } from "./components/practice-steps/constants"; import { useActivePersonas } from "../../hooks/useActivePersonas"; const STEP_LABELS = ["Practice", "Persona", "Questions", "Review"] as const; export function EditPracticeFlow() { const navigate = useNavigate(); const { level, programType, courseId: routeCourseId, unitId: routeUnitId, moduleId: routeModuleId, lessonId: routeLessonId, practiceId: routePracticeId, } = useParams<{ level?: string; programType?: string; courseId?: string; unitId?: string; moduleId?: string; lessonId?: string; practiceId?: string; }>(); const [searchParams] = useSearchParams(); const backToParam = searchParams.get("backTo"); const lessonTitleRaw = searchParams.get("lessonTitle"); const practiceId = routePracticeId ? Number(routePracticeId) : NaN; const validPracticeId = Number.isFinite(practiceId) && practiceId > 0; const isExamPrep = Boolean(programType?.trim()); const lessonId = routeLessonId ?? searchParams.get("lessonId"); const effectiveBackTo = useMemo(() => { if (backToParam?.trim()) return backToParam.trim(); if (routeLessonId) return "lesson"; if (isExamPrep && routeModuleId) return "module"; if (isExamPrep && routeCourseId) return "courses"; if (routeModuleId) return "module"; if (routeCourseId) return "courses"; return null; }, [ backToParam, routeLessonId, isExamPrep, routeModuleId, routeCourseId, ]); const courseId = isExamPrep ? routeCourseId ?? searchParams.get("courseId") : routeCourseId ?? searchParams.get("courseId"); const moduleId = isExamPrep ? routeModuleId ?? searchParams.get("moduleId") : routeModuleId ?? searchParams.get("moduleId"); const unitId = isExamPrep ? routeUnitId : null; const lessonTitleDisplay = (() => { const raw = lessonTitleRaw?.trim(); if (!raw) return null; try { return decodeURIComponent(raw); } catch { return raw; } })(); const isModuleContext = effectiveBackTo === "module"; const isCourseContext = effectiveBackTo === "courses" || effectiveBackTo === "modules"; const isLessonContext = effectiveBackTo === "lesson" || Boolean(routeLessonId); const isLessonPractice = useMemo(() => { const lid = lessonId ? Number(lessonId) : NaN; return Number.isFinite(lid) && lid > 0; }, [lessonId]); const isLearnEnglishLessonPractice = isLessonPractice && !isExamPrep; const parentSummary = useMemo(() => { if (lessonId) return `Lesson #${lessonId}${lessonTitleDisplay ? ` — ${lessonTitleDisplay}` : ""}`; if (isModuleContext && moduleId) return `Module #${moduleId}`; if (isCourseContext && courseId) return `Course #${courseId}`; return null; }, [ lessonId, lessonTitleDisplay, isModuleContext, isCourseContext, moduleId, courseId, ]); const programLabel = isExamPrep ? programType === "skill" ? "Skill-Based Courses" : "English Proficiency Exams" : level ? `Program ${level}` : null; const backLabel = effectiveBackTo === "lesson" ? "Back to lesson practices" : effectiveBackTo === "module" ? "Back to Module" : effectiveBackTo === "modules" ? "Back to Modules" : effectiveBackTo === "courses" ? "Back to Course" : isExamPrep ? "Back to Program" : "Back to Courses"; const backPath = useMemo(() => { if (isExamPrep) { if (routeLessonId && programType && courseId && unitId && moduleId) { const title = lessonTitleRaw ? `?lessonTitle=${encodeURIComponent(lessonTitleRaw)}` : ""; return `/new-content/courses/${programType}/${courseId}/${unitId}/${moduleId}/lessons/${routeLessonId}/practices${title}`; } if ( effectiveBackTo === "module" && programType && courseId && unitId && moduleId ) { return `/new-content/courses/${programType}/${courseId}/${unitId}/${moduleId}`; } if (effectiveBackTo === "courses" && programType && courseId) { return `/new-content/courses/${programType}/${courseId}`; } if (programType) return `/new-content/courses/${programType}`; return "/new-content"; } if (routeLessonId && level && courseId && moduleId) { const title = lessonTitleRaw ? `?lessonTitle=${encodeURIComponent(lessonTitleRaw)}` : ""; return `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}/lessons/${routeLessonId}/practices${title}`; } if (effectiveBackTo === "module" && level && courseId && moduleId) { return `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}`; } if (effectiveBackTo === "courses" && level && courseId) { return `/new-content/learn-english/${level}/courses/${courseId}`; } if (level) return `/new-content/learn-english/${level}/courses`; return "/new-content"; }, [ isExamPrep, routeLessonId, programType, courseId, unitId, moduleId, lessonTitleRaw, effectiveBackTo, level, ]); const [currentStep, setCurrentStep] = useState(1); const [isSaved, setIsSaved] = useState(false); const [submitting, setSubmitting] = useState(false); const [loadingPractice, setLoadingPractice] = useState(true); const [loadError, setLoadError] = useState(null); const [selectedPersona, setSelectedPersona] = useState(null); const [preservedQuestionSet, setPreservedQuestionSet] = useState({ timeLimitMinutes: null, passingScore: null, introVideoUrl: "", status: "PUBLISHED", }); const { personas, loading: personasLoading, error: personasError, reload: reloadPersonas, } = useActivePersonas(); const [formData, setFormData] = useState({ title: "", description: "", storyImageUrl: "", shuffleQuestions: false, tips: "", questions: [ { id: "q1", displayOrder: 1, serverQuestionId: null as number | null, questionTypeDefinitionId: null as number | null, text: "", difficultyLevel: "EASY" as "EASY" | "MEDIUM" | "HARD", points: 1, dynamicFieldValues: {} as Record, mcqOptions: [ { text: "", isCorrect: true }, { text: "", isCorrect: false }, { text: "", isCorrect: false }, { text: "", isCorrect: false }, ], trueFalseCorrect: true, shortAnswers: [""], }, ], }); const [typeDefinitions, setTypeDefinitions] = useState( [], ); const [definitionsLoading, setDefinitionsLoading] = useState(true); const [definitionsError, setDefinitionsError] = useState(null); useEffect(() => { let cancelled = false; (async () => { setDefinitionsLoading(true); setDefinitionsError(null); try { const { definitions: list } = await getQuestionTypeDefinitions({ include_system: true, status: "ACTIVE", }); if (!cancelled) setTypeDefinitions(list); } catch (e) { if (!cancelled) { setDefinitionsError(learnEnglishPracticeApiErrorMessage(e)); setTypeDefinitions([]); } } finally { if (!cancelled) setDefinitionsLoading(false); } })(); return () => { cancelled = true; }; }, []); useEffect(() => { if (!validPracticeId || definitionsLoading) return; let cancelled = false; (async () => { setLoadingPractice(true); setLoadError(null); try { const res = isExamPrep ? await getExamPrepPracticeFull(practiceId) : await getLearnEnglishPracticeFull(practiceId); const full = unwrapPracticeFullData(res); if (!full) throw new Error("Practice details were missing from the response."); const mapped = mapPracticeFullToFormState(full, typeDefinitions); if (cancelled) return; setFormData(mapped.formData); setPreservedQuestionSet(mapped.preservedQuestionSet); if (mapped.personaId != null) { setSelectedPersona(String(mapped.personaId)); } } catch (e) { if (!cancelled) { setLoadError(learnEnglishPracticeApiErrorMessage(e)); } } finally { if (!cancelled) setLoadingPractice(false); } })(); return () => { cancelled = true; }; }, [validPracticeId, practiceId, isExamPrep, typeDefinitions, definitionsLoading]); const submitPractice = async (status: "DRAFT" | "PUBLISHED") => { if (!validPracticeId) { toast.error("Invalid practice", { description: "Missing practice id in the URL." }); return; } if ( !isLearnEnglishLessonPractice && (!formData.title.trim() || !formData.description.trim()) ) { toast.error("Title and story description are required", { description: "Complete the first step before saving.", }); return; } if (!selectedPersona) { toast.error("Select a persona", { description: "Choose a character on the Persona step before saving.", }); return; } const personaId = personaIdNumber(selectedPersona); if (!personaId) { toast.error("Invalid persona", { description: "Re-select a persona from the list and try again.", }); return; } const mappedQuestions = formData.questions.map((q, index) => ({ questionText: String(q.text ?? "").trim(), questionTypeDefinitionId: Number(q.questionTypeDefinitionId), difficultyLevel: (q.difficultyLevel ?? "EASY") as "EASY" | "MEDIUM" | "HARD", points: Number.isFinite(Number(q.points)) ? Number(q.points) : 1, displayOrder: Number.isFinite(Number(q.displayOrder)) && Number(q.displayOrder) > 0 ? Number(q.displayOrder) : index + 1, serverQuestionId: q.serverQuestionId ?? null, dynamicFieldValues: { ...(q.dynamicFieldValues ?? {}) }, mcqOptions: (q.mcqOptions ?? []).map( (o: { text?: string; isCorrect?: boolean }) => ({ option_text: String(o.text ?? ""), is_correct: Boolean(o.isCorrect), }), ), trueFalseAnswerIsTrue: q.trueFalseCorrect !== false, shortAnswers: (q.shortAnswers ?? []).map((s: string) => String(s)), })); const validationMsg = validateLearnEnglishQuestionsWithDefinitions( mappedQuestions, typeDefinitions, ); if (validationMsg) { toast.error("Check your questions", { description: validationMsg }); return; } const lessonDefaultTitle = lessonTitleDisplay?.trim() || (lessonId ? `Lesson ${lessonId} practice` : "Lesson practice"); setSubmitting(true); try { await executePracticeUpdate({ practiceId, isExamPrep, status, formData, personaId, preservedQuestionSet: { ...preservedQuestionSet, status, }, questions: mappedQuestions, definitions: typeDefinitions, isLearnEnglishLessonPractice, lessonDefaultTitle, }); toast.success( status === "PUBLISHED" ? "Practice updated and published" : "Practice saved as draft", ); setIsSaved(true); } catch (e) { toast.error("Could not update practice", { description: learnEnglishPracticeApiErrorMessage(e), }); } finally { setSubmitting(false); } }; const nextStep = () => setCurrentStep((prev) => Math.min(prev + 1, STEP_LABELS.length)); const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 1)); if (!validPracticeId) { return (

Invalid practice link

); } if (loadingPractice || definitionsLoading) { return (

Loading practice details…

); } if (loadError) { return (

Could not load practice

{loadError}

); } if (isSaved) { return (
Success

Practice Updated Successfully!

Your changes to this practice have been saved.

); } const renderStep = () => { const useContextStep = isModuleContext || isCourseContext || isLessonContext; if (useContextStep) { switch (currentStep) { case 1: return ( navigate(backPath)} isLessonPractice={isLearnEnglishLessonPractice} lessonTitle={lessonTitleDisplay} parentSummary={parentSummary} /> ); case 2: return ( void reloadPersonas()} selectedPersona={selectedPersona} setSelectedPersona={setSelectedPersona} nextStep={nextStep} prevStep={prevStep} /> ); case 3: return ( ); case 4: return ( setCurrentStep(1)} onEditQuestions={() => setCurrentStep(3)} parentSummary={parentSummary} typeDefinitions={typeDefinitions} canPublish submitting={submitting} onSaveDraft={() => void submitPractice("DRAFT")} onPublish={() => void submitPractice("PUBLISHED")} publishLabel="Publish Updates" publishingLabel="Publishing updates…" /> ); default: return null; } } switch (currentStep) { case 1: return ( ); case 2: return ( void reloadPersonas()} selectedPersona={selectedPersona} setSelectedPersona={setSelectedPersona} nextStep={nextStep} prevStep={prevStep} /> ); case 3: return ( ); case 4: return ( setCurrentStep(1)} onEditQuestions={() => setCurrentStep(3)} parentSummary={parentSummary} typeDefinitions={typeDefinitions} canPublish submitting={submitting} onSaveDraft={() => void submitPractice("DRAFT")} onPublish={() => void submitPractice("PUBLISHED")} publishLabel="Publish Updates" publishingLabel="Publishing updates…" /> ); default: return null; } }; return (
{backLabel}

Edit Practice

Update story details, persona, and questions for practice #{practiceId}.

{renderStep()}
); }