import { useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import {
AlertTriangle,
ArrowLeft,
CheckCircle2,
Sparkles,
XCircle,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/sonner";
import {
useApproveExamReview,
useExamReviewDetail,
useRejectExamReview,
} from "@/hooks/queries/useExamReview";
import type { ExamReviewQuestion } from "@/types/exam-review";
const FAILING_THRESHOLD = 0.7;
function QuestionCard({ q, index }: { q: ExamReviewQuestion; index: number }) {
const failing = (q.quality_score ?? 0) < FAILING_THRESHOLD;
return (
Q{index + 1}
·
{q.skill}
{q.question_type}
{q.difficulty}
{q.ai_generated && (
AI
)}
{q.stem.length > 220 ? `${q.stem.slice(0, 220)}…` : q.stem}
= 0.85
? "default"
: failing
? "destructive"
: "secondary"
}
className="tabular-nums shrink-0"
>
{((q.quality_score ?? 0) * 100).toFixed(0)}%
{q.quality_report && Object.keys(q.quality_report).length > 0 && (
{JSON.stringify(q.quality_report, null, 2)}
)}
);
}
export default function ExamReviewDetail() {
const { examId } = useParams<{ examId: string }>();
const navigate = useNavigate();
const parsedId = examId ? Number(examId) : undefined;
const { data: exam, isLoading } = useExamReviewDetail(parsedId);
const approve = useApproveExamReview();
const reject = useRejectExamReview();
const [approveNotes, setApproveNotes] = useState("");
const [rejectNotes, setRejectNotes] = useState("");
const [approveOpen, setApproveOpen] = useState(false);
const [rejectOpen, setRejectOpen] = useState(false);
if (isLoading || !exam || !parsedId) {
return (
);
}
const handleApprove = async () => {
try {
await approve.mutateAsync({ examId: parsedId, notes: approveNotes });
toast.success("Exam approved and published.");
setApproveOpen(false);
navigate("/admin/exam/review-queue");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Approve failed");
}
};
const handleReject = async () => {
if (!rejectNotes.trim()) {
toast.error("Rejection notes are required.");
return;
}
try {
await reject.mutateAsync({ examId: parsedId, notes: rejectNotes });
toast.success("Exam rejected and returned to draft.");
setRejectOpen(false);
navigate("/admin/exam/review-queue");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Reject failed");
}
};
const summary = exam.summary;
const alreadyReviewed = exam.status !== "pending_review";
return (
{exam.title}
{exam.status.replace("_", " ")}
{exam.subject_name && · {exam.subject_name}}
{exam.teacher_name && · Created by {exam.teacher_name}}
{!alreadyReviewed && (
)}
{alreadyReviewed && exam.reviewed_by_name && (
Previously reviewed
{exam.reviewed_by_name}
{exam.reviewed_at ? ` · ${new Date(exam.reviewed_at).toLocaleString()}` : ""}
{exam.review_notes && (
{exam.review_notes}
)}
)}
Quality summary
Aggregated across every question in this exam.
Questions
{summary.question_count}
Average quality
{(summary.avg_quality_score * 100).toFixed(0)}%
Lowest score
{(summary.min_quality_score * 100).toFixed(0)}%
{exam.sections.length === 0 ? (
This exam has no sections yet.
) : (
exam.sections.map((section) => (
{section.title}
{section.skill || "general"}
· {section.questions.length} questions
{section.questions.map((q, idx) => (
))}
))
)}
);
}