diff --git a/src/components/alerts.tsx b/src/components/alerts.tsx deleted file mode 100644 index b2ca1fb..0000000 --- a/src/components/alerts.tsx +++ /dev/null @@ -1,411 +0,0 @@ -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 { Checkbox } from "@/components/ui/checkbox"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { AlertTriangle, Check, Plus, ShieldAlert } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import type { Database } from "@/integrations/supabase/types"; - -type AlertRow = Database["public"]["Tables"]["student_alerts"]["Row"]; -type AppRole = Database["public"]["Enums"]["app_role"]; - -const ALERT_TYPES = [ - { value: "parenting_plan", label: "Parenting plan" }, - { value: "custody", label: "Custody limitation" }, - { value: "unauthorized_pickup", label: "Unauthorized pickup" }, - { value: "medical", label: "Medical emergency" }, - { value: "allergy", label: "Severe allergy" }, - { value: "court_order", label: "Court-ordered restriction" }, - { value: "safety", label: "Safety concern" }, - { value: "dismissal", label: "Special dismissal" }, - { value: "other", label: "Other" }, -]; - -const STAFF_ROLES: AppRole[] = [ - "admin", - "org_admin", - "super_admin", - "campus_admin", - "management", - "teacher", - "staff", -]; - -const severityCls = (s: string) => - s === "critical" - ? "border-rose-600 bg-rose-50 dark:bg-rose-950/40" - : s === "high" - ? "border-amber-500 bg-amber-50 dark:bg-amber-950/40" - : "border-slate-300 bg-muted/40"; - -function useActiveAlerts(studentId: string) { - return useQuery({ - queryKey: ["student-alerts-active", studentId], - queryFn: async () => { - const today = new Date().toISOString().slice(0, 10); - const { data, error } = await supabase - .from("student_alerts") - .select("*") - .eq("student_id", studentId) - .eq("is_active", true) - .lte("effective_date", today) - .or(`expiration_date.is.null,expiration_date.gte.${today}`) - .order("severity"); - if (error) throw error; - return (data ?? []) as AlertRow[]; - }, - }); -} - -/** - * The red banner. Sits above everything on the student profile because the spec - * requires these to be visible to every authorized staff member who interacts - * with the student — not filed behind a tab. - */ -export function StudentAlertsBanner({ studentId }: { studentId: string }) { - const { user } = useAuth(); - const qc = useQueryClient(); - const { data: alerts } = useActiveAlerts(studentId); - const logged = useRef(false); - - const { data: acks } = useQuery({ - queryKey: ["my-alert-acks", studentId, user?.id], - enabled: !!user, - queryFn: async () => - ( - await supabase - .from("student_alert_acknowledgments") - .select("alert_id") - .eq("user_id", user!.id) - ).data ?? [], - }); - const ackedIds = new Set((acks ?? []).map((a) => a.alert_id)); - - // Every display is auditable, per the spec. Logged once per mount rather - // than per render so a re-render does not inflate the record. - useEffect(() => { - if (!user || !alerts?.length || logged.current) return; - logged.current = true; - void supabase - .from("student_alert_views") - .insert(alerts.map((a) => ({ alert_id: a.id, user_id: user.id }))); - }, [alerts, user]); - - const acknowledge = useMutation({ - mutationFn: async (alertId: string) => { - const { error } = await supabase - .from("student_alert_acknowledgments") - .insert({ alert_id: alertId, user_id: user!.id }); - if (error) throw error; - }, - onSuccess: () => { - qc.invalidateQueries({ queryKey: ["my-alert-acks", studentId, user?.id] }); - toast.success("Acknowledged"); - }, - onError: (e: Error) => toast.error(e.message), - }); - - if (!alerts?.length) return null; - - return ( -
- {alerts.map((a) => ( -
-
- -
-
{a.title}
-

{a.description}

-

- {ALERT_TYPES.find((t) => t.value === a.alert_type)?.label ?? a.alert_type} - {a.expiration_date && ` · until ${a.expiration_date}`} -

-
- {a.requires_acknowledgment && - (ackedIds.has(a.id) ? ( - - Acknowledged - - ) : ( - - ))} -
-
- ))} -
- ); -} - -/** Full management surface: history, who acknowledged, and creating new alerts. */ -export function StudentAlertsTab({ studentId, isAdmin }: { studentId: string; isAdmin: boolean }) { - const qc = useQueryClient(); - const { user } = useAuth(); - const [adding, setAdding] = useState(false); - - const { data: all } = useQuery({ - queryKey: ["student-alerts-all", studentId], - queryFn: async () => - (( - await supabase - .from("student_alerts") - .select("*") - .eq("student_id", studentId) - .order("effective_date", { ascending: false }) - ).data ?? []) as AlertRow[], - }); - - const { data: acks } = useQuery({ - queryKey: ["alert-acks", studentId], - queryFn: async () => - ( - await supabase - .from("student_alert_acknowledgments") - .select("alert_id, acknowledged_at, profiles:user_id(full_name, email)") - ).data ?? [], - }); - - const [form, setForm] = useState({ - alert_type: "safety", - severity: "critical", - title: "", - description: "", - effective_date: new Date().toISOString().slice(0, 10), - expiration_date: "", - requires_acknowledgment: true, - roles: STAFF_ROLES as AppRole[], - }); - - const create = useMutation({ - mutationFn: async () => { - if (!form.title.trim() || !form.description.trim()) - throw new Error("Title and description are required"); - const { error } = await supabase.from("student_alerts").insert({ - student_id: studentId, - alert_type: form.alert_type, - severity: form.severity, - title: form.title.trim(), - description: form.description.trim(), - effective_date: form.effective_date, - expiration_date: form.expiration_date || null, - requires_acknowledgment: form.requires_acknowledgment, - visible_to_roles: form.roles, - created_by: user?.id ?? null, - }); - if (error) throw error; - }, - onSuccess: () => { - setAdding(false); - setForm({ ...form, title: "", description: "" }); - qc.invalidateQueries({ queryKey: ["student-alerts-all", studentId] }); - qc.invalidateQueries({ queryKey: ["student-alerts-active", studentId] }); - toast.success("Alert created"); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const deactivate = useMutation({ - mutationFn: async (id: string) => { - const { error } = await supabase - .from("student_alerts") - .update({ is_active: false }) - .eq("id", id); - if (error) throw error; - }, - onSuccess: () => { - qc.invalidateQueries({ queryKey: ["student-alerts-all", studentId] }); - qc.invalidateQueries({ queryKey: ["student-alerts-active", studentId] }); - toast.success("Alert deactivated"); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const toggleRole = (r: AppRole, on: boolean) => - setForm((f) => ({ - ...f, - roles: on ? [...f.roles, r] : f.roles.filter((x) => x !== r), - })); - - return ( -
- {isAdmin && ( -
- {!adding ? ( - - ) : ( -
-
-
- - -
-
- - -
-
- -
- - setForm({ ...form, title: e.target.value })} - placeholder="e.g. Court-ordered pickup restriction" - /> -
-
- -