diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts
index 39f3455..5b3c21a 100644
--- a/src/integrations/supabase/types.ts
+++ b/src/integrations/supabase/types.ts
@@ -12,6 +12,31 @@ export type Database = {
__InternalSupabase: {
PostgrestVersion: "14.5"
}
+ graphql_public: {
+ Tables: {
+ [_ in never]: never
+ }
+ Views: {
+ [_ in never]: never
+ }
+ Functions: {
+ graphql: {
+ Args: {
+ extensions?: Json
+ operationName?: string
+ query?: string
+ variables?: Json
+ }
+ Returns: Json
+ }
+ }
+ Enums: {
+ [_ in never]: never
+ }
+ CompositeTypes: {
+ [_ in never]: never
+ }
+ }
public: {
Tables: {
attendance: {
@@ -54,27 +79,39 @@ export type Database = {
}
authorized_pickups: {
Row: {
+ alt_phone: string | null
created_at: string
id: string
+ kind: string
name: string
+ notes: string | null
phone: string | null
relationship: string | null
+ sort_order: number
student_id: string
}
Insert: {
+ alt_phone?: string | null
created_at?: string
id?: string
+ kind?: string
name: string
+ notes?: string | null
phone?: string | null
relationship?: string | null
+ sort_order?: number
student_id: string
}
Update: {
+ alt_phone?: string | null
created_at?: string
id?: string
+ kind?: string
name?: string
+ notes?: string | null
phone?: string | null
relationship?: string | null
+ sort_order?: number
student_id?: string
}
Relationships: [
@@ -415,41 +452,195 @@ export type Database = {
}
Relationships: []
}
+ student_curriculum_logins: {
+ Row: {
+ created_at: string
+ id: string
+ parent_account: string | null
+ parent_password: string | null
+ student_id: string
+ student_login: string | null
+ student_password: string | null
+ website: string | null
+ }
+ Insert: {
+ created_at?: string
+ id?: string
+ parent_account?: string | null
+ parent_password?: string | null
+ student_id: string
+ student_login?: string | null
+ student_password?: string | null
+ website?: string | null
+ }
+ Update: {
+ created_at?: string
+ id?: string
+ parent_account?: string | null
+ parent_password?: string | null
+ student_id?: string
+ student_login?: string | null
+ student_password?: string | null
+ website?: string | null
+ }
+ Relationships: [
+ {
+ foreignKeyName: "student_curriculum_logins_student_id_fkey"
+ columns: ["student_id"]
+ isOneToOne: false
+ referencedRelation: "students"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ student_guardians: {
+ Row: {
+ address: string | null
+ city: string | null
+ created_at: string
+ email: string | null
+ employer: string | null
+ full_name: string
+ id: string
+ is_primary: boolean
+ phone: string | null
+ relationship: string | null
+ state: string | null
+ student_id: string
+ zip: string | null
+ }
+ Insert: {
+ address?: string | null
+ city?: string | null
+ created_at?: string
+ email?: string | null
+ employer?: string | null
+ full_name: string
+ id?: string
+ is_primary?: boolean
+ phone?: string | null
+ relationship?: string | null
+ state?: string | null
+ student_id: string
+ zip?: string | null
+ }
+ Update: {
+ address?: string | null
+ city?: string | null
+ created_at?: string
+ email?: string | null
+ employer?: string | null
+ full_name?: string
+ id?: string
+ is_primary?: boolean
+ phone?: string | null
+ relationship?: string | null
+ state?: string | null
+ student_id?: string
+ zip?: string | null
+ }
+ Relationships: [
+ {
+ foreignKeyName: "student_guardians_student_id_fkey"
+ columns: ["student_id"]
+ isOneToOne: false
+ referencedRelation: "students"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
students: {
Row: {
+ agreement_signed_by: string | null
+ agreement_signed_date: string | null
allergies: string | null
+ backup_transport_plan: string | null
+ chronic_conditions: string | null
class_id: string | null
created_at: string
+ custody_agreement: string | null
+ disciplinary_history: string | null
+ dismissal_methods: string[]
+ dismissal_other: string | null
dob: string | null
first_name: string
+ gender: string | null
+ grade_level: string | null
+ home_ed_eval_due: string | null
id: string
+ interests: string | null
last_name: string
notes: string | null
+ other_info: string | null
photo_release: boolean
+ physician_phone: string | null
+ portfolio_keeper: string | null
+ preferred_start_date: string | null
+ previous_schools: string | null
+ primary_physician: string | null
+ special_needs: string | null
updated_at: string
}
Insert: {
+ agreement_signed_by?: string | null
+ agreement_signed_date?: string | null
allergies?: string | null
+ backup_transport_plan?: string | null
+ chronic_conditions?: string | null
class_id?: string | null
created_at?: string
+ custody_agreement?: string | null
+ disciplinary_history?: string | null
+ dismissal_methods?: string[]
+ dismissal_other?: string | null
dob?: string | null
first_name: string
+ gender?: string | null
+ grade_level?: string | null
+ home_ed_eval_due?: string | null
id?: string
+ interests?: string | null
last_name: string
notes?: string | null
+ other_info?: string | null
photo_release?: boolean
+ physician_phone?: string | null
+ portfolio_keeper?: string | null
+ preferred_start_date?: string | null
+ previous_schools?: string | null
+ primary_physician?: string | null
+ special_needs?: string | null
updated_at?: string
}
Update: {
+ agreement_signed_by?: string | null
+ agreement_signed_date?: string | null
allergies?: string | null
+ backup_transport_plan?: string | null
+ chronic_conditions?: string | null
class_id?: string | null
created_at?: string
+ custody_agreement?: string | null
+ disciplinary_history?: string | null
+ dismissal_methods?: string[]
+ dismissal_other?: string | null
dob?: string | null
first_name?: string
+ gender?: string | null
+ grade_level?: string | null
+ home_ed_eval_due?: string | null
id?: string
+ interests?: string | null
last_name?: string
notes?: string | null
+ other_info?: string | null
photo_release?: boolean
+ physician_phone?: string | null
+ portfolio_keeper?: string | null
+ preferred_start_date?: string | null
+ previous_schools?: string | null
+ primary_physician?: string | null
+ special_needs?: string | null
updated_at?: string
}
Relationships: [
@@ -659,6 +850,9 @@ export type CompositeTypes<
: never
export const Constants = {
+ graphql_public: {
+ Enums: {},
+ },
public: {
Enums: {
app_role: ["admin", "teacher", "parent"],
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index b4cf9a0..9847ac5 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -20,6 +20,7 @@ import { Route as AuthenticatedDashboardRouteImport } from './routes/_authentica
import { Route as AuthenticatedCalendarRouteImport } from './routes/_authenticated/calendar'
import { Route as AuthenticatedAttendanceRouteImport } from './routes/_authenticated/attendance'
import { Route as AuthenticatedAdminRouteImport } from './routes/_authenticated/admin'
+import { Route as AuthenticatedStudentsNewRouteImport } from './routes/_authenticated/students.new'
import { Route as AuthenticatedStudentsIdRouteImport } from './routes/_authenticated/students.$id'
const AuthRoute = AuthRouteImport.update({
@@ -76,6 +77,12 @@ const AuthenticatedAdminRoute = AuthenticatedAdminRouteImport.update({
path: '/admin',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
+const AuthenticatedStudentsNewRoute =
+ AuthenticatedStudentsNewRouteImport.update({
+ id: '/new',
+ path: '/new',
+ getParentRoute: () => AuthenticatedStudentsRoute,
+ } as any)
const AuthenticatedStudentsIdRoute = AuthenticatedStudentsIdRouteImport.update({
id: '/$id',
path: '/$id',
@@ -94,6 +101,7 @@ export interface FileRoutesByFullPath {
'/messages': typeof AuthenticatedMessagesRoute
'/students': typeof AuthenticatedStudentsRouteWithChildren
'/students/$id': typeof AuthenticatedStudentsIdRoute
+ '/students/new': typeof AuthenticatedStudentsNewRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
@@ -107,6 +115,7 @@ export interface FileRoutesByTo {
'/messages': typeof AuthenticatedMessagesRoute
'/students': typeof AuthenticatedStudentsRouteWithChildren
'/students/$id': typeof AuthenticatedStudentsIdRoute
+ '/students/new': typeof AuthenticatedStudentsNewRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -122,6 +131,7 @@ export interface FileRoutesById {
'/_authenticated/messages': typeof AuthenticatedMessagesRoute
'/_authenticated/students': typeof AuthenticatedStudentsRouteWithChildren
'/_authenticated/students/$id': typeof AuthenticatedStudentsIdRoute
+ '/_authenticated/students/new': typeof AuthenticatedStudentsNewRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -137,6 +147,7 @@ export interface FileRouteTypes {
| '/messages'
| '/students'
| '/students/$id'
+ | '/students/new'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -150,6 +161,7 @@ export interface FileRouteTypes {
| '/messages'
| '/students'
| '/students/$id'
+ | '/students/new'
id:
| '__root__'
| '/'
@@ -164,6 +176,7 @@ export interface FileRouteTypes {
| '/_authenticated/messages'
| '/_authenticated/students'
| '/_authenticated/students/$id'
+ | '/_authenticated/students/new'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -251,6 +264,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedAdminRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
+ '/_authenticated/students/new': {
+ id: '/_authenticated/students/new'
+ path: '/new'
+ fullPath: '/students/new'
+ preLoaderRoute: typeof AuthenticatedStudentsNewRouteImport
+ parentRoute: typeof AuthenticatedStudentsRoute
+ }
'/_authenticated/students/$id': {
id: '/_authenticated/students/$id'
path: '/$id'
@@ -263,10 +283,12 @@ declare module '@tanstack/react-router' {
interface AuthenticatedStudentsRouteChildren {
AuthenticatedStudentsIdRoute: typeof AuthenticatedStudentsIdRoute
+ AuthenticatedStudentsNewRoute: typeof AuthenticatedStudentsNewRoute
}
const AuthenticatedStudentsRouteChildren: AuthenticatedStudentsRouteChildren = {
AuthenticatedStudentsIdRoute: AuthenticatedStudentsIdRoute,
+ AuthenticatedStudentsNewRoute: AuthenticatedStudentsNewRoute,
}
const AuthenticatedStudentsRouteWithChildren =
diff --git a/src/routes/_authenticated/students.new.tsx b/src/routes/_authenticated/students.new.tsx
new file mode 100644
index 0000000..5884648
--- /dev/null
+++ b/src/routes/_authenticated/students.new.tsx
@@ -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
{label} {children}
;
+}
+function Section({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
+ return (
+
+
+
{title}
+ {description &&
{description}
}
+
+ {children}
+
+ );
+}
+
+// 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) => setG({ ...g, [k]: e.target.value });
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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) => setS({ ...s, [k]: e.target.value });
+
+ const [primary, setPrimary] = useState({ ...emptyGuardian });
+ const [secondary, setSecondary] = useState({ ...emptyGuardian });
+ const [contacts, setContacts] = useState([
+ { name: "", relationship: "", phone: "", alt_phone: "" },
+ { name: "", relationship: "", phone: "", alt_phone: "" },
+ ]);
+ const [pickups, setPickups] = useState([
+ { name: "", relationship: "", phone: "", notes: "" },
+ { name: "", relationship: "", phone: "", notes: "" },
+ { name: "", relationship: "", phone: "", notes: "" },
+ ]);
+ const [logins, setLogins] = useState([{ ...emptyLogin }]);
+ const [dismissal, setDismissal] = useState([]);
+ 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 Admins only Only admins can add students.
;
+ }
+
+ const canSave = s.first_name.trim() && s.last_name.trim() && !save.isPending;
+
+ return (
+
+
Back to students
+
New student
+
Student Information & Authorized Pick-Up — Bayside Academy
+
+
+
+ );
+}
diff --git a/src/routes/_authenticated/students.tsx b/src/routes/_authenticated/students.tsx
index 4476b52..d851b9e 100644
--- a/src/routes/_authenticated/students.tsx
+++ b/src/routes/_authenticated/students.tsx
@@ -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 (
@@ -69,28 +34,9 @@ function StudentsPage() {
{students?.length ?? 0} students visible to you
{isAdmin && (
-
- Add student
-
- New student
-
-
-
Date of birth setForm({ ...form, dob: e.target.value })} />
-
- Class
- setForm({ ...form, class_id: v })}>
-
- {(classes ?? []).map((c) => {c.name} )}
-
-
-
Allergies setForm({ ...form, allergies: e.target.value })} placeholder="None" />
-
createStudent.mutate()}>Create student
-
-
-
+
+
Add student
+
)}
diff --git a/supabase/migrations/20260719000000_expand_student_intake.sql b/supabase/migrations/20260719000000_expand_student_intake.sql
new file mode 100644
index 0000000..35509be
--- /dev/null
+++ b/supabase/migrations/20260719000000_expand_student_intake.sql
@@ -0,0 +1,89 @@
+-- Expand student intake to match Bayside Academy's paper forms:
+-- Student Information Form + Authorized Persons Pick-Up Form.
+
+-- ── Extra columns on students ────────────────────────────────────────────────
+ALTER TABLE public.students
+ ADD COLUMN IF NOT EXISTS gender TEXT,
+ ADD COLUMN IF NOT EXISTS grade_level TEXT,
+ ADD COLUMN IF NOT EXISTS preferred_start_date DATE,
+ -- health (allergies TEXT already exists)
+ ADD COLUMN IF NOT EXISTS chronic_conditions TEXT,
+ ADD COLUMN IF NOT EXISTS primary_physician TEXT,
+ ADD COLUMN IF NOT EXISTS physician_phone TEXT,
+ -- academic
+ ADD COLUMN IF NOT EXISTS home_ed_eval_due DATE,
+ ADD COLUMN IF NOT EXISTS previous_schools TEXT,
+ ADD COLUMN IF NOT EXISTS special_needs TEXT,
+ ADD COLUMN IF NOT EXISTS portfolio_keeper TEXT, -- 'parent' | 'bayside'
+ ADD COLUMN IF NOT EXISTS disciplinary_history TEXT,
+ ADD COLUMN IF NOT EXISTS custody_agreement TEXT, -- explanation; blank = none
+ -- independent dismissal
+ ADD COLUMN IF NOT EXISTS dismissal_methods TEXT[] NOT NULL DEFAULT '{}',
+ ADD COLUMN IF NOT EXISTS dismissal_other TEXT,
+ ADD COLUMN IF NOT EXISTS backup_transport_plan TEXT,
+ -- additional
+ ADD COLUMN IF NOT EXISTS interests TEXT,
+ ADD COLUMN IF NOT EXISTS other_info TEXT,
+ -- agreement
+ ADD COLUMN IF NOT EXISTS agreement_signed_by TEXT,
+ ADD COLUMN IF NOT EXISTS agreement_signed_date DATE;
+
+-- ── Primary / secondary guardians ────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.student_guardians (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
+ is_primary BOOLEAN NOT NULL DEFAULT TRUE,
+ full_name TEXT NOT NULL,
+ relationship TEXT,
+ phone TEXT,
+ address TEXT,
+ city TEXT,
+ state TEXT,
+ zip TEXT,
+ email TEXT,
+ employer TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_student_guardians_student ON public.student_guardians(student_id);
+ALTER TABLE public.student_guardians ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "guardians read" ON public.student_guardians FOR SELECT TO authenticated
+ USING (
+ public.current_user_has_role('admin')
+ OR public.is_parent_of(student_id)
+ OR public.teaches_student(student_id)
+ );
+CREATE POLICY "guardians write parent or admin" ON public.student_guardians FOR ALL TO authenticated
+ USING (public.current_user_has_role('admin') OR public.is_parent_of(student_id))
+ WITH CHECK (public.current_user_has_role('admin') OR public.is_parent_of(student_id));
+
+-- ── Curriculum platform logins (repeatable) ──────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.student_curriculum_logins (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
+ website TEXT,
+ student_login TEXT,
+ student_password TEXT,
+ parent_account TEXT,
+ parent_password TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_curriculum_logins_student ON public.student_curriculum_logins(student_id);
+ALTER TABLE public.student_curriculum_logins ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "curriculum read" ON public.student_curriculum_logins FOR SELECT TO authenticated
+ USING (
+ public.current_user_has_role('admin')
+ OR public.is_parent_of(student_id)
+ OR public.teaches_student(student_id)
+ );
+CREATE POLICY "curriculum write parent or admin" ON public.student_curriculum_logins FOR ALL TO authenticated
+ USING (public.current_user_has_role('admin') OR public.is_parent_of(student_id))
+ WITH CHECK (public.current_user_has_role('admin') OR public.is_parent_of(student_id));
+
+-- ── Authorized pickups: also hold emergency contacts + notes ─────────────────
+ALTER TABLE public.authorized_pickups
+ ADD COLUMN IF NOT EXISTS alt_phone TEXT,
+ ADD COLUMN IF NOT EXISTS notes TEXT,
+ ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'pickup', -- 'pickup' | 'emergency'
+ ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0;