diff --git a/supabase/migrations/20260807001100_reporting_views.sql b/supabase/migrations/20260807001100_reporting_views.sql deleted file mode 100644 index 7c46f94..0000000 --- a/supabase/migrations/20260807001100_reporting_views.sql +++ /dev/null @@ -1,232 +0,0 @@ --- Reporting and dashboards — spec section 14. --- --- Every view is declared WITH (security_invoker = true). Without it a view runs --- with the privileges of its owner and would hand any authenticated caller the --- whole table, quietly defeating the row-level security the rest of this build --- depends on. With it, a teacher querying v_student_directory sees exactly the --- students their own policies allow. - --- ============================================================================ --- STUDENTS --- ============================================================================ - -CREATE OR REPLACE VIEW public.v_student_directory -WITH (security_invoker = true) AS -SELECT - s.id AS student_id, - s.first_name, - s.middle_name, - s.last_name, - s.preferred_name, - s.dob, - s.grade_level, - s.enrollment_status, - s.attendance_basis, - s.calendar_basis, - s.is_support_student, - c.id AS primary_campus_id, - c.name AS primary_campus, - t.name AS tuition_tier, - hs.household_id, - h.name AS household_name, - -- Campuses beyond the primary one, for the multi-campus case. - (SELECT array_agg(DISTINCT cc.name) - FROM public.student_campus_schedules sc - JOIN public.campuses cc ON cc.id = sc.campus_id - WHERE sc.student_id = s.id - AND (sc.effective_end IS NULL OR sc.effective_end >= CURRENT_DATE) - ) AS scheduled_campuses, - (SELECT COALESCE(SUM(sc.scheduled_days_per_week), 0) - FROM public.student_campus_schedules sc - WHERE sc.student_id = s.id - AND sc.effective_start <= CURRENT_DATE - AND (sc.effective_end IS NULL OR sc.effective_end >= CURRENT_DATE) - ) AS scheduled_days_per_week, - (SELECT COUNT(*) FROM public.student_alerts a - WHERE a.student_id = s.id AND a.is_active - AND a.effective_date <= CURRENT_DATE - AND (a.expiration_date IS NULL OR a.expiration_date >= CURRENT_DATE) - ) AS active_alert_count -FROM public.students s -LEFT JOIN public.campuses c ON c.id = s.primary_campus_id -LEFT JOIN public.tuition_tiers t ON t.id = s.tuition_tier_id -LEFT JOIN public.household_students hs - ON hs.student_id = s.id AND hs.is_primary_household -LEFT JOIN public.households h ON h.id = hs.household_id; - -GRANT SELECT ON public.v_student_directory TO authenticated; - --- ============================================================================ --- BILLING --- ============================================================================ - -CREATE OR REPLACE VIEW public.v_outstanding_invoices -WITH (security_invoker = true) AS -SELECT - i.id AS invoice_id, - i.invoice_number, - i.student_id, - s.first_name || ' ' || s.last_name AS student_name, - i.household_id, - i.campus_id, - c.name AS campus_name, - i.billing_period_start, - i.billing_period_end, - i.invoice_date, - i.due_date, - i.total_cents, - i.amount_paid_cents, - i.balance_due_cents, - i.status, - CURRENT_DATE - i.due_date AS days_overdue -FROM public.invoices i -JOIN public.students s ON s.id = i.student_id -LEFT JOIN public.campuses c ON c.id = i.campus_id -WHERE i.status IN ('issued','partially_paid') - AND i.balance_due_cents > 0; - -GRANT SELECT ON public.v_outstanding_invoices TO authenticated; - --- Combined household position — the spec's "combined household account". -CREATE OR REPLACE VIEW public.v_household_balances -WITH (security_invoker = true) AS -SELECT - h.id AS household_id, - h.name AS household_name, - COUNT(DISTINCT hs.student_id) AS student_count, - COALESCE(SUM(i.total_cents), 0) AS invoiced_cents, - COALESCE(SUM(i.amount_paid_cents), 0) AS paid_cents, - COALESCE(SUM(i.balance_due_cents) FILTER ( - WHERE i.status IN ('issued','partially_paid')), 0) AS balance_due_cents -FROM public.households h -LEFT JOIN public.household_students hs ON hs.household_id = h.id -LEFT JOIN public.invoices i ON i.household_id = h.id AND i.status <> 'void' -GROUP BY h.id, h.name; - -GRANT SELECT ON public.v_household_balances TO authenticated; - --- ============================================================================ --- CAMPUS + ATTENDANCE --- ============================================================================ - -CREATE OR REPLACE VIEW public.v_campus_enrollment -WITH (security_invoker = true) AS -SELECT - c.id AS campus_id, - c.name AS campus_name, - c.is_active, - c.student_capacity, - COUNT(DISTINCT s.id) FILTER (WHERE s.enrollment_status = 'enrolled') AS enrolled_count, - COUNT(DISTINCT s.id) FILTER ( - WHERE s.enrollment_status = 'enrolled' AND s.attendance_basis = 'full_time') AS full_time_count, - COUNT(DISTINCT s.id) FILTER ( - WHERE s.enrollment_status = 'enrolled' AND s.attendance_basis = 'part_time') AS part_time_count, - COUNT(DISTINCT s.id) FILTER ( - WHERE s.enrollment_status = 'enrolled' AND s.is_support_student) AS support_count, - CASE WHEN c.student_capacity IS NULL OR c.student_capacity = 0 THEN NULL - ELSE ROUND(COUNT(DISTINCT s.id) FILTER (WHERE s.enrollment_status = 'enrolled')::numeric - * 100.0 / c.student_capacity, 1) - END AS capacity_used_percent -FROM public.campuses c -LEFT JOIN public.students s ON s.primary_campus_id = c.id -GROUP BY c.id, c.name, c.is_active, c.student_capacity; - -GRANT SELECT ON public.v_campus_enrollment TO authenticated; - -CREATE OR REPLACE VIEW public.v_attendance_summary -WITH (security_invoker = true) AS -SELECT - a.student_id, - s.first_name || ' ' || s.last_name AS student_name, - s.primary_campus_id, - date_trunc('month', a.date)::date AS month, - COUNT(*) AS recorded_days, - COUNT(*) FILTER (WHERE a.status = 'present') AS present_days, - COUNT(*) FILTER (WHERE a.status = 'absent') AS absent_days, - COUNT(*) FILTER (WHERE a.status = 'late') AS late_days, - COUNT(*) FILTER (WHERE a.status = 'excused') AS excused_days, - ROUND(COUNT(*) FILTER (WHERE a.status IN ('present','late'))::numeric - * 100.0 / NULLIF(COUNT(*), 0), 1) AS attendance_rate_percent -FROM public.attendance a -JOIN public.students s ON s.id = a.student_id -GROUP BY a.student_id, s.first_name, s.last_name, s.primary_campus_id, date_trunc('month', a.date); - -GRANT SELECT ON public.v_attendance_summary TO authenticated; - --- ============================================================================ --- COMPLIANCE + MANAGEMENT --- ============================================================================ - -CREATE OR REPLACE VIEW public.v_certification_expiry -WITH (security_invoker = true) AS -SELECT - sc.id AS certification_id, - sc.user_id, - p.full_name AS staff_name, - ct.name AS certification_name, - sc.issued_on, - sc.expires_on, - sc.status, - sc.expires_on - CURRENT_DATE AS days_until_expiry, - CASE - WHEN sc.expires_on IS NULL THEN 'no_expiry' - WHEN sc.expires_on < CURRENT_DATE THEN 'expired' - WHEN sc.expires_on < CURRENT_DATE + make_interval(days => ct.renewal_reminder_days) - THEN 'expiring_soon' - ELSE 'current' - END AS expiry_state -FROM public.staff_certifications sc -JOIN public.certification_types ct ON ct.id = sc.certification_type_id -LEFT JOIN public.profiles p ON p.id = sc.user_id -WHERE sc.status IN ('active','pending'); - -GRANT SELECT ON public.v_certification_expiry TO authenticated; - --- The management dashboard's task queue. -CREATE OR REPLACE VIEW public.v_open_tasks -WITH (security_invoker = true) AS -SELECT - t.id AS task_id, - t.title, - t.priority, - t.status, - t.due_date, - t.campus_id, - c.name AS campus_name, - t.assigned_to, - p.full_name AS assigned_to_name, - t.related_student_id, - CASE - WHEN t.due_date IS NULL THEN 'no_due_date' - WHEN t.due_date < CURRENT_DATE THEN 'overdue' - WHEN t.due_date <= CURRENT_DATE + 7 THEN 'due_soon' - ELSE 'scheduled' - END AS due_state -FROM public.tasks t -LEFT JOIN public.campuses c ON c.id = t.campus_id -LEFT JOIN public.profiles p ON p.id = t.assigned_to -WHERE t.status IN ('open','in_progress','blocked'); - -GRANT SELECT ON public.v_open_tasks TO authenticated; - --- Per-student follow-ups still owed, from the notes system. -CREATE OR REPLACE VIEW public.v_pending_followups -WITH (security_invoker = true) AS -SELECT - n.id AS note_id, - n.student_id, - s.first_name || ' ' || s.last_name AS student_name, - nc.name AS category, - n.title, - n.follow_up_date, - n.resolution_status, - n.author_id, - n.campus_id, - CURRENT_DATE - n.follow_up_date AS days_overdue -FROM public.student_notes n -JOIN public.students s ON s.id = n.student_id -JOIN public.note_categories nc ON nc.id = n.category_id -WHERE n.follow_up_date IS NOT NULL - AND n.resolution_status IN ('open','in_progress'); - -GRANT SELECT ON public.v_pending_followups TO authenticated;