Add full-page student intake form matching Bayside paper forms
- Migration: student intake fields (health, academic, dismissal, agreement), student_guardians + student_curriculum_logins tables, extend authorized_pickups (alt_phone, notes, kind) with RLS mirroring existing policies - New /students/new full-page multi-section form (replaces the add dialog) - Students list links to the full page; regenerate Supabase types Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
import { createFileRoute, useNavigate, 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 { 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 { ArrowLeft, Plus, Trash2, Lock, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/students/new")({
|
||||
head: () => ({ meta: [{ title: "New student — School Portal" }] }),
|
||||
component: NewStudentPage,
|
||||
});
|
||||
|
||||
type Guardian = {
|
||||
full_name: string; relationship: string; phone: string; address: string;
|
||||
city: string; state: string; zip: string; email: string; employer: string;
|
||||
};
|
||||
type Contact = { name: string; relationship: string; phone: string; alt_phone: string };
|
||||
type Pickup = { name: string; relationship: string; phone: string; notes: string };
|
||||
type Login = { website: string; student_login: string; student_password: string; parent_account: string; parent_password: string };
|
||||
|
||||
const emptyGuardian: Guardian = { full_name: "", relationship: "", phone: "", address: "", city: "", state: "", zip: "", email: "", employer: "" };
|
||||
const emptyLogin: Login = { website: "", student_login: "", student_password: "", parent_account: "", parent_password: "" };
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
// Small labeled-field helpers to keep the long form readable.
|
||||
function Field({ label, children, className = "" }: { label: string; children: React.ReactNode; className?: string }) {
|
||||
return <div className={className}><Label className="text-xs">{label}</Label>{children}</div>;
|
||||
}
|
||||
function Section({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="bg-card border rounded-lg p-5 space-y-4">
|
||||
<div>
|
||||
<h2 className="font-semibold">{title}</h2>
|
||||
{description && <p className="text-xs text-muted-foreground mt-0.5">{description}</p>}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Top-level (not defined inside the page) so the inputs don't remount / lose focus on each keystroke.
|
||||
function GuardianFields({ g, setG }: { g: Guardian; setG: (v: Guardian) => void }) {
|
||||
const f = (k: keyof Guardian) => (e: React.ChangeEvent<HTMLInputElement>) => setG({ ...g, [k]: e.target.value });
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<Field label="Full name" className="col-span-2 md:col-span-1"><Input value={g.full_name} onChange={f("full_name")} /></Field>
|
||||
<Field label="Relationship to student"><Input value={g.relationship} onChange={f("relationship")} /></Field>
|
||||
<Field label="Phone number"><Input value={g.phone} onChange={f("phone")} /></Field>
|
||||
<Field label="Address" className="col-span-2 md:col-span-3"><Input value={g.address} onChange={f("address")} /></Field>
|
||||
<Field label="City"><Input value={g.city} onChange={f("city")} /></Field>
|
||||
<Field label="State"><Input value={g.state} onChange={f("state")} /></Field>
|
||||
<Field label="Zip code"><Input value={g.zip} onChange={f("zip")} /></Field>
|
||||
<Field label="Email address"><Input type="email" value={g.email} onChange={f("email")} /></Field>
|
||||
<Field label="Employer"><Input value={g.employer} onChange={f("employer")} /></Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NewStudentPage() {
|
||||
const { roles } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const { data: classes } = useQuery({
|
||||
queryKey: ["classes"],
|
||||
queryFn: async () => (await supabase.from("classes").select("id, name").order("name")).data ?? [],
|
||||
});
|
||||
|
||||
// ── form state ─────────────────────────────────────────────────────────────
|
||||
const [s, setS] = useState({
|
||||
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: "",
|
||||
});
|
||||
const set = (k: keyof typeof s) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setS({ ...s, [k]: e.target.value });
|
||||
|
||||
const [primary, setPrimary] = useState<Guardian>({ ...emptyGuardian });
|
||||
const [secondary, setSecondary] = useState<Guardian>({ ...emptyGuardian });
|
||||
const [contacts, setContacts] = useState<Contact[]>([
|
||||
{ name: "", relationship: "", phone: "", alt_phone: "" },
|
||||
{ name: "", relationship: "", phone: "", alt_phone: "" },
|
||||
]);
|
||||
const [pickups, setPickups] = useState<Pickup[]>([
|
||||
{ name: "", relationship: "", phone: "", notes: "" },
|
||||
{ name: "", relationship: "", phone: "", notes: "" },
|
||||
{ name: "", relationship: "", phone: "", notes: "" },
|
||||
]);
|
||||
const [logins, setLogins] = useState<Login[]>([{ ...emptyLogin }]);
|
||||
const [dismissal, setDismissal] = useState<string[]>([]);
|
||||
const [agree, setAgree] = useState(false);
|
||||
|
||||
const toggleDismissal = (v: string) =>
|
||||
setDismissal((d) => (d.includes(v) ? d.filter((x) => x !== v) : [...d, v]));
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async () => {
|
||||
// 1. student
|
||||
const { data: student, error } = await supabase
|
||||
.from("students")
|
||||
.insert({
|
||||
first_name: s.first_name.trim(),
|
||||
last_name: s.last_name.trim(),
|
||||
dob: s.dob || null,
|
||||
gender: s.gender || null,
|
||||
grade_level: s.grade_level || null,
|
||||
preferred_start_date: s.preferred_start_date || null,
|
||||
class_id: s.class_id || null,
|
||||
allergies: s.allergies || null,
|
||||
chronic_conditions: s.chronic_conditions || null,
|
||||
primary_physician: s.primary_physician || null,
|
||||
physician_phone: s.physician_phone || null,
|
||||
home_ed_eval_due: s.home_ed_eval_due || null,
|
||||
previous_schools: s.previous_schools || null,
|
||||
special_needs: s.special_needs || null,
|
||||
portfolio_keeper: s.portfolio_keeper || null,
|
||||
disciplinary_history: s.disciplinary_history || null,
|
||||
custody_agreement: s.custody_agreement || null,
|
||||
dismissal_methods: dismissal,
|
||||
dismissal_other: s.dismissal_other || null,
|
||||
backup_transport_plan: s.backup_transport_plan || null,
|
||||
interests: s.interests || null,
|
||||
other_info: s.other_info || null,
|
||||
agreement_signed_by: s.agreement_signed_by || null,
|
||||
agreement_signed_date: s.agreement_signed_date || null,
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
if (error) throw error;
|
||||
const studentId = student.id;
|
||||
|
||||
// 2. guardians
|
||||
const guardianRows = [
|
||||
{ g: primary, is_primary: true },
|
||||
{ g: secondary, is_primary: false },
|
||||
]
|
||||
.filter(({ g }) => g.full_name.trim())
|
||||
.map(({ g, is_primary }) => ({
|
||||
student_id: studentId, is_primary,
|
||||
full_name: g.full_name.trim(), relationship: g.relationship || null, phone: g.phone || null,
|
||||
address: g.address || null, city: g.city || null, state: g.state || null, zip: g.zip || null,
|
||||
email: g.email || null, employer: g.employer || null,
|
||||
}));
|
||||
if (guardianRows.length) {
|
||||
const { error: gErr } = await supabase.from("student_guardians").insert(guardianRows);
|
||||
if (gErr) throw gErr;
|
||||
}
|
||||
|
||||
// 3. authorized pickups + emergency contacts (one table, distinguished by kind)
|
||||
const pickupRows = [
|
||||
...pickups.filter((p) => p.name.trim()).map((p, i) => ({
|
||||
student_id: studentId, kind: "pickup", sort_order: i,
|
||||
name: p.name.trim(), relationship: p.relationship || null, phone: p.phone || null,
|
||||
alt_phone: null as string | null, notes: p.notes || null,
|
||||
})),
|
||||
...contacts.filter((c) => c.name.trim()).map((c, i) => ({
|
||||
student_id: studentId, kind: "emergency", sort_order: i,
|
||||
name: c.name.trim(), relationship: c.relationship || null, phone: c.phone || null,
|
||||
alt_phone: c.alt_phone || null, notes: null as string | null,
|
||||
})),
|
||||
];
|
||||
if (pickupRows.length) {
|
||||
const { error: pErr } = await supabase.from("authorized_pickups").insert(pickupRows);
|
||||
if (pErr) throw pErr;
|
||||
}
|
||||
|
||||
// 4. curriculum logins
|
||||
const loginRows = logins
|
||||
.filter((l) => Object.values(l).some((v) => v.trim()))
|
||||
.map((l) => ({
|
||||
student_id: studentId,
|
||||
website: l.website || null, student_login: l.student_login || null, student_password: l.student_password || null,
|
||||
parent_account: l.parent_account || null, parent_password: l.parent_password || null,
|
||||
}));
|
||||
if (loginRows.length) {
|
||||
const { error: lErr } = await supabase.from("student_curriculum_logins").insert(loginRows);
|
||||
if (lErr) throw lErr;
|
||||
}
|
||||
|
||||
return studentId as string;
|
||||
},
|
||||
onSuccess: (id) => {
|
||||
toast.success("Student added");
|
||||
qc.invalidateQueries({ queryKey: ["students"] });
|
||||
navigate({ to: "/students/$id", params: { id } });
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
if (!roles.includes("admin")) {
|
||||
return <div className="p-8"><Lock className="h-6 w-6 text-muted-foreground" /><h1 className="mt-2 text-xl font-semibold">Admins only</h1><p className="text-sm text-muted-foreground">Only admins can add students.</p></div>;
|
||||
}
|
||||
|
||||
const canSave = s.first_name.trim() && s.last_name.trim() && !save.isPending;
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 max-w-4xl mx-auto pb-24">
|
||||
<Link to="/students" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"><ArrowLeft className="h-4 w-4" /> Back to students</Link>
|
||||
<h1 className="text-2xl font-semibold mb-1">New student</h1>
|
||||
<p className="text-muted-foreground text-sm mb-6">Student Information & Authorized Pick-Up — Bayside Academy</p>
|
||||
|
||||
<form onSubmit={(e) => { e.preventDefault(); if (canSave) save.mutate(); }} className="space-y-5">
|
||||
{/* Student */}
|
||||
<Section title="Student information">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<Field label="First name"><Input value={s.first_name} onChange={set("first_name")} required /></Field>
|
||||
<Field label="Last name"><Input value={s.last_name} onChange={set("last_name")} required /></Field>
|
||||
<Field label="Date of birth"><Input type="date" value={s.dob} onChange={set("dob")} /></Field>
|
||||
<Field label="Gender">
|
||||
<Select value={s.gender} onValueChange={(v) => setS({ ...s, gender: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="male">Male</SelectItem><SelectItem value="female">Female</SelectItem><SelectItem value="other">Other</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Grade level applying for"><Input value={s.grade_level} onChange={set("grade_level")} /></Field>
|
||||
<Field label="Preferred start date"><Input type="date" value={s.preferred_start_date} onChange={set("preferred_start_date")} /></Field>
|
||||
<Field label="Class (optional)">
|
||||
<Select value={s.class_id} onValueChange={(v) => setS({ ...s, class_id: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="Assign class" /></SelectTrigger>
|
||||
<SelectContent>{(classes ?? []).map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Guardians */}
|
||||
<Section title="Primary parent / guardian"><GuardianFields g={primary} setG={setPrimary} /></Section>
|
||||
<Section title="Secondary parent / guardian" description="Optional"><GuardianFields g={secondary} setG={setSecondary} /></Section>
|
||||
|
||||
{/* Health */}
|
||||
<Section title="Health information">
|
||||
<Field label="Allergies (leave blank if none)"><Textarea rows={2} value={s.allergies} onChange={set("allergies")} /></Field>
|
||||
<Field label="Chronic illnesses or medical conditions (leave blank if none)"><Textarea rows={2} value={s.chronic_conditions} onChange={set("chronic_conditions")} /></Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Primary physician"><Input value={s.primary_physician} onChange={set("primary_physician")} /></Field>
|
||||
<Field label="Physician's phone"><Input value={s.physician_phone} onChange={set("physician_phone")} /></Field>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Emergency contacts */}
|
||||
<Section title="Emergency contacts" description="If different from the guardians above.">
|
||||
{contacts.map((c, i) => (
|
||||
<div key={i} className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Field label="Full name"><Input value={c.name} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, name: e.target.value } : x))} /></Field>
|
||||
<Field label="Relationship"><Input value={c.relationship} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, relationship: e.target.value } : x))} /></Field>
|
||||
<Field label="Phone"><Input value={c.phone} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, phone: e.target.value } : x))} /></Field>
|
||||
<Field label="Alternate phone"><Input value={c.alt_phone} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, alt_phone: e.target.value } : x))} /></Field>
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
|
||||
{/* Authorized pickups */}
|
||||
<Section title="Authorized persons for pick-up" description="Only these individuals will be allowed to pick up the student. All must show valid ID.">
|
||||
{pickups.map((p, i) => (
|
||||
<div key={i} className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Field label="Name"><Input value={p.name} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, name: e.target.value } : x))} /></Field>
|
||||
<Field label="Relationship"><Input value={p.relationship} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, relationship: e.target.value } : x))} /></Field>
|
||||
<Field label="Phone"><Input value={p.phone} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, phone: e.target.value } : x))} /></Field>
|
||||
<Field label="Notes / restrictions"><Input value={p.notes} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, notes: e.target.value } : x))} /></Field>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => setPickups([...pickups, { name: "", relationship: "", phone: "", notes: "" }])}><Plus className="h-4 w-4 mr-1" /> Add another</Button>
|
||||
</Section>
|
||||
|
||||
{/* Academic */}
|
||||
<Section title="Academic information">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs">Curriculum platform logins</Label>
|
||||
{logins.map((l, i) => (
|
||||
<div key={i} className="border rounded-md p-3 space-y-2 relative">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<Field label="Website"><Input value={l.website} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, website: e.target.value } : x))} /></Field>
|
||||
<Field label="Student login"><Input value={l.student_login} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, student_login: e.target.value } : x))} /></Field>
|
||||
<Field label="Student password"><Input value={l.student_password} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, student_password: e.target.value } : x))} /></Field>
|
||||
<Field label="Parent account"><Input value={l.parent_account} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, parent_account: e.target.value } : x))} /></Field>
|
||||
<Field label="Parent password"><Input value={l.parent_password} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, parent_password: e.target.value } : x))} /></Field>
|
||||
</div>
|
||||
{logins.length > 1 && <Button type="button" size="icon" variant="ghost" className="absolute top-1 right-1" onClick={() => setLogins(logins.filter((_, j) => j !== i))}><Trash2 className="h-4 w-4" /></Button>}
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => setLogins([...logins, { ...emptyLogin }])}><Plus className="h-4 w-4 mr-1" /> Add another platform</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Home Education Annual Evaluation due to BPS"><Input type="date" value={s.home_ed_eval_due} onChange={set("home_ed_eval_due")} /></Field>
|
||||
<Field label="Previous school(s) attended"><Input value={s.previous_schools} onChange={set("previous_schools")} /></Field>
|
||||
</div>
|
||||
<Field label="Special academic needs or accommodations (leave blank if none)"><Textarea rows={2} value={s.special_needs} onChange={set("special_needs")} /></Field>
|
||||
<Field label="Who will keep a portfolio of work">
|
||||
<Select value={s.portfolio_keeper} onValueChange={(v) => setS({ ...s, portfolio_keeper: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="parent">Parent/Guardian</SelectItem><SelectItem value="bayside">Bayside Academy</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Disciplinary history (suspension, expulsion, asked not to return, etc.) — describe circumstances & supports, or leave blank"><Textarea rows={3} value={s.disciplinary_history} onChange={set("disciplinary_history")} /></Field>
|
||||
<Field label="Custody agreement we should be aware of (leave blank if none — please provide documentation)"><Textarea rows={2} value={s.custody_agreement} onChange={set("custody_agreement")} /></Field>
|
||||
</Section>
|
||||
|
||||
{/* Independent dismissal */}
|
||||
<Section title="Independent dismissal permission" description="Check all methods by which the student may leave campus independently.">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{DISMISSAL_OPTIONS.map((o) => (
|
||||
<label key={o.value} className="flex items-center gap-2 text-sm">
|
||||
<Checkbox checked={dismissal.includes(o.value)} onCheckedChange={() => toggleDismissal(o.value)} />
|
||||
{o.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{dismissal.includes("other") && <Field label="Other (describe)"><Input value={s.dismissal_other} onChange={set("dismissal_other")} /></Field>}
|
||||
<Field label="Backup transportation plan (required if independent dismissal is allowed)"><Textarea rows={2} value={s.backup_transport_plan} onChange={set("backup_transport_plan")} /></Field>
|
||||
</Section>
|
||||
|
||||
{/* Additional */}
|
||||
<Section title="Additional information">
|
||||
<Field label="Interests, hobbies, or extracurricular activities"><Textarea rows={2} value={s.interests} onChange={set("interests")} /></Field>
|
||||
<Field label="Anything else we should know about the student"><Textarea rows={2} value={s.other_info} onChange={set("other_info")} /></Field>
|
||||
</Section>
|
||||
|
||||
{/* Agreement */}
|
||||
<Section title="Agreement & signature">
|
||||
<label className="flex items-start gap-2 text-sm">
|
||||
<Checkbox checked={agree} onCheckedChange={(v) => setAgree(!!v)} className="mt-0.5" />
|
||||
<span>I certify that the information provided is accurate and complete to the best of my knowledge, and I understand false statements may result in discontinuation of enrollment. I understand the student will only be released to authorized individuals and I am responsible for keeping this information current.</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Signed by (parent/guardian name)"><Input value={s.agreement_signed_by} onChange={set("agreement_signed_by")} /></Field>
|
||||
<Field label="Date"><Input type="date" value={s.agreement_signed_date} onChange={set("agreement_signed_date")} /></Field>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="sticky bottom-0 -mx-6 md:-mx-8 px-6 md:px-8 py-3 bg-background/90 backdrop-blur border-t flex items-center gap-3">
|
||||
<Button type="submit" disabled={!canSave}>{save.isPending ? <><Loader2 className="h-4 w-4 mr-1 animate-spin" /> Saving…</> : "Save student"}</Button>
|
||||
<Link to="/students"><Button type="button" variant="ghost">Cancel</Button></Link>
|
||||
{!s.first_name.trim() || !s.last_name.trim() ? <span className="text-xs text-muted-foreground">First and last name are required.</span> : null}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,9 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Plus, ChevronRight } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/students")({
|
||||
head: () => ({ meta: [{ title: "Students — School Portal" }] }),
|
||||
@@ -19,7 +13,6 @@ export const Route = createFileRoute("/_authenticated/students")({
|
||||
function StudentsPage() {
|
||||
const { roles } = useAuth();
|
||||
const isAdmin = roles.includes("admin");
|
||||
const qc = useQueryClient();
|
||||
|
||||
const { data: students } = useQuery({
|
||||
queryKey: ["students"],
|
||||
@@ -33,34 +26,6 @@ function StudentsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: classes } = useQuery({
|
||||
queryKey: ["classes"],
|
||||
queryFn: async () => (await supabase.from("classes").select("*").order("name")).data ?? [],
|
||||
});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form, setForm] = useState({ first_name: "", last_name: "", dob: "", class_id: "", allergies: "" });
|
||||
|
||||
const createStudent = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { error } = await supabase.from("students").insert({
|
||||
first_name: form.first_name,
|
||||
last_name: form.last_name,
|
||||
dob: form.dob || null,
|
||||
class_id: form.class_id || null,
|
||||
allergies: form.allergies || null,
|
||||
});
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Student added");
|
||||
setOpen(false);
|
||||
setForm({ first_name: "", last_name: "", dob: "", class_id: "", allergies: "" });
|
||||
qc.invalidateQueries({ queryKey: ["students"] });
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-6xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
@@ -69,28 +34,9 @@ function StudentsPage() {
|
||||
<p className="text-muted-foreground text-sm">{students?.length ?? 0} students visible to you</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild><Button><Plus className="h-4 w-4 mr-1" /> Add student</Button></DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>New student</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div><Label>First name</Label><Input value={form.first_name} onChange={(e) => setForm({ ...form, first_name: e.target.value })} /></div>
|
||||
<div><Label>Last name</Label><Input value={form.last_name} onChange={(e) => setForm({ ...form, last_name: e.target.value })} /></div>
|
||||
</div>
|
||||
<div><Label>Date of birth</Label><Input type="date" value={form.dob} onChange={(e) => setForm({ ...form, dob: e.target.value })} /></div>
|
||||
<div>
|
||||
<Label>Class</Label>
|
||||
<Select value={form.class_id} onValueChange={(v) => setForm({ ...form, class_id: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="Choose class" /></SelectTrigger>
|
||||
<SelectContent>{(classes ?? []).map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div><Label>Allergies</Label><Input value={form.allergies} onChange={(e) => setForm({ ...form, allergies: e.target.value })} placeholder="None" /></div>
|
||||
<Button className="w-full" disabled={!form.first_name || !form.last_name || createStudent.isPending} onClick={() => createStudent.mutate()}>Create student</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link to="/students/new">
|
||||
<Button><Plus className="h-4 w-4 mr-1" /> Add student</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user