-- 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;