diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 5b3c21a..9d993cd 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -572,6 +572,7 @@ export type Database = { last_name: string notes: string | null other_info: string | null + photo_path: string | null photo_release: boolean physician_phone: string | null portfolio_keeper: string | null @@ -603,6 +604,7 @@ export type Database = { last_name: string notes?: string | null other_info?: string | null + photo_path?: string | null photo_release?: boolean physician_phone?: string | null portfolio_keeper?: string | null @@ -634,6 +636,7 @@ export type Database = { last_name?: string notes?: string | null other_info?: string | null + photo_path?: string | null photo_release?: boolean physician_phone?: string | null portfolio_keeper?: string | null diff --git a/src/routes/_authenticated/students.$id.tsx b/src/routes/_authenticated/students.$id.tsx index 87f0f52..ba196e0 100644 --- a/src/routes/_authenticated/students.$id.tsx +++ b/src/routes/_authenticated/students.$id.tsx @@ -7,8 +7,10 @@ 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, Upload, Trash2, Plus, FileText } from "lucide-react"; +import { ArrowLeft, Trash2, Plus, FileText, Loader2, ImageUp, User } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; @@ -17,11 +19,31 @@ export const Route = createFileRoute("/_authenticated/students/$id")({ 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" }, +]; + +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 StudentDetail() { const { id } = Route.useParams(); const { roles } = useAuth(); const isAdmin = roles.includes("admin"); - const qc = useQueryClient(); const { data: student } = useQuery({ queryKey: ["student", id], @@ -33,21 +55,28 @@ function StudentDetail() { }); return ( -
+
All students -

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

-

{(student?.classes as { name: string } | null)?.name ?? "No class"}

+
+ +
+

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

+

{(student?.classes as { name: string } | null)?.name ?? "No class"}

+
+
- - - Info - Authorized pickup + + + Profile + Family & pickup + Academics Attendance - Tuition ledger + Tuition {isAdmin && Contracts} - - + + + {isAdmin && } @@ -56,108 +85,392 @@ function StudentDetail() { ); } -function InfoTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) { +// ── 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 (all single-value student fields) ──────────────────────────────── +function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) { + const qc = useQueryClient(); + 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 ?? [], + }); + const [form, setForm] = useState | null>(null); + const c = (form ?? s) as Record | null; + const upd = (k: string, v: unknown) => setForm({ ...(c as Record), [k]: v }); + 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", + "home_ed_eval_due", "previous_schools", "special_needs", "portfolio_keeper", "disciplinary_history", "custody_agreement", + "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) || ""; + 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] }); setForm(null); }, + onError: (e: Error) => toast.error(e.message), + }); + + if (!c) return
; + const val = (k: string) => (c[k] as string) ?? ""; + const T = (k: string, label: string, className = "") => upd(k, e.target.value)} />; + const D = (k: string, label: string) => upd(k, e.target.value)} />; + const A = (k: string, label: string, rows = 2) =>