Modified by www.SourceFiles.app
This commit is contained in:
@@ -1,98 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticated/attendance")({
|
|
||||||
head: () => ({ meta: [{ title: "Attendance — School Portal" }] }),
|
|
||||||
component: AttendancePage,
|
|
||||||
});
|
|
||||||
|
|
||||||
function AttendancePage() {
|
|
||||||
const { user, roles, loading } = useAuth();
|
|
||||||
const isAdmin = roles.includes("admin");
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
|
|
||||||
const [classId, setClassId] = useState<string>("");
|
|
||||||
|
|
||||||
const { data: classes } = useQuery({
|
|
||||||
// isAdmin is in the key so the query refetches once roles resolve; gated on
|
|
||||||
// !loading so it never runs with a not-yet-known (false) admin status.
|
|
||||||
queryKey: ["my-classes", user?.id, isAdmin],
|
|
||||||
queryFn: async () => {
|
|
||||||
let q = supabase.from("classes").select("id, name, teacher_id");
|
|
||||||
if (!isAdmin) q = q.eq("teacher_id", user!.id);
|
|
||||||
return (await q.order("name")).data ?? [];
|
|
||||||
},
|
|
||||||
enabled: !!user && !loading,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: students } = useQuery({
|
|
||||||
queryKey: ["class-students", classId, date],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!classId) return [];
|
|
||||||
const { data } = await supabase.from("students").select("id, first_name, last_name, attendance(status, date, id)").eq("class_id", classId);
|
|
||||||
return data ?? [];
|
|
||||||
},
|
|
||||||
enabled: !!classId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mark = useMutation({
|
|
||||||
mutationFn: async ({ studentId, status }: { studentId: string; status: string }) => {
|
|
||||||
const { error } = await supabase.from("attendance").upsert(
|
|
||||||
{ student_id: studentId, date, status: status as "present" | "absent" | "late" | "excused", recorded_by: user!.id },
|
|
||||||
{ onConflict: "student_id,date" }
|
|
||||||
);
|
|
||||||
if (error) throw error;
|
|
||||||
},
|
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["class-students", classId, date] }),
|
|
||||||
onError: (e: Error) => toast.error(e.message),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getStatus = (s: { attendance: { date: string; status: string }[] }) =>
|
|
||||||
s.attendance.find((a) => a.date === date)?.status ?? "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-8 max-w-4xl">
|
|
||||||
<h1 className="text-2xl font-semibold mb-1">Attendance</h1>
|
|
||||||
<p className="text-muted-foreground text-sm mb-6">Mark today's attendance for your class.</p>
|
|
||||||
|
|
||||||
<div className="flex gap-3 mb-6">
|
|
||||||
<div>
|
|
||||||
<Select value={classId} onValueChange={setClassId}>
|
|
||||||
<SelectTrigger className="w-60"><SelectValue placeholder="Choose class" /></SelectTrigger>
|
|
||||||
<SelectContent>{(classes ?? []).map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<Input type="date" className="w-44" defaultValue={date} onChange={(e) => setDate(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{classId && (
|
|
||||||
<div className="bg-card border rounded-lg divide-y">
|
|
||||||
{(students ?? []).map((s) => {
|
|
||||||
const status = getStatus(s as { attendance: { date: string; status: string }[] });
|
|
||||||
return (
|
|
||||||
<div key={s.id} className="flex items-center justify-between p-3">
|
|
||||||
<div className="font-medium">{s.last_name}, {s.first_name}</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{["present", "absent", "late", "excused"].map((opt) => (
|
|
||||||
<Button key={opt} size="sm" variant={status === opt ? "default" : "outline"} onClick={() => mark.mutate({ studentId: s.id, status: opt })}>
|
|
||||||
{opt}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{students?.length === 0 && <div className="p-4 text-sm text-muted-foreground">No students in this class.</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!classId && <p className="text-sm text-muted-foreground">Select a class to begin.</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user