Files
encoach_backend_new_v2/frontend/src/pages/student/SubjectRegistrationPage.tsx
Yamen Ahmad f1c4953a63 feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
2026-04-10 17:26:42 +04:00

146 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Loader2 } from "lucide-react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { institutionalExamService } from "@/services/institutional-exam.service";
import { useToast } from "@/hooks/use-toast";
import type { SubjectRegistration } from "@/types/institutional-exam";
const stateBadgeVariant: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
draft: "outline",
confirm: "default",
reject: "destructive",
done: "secondary",
};
export default function SubjectRegistrationPage() {
const { toast } = useToast();
const qc = useQueryClient();
const [selectedSubjects, setSelectedSubjects] = useState<number[]>([]);
const { data: registrationsData, isLoading } = useQuery({
queryKey: ["subject-registrations", "list"],
queryFn: () => institutionalExamService.listSubjectRegistrations(),
});
const registrations = registrationsData?.items ?? [];
const { data: available = [], isLoading: loadingAvailable } = useQuery({
queryKey: ["subject-registrations", "available"],
queryFn: () => institutionalExamService.getAvailableSubjects(),
});
const createMutation = useMutation({
mutationFn: () => institutionalExamService.createSubjectRegistration({
subject_ids: selectedSubjects,
} as Parameters<typeof institutionalExamService.createSubjectRegistration>[0]),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["subject-registrations"] });
toast({ title: "Registration submitted" });
setSelectedSubjects([]);
},
onError: () => toast({ title: "Error", description: "Registration failed", variant: "destructive" }),
});
const toggleSubject = (id: number) => {
setSelectedSubjects(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]);
};
if (isLoading) return <div className="flex items-center justify-center min-h-[400px]"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /></div>;
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Subject Registration</h1>
<p className="text-muted-foreground">Register for subjects within your enrolled courses.</p>
</div>
<Card>
<CardHeader>
<CardTitle>Available Subjects</CardTitle>
<CardDescription>Select the subjects you want to register for this term.</CardDescription>
</CardHeader>
<CardContent>
{loadingAvailable ? (
<div className="flex justify-center py-8"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>
) : available.length > 0 ? (
<div className="space-y-4">
<div className="space-y-2">
{available.map((subj: SubjectRegistration) => (
<label
key={subj.id}
className="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent/50 cursor-pointer"
>
<Checkbox
checked={selectedSubjects.includes(subj.id)}
onCheckedChange={() => toggleSubject(subj.id)}
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{subj.subject_names?.join(", ") || subj.course_name}</p>
<p className="text-xs text-muted-foreground">
{subj.course_name} · {subj.batch_name}
{subj.min_unit_load > 0 && ` · Load: ${subj.min_unit_load}${subj.max_unit_load}`}
</p>
</div>
</label>
))}
</div>
<Button onClick={() => createMutation.mutate()} disabled={createMutation.isPending || selectedSubjects.length === 0}>
{createMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Submit Registration ({selectedSubjects.length} selected)
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">No subjects available for registration.</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>My Registrations</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Course</TableHead>
<TableHead>Batch</TableHead>
<TableHead>Subjects</TableHead>
<TableHead>State</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{registrations.map((reg: SubjectRegistration) => (
<TableRow key={reg.id}>
<TableCell className="font-medium">{reg.course_name}</TableCell>
<TableCell className="text-muted-foreground">{reg.batch_name}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{reg.subject_names.slice(0, 3).map((name, i) => (
<Badge key={i} variant="secondary" className="text-xs">{name}</Badge>
))}
{reg.subject_names.length > 3 && <Badge variant="secondary" className="text-xs">+{reg.subject_names.length - 3}</Badge>}
</div>
</TableCell>
<TableCell>
<Badge variant={stateBadgeVariant[reg.state] ?? "outline"} className="capitalize">{reg.state}</Badge>
</TableCell>
</TableRow>
))}
{registrations.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">No registrations yet.</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}