Profile model: quick-create + full editable student profile

- /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>
This commit is contained in:
2026-07-19 16:13:36 -04:00
co-authored by Claude Opus 4.8
parent 9d5b11bb10
commit 48cf441cba
4 changed files with 449 additions and 410 deletions
@@ -0,0 +1,26 @@
-- Student photo: a column for the stored path + a private storage bucket.
ALTER TABLE public.students ADD COLUMN IF NOT EXISTS photo_path TEXT;
INSERT INTO storage.buckets (id, name, public)
VALUES ('student-photos', 'student-photos', false)
ON CONFLICT (id) DO NOTHING;
-- Admins upload/replace/remove photos.
CREATE POLICY "student photos admin write" ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'student-photos' AND public.current_user_has_role('admin'));
CREATE POLICY "student photos admin update" ON storage.objects FOR UPDATE TO authenticated
USING (bucket_id = 'student-photos' AND public.current_user_has_role('admin'));
CREATE POLICY "student photos admin delete" ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'student-photos' AND public.current_user_has_role('admin'));
-- Read: admins, the student's teacher, or the student's parent.
-- Path convention is "<student_id>/<filename>", so folder[1] is the student id.
CREATE POLICY "student photos read" ON storage.objects FOR SELECT TO authenticated
USING (
bucket_id = 'student-photos' AND (
public.current_user_has_role('admin')
OR public.is_parent_of(((storage.foldername(name))[1])::uuid)
OR public.teaches_student(((storage.foldername(name))[1])::uuid)
)
);