From 77f00e9e735358beb59fe1d540fc40b1f248b7e2 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 7 Aug 2026 04:15:32 +0000 Subject: [PATCH] Modified by www.SourceFiles.app --- src/routes/_authenticated/admin.tsx | 284 ++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 src/routes/_authenticated/admin.tsx diff --git a/src/routes/_authenticated/admin.tsx b/src/routes/_authenticated/admin.tsx new file mode 100644 index 0000000..f46223f --- /dev/null +++ b/src/routes/_authenticated/admin.tsx @@ -0,0 +1,284 @@ +import { createFileRoute } 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 { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, Trash2, Lock, UserPlus, Copy } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { createUserFn } from "@/lib/user-admin.functions"; +import { genTempPassword } from "@/lib/temp-password"; + +type Role = "admin" | "teacher" | "parent"; + +export const Route = createFileRoute("/_authenticated/admin")({ + head: () => ({ meta: [{ title: "Admin — School Portal" }] }), + component: AdminPage, +}); + +function AdminPage() { + const { roles } = useAuth(); + if (!roles.includes("admin")) { + return

Admins only

; + } + return ( +
+

Admin

+

Manage users, roles, classes, and parent-student links.

+ + + Users & roles + Classes + Parent ↔ student + + + + + +
+ ); +} + +function UsersTab() { + const qc = useQueryClient(); + const { data } = useQuery({ + queryKey: ["admin-users"], + queryFn: async () => { + const { data: profiles } = await supabase.from("profiles").select("id, full_name, email"); + const { data: roles } = await supabase.from("user_roles").select("user_id, role"); + const rolesByUser: Record = {}; + (roles ?? []).forEach((r) => { rolesByUser[r.user_id] = [...(rolesByUser[r.user_id] ?? []), r.role as Role]; }); + return (profiles ?? []).map((p) => ({ ...p, roles: rolesByUser[p.id] ?? [] })); + }, + }); + + const setRole = useMutation({ + mutationFn: async ({ userId, role, on }: { userId: string; role: Role; on: boolean }) => { + if (on) { + const { error } = await supabase.from("user_roles").insert({ user_id: userId, role }); + if (error && !error.message.includes("duplicate")) throw error; + } else { + const { error } = await supabase.from("user_roles").delete().eq("user_id", userId).eq("role", role); + if (error) throw error; + } + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-users"] }), + onError: (e: Error) => toast.error(e.message), + }); + + const [nu, setNu] = useState({ fullName: "", email: "", role: "teacher" as Role }); + const [created, setCreated] = useState<{ email: string; password: string } | null>(null); + const addUser = useMutation({ + mutationFn: async () => { + const password = genTempPassword(); + const res = await createUserFn({ data: { fullName: nu.fullName, email: nu.email, password, role: nu.role } }); + return { email: res.email, password }; + }, + onSuccess: (r) => { + setCreated(r); + setNu({ fullName: "", email: "", role: "teacher" }); + qc.invalidateQueries({ queryKey: ["admin-users"] }); + qc.invalidateQueries({ queryKey: ["teachers"] }); + toast.success(`Created ${r.email}`); + }, + onError: (e: Error) => toast.error(e.message), + }); + + return ( +
+
+
Add a user
+
+ setNu({ ...nu, fullName: e.target.value })} /> + setNu({ ...nu, email: e.target.value })} /> + + +
+

Creates a login with a temporary password (shown once). They can change it after signing in. No email is sent.

+ {created && ( +
+
Account created — share these with {created.email}:
+
+ Email: {created.email} + Password: {created.password} + +
+
+ )} +
+ +
+ + + + {(data ?? []).map((u) => ( + + + + + + ))} + +
NameEmailRoles
{u.full_name || "—"}{u.email} +
+ {(["admin", "teacher", "parent"] as Role[]).map((r) => { + const on = u.roles.includes(r); + return ; + })} +
+
+
+
+ ); +} + +function ClassesTab() { + const qc = useQueryClient(); + const [name, setName] = useState(""); + const [teacherId, setTeacherId] = useState(""); + + const { data: classes } = useQuery({ + queryKey: ["admin-classes"], + queryFn: async () => { + const { data } = await supabase.from("classes").select("*").order("name"); + const tids = Array.from(new Set((data ?? []).map((c) => c.teacher_id).filter(Boolean))) as string[]; + const { data: profs } = tids.length ? await supabase.from("profiles").select("id, full_name").in("id", tids) : { data: [] }; + const map = Object.fromEntries((profs ?? []).map((p) => [p.id, p.full_name])); + return (data ?? []).map((c) => ({ ...c, teacher_name: c.teacher_id ? map[c.teacher_id] : null })); + }, + }); + const { data: teachers } = useQuery({ + queryKey: ["teachers"], + queryFn: async () => { + const { data: r } = await supabase.from("user_roles").select("user_id").eq("role", "teacher"); + const ids = (r ?? []).map((x) => x.user_id); + if (ids.length === 0) return []; + const { data } = await supabase.from("profiles").select("id, full_name, email").in("id", ids); + return data ?? []; + }, + }); + + const add = useMutation({ + mutationFn: async () => { + const { error } = await supabase.from("classes").insert({ name, teacher_id: teacherId || null }); + if (error) throw error; + }, + onSuccess: () => { setName(""); setTeacherId(""); qc.invalidateQueries({ queryKey: ["admin-classes"] }); toast.success("Class added"); }, + onError: (e: Error) => toast.error(e.message), + }); + const del = useMutation({ + mutationFn: async (id: string) => { const { error } = await supabase.from("classes").delete().eq("id", id); if (error) throw error; }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-classes"] }), + }); + + return ( +
+
+ {(classes ?? []).map((c) => ( +
+
+
{c.name}
+
Teacher: {c.teacher_name ?? "unassigned"}
+
+ +
+ ))} + {classes?.length === 0 &&
No classes yet.
} +
+
+
New class
+
+ setName(e.target.value)} /> + + +
+
+
+ ); +} + +function LinksTab() { + const qc = useQueryClient(); + const [parentId, setParentId] = useState(""); + const [studentId, setStudentId] = useState(""); + + const { data: links } = useQuery({ + queryKey: ["admin-links"], + queryFn: async () => { + const { data } = await supabase.from("parent_students").select("id, parent_id, students(first_name, last_name)"); + const ids = (data ?? []).map((d) => d.parent_id); + const { data: profs } = ids.length ? await supabase.from("profiles").select("id, full_name, email").in("id", ids) : { data: [] }; + const map = Object.fromEntries((profs ?? []).map((p) => [p.id, p])); + return (data ?? []).map((d) => ({ ...d, profile: map[d.parent_id] })); + }, + }); + const { data: parents } = useQuery({ + queryKey: ["all-parents"], + queryFn: async () => { + const { data: r } = await supabase.from("user_roles").select("user_id").eq("role", "parent"); + const ids = (r ?? []).map((x) => x.user_id); + if (ids.length === 0) return []; + const { data } = await supabase.from("profiles").select("id, full_name, email").in("id", ids); + return data ?? []; + }, + }); + const { data: students } = useQuery({ + queryKey: ["all-students-link"], + queryFn: async () => (await supabase.from("students").select("id, first_name, last_name").order("last_name")).data ?? [], + }); + + const add = useMutation({ + mutationFn: async () => { + const { error } = await supabase.from("parent_students").insert({ parent_id: parentId, student_id: studentId }); + if (error) throw error; + }, + onSuccess: () => { setParentId(""); setStudentId(""); qc.invalidateQueries({ queryKey: ["admin-links"] }); toast.success("Linked"); }, + onError: (e: Error) => toast.error(e.message), + }); + const del = useMutation({ + mutationFn: async (id: string) => { const { error } = await supabase.from("parent_students").delete().eq("id", id); if (error) throw error; }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-links"] }), + }); + + return ( +
+
+ {(links ?? []).map((l) => { + const p = l.profile as { full_name?: string; email?: string } | undefined; + const s = l.students as { first_name: string; last_name: string } | null; + return ( +
+ {p?.full_name || p?.email} → {s?.first_name} {s?.last_name} + +
+ ); + })} + {links?.length === 0 &&
No links yet.
} +
+
+
Link parent to student
+
+ + + +
+
+
+ ); +}