258 lines
11 KiB
TypeScript
258 lines
11 KiB
TypeScript
"use client"
|
|
|
|
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 } from "lucide-react"
|
|
import DatePicker from "react-datepicker"
|
|
import "react-datepicker/dist/react-datepicker.css"
|
|
|
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
|
|
|
export default function AdminTestsPage() {
|
|
const [tests, setTests] = useState<any[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [uploading, setUploading] = useState(false)
|
|
const { userId } = useActiveUser()
|
|
// Upload Form State
|
|
const [title, setTitle] = useState("")
|
|
const [examPattern, setExamPattern] = useState("JEE_Main")
|
|
const [timeLimit, setTimeLimit] = useState("180")
|
|
const [scheduledDate, setScheduledDate] = useState<Date | null>(null)
|
|
const [endDate, setEndDate] = useState<Date | null>(null)
|
|
const [questionsJson, setQuestionsJson] = useState("")
|
|
|
|
const fetchTests = async () => {
|
|
try {
|
|
const res = await fetch("http://localhost:8000/api/admin/tests")
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setTests(data)
|
|
}
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchTests()
|
|
}, [])
|
|
|
|
const handleToggle = async (testId: string) => {
|
|
try {
|
|
const res = await fetch(`http://localhost:8000/api/admin/tests/${testId}/toggle`, {
|
|
method: "PUT"
|
|
})
|
|
if (res.ok) {
|
|
fetchTests()
|
|
}
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
const handleUpload = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!questionsJson) return
|
|
if (!userId) return
|
|
|
|
setUploading(true)
|
|
|
|
let parsedQuestions = [];
|
|
try {
|
|
parsedQuestions = JSON.parse(questionsJson);
|
|
} catch(err) {
|
|
alert("Invalid JSON format. Please paste a valid JSON array.");
|
|
setUploading(false);
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
title,
|
|
exam_pattern: examPattern,
|
|
time_limit: parseInt(timeLimit),
|
|
admin_id: userId,
|
|
questions_json: parsedQuestions,
|
|
scheduled_date: scheduledDate ? scheduledDate.toISOString() : null,
|
|
end_date: endDate ? endDate.toISOString() : null,
|
|
};
|
|
|
|
try {
|
|
const res = await fetch("http://localhost:8000/api/admin/tests/create_manual", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
if (res.ok) {
|
|
alert("Test created successfully!")
|
|
setTitle("")
|
|
setQuestionsJson("")
|
|
fetchTests()
|
|
} else {
|
|
alert("Creation failed")
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
alert("Creation failed")
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Tests & DPPs</h1>
|
|
<p className="text-muted-foreground mt-2">Manage CBT examinations and daily practice sheets</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
{/* Upload Form */}
|
|
<Card className="lg:col-span-1 border-muted bg-card shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Upload className="w-5 h-5 text-primary" />
|
|
Create Manual Test
|
|
</CardTitle>
|
|
<CardDescription>Paste the structured JSON block of questions in LaTeX format to create a deterministic test.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleUpload} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Test Title</label>
|
|
<Input required value={title} onChange={e => setTitle(e.target.value)} placeholder="e.g. JEE Main Mock Test 1" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Exam Pattern</label>
|
|
<select
|
|
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 disabled:cursor-not-allowed disabled:opacity-50"
|
|
value={examPattern}
|
|
onChange={(e) => setExamPattern(e.target.value)}
|
|
>
|
|
<option value="JEE_Main">JEE Main</option>
|
|
<option value="JEE_Advanced">JEE Advanced</option>
|
|
<option value="NEET">NEET</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Time Limit (mins)</label>
|
|
<Input required type="number" value={timeLimit} onChange={e => setTimeLimit(e.target.value)} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2 flex flex-col">
|
|
<label className="text-sm font-medium">Scheduled Start (Optional)</label>
|
|
<DatePicker
|
|
selected={scheduledDate}
|
|
onChange={(date: Date | null) => 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="Select start date & time"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2 flex flex-col">
|
|
<label className="text-sm font-medium">Scheduled End (Optional)</label>
|
|
<DatePicker
|
|
selected={endDate}
|
|
onChange={(date: Date | null) => 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="Select end date & time"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Questions JSON (LaTeX Format)</label>
|
|
<textarea
|
|
required
|
|
value={questionsJson}
|
|
onChange={e => setQuestionsJson(e.target.value)}
|
|
className="w-full min-h-[250px] p-3 text-sm font-mono border rounded-md bg-background focus:ring-primary focus:border-primary"
|
|
placeholder='[{"section": "Physics", "text_en": "Question text...", "options_en": ["A", "B", "C", "D"], "correct_answer": "A"}]'
|
|
/>
|
|
<p className="text-xs text-muted-foreground mt-1">Paste your questions as a JSON array.</p>
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full" disabled={uploading}>
|
|
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />}
|
|
{uploading ? "Creating Test..." : "Create Test from JSON"}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Existing Tests List */}
|
|
<Card className="lg:col-span-2 border-muted bg-card shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<FileText className="w-5 h-5 text-primary" />
|
|
Test Repository
|
|
</CardTitle>
|
|
<CardDescription>Manage visibility and schedule for parsed tests.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="flex justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /></div>
|
|
) : tests.length === 0 ? (
|
|
<div className="text-center p-8 text-muted-foreground border border-dashed rounded-lg">
|
|
No tests uploaded yet.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{tests.map(test => (
|
|
<div key={test.id} className="flex items-center justify-between p-4 border rounded-lg bg-background">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold">{test.title}</h3>
|
|
<Badge variant="outline">{test.exam_pattern.replace("_", " ")}</Badge>
|
|
<Badge variant={test.is_active ? "default" : "secondary"}>
|
|
{test.is_active ? "Active" : "Inactive / Processing"}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{test.total_time} mins
|
|
</div>
|
|
{test.scheduled_date && (
|
|
<div className="flex items-center gap-1">
|
|
<CalendarIcon className="w-3 h-3" />
|
|
{new Date(test.scheduled_date).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium">{test.is_active ? "Published" : "Hidden"}</span>
|
|
<Switch checked={test.is_active} onCheckedChange={() => handleToggle(test.id)} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|