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