"use client" import { useMemo, useState } from "react" import { useFormStatus } from "react-dom" import { useRouter } from "next/navigation" import { toast } from "sonner" import { Search } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Button } from "@/shared/components/ui/button" import { Input } from "@/shared/components/ui/input" import { Label } from "@/shared/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" import { ScrollArea } from "@/shared/components/ui/scroll-area" import { Separator } from "@/shared/components/ui/separator" import { Badge } from "@/shared/components/ui/badge" import type { Question } from "@/modules/questions/types" import { updateExamAction } from "@/modules/exams/actions" import { StructureEditor } from "./assembly/structure-editor" import { QuestionBankList } from "./assembly/question-bank-list" import type { ExamNode } from "./assembly/selected-question-list" import { createId } from "@paralleldrive/cuid2" type ExamAssemblyProps = { examId: string title: string subject: string grade: string difficulty: number totalScore: number durationMin: number initialSelected?: Array<{ id: string; score: number }> initialStructure?: ExamNode[] // New prop questionOptions: Question[] } function SubmitButton({ label }: { label: string }) { const { pending } = useFormStatus() return ( ) } export function ExamAssembly(props: ExamAssemblyProps) { const router = useRouter() const [search, setSearch] = useState("") const [typeFilter, setTypeFilter] = useState("all") const [difficultyFilter, setDifficultyFilter] = useState("all") // Initialize structure state const [structure, setStructure] = useState(() => { // Hydrate structure with full question objects const hydrate = (nodes: ExamNode[]): ExamNode[] => { return nodes.map(node => { if (node.type === 'question') { const q = props.questionOptions.find(opt => opt.id === node.questionId) return { ...node, question: q } } if (node.type === 'group') { return { ...node, children: hydrate(node.children || []) } } return node }) } // Use initialStructure if provided (Server generated or DB stored) if (props.initialStructure && props.initialStructure.length > 0) { return hydrate(props.initialStructure) } // Fallback logic removed as Server Component handles initial migration return [] }) const filteredQuestions = useMemo(() => { let list: Question[] = [...props.questionOptions] if (search) { const lower = search.toLowerCase() list = list.filter(q => { const content = q.content as { text?: string } return content.text?.toLowerCase().includes(lower) }) } if (typeFilter !== "all") { list = list.filter((q) => q.type === (typeFilter as Question["type"])) } if (difficultyFilter !== "all") { const d = parseInt(difficultyFilter) list = list.filter((q) => q.difficulty === d) } return list }, [search, typeFilter, difficultyFilter, props.questionOptions]) // Recursively calculate total score const assignedTotal = useMemo(() => { const calc = (nodes: ExamNode[]): number => { return nodes.reduce((sum, node) => { if (node.type === 'question') return sum + (node.score || 0) if (node.type === 'group') return sum + calc(node.children || []) return sum }, 0) } return calc(structure) }, [structure]) const progress = Math.min(100, Math.max(0, (assignedTotal / props.totalScore) * 100)) const handleAdd = (question: Question) => { setStructure(prev => [ ...prev, { id: createId(), type: 'question', questionId: question.id, score: 10, question } ]) } const handleAddGroup = () => { setStructure(prev => [ ...prev, { id: createId(), type: 'group', title: 'New Section', children: [] } ]) } const handleRemove = (id: string) => { const removeRecursive = (nodes: ExamNode[]): ExamNode[] => { return nodes.filter(n => n.id !== id).map(n => { if (n.type === 'group') { return { ...n, children: removeRecursive(n.children || []) } } return n }) } setStructure(prev => removeRecursive(prev)) } const handleScoreChange = (id: string, score: number) => { const updateRecursive = (nodes: ExamNode[]): ExamNode[] => { return nodes.map(n => { if (n.id === id) return { ...n, score } if (n.type === 'group') return { ...n, children: updateRecursive(n.children || []) } return n }) } setStructure(prev => updateRecursive(prev)) } const handleGroupTitleChange = (id: string, title: string) => { const updateRecursive = (nodes: ExamNode[]): ExamNode[] => { return nodes.map(n => { if (n.id === id) return { ...n, title } if (n.type === 'group') return { ...n, children: updateRecursive(n.children || []) } return n }) } setStructure(prev => updateRecursive(prev)) } // Helper to extract flat list for DB examQuestions table const getFlatQuestions = () => { const list: Array<{ id: string; score: number }> = [] const traverse = (nodes: ExamNode[]) => { nodes.forEach(n => { if (n.type === 'question' && n.questionId) { list.push({ id: n.questionId, score: n.score || 0 }) } if (n.type === 'group') { traverse(n.children || []) } }) } traverse(structure) return list } // Helper to strip runtime question objects for DB structure storage const getCleanStructure = () => { const clean = (nodes: ExamNode[]): any[] => { return nodes.map(n => { const { question, ...rest } = n if (n.type === 'group') { return { ...rest, children: clean(n.children || []) } } return rest }) } return clean(structure) } const handleSave = async (formData: FormData) => { formData.set("examId", props.examId) formData.set("questionsJson", JSON.stringify(getFlatQuestions())) formData.set("structureJson", JSON.stringify(getCleanStructure())) const result = await updateExamAction(null, formData) if (result.success) { toast.success("Saved draft") } else { toast.error(result.message || "Save failed") } } const handlePublish = async (formData: FormData) => { formData.set("examId", props.examId) formData.set("questionsJson", JSON.stringify(getFlatQuestions())) formData.set("structureJson", JSON.stringify(getCleanStructure())) formData.set("status", "published") const result = await updateExamAction(null, formData) if (result.success) { toast.success("Published exam") router.push("/teacher/exams/all") } else { toast.error(result.message || "Publish failed") } } return (
{/* Left: Preview (3 cols) */}
Exam Structure
{assignedTotal} / {props.totalScore} Total Score
props.totalScore ? "bg-destructive" : "bg-primary" }`} style={{ width: `${progress}%` }} />
{props.subject}
{props.grade}
Duration: {props.durationMin} min
{/* Right: Question Bank (2 cols) */} Question Bank
setSearch(e.target.value)} />
{ // Check if question is added anywhere in the structure const isAddedRecursive = (nodes: ExamNode[]): boolean => { return nodes.some(n => { if (n.type === 'question' && n.questionId === id) return true if (n.type === 'group' && n.children) return isAddedRecursive(n.children) return false }) } return isAddedRecursive(structure) }} />
) }