"use client" import { fetchWithAuth } from "@/lib/apiClient"; import { useState, useEffect, useRef } from "react" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Switch } from "@/components/ui/switch" import { Input } from "@/components/ui/input" import { Loader2, Upload, FileText, Clock, CalendarIcon, CheckCircle2, AlertCircle, X, Trash2 } from "lucide-react" import DatePicker from "react-datepicker" import "react-datepicker/dist/react-datepicker.css" import { z } from "zod" import { useActiveUser } from "@/hooks/useActiveUser" // Strict Zod schema for client-side validation const QuestionSchema = z.object({ id: z.string().optional(), section: z.string().default("General"), subject: z.string(), topic: z.string(), subtopic: z.string(), difficulty: z.string(), type: z.enum(["MCQ_Single", "MCQ_Multiple", "Numerical"]), question_text: z.string(), options: z.record(z.string(), z.any()).nullable().optional(), correct_answer: z.object({ value: z.any() }), solution_text: z.string(), language: z.enum(["English", "Hindi", "Bilingual"]) }).passthrough(); const TestSchema = z.array(QuestionSchema); export default function AdminTestsPage() { const [tests, setTests] = useState([]) const [loading, setLoading] = useState(true) const [uploading, setUploading] = useState(false) const { userId } = useActiveUser() // Upload Form State const [title, setTitle] = useState("") const [testType, setTestType] = useState("CBT") const [examPattern, setExamPattern] = useState("JEE_Main") const [timeLimit, setTimeLimit] = useState("180") const [scheduledDate, setScheduledDate] = useState(null) const [endDate, setEndDate] = useState(null) // Drag and Drop State const [isDragging, setIsDragging] = useState(false) const [validQuestions, setValidQuestions] = useState[] | null>(null) const [validationErrors, setValidationErrors] = useState<{ path: string, message: string }[]>([]) const fileInputRef = useRef(null) const fetchTests = async () => { try { const res = await fetchWithAuth("/api/admin/tests") if (res.ok) { const data = await res.json() setTests(data) } } catch (e) { console.error(e) } finally { setLoading(false) } } useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetchTests() }, []) const handleToggle = async (testId: string) => { try { const res = await fetchWithAuth(`/api/admin/tests/${testId}/toggle`, { method: "PUT" }) if (res.ok) { fetchTests() } } catch (e) { console.error(e) } } const handleDelete = async (testId: string) => { if (!confirm("Are you sure you want to delete this test and all its questions? This action cannot be undone.")) return try { const res = await fetchWithAuth(`/api/admin/tests/${testId}`, { method: "DELETE" }) if (res.ok) { alert("Test deleted successfully!") fetchTests() } else { alert("Failed to delete test.") } } catch (e) { console.error(e) alert("Error deleting test") } } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { processFile(e.dataTransfer.files[0]) } } const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { processFile(e.target.files[0]) } } const processFile = (file: File) => { if (!file.name.endsWith('.json')) { setValidationErrors([{ path: "File", message: "Only .json files are allowed." }]) setValidQuestions(null) return } const reader = new FileReader() reader.onload = (e) => { try { const json = JSON.parse(e.target?.result as string) // Zod validation const result = TestSchema.safeParse(json) if (result.success) { setValidQuestions(result.data) setValidationErrors([]) } else { setValidQuestions(null) const errors = (result as any).error.errors.map((err: any) => ({ path: err.path.join('.'), message: err.message })) setValidationErrors(errors) } } catch { setValidQuestions(null) setValidationErrors([{ path: "JSON Parsing", message: "Invalid JSON format. Check for syntax errors." }]) } } reader.readAsText(file) } const removeFile = () => { setValidQuestions(null) setValidationErrors([]) if (fileInputRef.current) { fileInputRef.current.value = "" } } const handleUpload = async (e: React.FormEvent) => { e.preventDefault() if (testType === 'DPP_PDF') { const file = fileInputRef.current?.files?.[0] if (!file) { alert("Please select a PDF file to upload to Minio.") return } if (!userId) return setUploading(true) try { const formData = new FormData() formData.append("file", file) formData.append("title", title) formData.append("exam_pattern", examPattern) formData.append("time_limit", timeLimit) formData.append("admin_id", userId as string) if (scheduledDate) formData.append("scheduled_date", scheduledDate.toISOString()) const res = await fetchWithAuth("/api/admin/tests/upload", { method: "POST", body: formData }) if (res.ok) { alert("PDF uploaded successfully to Minio and connected!") setTitle("") removeFile() fetchTests() } else { const err = await res.json() alert(`Upload failed: ${JSON.stringify(err)}`) } } catch (err) { alert("Upload failed") } finally { setUploading(false) } return } if (!validQuestions || validQuestions.length === 0) { alert("Please upload a valid JSON file with questions first.") return } if (!userId) return setUploading(true) const payload = { title, type: testType, exam_pattern: examPattern, time_limit: parseInt(timeLimit), admin_id: userId, questions_json: validQuestions, scheduled_date: scheduledDate ? scheduledDate.toISOString() : null, end_date: endDate ? endDate.toISOString() : null, }; try { const res = await fetchWithAuth("/api/admin/tests/create_manual", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) if (res.ok) { alert("Test created successfully!") setTitle("") removeFile() fetchTests() } else { const err = await res.json() alert(`Creation failed: ${JSON.stringify(err)}`) } } catch (error) { console.error(error) alert("Creation failed") } finally { setUploading(false) } } return (

Tests & DPPs

Manage CBT examinations and daily practice sheets

{/* Upload Form */} Create Manual Test Upload a structured JSON array of questions in LaTeX format to create a deterministic test.
setTitle(e.target.value)} placeholder="e.g. JEE Main Mock Test 1" />
{/* TEST TYPE — KEY DIFFERENTIATOR */}

{testType === 'CBT' && '✅ Purely for CBT. Appears under Dashboard → Mock Tests.'} {testType === 'PREP_PILOT' && '✅ Purely for CBT. Appears under AI Coach → PrepPilot AI.'} {testType === 'DPP_PDF' && '✅ Uploads a PDF to Minio storage for Class/Plan aligned DPP.'}

setTimeLimit(e.target.value)} />
setScheduledDate(date)} showTimeSelect timeFormat="HH:mm" timeIntervals={15} timeCaption="time" dateFormat="MMMM d, yyyy h:mm aa" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" placeholderText="Optional" />
setEndDate(date)} showTimeSelect timeFormat="HH:mm" timeIntervals={15} timeCaption="time" dateFormat="MMMM d, yyyy h:mm aa" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" placeholderText="Optional" />
{testType === 'DPP_PDF' ? (
{ const file = e.target.files?.[0] if(file) setValidationErrors([]) }} />
) : validQuestions ? (
{validQuestions.length} questions validated
) : (

Drag and drop your .json file here

or click to browse files

)} {validationErrors.length > 0 && (
Validation Errors Found
    {validationErrors.slice(0, 50).map((err, i) => (
  • {err.path} {err.message}
  • ))} {validationErrors.length > 50 && (
  • ...and {validationErrors.length - 50} more errors
  • )}
)}
{/* Existing Tests List */} Test Repository Manage visibility and schedule for parsed tests. {loading ? (
) : tests.length === 0 ? (
No tests uploaded yet.
) : (
{tests.map(test => (

{test.title}

{test.exam_pattern.replace("_", " ")} {test.is_active ? "Active" : "Inactive / Processing"}
{test.total_time} mins
{test.scheduled_date && (
{new Date(test.scheduled_date).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}
)}
{test.is_active ? "Published" : "Hidden"} handleToggle(test.id)} />
))}
)}
) }