import { createFileRoute, Link } 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 { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { ArrowLeft, Trash2, Plus, FileText, Loader2, ImageUp, User, Pencil, Check, UserPlus, Copy, Link2, Mail } from "lucide-react"; import { useState, useRef } from "react"; import { toast } from "sonner"; import { createUserFn } from "@/lib/user-admin.functions"; import { createIntakeToken } from "@/lib/intake.functions"; import { genTempPassword } from "@/lib/temp-password"; import { StudentGradeReport } from "@/components/gradebook"; import { StudentPlansTab } from "@/components/plans"; import { StudentEnrollmentTab } from "@/components/enrollment"; import { StudentAlertsBanner, StudentAlertsTab } from "@/components/alerts"; import { money } from "@/lib/reports"; export const Route = createFileRoute("/_authenticated/students/$id")({ head: () => ({ meta: [{ title: "Student — School Portal" }] }), component: StudentDetail, }); const DISMISSAL_OPTIONS = [ { value: "not_allowed", label: "NOT ALLOWED" }, { value: "bicycle", label: "Bicycle" }, { value: "scooter", label: "Scooter" }, { value: "walking", label: "Walking" }, { value: "rideshare", label: "Rideshare (Uber/Lyft)" }, { value: "other", label: "Other" }, ]; const dismissalLabel = (v: string) => DISMISSAL_OPTIONS.find((o) => o.value === v)?.label ?? v; function Field({ label, children, className = "" }: { label: string; children: React.ReactNode; className?: string }) { return
{children}
; } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function ViewRow({ label, value }: { label: string; value?: string | null }) { return (
{label} {value || "—"}
); } function EditToggle({ editing, setEditing, canEdit }: { editing: boolean; setEditing: (v: boolean) => void; canEdit: boolean }) { if (!canEdit) return null; return editing ? : ; } function StudentDetail() { const { id } = Route.useParams(); const { roles } = useAuth(); const isAdmin = roles.includes("admin"); const canEdit = isAdmin; // student profile data is admin-edit-only; parents/teachers view only const { data: student } = useQuery({ queryKey: ["student", id], queryFn: async () => { const { data, error } = await supabase.from("students").select("*, classes(name)").eq("id", id).single(); if (error) throw error; return data; }, }); return (
All students

{student?.first_name} {student?.last_name}

{student?.grade_level ? `Grade ${student.grade_level}` : "No grade level set"}

Profile Enrollment & campus Alerts Family & pickup Academics 504 / IEP Grades Attendance Tuition {isAdmin && Contracts} {isAdmin && }
); } // ── Photo ─────────────────────────────────────────────────────────────────── function PhotoAvatar({ studentId, photoPath, canEdit }: { studentId: string; photoPath: string | null | undefined; canEdit: boolean }) { const qc = useQueryClient(); const [uploading, setUploading] = useState(false); const { data: url } = useQuery({ queryKey: ["student-photo", photoPath], enabled: !!photoPath, queryFn: async () => { const { data } = await supabase.storage.from("student-photos").createSignedUrl(photoPath as string, 3600); return data?.signedUrl ?? null; }, }); const onUpload = async (file: File) => { if (!file) return; setUploading(true); try { const ext = file.name.split(".").pop() || "jpg"; const path = `${studentId}/photo-${Date.now()}.${ext}`; const { error: upErr } = await supabase.storage.from("student-photos").upload(path, file, { upsert: true }); if (upErr) throw upErr; const { error } = await supabase.from("students").update({ photo_path: path }).eq("id", studentId); if (error) throw error; qc.invalidateQueries({ queryKey: ["student", studentId] }); toast.success("Photo updated"); } catch (e) { toast.error((e as Error).message); } finally { setUploading(false); } }; return (
{url ? Student : }
{canEdit && ( )}
); } // ── Profile ────────────────────────────────────────────────────────────────── function ProfileTab({ studentId, canEdit, isAdmin }: { studentId: string; canEdit: boolean; isAdmin: boolean }) { const qc = useQueryClient(); const [editing, setEditing] = useState(false); const { data: s } = useQuery({ queryKey: ["student", studentId], queryFn: async () => (await supabase.from("students").select("*").eq("id", studentId).single()).data, }); const { data: classes } = useQuery({ queryKey: ["classes"], queryFn: async () => (await supabase.from("classes").select("id, name").order("name")).data ?? [], }); // In edit mode we work on a snapshot (`form`) taken when Edit is pressed, so a // background refetch can never clobber in-progress input. Functional updates // avoid stale-closure races. const [form, setForm] = useState>({}); // daily_tuition_cents is stored in cents but edited in dollars, so it gets its // own form key and is converted on save rather than going through the string // field helpers below. const startEdit = () => { setForm({ ...(s as Record), tuition_rate_input: s?.daily_tuition_cents != null ? (s.daily_tuition_cents / 100).toFixed(2) : "", }); setEditing(true); }; const c = (editing ? form : s) as Record | null; const upd = (k: string, v: unknown) => setForm((f) => ({ ...f, [k]: v })); // Read date inputs straight from the DOM at save time (Safari doesn't reliably // fire onChange for native date pickers, so state can be stale/empty). const dateRefs = useRef>({}); const dismissal = (c?.dismissal_methods as string[]) ?? []; const toggleDismissal = (v: string) => upd("dismissal_methods", dismissal.includes(v) ? dismissal.filter((x) => x !== v) : [...dismissal, v]); const save = useMutation({ mutationFn: async () => { if (!c) return; const keys = [ "first_name", "last_name", "dob", "gender", "grade_level", "preferred_start_date", "class_id", "allergies", "chronic_conditions", "primary_physician", "physician_phone", "dismissal_other", "backup_transport_plan", "interests", "other_info", "agreement_signed_by", "agreement_signed_date", "notes", ]; const payload: Record = { dismissal_methods: dismissal, photo_release: !!c.photo_release }; for (const k of keys) payload[k] = (c[k] as string) || null; payload.first_name = (c.first_name as string) || ""; payload.last_name = (c.last_name as string) || ""; // Dates: read the live DOM value so a Safari pick can't be lost. for (const k of ["dob", "preferred_start_date", "agreement_signed_date"]) { const el = dateRefs.current[k]; if (el) payload[k] = el.value || null; } // Dollars -> cents. Blank clears the rate, which stops auto-charging. const rate = String(c.tuition_rate_input ?? "").trim(); const rateNum = Number(rate); if (rate !== "" && (!Number.isFinite(rateNum) || rateNum < 0)) { throw new Error("Daily tuition rate must be a positive amount"); } payload.daily_tuition_cents = rate === "" ? null : Math.round(rateNum * 100); const { error } = await supabase.from("students").update(payload as never).eq("id", studentId); if (error) throw error; }, onSuccess: () => { toast.success("Profile saved"); qc.invalidateQueries({ queryKey: ["student", studentId] }); setEditing(false); }, onError: (e: Error) => toast.error(e.message), }); if (!c) return
; const val = (k: string) => (c[k] as string) ?? ""; const className = (classes ?? []).find((cl) => cl.id === c.class_id)?.name ?? null; // ── View mode ── if (!editing) { return (
{canEdit && }
{dismissal.includes("other") && }
{isAdmin && }
{isAdmin && (
)}
); } // ── Edit mode ── const T = (k: string, label: string) => upd(k, e.target.value)} />; // Uncontrolled (defaultValue) so Safari's native date picker doesn't get reset by React's controlled value. const D = (k: string, label: string) => { dateRefs.current[k] = el; }} onChange={(e) => upd(k, e.target.value)} />; const A = (k: string, label: string, rows = 2) =>