From c86f75421b79d8cc6306848c6c6d961e345c6cb0 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 7 Aug 2026 05:42:31 +0000 Subject: [PATCH] Modified by www.SourceFiles.app --- src/components/enrollment.tsx | 564 ++++++++++++++++++++++++++++++++++ 1 file changed, 564 insertions(+) create mode 100644 src/components/enrollment.tsx diff --git a/src/components/enrollment.tsx b/src/components/enrollment.tsx new file mode 100644 index 0000000..0833036 --- /dev/null +++ b/src/components/enrollment.tsx @@ -0,0 +1,564 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Pencil, Check, Plus, Trash2, Building2, Tag } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import type { Database } from "@/integrations/supabase/types"; + +type StudentUpdate = Database["public"]["Tables"]["students"]["Update"]; +type ScheduleUpdate = Database["public"]["Tables"]["student_campus_schedules"]["Update"]; + +const DAYS = [ + { key: "monday", short: "M" }, + { key: "tuesday", short: "T" }, + { key: "wednesday", short: "W" }, + { key: "thursday", short: "Th" }, + { key: "friday", short: "F" }, + { key: "saturday", short: "Sa" }, + { key: "sunday", short: "Su" }, +] as const; + +type DayKey = (typeof DAYS)[number]["key"]; + +const ENROLLMENT_STATUSES = [ + "prospective", + "enrolled", + "waitlisted", + "withdrawn", + "graduated", + "on_hold", +]; + +function EditToggle({ + editing, + setEditing, + canEdit, +}: { + editing: boolean; + setEditing: (v: boolean) => void; + canEdit: boolean; +}) { + if (!canEdit) return null; + return editing ? ( + + ) : ( + + ); +} + +export function StudentEnrollmentTab({ + studentId, + isAdmin, +}: { + studentId: string; + isAdmin: boolean; +}) { + return ( +
+ + + +
+ ); +} + +// ── Enrollment ────────────────────────────────────────────────────────────── +function EnrollmentCard({ studentId, canEdit }: { studentId: string; canEdit: boolean }) { + const qc = useQueryClient(); + const [editing, setEditing] = useState(false); + + const { data: student } = useQuery({ + queryKey: ["student-enrollment", studentId], + queryFn: async () => + ( + await supabase + .from("students") + .select( + "id, enrollment_status, enrollment_start_date, enrollment_end_date, attendance_basis, calendar_basis, is_support_student, primary_campus_id, tuition_tier_id", + ) + .eq("id", studentId) + .single() + ).data, + }); + const { data: campuses } = useQuery({ + queryKey: ["campuses"], + queryFn: async () => + (await supabase.from("campuses").select("id, name").order("name")).data ?? [], + }); + const { data: tiers } = useQuery({ + queryKey: ["tuition-tiers"], + queryFn: async () => + (await supabase.from("tuition_tiers").select("id, name").order("sort_order")).data ?? [], + }); + + const save = useMutation({ + mutationFn: async (patch: StudentUpdate) => { + const { error } = await supabase.from("students").update(patch).eq("id", studentId); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["student-enrollment", studentId] }); + qc.invalidateQueries({ queryKey: ["student", studentId] }); + }, + onError: (e: Error) => toast.error(e.message), + }); + + if (!student) return null; + + const campusName = (campuses ?? []).find((c) => c.id === student.primary_campus_id)?.name; + const tierName = (tiers ?? []).find((t) => t.id === student.tuition_tier_id)?.name; + + return ( +
+
+

+ Enrollment +

+ +
+ + {student.enrollment_status !== "enrolled" && ( +

+ Only students marked enrolled are picked up by invoice runs. +

+ )} + + {editing ? ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + save.mutate({ enrollment_start_date: e.target.value || null })} + /> +
+
+ + save.mutate({ enrollment_end_date: e.target.value || null })} + /> +
+ +
+ ) : ( +
+ + + + + + + + +
+ )} +
+ ); +} + +function Row({ label, value }: { label: string; value?: string | null }) { + return ( +
+ {label} + {value || "—"} +
+ ); +} + +// ── Weekly campus schedule ────────────────────────────────────────────────── +function CampusScheduleCard({ studentId, canEdit }: { studentId: string; canEdit: boolean }) { + const qc = useQueryClient(); + const [editing, setEditing] = useState(false); + const [newCampus, setNewCampus] = useState(""); + + const { data: campuses } = useQuery({ + queryKey: ["campuses"], + queryFn: async () => + (await supabase.from("campuses").select("id, name").order("name")).data ?? [], + }); + const { data: rows } = useQuery({ + queryKey: ["student-schedules", studentId], + queryFn: async () => + ( + await supabase + .from("student_campus_schedules") + .select("*") + .eq("student_id", studentId) + .order("effective_start", { ascending: false }) + ).data ?? [], + }); + + const invalidate = () => { + qc.invalidateQueries({ queryKey: ["student-schedules", studentId] }); + }; + + const add = useMutation({ + mutationFn: async () => { + if (!newCampus) throw new Error("Pick a campus"); + const { error } = await supabase.from("student_campus_schedules").insert({ + student_id: studentId, + campus_id: newCampus, + assignment_type: (rows ?? []).length === 0 ? "primary" : "additional", + }); + if (error) throw error; + }, + onSuccess: () => { + setNewCampus(""); + invalidate(); + toast.success("Schedule added"); + }, + onError: (e: Error) => toast.error(e.message), + }); + + const patch = useMutation({ + mutationFn: async ({ id, values }: { id: string; values: ScheduleUpdate }) => { + const { error } = await supabase.from("student_campus_schedules").update(values).eq("id", id); + if (error) throw error; + }, + onSuccess: invalidate, + onError: (e: Error) => toast.error(e.message), + }); + + const remove = useMutation({ + mutationFn: async (id: string) => { + const { error } = await supabase.from("student_campus_schedules").delete().eq("id", id); + if (error) throw error; + }, + onSuccess: () => { + invalidate(); + toast.success("Schedule removed"); + }, + onError: (e: Error) => toast.error(e.message), + }); + + const campusName = (id: string) => (campuses ?? []).find((c) => c.id === id)?.name ?? "—"; + + return ( +
+
+

+ Weekly campus schedule +

+ +
+

+ One row per campus. A student can attend more than one campus in the same week — billing + charges each campus at its own rate. +

+ +
+ {(rows ?? []).map((r) => ( +
+
+ + + {campusName(r.campus_id)} + + {r.assignment_type} + + + + + {r.scheduled_days_per_week} day{r.scheduled_days_per_week === 1 ? "" : "s"}/week + + {editing && ( + + )} + +
+ +
+ {DAYS.map((d) => ( + + ))} +
+ + {editing && ( +
+
+ + + patch.mutate({ id: r.id, values: { effective_start: e.target.value } }) + } + /> +
+
+ + + patch.mutate({ id: r.id, values: { effective_end: e.target.value || null } }) + } + /> +
+ + +
+ )} +
+ ))} + + {(rows ?? []).length === 0 && ( +

+ No campus schedule yet. Invoice runs skip students without one. +

+ )} + + {editing && ( +
+ + +
+ )} +
+
+ ); +} + +// ── Tags ──────────────────────────────────────────────────────────────────── +function TagsCard({ studentId, canEdit }: { studentId: string; canEdit: boolean }) { + const qc = useQueryClient(); + const [editing, setEditing] = useState(false); + + const { data: tags } = useQuery({ + queryKey: ["student-tags"], + queryFn: async () => + ( + await supabase + .from("student_tags") + .select("id, name") + .eq("is_active", true) + .order("sort_order") + ).data ?? [], + }); + const { data: assigned } = useQuery({ + queryKey: ["student-tag-assignments", studentId], + queryFn: async () => + ( + await supabase + .from("student_tag_assignments") + .select("id, tag_id") + .eq("student_id", studentId) + ).data ?? [], + }); + + const assignedIds = new Set((assigned ?? []).map((a) => a.tag_id)); + + const toggle = useMutation({ + mutationFn: async ({ tagId, on }: { tagId: string; on: boolean }) => { + if (on) { + const { error } = await supabase + .from("student_tag_assignments") + .insert({ student_id: studentId, tag_id: tagId }); + if (error) throw error; + } else { + const { error } = await supabase + .from("student_tag_assignments") + .delete() + .eq("student_id", studentId) + .eq("tag_id", tagId); + if (error) throw error; + } + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["student-tag-assignments", studentId] }), + onError: (e: Error) => toast.error(e.message), + }); + + return ( +
+
+

+ Tags +

+ +
+ + {editing ? ( +
+ {(tags ?? []).map((t) => ( + + ))} +
+ ) : ( +
+ {(tags ?? []) + .filter((t) => assignedIds.has(t.id)) + .map((t) => ( + + + {t.name} + + ))} + {(tags ?? []).filter((t) => assignedIds.has(t.id)).length === 0 && ( + No tags. + )} +
+ )} +
+ ); +}