import { useState, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Search, Plus, Trash2, Loader2, Calendar, Users, Clock, CheckCircle2, Archive, AlertTriangle, PlayCircle, Eye } from "lucide-react"; import { assignmentsService } from "@/services/assignments.service"; import { api } from "@/lib/api-client"; import { useBatches, useStudents } from "@/hooks/queries"; import { useToast } from "@/hooks/use-toast"; import type { ExamSchedule, ScheduleState, ExamScheduleCreateRequest } from "@/types"; interface CustomExam { id: number; title: string; status: string; } interface Entity { id: number; name: string; } const STATE_TABS: { key: ScheduleState | "all"; label: string; icon: React.ReactNode }[] = [ { key: "all", label: "All", icon: }, { key: "active", label: "Active", icon: }, { key: "planned", label: "Planned", icon: }, { key: "past", label: "Past", icon: }, { key: "start_expired", label: "Start Expired", icon: }, { key: "archived", label: "Archived", icon: }, ]; const OPTION_FIELDS: { key: keyof ExamScheduleCreateRequest; label: string }[] = [ { key: "full_length", label: "Full length exams" }, { key: "generate_different", label: "Generate different exams" }, { key: "auto_release_results", label: "Auto release results" }, { key: "auto_start", label: "Auto start exam" }, { key: "official_exam", label: "Official Exam" }, { key: "hide_assignee_details", label: "Hide Assignees Details from Teachers" }, ]; const stateBadgeVariant = (s: ScheduleState): "default" | "secondary" | "outline" | "destructive" => { switch (s) { case "active": return "default"; case "planned": return "secondary"; case "past": return "outline"; case "start_expired": return "destructive"; case "archived": return "outline"; default: return "secondary"; } }; type FormState = { name: string; exam_id: string; entity_id: string; start_date: string; start_time: string; end_date: string; end_time: string; assign_mode: "entity" | "batch" | "individual"; batch_ids: Set; student_ids: Set; full_length: boolean; generate_different: boolean; auto_release_results: boolean; auto_start: boolean; official_exam: boolean; hide_assignee_details: boolean; }; const _iso = (d: Date) => { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; }; const emptyForm = (): FormState => { const today = new Date(); const inOneWeek = new Date(); inOneWeek.setDate(today.getDate() + 7); return { name: "", exam_id: "", entity_id: "", start_date: _iso(today), start_time: "09:00", end_date: _iso(inOneWeek), end_time: "17:00", assign_mode: "batch", batch_ids: new Set(), student_ids: new Set(), full_length: true, generate_different: false, auto_release_results: false, auto_start: false, official_exam: false, hide_assignee_details: false, }; }; export default function AssignmentsPage() { const [search, setSearch] = useState(""); const [stateFilter, setStateFilter] = useState("all"); const [createOpen, setCreateOpen] = useState(false); const [form, setForm] = useState(emptyForm()); const [studentQuery, setStudentQuery] = useState(""); const { toast } = useToast(); const qc = useQueryClient(); const schedulesQ = useQuery({ queryKey: ["exam-schedules", stateFilter], queryFn: () => assignmentsService.listSchedules( stateFilter === "all" ? {} : { state: stateFilter } ), }); const schedules = schedulesQ.data?.items ?? []; const customExamsQ = useQuery({ queryKey: ["custom-exams-for-assign"], queryFn: () => api.get<{ items: CustomExam[]; total: number }>("/exam/custom/list?per_page=200"), }); const publishedExams = (customExamsQ.data?.items ?? []).filter( (e) => e.status === "published" || e.status === "draft" ); const entitiesQ = useQuery({ queryKey: ["entities-for-assign"], queryFn: () => api.get<{ items: Entity[] }>("/entities"), }); const entities = entitiesQ.data?.items ?? []; const batchesQ = useBatches({ size: 200 }); const batches = batchesQ.data?.items ?? []; const studentsQ = useStudents({ size: 200 }); const students = studentsQ.data?.items ?? []; const filteredStudents = useMemo(() => { const q = studentQuery.trim().toLowerCase(); if (!q) return students; return students.filter((s) => (s.name || "").toLowerCase().includes(q) || (s.email || "").toLowerCase().includes(q), ); }, [students, studentQuery]); const stateCounts = useMemo(() => { const all = schedulesQ.data?.items ?? []; const counts: Record = { all: all.length }; for (const s of all) { counts[s.state] = (counts[s.state] || 0) + 1; } return counts; }, [schedulesQ.data]); const allSchedulesQ = useQuery({ queryKey: ["exam-schedules", "all"], queryFn: () => assignmentsService.listSchedules({}), enabled: stateFilter !== "all", }); const totalCounts = useMemo(() => { const items = stateFilter === "all" ? schedules : (allSchedulesQ.data?.items ?? []); const counts: Record = { all: items.length }; for (const s of items) { counts[s.state] = (counts[s.state] || 0) + 1; } return counts; }, [stateFilter, schedules, allSchedulesQ.data]); const createMut = useMutation({ mutationFn: (data: ExamScheduleCreateRequest) => assignmentsService.createSchedule(data), onSuccess: () => { qc.invalidateQueries({ queryKey: ["exam-schedules"] }); setCreateOpen(false); setForm(emptyForm()); toast({ title: "Exam scheduled successfully" }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Error", description: err.message }), }); const deleteMut = useMutation({ mutationFn: (id: number) => assignmentsService.deleteSchedule(id), onSuccess: () => { qc.invalidateQueries({ queryKey: ["exam-schedules"] }); toast({ title: "Schedule deleted" }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Error", description: err.message }), }); const archiveMut = useMutation({ mutationFn: (id: number) => assignmentsService.archiveSchedule(id), onSuccess: () => { qc.invalidateQueries({ queryKey: ["exam-schedules"] }); toast({ title: "Schedule archived" }); }, }); const q = search.toLowerCase(); const filtered = schedules.filter( (s) => s.name.toLowerCase().includes(q) || s.exam_title.toLowerCase().includes(q) ); function handleCreate() { const startDt = form.start_date && form.start_time ? `${form.start_date}T${form.start_time}:00` : ""; const endDt = form.end_date && form.end_time ? `${form.end_date}T${form.end_time}:00` : ""; if (!form.name || !form.exam_id || !startDt || !endDt) { toast({ variant: "destructive", title: "Missing fields", description: "Please fill in name, exam, start date/time, and end date/time." }); return; } createMut.mutate({ name: form.name, exam_id: Number(form.exam_id), entity_id: form.entity_id ? Number(form.entity_id) : undefined, start_date: startDt, end_date: endDt, assign_mode: form.assign_mode, batch_ids: [...form.batch_ids], student_ids: [...form.student_ids], full_length: form.full_length, generate_different: form.generate_different, auto_release_results: form.auto_release_results, auto_start: form.auto_start, official_exam: form.official_exam, hide_assignee_details: form.hide_assignee_details, }); } const updateF = (patch: Partial) => setForm((p) => ({ ...p, ...patch })); return (

Assignments

Schedule exams and assign them to entities, classes, or students.

{/* State tabs */}
{STATE_TABS.map((tab) => ( ))}
setSearch(e.target.value)} />
{schedulesQ.isLoading ? (
) : filtered.length === 0 ? (

No exam schedules found

Create a new schedule to assign exams to students.

) : ( Schedule Name Exam Entity Start End State Assignees Completed Actions {filtered.map((s) => ( {s.name} {s.exam_title} {s.entity_name || "—"} {s.start_date ? new Date(s.start_date).toLocaleString() : "—"} {s.end_date ? new Date(s.end_date).toLocaleString() : "—"} {s.state.replace("_", " ")}
{s.assignee_count}
{s.completed_count}
{s.state !== "archived" && ( )}
))}
)} {/* Create Schedule Dialog */} Schedule Exam Select an exam, assign it to students, and set scheduling options.
{/* Basic info */}
updateF({ name: e.target.value })} placeholder="e.g. Q2 IELTS Exam" />
{/* Date/Time */}
updateF({ start_date: e.target.value })} />
updateF({ start_time: e.target.value })} />
updateF({ end_date: e.target.value })} />
updateF({ end_time: e.target.value })} />
{/* Options checkboxes */}
{OPTION_FIELDS.map((opt) => ( ))}
{/* Assignment target */}
{/* Batch selection */} {form.assign_mode === "batch" && (
{batches.map((b) => ( ))} {batches.length === 0 &&

No classes available

}
)} {/* Individual student selection */} {form.assign_mode === "individual" && (
{form.student_ids.size} selected · {students.length} total
{/* Search box — needed because QA reported long student lists made bottom entries unreachable inside the inner scroll container. Filtering keeps the list short and predictable. */} setStudentQuery(e.target.value)} className="h-8 text-xs" />
{filteredStudents.map((s) => ( ))} {filteredStudents.length === 0 && students.length > 0 && (

No students match “{studentQuery}”.

)} {students.length === 0 &&

No students available

}
)}
); }