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
This commit is contained in:
216
src/pages/AdminDashboard.tsx
Normal file
216
src/pages/AdminDashboard.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Users, GraduationCap, Building2, UserCog, BarChart3, School, Building, Ticket, BookOpen, DollarSign } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useStudents, useTeachers } from "@/hooks/queries";
|
||||
import { useEntities } from "@/hooks/queries/useEntities";
|
||||
import AiInsightsPanel from "@/components/ai/AiInsightsPanel";
|
||||
|
||||
interface PlatformStats {
|
||||
total_students?: number;
|
||||
total_teachers?: number;
|
||||
total_entities?: number;
|
||||
total_courses?: number;
|
||||
active_courses?: number;
|
||||
total_batches?: number;
|
||||
active_batches?: number;
|
||||
total_classrooms?: number;
|
||||
total_tickets?: number;
|
||||
open_tickets?: number;
|
||||
total_exams?: number;
|
||||
total_assignments?: number;
|
||||
total_revenue?: number;
|
||||
total_payments?: number;
|
||||
total_subjects?: number;
|
||||
total_resources?: number;
|
||||
total_exam_sessions?: number;
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { data: stats } = useQuery<PlatformStats>({
|
||||
queryKey: ["platform", "stats"],
|
||||
queryFn: async () => {
|
||||
const res = await api.get<{ data: PlatformStats }>("/stats");
|
||||
return (res as { data: PlatformStats }).data ?? res;
|
||||
},
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const { data: studentsData, isLoading: ls } = useStudents({ size: 5 });
|
||||
const { data: teachersData, isLoading: lt } = useTeachers({ size: 5 });
|
||||
const { data: entitiesData, isLoading: le } = useEntities({ size: 50 });
|
||||
|
||||
const students = studentsData?.items ?? [];
|
||||
const teachers = teachersData?.items ?? [];
|
||||
const entities = entitiesData?.items ?? [];
|
||||
|
||||
const loading = ls || lt || le;
|
||||
|
||||
const statCards = [
|
||||
{ label: "Students", count: stats?.total_students ?? 0, icon: GraduationCap, color: "bg-primary/10 text-primary" },
|
||||
{ label: "Teachers", count: stats?.total_teachers ?? 0, icon: UserCog, color: "bg-info/10 text-info" },
|
||||
{ label: "Entities", count: stats?.total_entities ?? 0, icon: Building2, color: "bg-warning/10 text-warning" },
|
||||
{ label: "Active Courses", count: stats?.active_courses ?? 0, icon: BookOpen, color: "bg-success/10 text-success" },
|
||||
];
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "Exams", count: stats?.total_exams ?? 0, url: "/admin/exams", icon: BarChart3 },
|
||||
{ label: "Classrooms", count: stats?.total_classrooms ?? 0, url: "/admin/classrooms", icon: School },
|
||||
{ label: "Entities", count: stats?.total_entities ?? 0, url: "/admin/entities", icon: Building2 },
|
||||
{ label: "Tickets", count: `${stats?.open_tickets ?? 0} open`, url: "/admin/tickets", icon: Ticket },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
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 tracking-tight">Platform Dashboard</h1>
|
||||
<p className="text-muted-foreground">Overview of platform activity and user statistics.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{statCards.map((s) => (
|
||||
<Card key={s.label} className="border-0 shadow-sm">
|
||||
<CardContent className="p-5 flex items-center gap-4">
|
||||
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${s.color}`}>
|
||||
<s.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{s.count.toLocaleString()}</p>
|
||||
<p className="text-sm text-muted-foreground">{s.label}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AiInsightsPanel />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{shortcuts.map((s) => (
|
||||
<Link key={s.label} to={s.url}>
|
||||
<Card className="border-0 shadow-sm hover:shadow-md transition-shadow cursor-pointer">
|
||||
<CardContent className="p-5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<s.icon className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{s.label}</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{s.count}</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Batches", value: `${stats?.active_batches ?? 0} / ${stats?.total_batches ?? 0}` },
|
||||
{ label: "Subjects", value: stats?.total_subjects ?? 0 },
|
||||
{ label: "Resources", value: stats?.total_resources ?? 0 },
|
||||
{ label: "Revenue", value: `$${(stats?.total_revenue ?? 0).toLocaleString()}` },
|
||||
].map(s => (
|
||||
<Card key={s.label} className="border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<p className="text-lg font-bold">{s.value}</p>
|
||||
<p className="text-xs text-muted-foreground">{s.label}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base font-semibold">Recent Students</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{students.length === 0 && <p className="text-sm text-muted-foreground">No students found.</p>}
|
||||
{students.map((s) => (
|
||||
<div key={s.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-semibold text-primary">
|
||||
{(s.name || "?").split(" ").map(n => n[0]).join("").slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{s.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{s.email || "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">{s.status || "active"}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base font-semibold">Recent Teachers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{teachers.length === 0 && <p className="text-sm text-muted-foreground">No teachers found.</p>}
|
||||
{teachers.map((t) => (
|
||||
<div key={t.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-info/10 flex items-center justify-center text-xs font-semibold text-info">
|
||||
{(t.name || "?").split(" ").map(n => n[0]).join("").slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{t.email || "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">{t.department_name || "—"}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base font-semibold">Entities</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{entities.length === 0 && <p className="text-sm text-muted-foreground">No entities found.</p>}
|
||||
{entities.map((e) => (
|
||||
<div key={e.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Building className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium">{e.name}</p>
|
||||
</div>
|
||||
<Badge variant="outline">{e.entity_type || e.type || "—"}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base font-semibold">Platform Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{[
|
||||
{ label: "Exam Sessions", value: stats?.total_exam_sessions ?? 0 },
|
||||
{ label: "Assignments", value: stats?.total_assignments ?? 0 },
|
||||
{ label: "Total Tickets", value: stats?.total_tickets ?? 0 },
|
||||
{ label: "Completed Payments", value: stats?.total_payments ?? 0 },
|
||||
].map(item => (
|
||||
<div key={item.label} className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">{item.label}</p>
|
||||
<p className="text-sm font-semibold">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user