Modified by www.SourceFiles.app
This commit is contained in:
@@ -1,126 +0,0 @@
|
|||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
||||||
import { useAuth, highestRole } from "@/hooks/use-auth";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
|
||||||
import { Users, ClipboardCheck, Receipt, CalendarDays, ClipboardList, AlertTriangle } from "lucide-react";
|
|
||||||
import { COMPLIANCE_CLASS, complianceState, formatDate } from "@/lib/plans";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticated/dashboard")({
|
|
||||||
head: () => ({ meta: [{ title: "Dashboard — School Portal" }] }),
|
|
||||||
component: Dashboard,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Dashboard() {
|
|
||||||
const { user, roles } = useAuth();
|
|
||||||
const role = highestRole(roles);
|
|
||||||
|
|
||||||
const { data: studentCount } = useQuery({
|
|
||||||
queryKey: ["students-count"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { count } = await supabase.from("students").select("*", { count: "exact", head: true });
|
|
||||||
return count ?? 0;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: todayAttendance } = useQuery({
|
|
||||||
queryKey: ["attendance-today"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
const { count } = await supabase.from("attendance").select("*", { count: "exact", head: true }).eq("date", today);
|
|
||||||
return count ?? 0;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: upcoming } = useQuery({
|
|
||||||
queryKey: ["upcoming-events"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
const { data } = await supabase.from("calendar_events").select("*").gte("date", today).order("date").limit(5);
|
|
||||||
return data ?? [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// RLS scopes this to plans the viewer may see, so it is empty for most users.
|
|
||||||
const { data: planAlerts } = useQuery({
|
|
||||||
queryKey: ["plan-alerts"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from("student_plans")
|
|
||||||
.select("id, student_id, next_annual_review_date, next_reevaluation_date, students(first_name, last_name)")
|
|
||||||
.neq("status", "archived");
|
|
||||||
return (data ?? []).flatMap((p) => {
|
|
||||||
const rows: { key: string; student_id: string; name: string; kind: string; date: string }[] = [];
|
|
||||||
const name = `${p.students?.first_name ?? ""} ${p.students?.last_name ?? ""}`.trim();
|
|
||||||
if (p.next_annual_review_date) rows.push({ key: `${p.id}-ar`, student_id: p.student_id, name, kind: "Annual review", date: p.next_annual_review_date });
|
|
||||||
if (p.next_reevaluation_date) rows.push({ key: `${p.id}-re`, student_id: p.student_id, name, kind: "Re-evaluation", date: p.next_reevaluation_date });
|
|
||||||
return rows;
|
|
||||||
})
|
|
||||||
.filter((r) => complianceState(r.date) === "overdue" || complianceState(r.date) === "due_soon")
|
|
||||||
.sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-8 max-w-6xl">
|
|
||||||
<h1 className="text-2xl font-semibold">Welcome back</h1>
|
|
||||||
<p className="text-muted-foreground">Signed in as <span className="capitalize">{role}</span></p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
||||||
<Stat icon={Users} label="Students" value={studentCount ?? "—"} />
|
|
||||||
<Stat icon={ClipboardCheck} label="Attendance recorded today" value={todayAttendance ?? "—"} />
|
|
||||||
<Stat icon={CalendarDays} label="Upcoming events" value={upcoming?.length ?? "—"} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{planAlerts && planAlerts.length > 0 && (
|
|
||||||
<div className="mt-8 bg-card border rounded-lg p-6">
|
|
||||||
<h2 className="font-semibold mb-3 flex items-center gap-2">
|
|
||||||
<ClipboardList className="h-4 w-4" /> 504 / IEP compliance needing attention
|
|
||||||
</h2>
|
|
||||||
<ul className="space-y-2 text-sm">
|
|
||||||
{planAlerts.slice(0, 8).map((a) => (
|
|
||||||
<li key={a.key} className="flex justify-between gap-3 border-b last:border-0 py-2">
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{complianceState(a.date) === "overdue" && <AlertTriangle className="h-3.5 w-3.5 text-destructive shrink-0" />}
|
|
||||||
<Link to="/students/$id" params={{ id: a.student_id }} className="text-primary hover:underline">{a.name}</Link>
|
|
||||||
<span className="text-muted-foreground">· {a.kind}</span>
|
|
||||||
</span>
|
|
||||||
<span className={COMPLIANCE_CLASS[complianceState(a.date)]}>{formatDate(a.date)}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{planAlerts.length > 8 && (
|
|
||||||
<Link to="/plans" className="text-xs text-primary hover:underline mt-3 inline-block">
|
|
||||||
View all {planAlerts.length} alerts
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-8 bg-card border rounded-lg p-6">
|
|
||||||
<h2 className="font-semibold mb-3 flex items-center gap-2"><CalendarDays className="h-4 w-4" /> Upcoming on the calendar</h2>
|
|
||||||
{upcoming && upcoming.length > 0 ? (
|
|
||||||
<ul className="space-y-2 text-sm">
|
|
||||||
{upcoming.map((e) => (
|
|
||||||
<li key={e.id} className="flex justify-between border-b last:border-0 py-2">
|
|
||||||
<span>{e.title}</span>
|
|
||||||
<span className="text-muted-foreground">{new Date(e.date).toLocaleDateString()}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">No upcoming events.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stat({ icon: Icon, label, value }: { icon: typeof Users; label: string; value: number | string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-card border rounded-lg p-6">
|
|
||||||
<Icon className="h-5 w-5 text-primary mb-2" />
|
|
||||||
<div className="text-3xl font-semibold">{value}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">{label}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user