295 lines
8.8 KiB
PL/PgSQL
295 lines
8.8 KiB
PL/PgSQL
-- =====================================================
|
|
-- 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;
|