Modified by www.SourceFiles.app
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { GradingConfigEditor } from "@/components/gradebook";
|
||||
import { BookOpen, ChevronRight, Plus, SlidersHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/classes/")({
|
||||
head: () => ({ meta: [{ title: "Classes — School Portal" }] }),
|
||||
component: ClassesIndex,
|
||||
});
|
||||
|
||||
function ClassesIndex() {
|
||||
const { user, roles } = useAuth();
|
||||
const isAdmin = roles.includes("admin");
|
||||
const qc = useQueryClient();
|
||||
|
||||
const { data: classes } = useQuery({
|
||||
queryKey: ["classes-list"],
|
||||
queryFn: async () => (await supabase.from("classes").select("id, name, teacher_id").order("name")).data ?? [],
|
||||
});
|
||||
const visible = isAdmin ? (classes ?? []) : (classes ?? []).filter((c) => c.teacher_id === user?.id);
|
||||
|
||||
const { data: teachers } = useQuery({
|
||||
queryKey: ["teachers"], enabled: isAdmin,
|
||||
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) return [];
|
||||
return (await supabase.from("profiles").select("id, full_name, email").in("id", ids)).data ?? [];
|
||||
},
|
||||
});
|
||||
const [name, setName] = useState("");
|
||||
const [teacherId, setTeacherId] = useState("");
|
||||
const create = useMutation({
|
||||
mutationFn: async () => { const { error } = await supabase.from("classes").insert({ name: name.trim(), teacher_id: teacherId || null }); if (error) throw error; },
|
||||
onSuccess: () => { setName(""); setTeacherId(""); qc.invalidateQueries({ queryKey: ["classes-list"] }); toast.success("Class created"); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
const [showDefaults, setShowDefaults] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl">
|
||||
<h1 className="text-2xl font-semibold mb-1">Classes</h1>
|
||||
<p className="text-muted-foreground text-sm mb-6">Rosters and gradebooks.</p>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="bg-card border rounded-lg p-4 space-y-3 mb-4">
|
||||
<div className="font-medium text-sm">New class</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<Input placeholder="Class name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<Select value={teacherId} onValueChange={setTeacherId}>
|
||||
<SelectTrigger><SelectValue placeholder="Teacher (optional)" /></SelectTrigger>
|
||||
<SelectContent>{(teachers ?? []).map((t) => <SelectItem key={t.id} value={t.id}>{t.full_name || t.email}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => create.mutate()} disabled={!name.trim() || create.isPending}><Plus className="h-4 w-4 mr-1" /> Add class</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-card border rounded-lg divide-y mb-6">
|
||||
{visible.map((c) => (
|
||||
<Link key={c.id} to="/classes/$id" params={{ id: c.id }} className="flex items-center justify-between p-3 hover:bg-muted/50">
|
||||
<span className="flex items-center gap-2 font-medium"><BookOpen className="h-4 w-4 text-muted-foreground" /> {c.name}</span>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
))}
|
||||
{visible.length === 0 && <div className="p-4 text-sm text-muted-foreground">{isAdmin ? "No classes yet — create one above." : "You have no classes assigned."}</div>}
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<button className="flex items-center gap-2 font-medium text-sm" onClick={() => setShowDefaults((s) => !s)}>
|
||||
<SlidersHorizontal className="h-4 w-4" /> School grading defaults
|
||||
</button>
|
||||
<p className="text-xs text-muted-foreground mt-1">Applied to any class that hasn't set its own categories/scale.</p>
|
||||
{showDefaults && <div className="mt-4"><GradingConfigEditor classId={null} /></div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user