React 18 + TypeScript + Vite 5 + Tailwind CSS + shadcn/ui frontend: - Multi-role auth: student, teacher, admin, corporate, agent, developer - Student portal: dashboard, courses, adaptive learning, exams, placement - Teacher portal: courses, assignments, grading, attendance, AI workbench - Admin portal: 60+ management pages (LMS, exams, users, entities, AI) - AI-powered features: study coach, generation page, gap analysis, TTS - Exam system: session, auto-save, results, grading queue - Full IELTS workflow: reading/listening/writing/speaking modules Made-with: Cursor
98 lines
6.3 KiB
TypeScript
98 lines
6.3 KiB
TypeScript
import { useCallback } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { useCourses, useStudents } from "@/hooks/queries";
|
|
import { Download } from "lucide-react";
|
|
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
|
|
import AiReportNarrative from "@/components/ai/AiReportNarrative";
|
|
|
|
export default function AdminReports() {
|
|
const { data: coursesData, isLoading: lc } = useCourses();
|
|
const { data: studentsData, isLoading: ls } = useStudents({ size: 500 });
|
|
const courses = coursesData?.items ?? [];
|
|
const students = studentsData?.items ?? [];
|
|
|
|
const enrollmentByCourse = courses.map(c => ({
|
|
name: c.title.length > 12 ? c.title.slice(0, 12) + "…" : c.title,
|
|
enrolled: c.enrolled ?? 0,
|
|
capacity: c.max_capacity ?? 0,
|
|
}));
|
|
|
|
const capacityByCourse = courses.filter(c => c.status === "active").map(c => {
|
|
const pct = c.max_capacity ? Math.round((c.enrolled / c.max_capacity) * 100) : 0;
|
|
return { name: c.title.length > 12 ? c.title.slice(0, 12) + "…" : c.title, fillRate: pct };
|
|
});
|
|
|
|
const statusPie = [
|
|
{ name: "Active", value: students.filter((s) => s.status === "active").length, color: "hsl(142, 71%, 45%)" },
|
|
{ name: "Inactive", value: students.filter((s) => s.status !== "active").length, color: "hsl(38, 92%, 50%)" },
|
|
].filter(d => d.value > 0);
|
|
|
|
const enrollmentReportData = { series: enrollmentByCourse, studentCount: students.length, courseCount: courses.length };
|
|
const performanceReportData = { byCourse: capacityByCourse };
|
|
const riskReportData = { distribution: statusPie, studentCount: students.length };
|
|
const attendanceReportData = { note: "No attendance data recorded yet." };
|
|
|
|
const exportCsv = useCallback(() => {
|
|
const rows = [
|
|
["Type", "Label", "Enrolled", "Capacity"],
|
|
...enrollmentByCourse.map(r => ["Enrollment", r.name, String(r.enrolled), String(r.capacity)]),
|
|
...capacityByCourse.map(r => ["Fill Rate", r.name, String(r.fillRate) + "%", ""]),
|
|
...statusPie.map(r => ["Status", r.name, String(r.value), ""]),
|
|
];
|
|
const csv = rows.map(r => r.join(",")).join("\n");
|
|
const blob = new Blob([csv], { type: "text/csv" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "encoach-report.csv";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}, [enrollmentByCourse, capacityByCourse, statusPie]);
|
|
|
|
if (lc || ls) 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 className="flex items-center justify-between">
|
|
<div><h1 className="text-2xl font-bold">Reports</h1><p className="text-muted-foreground">Analytics and reporting dashboard.</p></div>
|
|
<Button variant="outline" onClick={exportCsv}><Download className="mr-2 h-4 w-4" />Export CSV</Button>
|
|
</div>
|
|
<Tabs defaultValue="enrollment">
|
|
<TabsList><TabsTrigger value="enrollment">Enrollment</TabsTrigger><TabsTrigger value="performance">Performance</TabsTrigger><TabsTrigger value="risk">Student Risk</TabsTrigger><TabsTrigger value="attendance">Attendance</TabsTrigger></TabsList>
|
|
<TabsContent value="enrollment" className="mt-4 space-y-4">
|
|
<AiReportNarrative report_type="enrollment" data={enrollmentReportData} />
|
|
<Card><CardHeader><CardTitle className="text-lg">Enrollment by Course</CardTitle></CardHeader><CardContent>
|
|
{enrollmentByCourse.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}><BarChart data={enrollmentByCourse}><CartesianGrid strokeDasharray="3 3" className="stroke-border" /><XAxis dataKey="name" tick={{fontSize:11}} /><YAxis /><Tooltip /><Bar dataKey="enrolled" name="Enrolled" fill="hsl(192, 82%, 32%)" radius={[4,4,0,0]} /><Bar dataKey="capacity" name="Capacity" fill="hsl(192, 82%, 32%, 0.3)" radius={[4,4,0,0]} /></BarChart></ResponsiveContainer>
|
|
) : <p className="text-center text-muted-foreground py-8">No courses available.</p>}
|
|
</CardContent></Card>
|
|
</TabsContent>
|
|
<TabsContent value="performance" className="mt-4 space-y-4">
|
|
<AiReportNarrative report_type="performance" data={performanceReportData} />
|
|
<Card><CardHeader><CardTitle className="text-lg">Course Fill Rate (%)</CardTitle></CardHeader><CardContent>
|
|
{capacityByCourse.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}><BarChart data={capacityByCourse}><CartesianGrid strokeDasharray="3 3" className="stroke-border" /><XAxis dataKey="name" tick={{fontSize:11}} /><YAxis domain={[0,100]} /><Tooltip /><Bar dataKey="fillRate" name="Fill %" fill="hsl(187, 72%, 55%)" radius={[4,4,0,0]} /></BarChart></ResponsiveContainer>
|
|
) : <p className="text-center text-muted-foreground py-8">No active courses.</p>}
|
|
</CardContent></Card>
|
|
</TabsContent>
|
|
<TabsContent value="risk" className="mt-4 space-y-4">
|
|
<AiReportNarrative report_type="risk" data={riskReportData} />
|
|
<Card><CardHeader><CardTitle className="text-lg">Student Status Distribution</CardTitle></CardHeader><CardContent className="flex justify-center">
|
|
{statusPie.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}><PieChart><Pie data={statusPie} dataKey="value" cx="50%" cy="50%" outerRadius={100} label>{statusPie.map((d,i) => <Cell key={i} fill={d.color} />)}</Pie><Tooltip /><Legend /></PieChart></ResponsiveContainer>
|
|
) : <p className="text-center text-muted-foreground py-8">No students available.</p>}
|
|
</CardContent></Card>
|
|
</TabsContent>
|
|
<TabsContent value="attendance" className="mt-4 space-y-4">
|
|
<AiReportNarrative report_type="attendance" data={attendanceReportData} />
|
|
<Card><CardHeader><CardTitle className="text-lg">Attendance</CardTitle></CardHeader><CardContent>
|
|
<p className="text-center text-muted-foreground py-8">No attendance data recorded yet. Attendance tracking will appear here once sessions are recorded.</p>
|
|
</CardContent></Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|