diff --git a/src/routes/_authenticated/plans.tsx b/src/routes/_authenticated/plans.tsx deleted file mode 100644 index b40b41d..0000000 --- a/src/routes/_authenticated/plans.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import { useQuery } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; -import { Input } from "@/components/ui/input"; -import { Badge } from "@/components/ui/badge"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { AlertTriangle, ClipboardList, Loader2 } from "lucide-react"; -import { useMemo, useState } from "react"; -import { - COMPLIANCE_CLASS, - PLAN_STATUS_LABEL, - PLAN_TYPE_LABEL, - complianceState, - formatDate, - type PlanStatus, - type PlanType, -} from "@/lib/plans"; - -export const Route = createFileRoute("/_authenticated/plans")({ - head: () => ({ meta: [{ title: "504 / IEP Plans — School Portal" }] }), - component: PlansList, -}); - -type Row = { - id: string; - student_id: string; - plan_type: PlanType; - status: PlanStatus; - case_manager_id: string | null; - next_annual_review_date: string | null; - next_reevaluation_date: string | null; - students: { first_name: string; last_name: string; grade_level: string | null } | null; -}; - -type SortKey = "last_name" | "type" | "next_ar" | "next_eval"; - -function PlansList() { - const [q, setQ] = useState(""); - const [filter, setFilter] = useState("all"); - const [sort, setSort] = useState("next_ar"); - - const { data: rows, isLoading } = useQuery({ - queryKey: ["plans-compliance"], - queryFn: async () => { - const { data, error } = await supabase - .from("student_plans") - .select( - "id, student_id, plan_type, status, case_manager_id, next_annual_review_date, next_reevaluation_date, students(first_name, last_name, grade_level)", - ); - if (error) throw error; - return (data ?? []) as unknown as Row[]; - }, - }); - - // profiles is admin-readable only, so non-admins simply get no names here. - const { data: staff } = useQuery({ - queryKey: ["plan-staff"], - queryFn: async () => { - const { data: roles } = await supabase - .from("user_roles") - .select("user_id, role") - .in("role", ["admin", "teacher"]); - const ids = [...new Set((roles ?? []).map((r) => r.user_id))]; - if (!ids.length) - return [] as { id: string; full_name: string | null; email: string | null }[]; - const { data } = await supabase.from("profiles").select("id, full_name, email").in("id", ids); - return data ?? []; - }, - }); - const managerName = (id: string | null) => { - if (!id) return "Unassigned"; - const m = (staff ?? []).find((s) => s.id === id); - return m ? m.full_name || m.email || "Assigned" : "Assigned"; - }; - - const worst = (r: Row) => { - const a = complianceState(r.next_annual_review_date); - const b = complianceState(r.next_reevaluation_date); - if (a === "overdue" || b === "overdue") return "overdue"; - if (a === "due_soon" || b === "due_soon") return "due_soon"; - return "ok"; - }; - - const filtered = useMemo(() => { - let list = rows ?? []; - const term = q.trim().toLowerCase(); - if (term) { - list = list.filter((r) => - `${r.students?.first_name ?? ""} ${r.students?.last_name ?? ""}` - .toLowerCase() - .includes(term), - ); - } - if (filter === "overdue") list = list.filter((r) => worst(r) === "overdue"); - else if (filter === "due_soon") list = list.filter((r) => worst(r) === "due_soon"); - else if (filter === "iep") list = list.filter((r) => r.plan_type === "iep"); - else if (filter === "section_504") list = list.filter((r) => r.plan_type === "section_504"); - else if (filter === "active") list = list.filter((r) => r.status === "active"); - - // Nulls sort last so unset compliance dates never masquerade as urgent. - const byDate = (x: string | null, y: string | null) => - x === y ? 0 : x === null ? 1 : y === null ? -1 : x < y ? -1 : 1; - - return [...list].sort((a, b) => { - switch (sort) { - case "last_name": - return (a.students?.last_name ?? "").localeCompare(b.students?.last_name ?? ""); - case "type": - return a.plan_type.localeCompare(b.plan_type); - case "next_eval": - return byDate(a.next_reevaluation_date, b.next_reevaluation_date); - default: - return byDate(a.next_annual_review_date, b.next_annual_review_date); - } - }); - }, [rows, q, filter, sort]); - - const overdueCount = (rows ?? []).filter((r) => worst(r) === "overdue").length; - const dueSoonCount = (rows ?? []).filter((r) => worst(r) === "due_soon").length; - - return ( -
-

- 504 / IEP Plans -

-

- Compliance clocks for every plan you have access to. Overdue dates are shown in red. -

- -
-
-
{rows?.length ?? "—"}
-
Plans on file
-
-
-
0 ? "text-destructive" : ""}`}> - {overdueCount} -
-
- {overdueCount > 0 && } Overdue -
-
-
-
0 ? "text-amber-600" : ""}`}> - {dueSoonCount} -
-
Due within 30 days
-
-
- -
- setQ(e.target.value)} - /> - - -
- -
- {isLoading ? ( -
- Loading plans… -
- ) : filtered.length === 0 ? ( -
- {rows?.length === 0 - ? "No 504 or IEP plans on file yet." - : "No plans match this filter."} -
- ) : ( - - - - - - - - - - - - - - {filtered.map((r) => ( - - - - - - - - - - ))} - -
StudentGradeTypeStatusCase managerNext annual reviewNext re-evaluation
- - {r.students?.last_name}, {r.students?.first_name} - - {r.students?.grade_level || "—"} - - {PLAN_TYPE_LABEL[r.plan_type]} - - {PLAN_STATUS_LABEL[r.status]}{managerName(r.case_manager_id)} - {formatDate(r.next_annual_review_date)} - - {formatDate(r.next_reevaluation_date)} -
- )} -
-
- ); -}