-- ===================================================== -- Module Statistics RPCs -- -- A) Course statistics — counts per status, participants, -- average occupancy, total revenue -- B) Event statistics — counts, upcoming/past, registrations, -- average occupancy -- C) Booking statistics — counts, revenue, avg stay, -- occupancy rate for a date range -- D) Event registration counts — batch lookup replacing -- N+1 JS iteration -- ===================================================== -- ------------------------------------------------------- -- A) Course statistics -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_course_statistics(p_account_id uuid) RETURNS TABLE ( total_courses bigint, open_courses bigint, running_courses bigint, completed_courses bigint, cancelled_courses bigint, total_participants bigint, total_waitlisted bigint, avg_occupancy_rate numeric, total_revenue numeric ) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$ BEGIN -- Access check IF NOT public.has_role_on_account(p_account_id) THEN RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501'; END IF; RETURN QUERY WITH course_stats AS ( SELECT count(*)::bigint AS total_courses, count(*) FILTER (WHERE c.status = 'open')::bigint AS open_courses, count(*) FILTER (WHERE c.status = 'running')::bigint AS running_courses, count(*) FILTER (WHERE c.status = 'completed')::bigint AS completed_courses, count(*) FILTER (WHERE c.status = 'cancelled')::bigint AS cancelled_courses FROM public.courses c WHERE c.account_id = p_account_id ), participant_stats AS ( SELECT count(*) FILTER (WHERE cp.status = 'enrolled')::bigint AS total_participants, count(*) FILTER (WHERE cp.status = 'waitlisted')::bigint AS total_waitlisted FROM public.course_participants cp JOIN public.courses c ON c.id = cp.course_id WHERE c.account_id = p_account_id ), occupancy_stats AS ( SELECT ROUND( AVG( CASE WHEN c.capacity > 0 THEN enrolled_ct::numeric / c.capacity * 100 ELSE 0 END ), 1 ) AS avg_occupancy_rate FROM public.courses c LEFT JOIN LATERAL ( SELECT count(*)::numeric AS enrolled_ct FROM public.course_participants cp WHERE cp.course_id = c.id AND cp.status = 'enrolled' ) ec ON true WHERE c.account_id = p_account_id AND c.status != 'cancelled' ), revenue_stats AS ( SELECT COALESCE(SUM(c.fee * enrolled_ct), 0)::numeric AS total_revenue FROM public.courses c LEFT JOIN LATERAL ( SELECT count(*)::numeric AS enrolled_ct FROM public.course_participants cp WHERE cp.course_id = c.id AND cp.status IN ('enrolled', 'completed') ) ec ON true WHERE c.account_id = p_account_id AND c.status != 'cancelled' ) SELECT cs.total_courses, cs.open_courses, cs.running_courses, cs.completed_courses, cs.cancelled_courses, ps.total_participants, ps.total_waitlisted, os.avg_occupancy_rate, rs.total_revenue FROM course_stats cs CROSS JOIN participant_stats ps CROSS JOIN occupancy_stats os CROSS JOIN revenue_stats rs; END; $$; GRANT EXECUTE ON FUNCTION public.get_course_statistics(uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.get_course_statistics(uuid) TO service_role; -- ------------------------------------------------------- -- B) Event statistics -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_event_statistics(p_account_id uuid) RETURNS TABLE ( total_events bigint, upcoming_events bigint, past_events bigint, total_registrations bigint, avg_occupancy_rate numeric ) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$ BEGIN -- Access check IF NOT public.has_role_on_account(p_account_id) THEN RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501'; END IF; RETURN QUERY WITH event_counts AS ( SELECT count(*)::bigint AS total_events, count(*) FILTER ( WHERE e.event_date >= current_date AND e.status NOT IN ('cancelled', 'completed') )::bigint AS upcoming_events, count(*) FILTER ( WHERE e.event_date < current_date OR e.status IN ('completed') )::bigint AS past_events FROM public.events e WHERE e.account_id = p_account_id ), reg_counts AS ( SELECT count(*)::bigint AS total_registrations FROM public.event_registrations er JOIN public.events e ON e.id = er.event_id WHERE e.account_id = p_account_id AND er.status IN ('confirmed', 'pending') ), occupancy AS ( SELECT ROUND( AVG( CASE WHEN e.capacity IS NOT NULL AND e.capacity > 0 THEN reg_ct::numeric / e.capacity * 100 ELSE NULL END ), 1 ) AS avg_occupancy_rate FROM public.events e LEFT JOIN LATERAL ( SELECT count(*)::numeric AS reg_ct FROM public.event_registrations er WHERE er.event_id = e.id AND er.status IN ('confirmed', 'pending') ) rc ON true WHERE e.account_id = p_account_id AND e.status != 'cancelled' ) SELECT ec.total_events, ec.upcoming_events, ec.past_events, rc.total_registrations, COALESCE(occ.avg_occupancy_rate, 0)::numeric FROM event_counts ec CROSS JOIN reg_counts rc CROSS JOIN occupancy occ; END; $$; GRANT EXECUTE ON FUNCTION public.get_event_statistics(uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.get_event_statistics(uuid) TO service_role; -- ------------------------------------------------------- -- C) Booking statistics -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_booking_statistics( p_account_id uuid, p_from date DEFAULT NULL, p_to date DEFAULT NULL ) RETURNS TABLE ( total_bookings bigint, active_bookings bigint, checked_in_count bigint, total_revenue numeric, avg_stay_nights numeric, occupancy_rate numeric ) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_from date; v_to date; v_total_rooms bigint; v_total_room_nights numeric; v_booked_room_nights numeric; BEGIN -- Access check IF NOT public.has_role_on_account(p_account_id) THEN RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501'; END IF; -- Default date range: current month v_from := COALESCE(p_from, date_trunc('month', current_date)::date); v_to := COALESCE(p_to, (date_trunc('month', current_date) + interval '1 month' - interval '1 day')::date); -- Calculate total available room-nights SELECT count(*)::bigint INTO v_total_rooms FROM public.rooms WHERE account_id = p_account_id AND is_active = true; v_total_room_nights := v_total_rooms::numeric * (v_to - v_from + 1); -- Calculate booked room-nights in range (non-cancelled) SELECT COALESCE(SUM( LEAST(b.check_out, v_to + 1) - GREATEST(b.check_in, v_from) ), 0)::numeric INTO v_booked_room_nights FROM public.bookings b WHERE b.account_id = p_account_id AND b.status NOT IN ('cancelled', 'no_show') AND b.check_in <= v_to AND b.check_out > v_from; RETURN QUERY SELECT count(*)::bigint AS total_bookings, count(*) FILTER (WHERE b.status IN ('confirmed', 'checked_in'))::bigint AS active_bookings, count(*) FILTER (WHERE b.status = 'checked_in')::bigint AS checked_in_count, COALESCE(SUM(b.total_price) FILTER (WHERE b.status != 'cancelled'), 0)::numeric AS total_revenue, ROUND( COALESCE(AVG((b.check_out - b.check_in)::numeric) FILTER (WHERE b.status != 'cancelled'), 0), 1 ) AS avg_stay_nights, CASE WHEN v_total_room_nights > 0 THEN ROUND(v_booked_room_nights / v_total_room_nights * 100, 1) ELSE 0 END AS occupancy_rate FROM public.bookings b WHERE b.account_id = p_account_id AND b.check_in <= v_to AND b.check_out > v_from; END; $$; GRANT EXECUTE ON FUNCTION public.get_booking_statistics(uuid, date, date) TO authenticated; GRANT EXECUTE ON FUNCTION public.get_booking_statistics(uuid, date, date) TO service_role; -- ------------------------------------------------------- -- D) Event registration counts (batch lookup) -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_event_registration_counts(p_event_ids uuid[]) RETURNS TABLE (event_id uuid, registration_count bigint) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$ SELECT er.event_id, count(*)::bigint AS registration_count FROM public.event_registrations er WHERE er.event_id = ANY(p_event_ids) AND er.status IN ('confirmed', 'pending') GROUP BY er.event_id; $$; GRANT EXECUTE ON FUNCTION public.get_event_registration_counts(uuid[]) TO authenticated; GRANT EXECUTE ON FUNCTION public.get_event_registration_counts(uuid[]) TO service_role;