feat: enhance member management features; add quick stats and search capabilities

This commit is contained in:
T. Zehetbauer
2026-04-02 22:56:04 +02:00
parent 0932c57fa1
commit f43770999f
35 changed files with 4370 additions and 159 deletions

View File

@@ -0,0 +1,100 @@
-- Migration: Enhanced member search and quick stats
-- Adds: full-text search index, quick stats RPC, next member number function
-- Full-text search index (German) for faster member search
CREATE INDEX IF NOT EXISTS ix_members_fulltext ON public.members
USING gin(
to_tsvector(
'german',
coalesce(first_name, '') || ' ' ||
coalesce(last_name, '') || ' ' ||
coalesce(email, '') || ' ' ||
coalesce(member_number, '') || ' ' ||
coalesce(city, '')
)
);
-- Trigram index on names for fuzzy / ILIKE search
CREATE INDEX IF NOT EXISTS ix_members_name_trgm
ON public.members
USING gin ((lower(first_name || ' ' || last_name)) gin_trgm_ops);
-- Quick stats RPC — returns a single row with KPI counts
-- Includes has_role_on_account guard to prevent cross-tenant data leaks
CREATE OR REPLACE FUNCTION public.get_member_quick_stats(p_account_id uuid)
RETURNS TABLE(
total bigint,
active bigint,
inactive bigint,
pending bigint,
resigned bigint,
new_this_year bigint,
pending_applications bigint
)
LANGUAGE plpgsql STABLE SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
-- Verify caller has access to this account
IF NOT public.has_role_on_account(p_account_id) THEN
RAISE EXCEPTION 'Access denied to account %', p_account_id;
END IF;
RETURN QUERY
SELECT
count(*)::bigint AS total,
count(*) FILTER (WHERE m.status = 'active')::bigint AS active,
count(*) FILTER (WHERE m.status = 'inactive')::bigint AS inactive,
count(*) FILTER (WHERE m.status = 'pending')::bigint AS pending,
count(*) FILTER (WHERE m.status = 'resigned')::bigint AS resigned,
count(*) FILTER (WHERE m.status = 'active'
AND m.entry_date >= date_trunc('year', current_date)::date)::bigint AS new_this_year,
(
SELECT count(*)
FROM public.membership_applications a
WHERE a.account_id = p_account_id
AND a.status = 'submitted'
)::bigint AS pending_applications
FROM public.members m
WHERE m.account_id = p_account_id;
END;
$$;
GRANT EXECUTE ON FUNCTION public.get_member_quick_stats(uuid) TO authenticated;
-- Next member number: returns max(member_number) + 1 as text
-- Includes has_role_on_account guard
CREATE OR REPLACE FUNCTION public.get_next_member_number(p_account_id uuid)
RETURNS text
LANGUAGE plpgsql STABLE SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_result text;
BEGIN
-- Verify caller has access to this account
IF NOT public.has_role_on_account(p_account_id) THEN
RAISE EXCEPTION 'Access denied to account %', p_account_id;
END IF;
SELECT LPAD(
(COALESCE(
MAX(
CASE
WHEN member_number ~ '^\d+$' THEN member_number::integer
ELSE 0
END
),
0
) + 1)::text,
4,
'0'
) INTO v_result
FROM public.members
WHERE account_id = p_account_id;
RETURN v_result;
END;
$$;
GRANT EXECUTE ON FUNCTION public.get_next_member_number(uuid) TO authenticated;