- /students/new: quick create (name + DOB) -> redirects into the profile - /students/$id: full editable profile with photo upload, and tabs for Profile / Family & pickup (guardians, emergency contacts, pickups) / Academics (curriculum logins + records) / Attendance / Tuition / Contracts - Migration: students.photo_path + private student-photos storage bucket (admin write, linked-user read); regenerate types Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
3.1 KiB
TypeScript
68 lines
3.1 KiB
TypeScript
import { createFileRoute, useNavigate, Link } from "@tanstack/react-router";
|
|
import { 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 { ArrowLeft, 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,
|
|
});
|
|
|
|
function NewStudentPage() {
|
|
const { roles } = useAuth();
|
|
const navigate = useNavigate();
|
|
const qc = useQueryClient();
|
|
const [firstName, setFirstName] = useState("");
|
|
const [lastName, setLastName] = useState("");
|
|
const [dob, setDob] = useState("");
|
|
|
|
const create = useMutation({
|
|
mutationFn: async () => {
|
|
const { data, error } = await supabase
|
|
.from("students")
|
|
.insert({ first_name: firstName.trim(), last_name: lastName.trim(), dob: dob || null })
|
|
.select("id")
|
|
.single();
|
|
if (error) throw error;
|
|
return data.id as string;
|
|
},
|
|
onSuccess: (id) => {
|
|
qc.invalidateQueries({ queryKey: ["students"] });
|
|
toast.success("Student created — fill out the rest of the profile");
|
|
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 = firstName.trim() && lastName.trim() && !create.isPending;
|
|
|
|
return (
|
|
<div className="p-8 max-w-lg">
|
|
<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">Start with a name — you'll fill out the full profile (or send it to a parent) next.</p>
|
|
|
|
<form onSubmit={(e) => { e.preventDefault(); if (canSave) create.mutate(); }} className="bg-card border rounded-lg p-5 space-y-4">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div><Label>First name</Label><Input value={firstName} onChange={(e) => setFirstName(e.target.value)} required autoFocus /></div>
|
|
<div><Label>Last name</Label><Input value={lastName} onChange={(e) => setLastName(e.target.value)} required /></div>
|
|
</div>
|
|
<div><Label>Date of birth (optional)</Label><Input type="date" value={dob} onChange={(e) => setDob(e.target.value)} /></div>
|
|
<Button type="submit" disabled={!canSave} className="w-full">
|
|
{create.isPending ? <><Loader2 className="h-4 w-4 mr-1 animate-spin" /> Creating…</> : "Create profile"}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|