Modified by www.SourceFiles.app
This commit is contained in:
@@ -1,191 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useQuery } 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 { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { FileText, Printer } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { IepProgressDoc, ReportCardDoc } from "@/components/reports";
|
|
||||||
import { REPORT_TYPES, monthToDateRange, schoolYearRange, type ReportType } from "@/lib/reports";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticated/reports")({
|
|
||||||
head: () => ({ meta: [{ title: "Reports — School Portal" }] }),
|
|
||||||
component: ReportsPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ALL = "__all__";
|
|
||||||
|
|
||||||
function ReportsPage() {
|
|
||||||
const { user, roles, loading } = useAuth();
|
|
||||||
const isAdmin = roles.includes("admin");
|
|
||||||
|
|
||||||
const [type, setType] = useState<ReportType>("report_card");
|
|
||||||
const [classId, setClassId] = useState("");
|
|
||||||
const [studentId, setStudentId] = useState(ALL);
|
|
||||||
const year = schoolYearRange();
|
|
||||||
const [from, setFrom] = useState(year.from);
|
|
||||||
const [to, setTo] = useState(year.to);
|
|
||||||
|
|
||||||
const { data: classes } = useQuery({
|
|
||||||
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: ["report-class-students", classId],
|
|
||||||
enabled: !!classId,
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await supabase
|
|
||||||
.from("students")
|
|
||||||
.select("id, first_name, last_name")
|
|
||||||
.eq("class_id", classId)
|
|
||||||
.order("last_name")
|
|
||||||
).data ?? [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const selected =
|
|
||||||
studentId === ALL ? (students ?? []) : (students ?? []).filter((s) => s.id === studentId);
|
|
||||||
|
|
||||||
const applyPreset = (preset: "year" | "month") => {
|
|
||||||
const r = preset === "year" ? schoolYearRange() : monthToDateRange();
|
|
||||||
setFrom(r.from);
|
|
||||||
setTo(r.to);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 md:p-8 max-w-4xl">
|
|
||||||
<div className="no-print">
|
|
||||||
<h1 className="text-2xl font-semibold flex items-center gap-2">
|
|
||||||
<FileText className="h-6 w-6 text-primary" /> Reports
|
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground text-sm mt-1">
|
|
||||||
Build report cards, academic progress reports, and 504 / IEP goal progress reports. Print
|
|
||||||
or save as PDF from your browser — each student starts on a new page.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="bg-card border rounded-lg p-4 mt-6 space-y-3">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Report</Label>
|
|
||||||
<Select value={type} onValueChange={(v) => setType(v as ReportType)}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{REPORT_TYPES.map((r) => (
|
|
||||||
<SelectItem key={r.value} value={r.value}>
|
|
||||||
{r.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Class</Label>
|
|
||||||
<Select
|
|
||||||
value={classId}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setClassId(v);
|
|
||||||
setStudentId(ALL);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Choose class" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{(classes ?? []).map((c) => (
|
|
||||||
<SelectItem key={c.id} value={c.id}>
|
|
||||||
{c.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Student</Label>
|
|
||||||
<Select value={studentId} onValueChange={setStudentId} disabled={!classId}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={ALL}>All students in class</SelectItem>
|
|
||||||
{(students ?? []).map((s) => (
|
|
||||||
<SelectItem key={s.id} value={s.id}>
|
|
||||||
{s.last_name}, {s.first_name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 items-end">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">From</Label>
|
|
||||||
{/* Uncontrolled + keyed so Safari's native picker isn't reset by React. */}
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
key={`f${from}`}
|
|
||||||
defaultValue={from}
|
|
||||||
onChange={(e) => setFrom(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">To</Label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
key={`t${to}`}
|
|
||||||
defaultValue={to}
|
|
||||||
onChange={(e) => setTo(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => applyPreset("year")}>
|
|
||||||
School year
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => applyPreset("month")}>
|
|
||||||
Month to date
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-1">
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{classId
|
|
||||||
? `${selected.length} document${selected.length === 1 ? "" : "s"} ready`
|
|
||||||
: "Select a class to begin."}
|
|
||||||
</span>
|
|
||||||
<Button size="sm" onClick={() => window.print()} disabled={selected.length === 0}>
|
|
||||||
<Printer className="h-4 w-4 mr-1" /> Print
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6 mt-6">
|
|
||||||
{selected.map((s) =>
|
|
||||||
type === "iep" ? (
|
|
||||||
<IepProgressDoc key={s.id} studentId={s.id} from={from} to={to} />
|
|
||||||
) : (
|
|
||||||
<ReportCardDoc key={s.id} studentId={s.id} variant={type} from={from} to={to} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user