diff --git a/src/routes/_authenticated/students.$id.tsx b/src/routes/_authenticated/students.$id.tsx deleted file mode 100644 index d6145ed..0000000 --- a/src/routes/_authenticated/students.$id.tsx +++ /dev/null @@ -1,851 +0,0 @@ -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 { 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 - 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) =>