refactor: remove obsolete member management API module
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
-- =====================================================
|
||||
-- Reporting & Analytics RPC Functions
|
||||
--
|
||||
-- Enterprise-grade reporting: demographics, retention,
|
||||
-- geographic distribution, dues collection, membership
|
||||
-- duration analysis.
|
||||
-- =====================================================
|
||||
|
||||
-- 1. Age demographics by gender
|
||||
CREATE OR REPLACE FUNCTION public.get_member_demographics(p_account_id uuid)
|
||||
RETURNS TABLE (
|
||||
age_group text,
|
||||
male_count bigint,
|
||||
female_count bigint,
|
||||
diverse_count bigint,
|
||||
unknown_count bigint,
|
||||
total bigint
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied';
|
||||
END IF;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
CASE
|
||||
WHEN age < 18 THEN 'Unter 18'
|
||||
WHEN age BETWEEN 18 AND 30 THEN '18-30'
|
||||
WHEN age BETWEEN 31 AND 50 THEN '31-50'
|
||||
WHEN age BETWEEN 51 AND 65 THEN '51-65'
|
||||
WHEN age > 65 THEN 'Über 65'
|
||||
ELSE 'Unbekannt'
|
||||
END AS age_group,
|
||||
count(*) FILTER (WHERE m.gender = 'male') AS male_count,
|
||||
count(*) FILTER (WHERE m.gender = 'female') AS female_count,
|
||||
count(*) FILTER (WHERE m.gender = 'diverse') AS diverse_count,
|
||||
count(*) FILTER (WHERE m.gender IS NULL OR m.gender NOT IN ('male', 'female', 'diverse')) AS unknown_count,
|
||||
count(*) AS total
|
||||
FROM public.members m
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT CASE
|
||||
WHEN m.date_of_birth IS NOT NULL THEN
|
||||
extract(year FROM age(current_date, m.date_of_birth))::int
|
||||
ELSE NULL
|
||||
END AS age
|
||||
) ages ON true
|
||||
WHERE m.account_id = p_account_id
|
||||
AND m.status = 'active'
|
||||
AND m.is_archived = false
|
||||
GROUP BY age_group
|
||||
ORDER BY
|
||||
CASE age_group
|
||||
WHEN 'Unter 18' THEN 1
|
||||
WHEN '18-30' THEN 2
|
||||
WHEN '31-50' THEN 3
|
||||
WHEN '51-65' THEN 4
|
||||
WHEN 'Über 65' THEN 5
|
||||
ELSE 6
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_member_demographics(uuid) TO authenticated;
|
||||
|
||||
-- 2. Year-over-year membership retention
|
||||
CREATE OR REPLACE FUNCTION public.get_member_retention(
|
||||
p_account_id uuid,
|
||||
p_years int DEFAULT 5
|
||||
)
|
||||
RETURNS TABLE (
|
||||
year int,
|
||||
members_start bigint,
|
||||
new_members bigint,
|
||||
resigned_members bigint,
|
||||
members_end bigint,
|
||||
retention_rate numeric
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied';
|
||||
END IF;
|
||||
|
||||
RETURN QUERY
|
||||
WITH years AS (
|
||||
SELECT generate_series(
|
||||
extract(year FROM current_date)::int - p_years + 1,
|
||||
extract(year FROM current_date)::int
|
||||
) AS yr
|
||||
),
|
||||
stats AS (
|
||||
SELECT
|
||||
y.yr,
|
||||
count(*) FILTER (WHERE m.entry_date < make_date(y.yr, 1, 1)
|
||||
AND (m.exit_date IS NULL OR m.exit_date >= make_date(y.yr, 1, 1))) AS members_start,
|
||||
count(*) FILTER (WHERE extract(year FROM m.entry_date) = y.yr) AS new_members,
|
||||
count(*) FILTER (WHERE extract(year FROM m.exit_date) = y.yr) AS resigned_members,
|
||||
count(*) FILTER (WHERE m.entry_date <= make_date(y.yr, 12, 31)
|
||||
AND (m.exit_date IS NULL OR m.exit_date > make_date(y.yr, 12, 31))) AS members_end
|
||||
FROM years y
|
||||
CROSS JOIN public.members m
|
||||
WHERE m.account_id = p_account_id AND m.is_archived = false
|
||||
GROUP BY y.yr
|
||||
)
|
||||
SELECT
|
||||
s.yr AS year,
|
||||
s.members_start,
|
||||
s.new_members,
|
||||
s.resigned_members,
|
||||
s.members_end,
|
||||
CASE WHEN s.members_start > 0
|
||||
THEN round((s.members_end::numeric / s.members_start) * 100, 1)
|
||||
ELSE 0
|
||||
END AS retention_rate
|
||||
FROM stats s
|
||||
ORDER BY s.yr;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_member_retention(uuid, int) TO authenticated;
|
||||
|
||||
-- 3. Geographic distribution by postal code prefix
|
||||
CREATE OR REPLACE FUNCTION public.get_member_geographic_distribution(p_account_id uuid)
|
||||
RETURNS TABLE (
|
||||
postal_prefix text,
|
||||
city text,
|
||||
member_count bigint
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied';
|
||||
END IF;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
CASE
|
||||
WHEN m.postal_code IS NULL OR m.postal_code = '' THEN 'Keine Angabe'
|
||||
ELSE left(m.postal_code, 2)
|
||||
END AS postal_prefix,
|
||||
COALESCE(NULLIF(m.city, ''), 'Keine Angabe') AS city,
|
||||
count(*) AS member_count
|
||||
FROM public.members m
|
||||
WHERE m.account_id = p_account_id
|
||||
AND m.status = 'active'
|
||||
AND m.is_archived = false
|
||||
GROUP BY postal_prefix, m.city
|
||||
ORDER BY member_count DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_member_geographic_distribution(uuid) TO authenticated;
|
||||
|
||||
-- 4. Dues collection rates by category
|
||||
CREATE OR REPLACE FUNCTION public.get_dues_collection_report(p_account_id uuid)
|
||||
RETURNS TABLE (
|
||||
category_name text,
|
||||
member_count bigint,
|
||||
expected_amount numeric,
|
||||
paid_count bigint,
|
||||
collection_rate numeric
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied';
|
||||
END IF;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
COALESCE(dc.name, 'Keine Kategorie') AS category_name,
|
||||
count(m.id) AS member_count,
|
||||
COALESCE(sum(dc.amount), 0) AS expected_amount,
|
||||
count(*) FILTER (WHERE m.dues_paid = true) AS paid_count,
|
||||
CASE WHEN count(m.id) > 0
|
||||
THEN round((count(*) FILTER (WHERE m.dues_paid = true)::numeric / count(m.id)) * 100, 1)
|
||||
ELSE 0
|
||||
END AS collection_rate
|
||||
FROM public.members m
|
||||
LEFT JOIN public.dues_categories dc ON dc.id = m.dues_category_id
|
||||
WHERE m.account_id = p_account_id
|
||||
AND m.status = 'active'
|
||||
AND m.is_archived = false
|
||||
GROUP BY dc.name, dc.sort_order
|
||||
ORDER BY dc.sort_order NULLS LAST;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_dues_collection_report(uuid) TO authenticated;
|
||||
|
||||
-- 5. Membership duration analysis
|
||||
CREATE OR REPLACE FUNCTION public.get_membership_duration_analysis(p_account_id uuid)
|
||||
RETURNS TABLE (
|
||||
duration_bucket text,
|
||||
member_count bigint,
|
||||
percentage numeric
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
v_total bigint;
|
||||
BEGIN
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied';
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO v_total
|
||||
FROM public.members
|
||||
WHERE account_id = p_account_id AND status = 'active' AND is_archived = false;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
CASE
|
||||
WHEN years < 1 THEN 'Unter 1 Jahr'
|
||||
WHEN years BETWEEN 1 AND 5 THEN '1-5 Jahre'
|
||||
WHEN years BETWEEN 6 AND 10 THEN '6-10 Jahre'
|
||||
WHEN years BETWEEN 11 AND 25 THEN '11-25 Jahre'
|
||||
WHEN years > 25 THEN 'Über 25 Jahre'
|
||||
ELSE 'Unbekannt'
|
||||
END AS duration_bucket,
|
||||
count(*) AS member_count,
|
||||
CASE WHEN v_total > 0
|
||||
THEN round((count(*)::numeric / v_total) * 100, 1)
|
||||
ELSE 0
|
||||
END AS percentage
|
||||
FROM (
|
||||
SELECT
|
||||
CASE WHEN m.entry_date IS NOT NULL
|
||||
THEN extract(year FROM age(current_date, m.entry_date))::int
|
||||
ELSE NULL
|
||||
END AS years
|
||||
FROM public.members m
|
||||
WHERE m.account_id = p_account_id
|
||||
AND m.status = 'active'
|
||||
AND m.is_archived = false
|
||||
) sub
|
||||
GROUP BY duration_bucket
|
||||
ORDER BY
|
||||
CASE duration_bucket
|
||||
WHEN 'Unter 1 Jahr' THEN 1
|
||||
WHEN '1-5 Jahre' THEN 2
|
||||
WHEN '6-10 Jahre' THEN 3
|
||||
WHEN '11-25 Jahre' THEN 4
|
||||
WHEN 'Über 25 Jahre' THEN 5
|
||||
ELSE 6
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_membership_duration_analysis(uuid) TO authenticated;
|
||||
|
||||
-- 6. Department distribution
|
||||
CREATE OR REPLACE FUNCTION public.get_department_distribution(p_account_id uuid)
|
||||
RETURNS TABLE (
|
||||
department_name text,
|
||||
member_count bigint,
|
||||
percentage numeric
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
v_total bigint;
|
||||
BEGIN
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied';
|
||||
END IF;
|
||||
|
||||
SELECT count(DISTINCT m.id) INTO v_total
|
||||
FROM public.members m
|
||||
WHERE m.account_id = p_account_id AND m.status = 'active' AND m.is_archived = false;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
d.name AS department_name,
|
||||
count(DISTINCT mda.member_id) AS member_count,
|
||||
CASE WHEN v_total > 0
|
||||
THEN round((count(DISTINCT mda.member_id)::numeric / v_total) * 100, 1)
|
||||
ELSE 0
|
||||
END AS percentage
|
||||
FROM public.member_departments d
|
||||
LEFT JOIN public.member_department_assignments mda ON mda.department_id = d.id
|
||||
LEFT JOIN public.members m ON m.id = mda.member_id
|
||||
AND m.status = 'active' AND m.is_archived = false
|
||||
WHERE d.account_id = p_account_id
|
||||
GROUP BY d.name, d.sort_order
|
||||
ORDER BY member_count DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_department_distribution(uuid) TO authenticated;
|
||||
Reference in New Issue
Block a user