import { useCallback, useEffect, useMemo, useState } from "react" import { Link } from "react-router-dom" import { Plus, Search, Edit, Trash2, HelpCircle, X } from "lucide-react" import { Button } from "../../components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" import { Input } from "../../components/ui/input" import { Select } from "../../components/ui/select" import { Textarea } from "../../components/ui/textarea" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table" import { Badge } from "../../components/ui/badge" import { deleteQuestion, getQuestionById, getQuestions, updateQuestion } from "../../api/courses.api" import type { QuestionDetail } from "../../types/course.types" type QuestionTypeFilter = "all" | "MCQ" | "TRUE_FALSE" | "SHORT_ANSWER" | "AUDIO" type DifficultyFilter = "all" | "EASY" | "MEDIUM" | "HARD" type StatusFilter = "all" | "DRAFT" | "PUBLISHED" | "INACTIVE" type QuestionTypeEdit = "MCQ" | "TRUE_FALSE" | "SHORT_ANSWER" | "AUDIO" interface EditOption { option_text: string option_order: number is_correct: boolean } const typeLabels: Record = { MCQ: "Multiple Choice", TRUE_FALSE: "True/False", SHORT_ANSWER: "Short Answer", SHORT: "Short Answer", AUDIO: "Audio", } const typeColors: Record = { MCQ: "bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-200", TRUE_FALSE: "bg-brand-100 text-brand-600 ring-1 ring-inset ring-brand-200", SHORT_ANSWER: "bg-mint-100 text-green-700 ring-1 ring-inset ring-green-200", SHORT: "bg-mint-100 text-green-700 ring-1 ring-inset ring-green-200", AUDIO: "bg-purple-100 text-purple-700 ring-1 ring-inset ring-purple-200", } export function QuestionsPage() { const [questions, setQuestions] = useState([]) const [loading, setLoading] = useState(false) const [deleting, setDeleting] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [typeFilter, setTypeFilter] = useState("all") const [difficultyFilter, setDifficultyFilter] = useState("all") const [statusFilter, setStatusFilter] = useState("all") const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [selectedIds, setSelectedIds] = useState([]) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [pendingDeleteIds, setPendingDeleteIds] = useState([]) const [detailsOpen, setDetailsOpen] = useState(false) const [editOpen, setEditOpen] = useState(false) const [activeQuestionId, setActiveQuestionId] = useState(null) const [detailLoading, setDetailLoading] = useState(false) const [detailData, setDetailData] = useState(null) const [savingEdit, setSavingEdit] = useState(false) const [editQuestionText, setEditQuestionText] = useState("") const [editQuestionType, setEditQuestionType] = useState("MCQ") const [editDifficulty, setEditDifficulty] = useState("EASY") const [editPoints, setEditPoints] = useState(1) const [editStatus, setEditStatus] = useState("PUBLISHED") const [editTips, setEditTips] = useState("") const [editExplanation, setEditExplanation] = useState("") const [editVoicePrompt, setEditVoicePrompt] = useState("") const [editSampleAnswerVoicePrompt, setEditSampleAnswerVoicePrompt] = useState("") const [editShortAnswer, setEditShortAnswer] = useState("") const [editOptions, setEditOptions] = useState([ { option_text: "", option_order: 1, is_correct: true }, { option_text: "", option_order: 2, is_correct: false }, ]) const fetchQuestions = useCallback(async () => { setLoading(true) try { const batchSize = 100 let nextOffset = 0 let allRows: QuestionDetail[] = [] let expectedTotal = Number.POSITIVE_INFINITY while (allRows.length < expectedTotal) { const res = await getQuestions({ question_type: typeFilter === "all" ? undefined : typeFilter, difficulty: difficultyFilter === "all" ? undefined : difficultyFilter, status: statusFilter === "all" ? undefined : statusFilter, limit: batchSize, offset: nextOffset, }) const payload = res.data?.data as unknown const meta = res.data?.metadata as { total_count?: number } | null | undefined let chunk: QuestionDetail[] = [] let chunkTotal: number | undefined if (Array.isArray(payload)) { chunk = payload as QuestionDetail[] chunkTotal = meta?.total_count } else if ( payload && typeof payload === "object" && Array.isArray((payload as { questions?: unknown[] }).questions) ) { const data = payload as { questions: QuestionDetail[]; total_count?: number } chunk = data.questions chunkTotal = data.total_count ?? meta?.total_count } allRows = [...allRows, ...chunk] if (typeof chunkTotal === "number" && Number.isFinite(chunkTotal)) { expectedTotal = chunkTotal } if (chunk.length < batchSize) break nextOffset += chunk.length } setQuestions(allRows) } catch (error) { console.error("Failed to fetch questions:", error) setQuestions([]) } finally { setLoading(false) } }, [typeFilter, difficultyFilter, statusFilter]) useEffect(() => { fetchQuestions() }, [fetchQuestions]) useEffect(() => { setPage(1) setSelectedIds([]) }, [searchQuery, pageSize, typeFilter, difficultyFilter, statusFilter]) const filteredQuestions = useMemo(() => { if (!searchQuery.trim()) return questions return questions.filter((q) => q.question_text.toLowerCase().includes(searchQuery.toLowerCase()), ) }, [questions, searchQuery]) const paginatedQuestions = useMemo(() => { const start = (page - 1) * pageSize return filteredQuestions.slice(start, start + pageSize) }, [filteredQuestions, page, pageSize]) const handleDeleteRequest = (ids: number[]) => { setPendingDeleteIds(ids) setDeleteDialogOpen(true) } const handleDeleteConfirm = async () => { if (pendingDeleteIds.length === 0) return setDeleting(true) try { await Promise.all(pendingDeleteIds.map((id) => deleteQuestion(id))) setDeleteDialogOpen(false) setPendingDeleteIds([]) setSelectedIds((prev) => prev.filter((id) => !pendingDeleteIds.includes(id))) await fetchQuestions() } catch (error) { console.error("Failed to delete question(s):", error) } finally { setDeleting(false) } } const openDetails = async (id: number) => { setDetailsOpen(true) setDetailLoading(true) setDetailData(null) try { const res = await getQuestionById(id) setDetailData(res.data.data) } catch (error) { console.error("Failed to fetch question details:", error) } finally { setDetailLoading(false) } } const openEdit = async (id: number) => { setEditOpen(true) setDetailLoading(true) setActiveQuestionId(id) try { const res = await getQuestionById(id) const q = res.data.data setDetailData(q) setEditQuestionText(q.question_text || "") setEditQuestionType((q.question_type as QuestionTypeEdit) || "MCQ") setEditDifficulty((q.difficulty_level as string) || "EASY") setEditPoints(q.points ?? 1) setEditStatus(q.status || "PUBLISHED") setEditTips(q.tips || "") setEditExplanation(q.explanation || "") setEditVoicePrompt(q.voice_prompt || "") setEditSampleAnswerVoicePrompt(q.sample_answer_voice_prompt || "") const incomingShort = 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 || "") : "" setEditShortAnswer(incomingShort) const mappedOptions = (q.options ?? []) .slice() .sort((a, b) => a.option_order - b.option_order) .map((opt) => ({ option_text: opt.option_text, option_order: opt.option_order, is_correct: opt.is_correct, })) || [] setEditOptions( mappedOptions.length > 0 ? mappedOptions : [ { option_text: "", option_order: 1, is_correct: true }, { option_text: "", option_order: 2, is_correct: false }, ], ) } catch (error) { console.error("Failed to fetch question for edit:", error) } finally { setDetailLoading(false) } } const saveEdit = async () => { if (!activeQuestionId) return setSavingEdit(true) try { const normalizedOptions = editOptions .filter((o) => o.option_text.trim()) .map((o, idx) => ({ option_text: o.option_text.trim(), option_order: idx + 1, is_correct: o.is_correct, })) await updateQuestion(activeQuestionId, { question_text: editQuestionText, question_type: editQuestionType, difficulty_level: editDifficulty, points: editPoints, status: editStatus, tips: editTips || undefined, explanation: editExplanation || undefined, voice_prompt: editVoicePrompt || undefined, sample_answer_voice_prompt: editSampleAnswerVoicePrompt || undefined, options: editQuestionType === "SHORT_ANSWER" ? undefined : normalizedOptions, short_answers: editQuestionType === "SHORT_ANSWER" ? [ { acceptable_answer: editShortAnswer, match_type: "EXACT" }, { acceptable_answer: editShortAnswer, match_type: "CASE_INSENSITIVE" }, ] : undefined, }) setEditOpen(false) await fetchQuestions() } catch (error) { console.error("Failed to update question:", error) } finally { setSavingEdit(false) } } const toggleOne = (id: number) => { setSelectedIds((prev) => prev.includes(id) ? prev.filter((selectedId) => selectedId !== id) : [...prev, id], ) } const currentPageIds = paginatedQuestions.map((q) => q.id) const isAllCurrentPageSelected = currentPageIds.length > 0 && currentPageIds.every((id) => selectedIds.includes(id)) const toggleSelectAllCurrentPage = () => { setSelectedIds((prev) => { if (isAllCurrentPageSelected) { return prev.filter((id) => !currentPageIds.includes(id)) } const merged = new Set([...prev, ...currentPageIds]) return Array.from(merged) }) } const totalCount = filteredQuestions.length const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)) const canGoPrev = page > 1 const canGoNext = page < totalPages return (
{/* Page Header */}

Questions

Create and manage your question bank

Question Management {/* Search and Filters */}
setSearchQuery(e.target.value)} className="pl-10 transition-colors focus:border-brand-300 focus:ring-brand-200" />
{/* Results count */}
Showing {paginatedQuestions.length} of {totalCount} questions
{/* Questions Table */} {loading ? (

Loading questions...

) : filteredQuestions.length > 0 ? (
Question Type Difficulty Status Points Actions {paginatedQuestions.map((question, index) => ( openDetails(question.id)} className={`cursor-pointer transition-colors hover:bg-brand-100/30 ${ index % 2 === 0 ? "bg-white" : "bg-grayScale-100/50" }`} > e.stopPropagation()} onChange={() => toggleOne(question.id)} aria-label={`Select question ${question.id}`} />
{question.question_text}
{question.question_type === "MCQ" && (question.options?.length ?? 0) > 0 && (
Options: {question.options?.map((opt) => opt.option_text).join(", ")}
)}
{typeLabels[question.question_type] || question.question_type} {question.difficulty_level && ( {question.difficulty_level} )} {question.status || "—"} {question.points ?? 0}
))}
) : (

No questions found

Try adjusting your search or filter criteria to find what you're looking for.

)}

Page {page} of {totalPages}

{deleteDialogOpen && (

Delete {pendingDeleteIds.length > 1 ? "Questions" : "Question"}

Are you sure you want to delete{" "} {pendingDeleteIds.length} question{pendingDeleteIds.length > 1 ? "s" : ""} ? This action cannot be undone.

)} {detailsOpen && (

Question Details

{detailLoading || !detailData ? (

Loading details...

) : ( <>

Question

{detailData.question_text}

Type: {typeLabels[detailData.question_type] || detailData.question_type}

Difficulty: {detailData.difficulty_level || "—"}

Points: {detailData.points ?? 0}

Status: {detailData.status || "—"}

{(detailData.options ?? []).length > 0 && (

Options

{(detailData.options ?? []) .slice() .sort((a, b) => a.option_order - b.option_order) .map((opt) => (
{opt.option_order}. {opt.option_text}
))}
)} )}
)} {editOpen && (

Edit Question