Roadmap P0
- Ship /logo.svg fallback and rewire asset references (superseded later by
the project-manager PNG in a separate commit).
Roadmap P1
- Response envelope alignment ({items,total,page,size}) across services
and query hooks.
- Approval/ticket UX updates tied to the new backend rollback semantics.
Roadmap P2
- React.lazy + Vite manualChunks to split vendor-radix/charts/icons/forms
from the main bundle and cut initial JS.
- Fix the 181 tsc errors (PaginationParams class + per-service generics).
- Auto-refresh flow for JWT refresh tokens wired into api-client.ts.
Roadmap P3
- i18n: i18next + language detector, en/ar locales, RTL auto-switch,
LanguageToggle component in RoleLayout and AdminLmsLayout.
- GDPR: PrivacyCenter page for data export and right-to-erasure, backed
by gdpr.service.ts; logout via AuthContext after erasure.
- Human-in-the-loop exam review UI: admin ExamReviewQueue + ExamReviewDetail
pages, useExamReview hooks, exam-review.service.ts.
- AI quality loop: AIPromptEditor (versioning + render preview),
AIFeedbackButtons for students, AIFeedbackTriage admin page, dedicated
services, hooks, and types.
- Dark mode: ThemeProvider + ThemeToggle + chart color tokens.
- Accessibility: DialogDescription on every DialogContent; alert-dialog
and sheet updates.
- Retire dead pages: ExamPage marketing, Index, ProfilePage import.
- Playwright e2e scaffolding: playwright.config.ts + e2e/login.spec.ts
smoke tests, npm scripts test:e2e / test:e2e:install.
Made-with: Cursor
76 lines
3.4 KiB
TypeScript
76 lines
3.4 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { useAttendance } from "@/hooks/queries";
|
|
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts";
|
|
import AiTipBanner from "@/components/ai/AiTipBanner";
|
|
|
|
const badgeVariant = (s: string) => s === "present" ? "default" as const : s === "absent" ? "destructive" as const : "secondary" as const;
|
|
|
|
export default function StudentAttendance() {
|
|
const { data: attendanceRecords = [], isLoading } = useAttendance();
|
|
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>;
|
|
|
|
const counts = { present: 0, absent: 0, late: 0, excused: 0 };
|
|
attendanceRecords.forEach(a => counts[a.status]++);
|
|
const total = attendanceRecords.length;
|
|
const rate = total > 0 ? ((counts.present + counts.late) / total * 100).toFixed(1) : "0";
|
|
|
|
const pieData = [
|
|
{ name: "Present", value: counts.present, color: "hsl(142, 71%, 45%)" },
|
|
{ name: "Late", value: counts.late, color: "hsl(38, 92%, 50%)" },
|
|
{ name: "Absent", value: counts.absent, color: "hsl(0, 72%, 51%)" },
|
|
{ name: "Excused", value: counts.excused, color: "hsl(199, 89%, 48%)" },
|
|
].filter(d => d.value > 0);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Attendance</h1>
|
|
<p className="text-muted-foreground">Your attendance record across all courses.</p>
|
|
</div>
|
|
|
|
<AiTipBanner context="student-attendance" variant="insight" />
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<Card className="lg:col-span-1">
|
|
<CardHeader><CardTitle className="text-lg">Summary</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<div className="text-center mb-4">
|
|
<p className="text-4xl font-bold text-primary">{rate}%</p>
|
|
<p className="text-sm text-muted-foreground">Attendance Rate</p>
|
|
</div>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<PieChart>
|
|
<Pie data={pieData} dataKey="value" cx="50%" cy="50%" innerRadius={50} outerRadius={80}>
|
|
{pieData.map((d, i) => <Cell key={i} fill={d.color} />)}
|
|
</Pie>
|
|
<Tooltip />
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader><CardTitle className="text-lg">Attendance Records</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader><TableRow><TableHead>Date</TableHead><TableHead>Course</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
|
|
<TableBody>
|
|
{attendanceRecords.map(a => (
|
|
<TableRow key={a.id}>
|
|
<TableCell>{a.date}</TableCell>
|
|
<TableCell>{a.course_name}</TableCell>
|
|
<TableCell><Badge variant={badgeVariant(a.status)} className="capitalize">{a.status}</Badge></TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|