-- Gradebook: weighted-category grading, per-class assignments & grades, student role. -- (Applied to the remote via the Supabase MCP; kept here as the source of record.) ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'student'; ALTER TABLE public.students ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_students_user ON public.students(user_id); CREATE OR REPLACE FUNCTION public.teaches_class(_class UUID) RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS (SELECT 1 FROM public.classes c WHERE c.id = _class AND c.teacher_id = auth.uid()) $$; CREATE OR REPLACE FUNCTION public.is_student_account(_student UUID) RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS (SELECT 1 FROM public.students s WHERE s.id = _student AND s.user_id = auth.uid()) $$; CREATE TABLE IF NOT EXISTS public.grade_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), class_id UUID REFERENCES public.classes(id) ON DELETE CASCADE, name TEXT NOT NULL, weight NUMERIC NOT NULL DEFAULT 0, sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE public.grade_categories ENABLE ROW LEVEL SECURITY; CREATE TABLE IF NOT EXISTS public.grade_scale ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), class_id UUID REFERENCES public.classes(id) ON DELETE CASCADE, letter TEXT NOT NULL, min_percent NUMERIC NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE public.grade_scale ENABLE ROW LEVEL SECURITY; CREATE TABLE IF NOT EXISTS public.assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), class_id UUID NOT NULL REFERENCES public.classes(id) ON DELETE CASCADE, category_id UUID REFERENCES public.grade_categories(id) ON DELETE SET NULL, name TEXT NOT NULL, max_points NUMERIC NOT NULL DEFAULT 100, date DATE, created_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_assignments_class ON public.assignments(class_id); ALTER TABLE public.assignments ENABLE ROW LEVEL SECURITY; CREATE TABLE IF NOT EXISTS public.grades ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), assignment_id UUID NOT NULL REFERENCES public.assignments(id) ON DELETE CASCADE, student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE, points NUMERIC, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (assignment_id, student_id) ); CREATE INDEX IF NOT EXISTS idx_grades_student ON public.grades(student_id); ALTER TABLE public.grades ENABLE ROW LEVEL SECURITY; CREATE POLICY "grade_categories read" ON public.grade_categories FOR SELECT TO authenticated USING (TRUE); CREATE POLICY "grade_categories admin" ON public.grade_categories FOR ALL TO authenticated USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin')); CREATE POLICY "grade_categories teacher" ON public.grade_categories FOR ALL TO authenticated USING (class_id IS NOT NULL AND public.teaches_class(class_id)) WITH CHECK (class_id IS NOT NULL AND public.teaches_class(class_id)); CREATE POLICY "grade_scale read" ON public.grade_scale FOR SELECT TO authenticated USING (TRUE); CREATE POLICY "grade_scale admin" ON public.grade_scale FOR ALL TO authenticated USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin')); CREATE POLICY "grade_scale teacher" ON public.grade_scale FOR ALL TO authenticated USING (class_id IS NOT NULL AND public.teaches_class(class_id)) WITH CHECK (class_id IS NOT NULL AND public.teaches_class(class_id)); CREATE POLICY "assignments read" ON public.assignments FOR SELECT TO authenticated USING ( public.current_user_has_role('admin') OR public.teaches_class(class_id) OR EXISTS (SELECT 1 FROM public.students s WHERE s.class_id = assignments.class_id AND (public.is_parent_of(s.id) OR public.is_student_account(s.id))) ); CREATE POLICY "assignments write" ON public.assignments FOR ALL TO authenticated USING (public.current_user_has_role('admin') OR public.teaches_class(class_id)) WITH CHECK (public.current_user_has_role('admin') OR public.teaches_class(class_id)); CREATE POLICY "grades read" ON public.grades FOR SELECT TO authenticated USING ( public.current_user_has_role('admin') OR public.is_parent_of(student_id) OR public.is_student_account(student_id) OR EXISTS (SELECT 1 FROM public.assignments a WHERE a.id = grades.assignment_id AND public.teaches_class(a.class_id)) ); CREATE POLICY "grades write" ON public.grades FOR ALL TO authenticated USING (public.current_user_has_role('admin') OR EXISTS (SELECT 1 FROM public.assignments a WHERE a.id = grades.assignment_id AND public.teaches_class(a.class_id))) WITH CHECK (public.current_user_has_role('admin') OR EXISTS (SELECT 1 FROM public.assignments a WHERE a.id = grades.assignment_id AND public.teaches_class(a.class_id))); INSERT INTO public.grade_categories (class_id, name, weight, sort_order) SELECT v.class_id, v.name, v.weight, v.sort_order FROM (VALUES (NULL::UUID, 'Homework', 20::NUMERIC, 0), (NULL, 'Quizzes', 30, 1), (NULL, 'Tests', 50, 2)) AS v(class_id, name, weight, sort_order) WHERE NOT EXISTS (SELECT 1 FROM public.grade_categories WHERE class_id IS NULL); INSERT INTO public.grade_scale (class_id, letter, min_percent) SELECT v.class_id, v.letter, v.min_percent FROM (VALUES (NULL::UUID, 'A', 90::NUMERIC), (NULL, 'B', 80), (NULL, 'C', 70), (NULL, 'D', 60), (NULL, 'F', 0)) AS v(class_id, letter, min_percent) WHERE NOT EXISTS (SELECT 1 FROM public.grade_scale WHERE class_id IS NULL);