159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
import type { ReactNode } from "react"
|
|
import { Users, LayoutDashboard, BookOpen, FileText, ClipboardList, Library, Activity } from "lucide-react"
|
|
|
|
import type { AdminDashboardData } from "@/modules/dashboard/types"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { EmptyState } from "@/shared/components/ui/empty-state"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
|
|
import { formatDate } from "@/shared/lib/utils"
|
|
|
|
export function AdminDashboardView({ data }: { data: AdminDashboardData }) {
|
|
return (
|
|
<div className="flex h-full flex-col space-y-8 p-8">
|
|
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
|
<div className="space-y-1">
|
|
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
|
<div className="text-sm text-muted-foreground">System overview across users, learning content, and activity.</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline" className="gap-2">
|
|
<Activity className="h-4 w-4" />
|
|
{data.activeSessionsCount} active sessions
|
|
</Badge>
|
|
<Badge variant="outline" className="gap-2">
|
|
<Users className="h-4 w-4" />
|
|
{data.userCount} users
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<KpiCard title="Users" value={data.userCount} icon={<Users className="h-4 w-4" />} />
|
|
<KpiCard title="Classes" value={data.classCount} icon={<LayoutDashboard className="h-4 w-4" />} />
|
|
<KpiCard title="Homework (published)" value={data.homeworkAssignmentPublishedCount} icon={<ClipboardList className="h-4 w-4" />} />
|
|
<KpiCard title="To grade" value={data.homeworkSubmissionToGradeCount} icon={<FileText className="h-4 w-4" />} />
|
|
</div>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-3">
|
|
<Card className="lg:col-span-1">
|
|
<CardHeader>
|
|
<CardTitle>User Roles</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{data.userRoleCounts.length === 0 ? (
|
|
<EmptyState title="No users" description="No user records found." />
|
|
) : (
|
|
data.userRoleCounts.map((r) => (
|
|
<div key={r.role} className="flex items-center justify-between">
|
|
<Badge variant="secondary">{r.role}</Badge>
|
|
<div className="text-sm font-medium tabular-nums">{r.count}</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="lg:col-span-1">
|
|
<CardHeader>
|
|
<CardTitle>Content</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-3">
|
|
<ContentRow label="Textbooks" value={data.textbookCount} icon={<Library className="h-4 w-4 text-muted-foreground" />} />
|
|
<ContentRow label="Chapters" value={data.chapterCount} icon={<BookOpen className="h-4 w-4 text-muted-foreground" />} />
|
|
<ContentRow label="Questions" value={data.questionCount} icon={<FileText className="h-4 w-4 text-muted-foreground" />} />
|
|
<ContentRow label="Exams" value={data.examCount} icon={<ClipboardList className="h-4 w-4 text-muted-foreground" />} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="lg:col-span-1">
|
|
<CardHeader>
|
|
<CardTitle>Homework Activity</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-3">
|
|
<ContentRow label="Assignments" value={data.homeworkAssignmentCount} icon={<ClipboardList className="h-4 w-4 text-muted-foreground" />} />
|
|
<ContentRow label="Submissions" value={data.homeworkSubmissionCount} icon={<FileText className="h-4 w-4 text-muted-foreground" />} />
|
|
<ContentRow label="To grade" value={data.homeworkSubmissionToGradeCount} icon={<Activity className="h-4 w-4 text-muted-foreground" />} />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Recent Users</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{data.recentUsers.length === 0 ? (
|
|
<EmptyState title="No users yet" description="Seed the database to see users here." />
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Role</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.recentUsers.map((u) => (
|
|
<TableRow key={u.id}>
|
|
<TableCell className="font-medium">{u.name || "-"}</TableCell>
|
|
<TableCell className="text-muted-foreground">{u.email}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="secondary">{u.role ?? "unknown"}</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">{formatDate(u.createdAt)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function KpiCard({
|
|
title,
|
|
value,
|
|
icon,
|
|
}: {
|
|
title: string
|
|
value: number
|
|
icon: ReactNode
|
|
}) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
|
<div className="text-muted-foreground">{icon}</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold tabular-nums">{value}</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function ContentRow({
|
|
label,
|
|
value,
|
|
icon,
|
|
}: {
|
|
label: string
|
|
value: number
|
|
icon: ReactNode
|
|
}) {
|
|
return (
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{icon}
|
|
<div className="text-sm text-muted-foreground">{label}</div>
|
|
</div>
|
|
<div className="text-sm font-medium tabular-nums">{value}</div>
|
|
</div>
|
|
)
|
|
}
|