From e2c61385aed5074b6f6eee3e61a843687e513a63 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 10 Mar 2026 08:12:40 -0700 Subject: [PATCH] speaking section partly integration + more table filters + practice and question pages fixes for real data --- src/api/courses.api.ts | 30 +- src/api/progress.api.ts | 10 +- .../content-management/AddQuestionPage.tsx | 302 ++++- .../PracticeDetailsPage.tsx | 508 ++++---- .../PracticeQuestionsPage.tsx | 1098 +++++++++++++---- .../content-management/QuestionsPage.tsx | 766 +++++++++--- src/pages/content-management/SpeakingPage.tsx | 333 ++++- src/pages/user-management/UserDetailPage.tsx | 34 +- src/pages/user-management/UsersListPage.tsx | 4 +- src/types/course.types.ts | 127 +- src/types/progress.types.ts | 16 + src/types/user.types.ts | 2 + 12 files changed, 2482 insertions(+), 748 deletions(-) diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 98e4bc5..e81a848 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -15,7 +15,6 @@ import type { CreatePracticeRequest, UpdatePracticeRequest, UpdatePracticeStatusRequest, - GetPracticeQuestionsResponse, CreatePracticeQuestionRequest, UpdatePracticeQuestionRequest, GetProgramsResponse, @@ -31,11 +30,17 @@ import type { UpdateModuleRequest, UpdateModuleStatusRequest, GetQuestionSetsResponse, + GetQuestionSetsParams, + GetQuestionSetDetailResponse, + GetQuestionSetQuestionsResponse, CreateQuestionSetRequest, CreateQuestionSetResponse, AddQuestionToSetRequest, CreateQuestionRequest, CreateQuestionResponse, + GetQuestionDetailResponse, + GetQuestionsParams, + GetQuestionsResponse, CreateVimeoVideoRequest, CreateCourseCategoryRequest, GetSubCoursePrerequisitesResponse, @@ -119,7 +124,7 @@ export const deletePractice = (practiceId: number) => // Practice Questions APIs export const getPracticeQuestions = (practiceId: number) => - http.get(`/course-management/practices/${practiceId}/questions`) + http.get(`/question-sets/${practiceId}/questions`) export const createPracticeQuestion = (data: CreatePracticeQuestionRequest) => http.post("/course-management/practice-questions", data) @@ -187,11 +192,20 @@ export const getPracticesByModule = (moduleId: number) => http.get(`/course-management/modules/${moduleId}/practices`) // Question Sets API +export const getQuestionSets = (params?: GetQuestionSetsParams) => + http.get("/question-sets", { params }) + export const getQuestionSetsByOwner = (ownerType: string, ownerId: number) => http.get("/question-sets/by-owner", { params: { owner_type: ownerType, owner_id: ownerId }, }) +export const getQuestionSetById = (questionSetId: number) => + http.get(`/question-sets/${questionSetId}`) + +export const getQuestionSetQuestions = (questionSetId: number) => + http.get(`/question-sets/${questionSetId}/questions`) + export const createQuestionSet = (data: CreateQuestionSetRequest) => http.post("/question-sets", data) @@ -201,6 +215,18 @@ export const addQuestionToSet = (questionSetId: number, data: AddQuestionToSetRe export const createQuestion = (data: CreateQuestionRequest) => http.post("/questions", data) +export const getQuestions = (params: GetQuestionsParams) => + http.get("/questions", { params }) + +export const getQuestionById = (questionId: number) => + http.get(`/questions/${questionId}`) + +export const deleteQuestion = (questionId: number) => + http.delete(`/questions/${questionId}`) + +export const updateQuestion = (questionId: number, data: CreateQuestionRequest) => + http.put(`/questions/${questionId}`, data) + export const deleteQuestionSet = (questionSetId: number) => http.delete(`/question-sets/${questionSetId}`) diff --git a/src/api/progress.api.ts b/src/api/progress.api.ts index 2f78c53..448caa5 100644 --- a/src/api/progress.api.ts +++ b/src/api/progress.api.ts @@ -1,5 +1,13 @@ import http from "./http" -import type { LearnerCourseProgressResponse } from "../types/progress.types" +import type { + LearnerCourseProgressResponse, + LearnerCourseProgressSummaryResponse, +} from "../types/progress.types" export const getAdminLearnerCourseProgress = (userId: number, courseId: number) => http.get(`/admin/users/${userId}/progress/courses/${courseId}`) + +export const getAdminLearnerCourseProgressSummary = (userId: number, courseId: number) => + http.get( + `/admin/users/${userId}/progress/courses/${courseId}/summary`, + ) diff --git a/src/pages/content-management/AddQuestionPage.tsx b/src/pages/content-management/AddQuestionPage.tsx index 85f7251..9f65077 100644 --- a/src/pages/content-management/AddQuestionPage.tsx +++ b/src/pages/content-management/AddQuestionPage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useEffect, useState } from "react" import { useNavigate, useParams } from "react-router-dom" import { ArrowLeft, Plus, X } from "lucide-react" import { toast } from "sonner" @@ -7,30 +7,41 @@ import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/ca import { Input } from "../../components/ui/input" import { Textarea } from "../../components/ui/textarea" import { Select } from "../../components/ui/select" +import { createQuestion, getQuestionById, updateQuestion } from "../../api/courses.api" -type QuestionType = "multiple-choice" | "short-answer" | "true-false" +type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT_ANSWER" | "AUDIO" +type Difficulty = "EASY" | "MEDIUM" | "HARD" +type QuestionStatus = "DRAFT" | "PUBLISHED" | "INACTIVE" interface Question { - id: string + id?: number question: string type: QuestionType options: string[] correctAnswer: string points: number - category?: string - difficulty?: string + difficulty: Difficulty + status: QuestionStatus + tips: string + explanation: string + voicePrompt: string + sampleAnswerVoicePrompt: string + audioCorrectAnswerText: string } -// Mock data for editing -const mockQuestion: Question = { - id: "1", +const initialForm: Question = { question: "", - type: "multiple-choice", + type: "MCQ", options: ["", "", "", ""], correctAnswer: "", - points: 10, - category: "", - difficulty: "", + points: 1, + difficulty: "EASY", + status: "PUBLISHED", + tips: "", + explanation: "", + voicePrompt: "", + sampleAnswerVoicePrompt: "", + audioCorrectAnswerText: "", } export function AddQuestionPage() { @@ -38,36 +49,83 @@ export function AddQuestionPage() { const { id } = useParams<{ id?: string }>() const isEditing = !!id - const [formData, setFormData] = useState( - isEditing - ? mockQuestion // In a real app, fetch the question by id - : { - id: Date.now().toString(), - question: "", - type: "multiple-choice", - options: ["", "", "", ""], - correctAnswer: "", - points: 10, - category: "", - difficulty: "", - }, - ) + const [formData, setFormData] = useState(initialForm) + const [loading, setLoading] = useState(false) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + const loadQuestion = async () => { + if (!isEditing || !id) return + setLoading(true) + try { + const res = await getQuestionById(Number(id)) + const q = res.data.data + const mappedType: QuestionType = + q.question_type === "MCQ" || + q.question_type === "TRUE_FALSE" || + q.question_type === "SHORT_ANSWER" || + q.question_type === "AUDIO" + ? q.question_type + : "MCQ" + const shortAnswer = Array.isArray(q.short_answers) && q.short_answers.length > 0 + ? typeof q.short_answers[0] === "string" + ? String(q.short_answers[0] || "") + : String((q.short_answers[0] as { acceptable_answer?: string }).acceptable_answer || "") + : "" + setFormData({ + id: q.id, + question: q.question_text || "", + type: mappedType, + options: (q.options ?? []) + .slice() + .sort((a, b) => a.option_order - b.option_order) + .map((o) => o.option_text) || ["", "", "", ""], + correctAnswer: + mappedType === "SHORT_ANSWER" + ? shortAnswer + : mappedType === "AUDIO" + ? q.audio_correct_answer_text || "" + : (q.options ?? []).find((o) => o.is_correct)?.option_text || "", + points: q.points ?? 1, + difficulty: + q.difficulty_level === "EASY" || q.difficulty_level === "MEDIUM" || q.difficulty_level === "HARD" + ? q.difficulty_level + : "EASY", + status: + q.status === "DRAFT" || q.status === "PUBLISHED" || q.status === "INACTIVE" + ? q.status + : "PUBLISHED", + tips: q.tips || "", + explanation: q.explanation || "", + voicePrompt: q.voice_prompt || "", + sampleAnswerVoicePrompt: q.sample_answer_voice_prompt || "", + audioCorrectAnswerText: q.audio_correct_answer_text || "", + }) + } catch (error) { + console.error("Failed to load question:", error) + toast.error("Failed to load question details") + } finally { + setLoading(false) + } + } + loadQuestion() + }, [isEditing, id]) const handleTypeChange = (type: QuestionType) => { setFormData((prev) => { - if (type === "true-false") { + if (type === "TRUE_FALSE") { return { ...prev, type, options: ["True", "False"], correctAnswer: prev.correctAnswer === "True" || prev.correctAnswer === "False" ? prev.correctAnswer : "", } - } else if (type === "short-answer") { + } else if (type === "SHORT_ANSWER" || type === "AUDIO") { return { ...prev, type, options: [], - correctAnswer: "", + correctAnswer: type === "AUDIO" ? prev.audioCorrectAnswerText : prev.correctAnswer, } } else { return { @@ -101,7 +159,7 @@ export function AddQuestionPage() { })) } - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() // Validation @@ -112,14 +170,14 @@ export function AddQuestionPage() { return } - if (formData.type === "multiple-choice" || formData.type === "true-false") { + if (formData.type === "MCQ" || formData.type === "TRUE_FALSE") { if (!formData.correctAnswer) { toast.error("Missing correct answer", { description: "Select the correct answer for this question.", }) return } - if (formData.type === "multiple-choice") { + if (formData.type === "MCQ") { const hasEmptyOptions = formData.options.some((opt) => !opt.trim()) if (hasEmptyOptions) { toast.error("Incomplete options", { @@ -128,23 +186,74 @@ export function AddQuestionPage() { return } } - } else if (formData.type === "short-answer") { + } else if (formData.type === "SHORT_ANSWER") { if (!formData.correctAnswer.trim()) { toast.error("Missing correct answer", { description: "Enter the expected correct answer.", }) return } + } else if (formData.type === "AUDIO") { + if (!formData.voicePrompt.trim() || !formData.sampleAnswerVoicePrompt.trim() || !formData.audioCorrectAnswerText.trim()) { + toast.error("Missing audio fields", { + description: "Voice prompt, sample answer voice prompt, and audio correct answer text are required for AUDIO questions.", + }) + return + } } - // In a real app, save the question here - console.log("Saving question:", formData) - toast.success(isEditing ? "Question updated" : "Question created", { - description: isEditing - ? "The question has been updated successfully." - : "Your new question has been created.", - }) - navigate("/content/questions") + setSubmitting(true) + try { + const optionsPayload = + formData.type === "MCQ" || formData.type === "TRUE_FALSE" + ? formData.options + .filter((o) => o.trim()) + .map((optionText, index) => ({ + option_text: optionText.trim(), + option_order: index + 1, + is_correct: optionText === formData.correctAnswer, + })) + : undefined + const shortAnswersPayload = + formData.type === "SHORT_ANSWER" + ? [ + { acceptable_answer: formData.correctAnswer.trim(), match_type: "EXACT" as const }, + { acceptable_answer: formData.correctAnswer.trim(), match_type: "CASE_INSENSITIVE" as const }, + ] + : undefined + const payload = { + question_text: formData.question, + question_type: formData.type, + status: formData.status, + difficulty_level: formData.difficulty, + points: formData.points, + tips: formData.tips || undefined, + explanation: formData.explanation || undefined, + options: optionsPayload, + short_answers: shortAnswersPayload, + voice_prompt: formData.type === "AUDIO" ? formData.voicePrompt : formData.voicePrompt || undefined, + sample_answer_voice_prompt: + formData.type === "AUDIO" ? formData.sampleAnswerVoicePrompt : formData.sampleAnswerVoicePrompt || undefined, + audio_correct_answer_text: + formData.type === "AUDIO" ? formData.audioCorrectAnswerText : undefined, + } + if (isEditing && id) { + await updateQuestion(Number(id), payload) + } else { + await createQuestion(payload) + } + toast.success(isEditing ? "Question updated" : "Question created", { + description: isEditing + ? "The question has been updated successfully." + : "Your new question has been created.", + }) + navigate("/content/questions") + } catch (error) { + console.error("Failed to save question:", error) + toast.error("Failed to save question") + } finally { + setSubmitting(false) + } } return ( @@ -170,6 +279,11 @@ export function AddQuestionPage() {
+ {loading && ( + + Loading question details... + + )}
@@ -185,9 +299,10 @@ export function AddQuestionPage() { value={formData.type} onChange={(e) => handleTypeChange(e.target.value as QuestionType)} > - - - + + + +
@@ -209,7 +324,7 @@ export function AddQuestionPage() { {/* Options for Multiple Choice */} - {(formData.type === "multiple-choice" || formData.type === "true-false") && ( + {(formData.type === "MCQ" || formData.type === "TRUE_FALSE") && (