376 lines
16 KiB
PL/PgSQL
376 lines
16 KiB
PL/PgSQL
/*
|
|
* -------------------------------------------------------
|
|
* Verbandsverwaltung (Association Management) Schema
|
|
* Association types, member clubs, contacts, fees,
|
|
* billing, notes, history
|
|
* -------------------------------------------------------
|
|
*/
|
|
|
|
-- =====================================================
|
|
-- 1. Extend app_permissions
|
|
-- (Moved to 20260411900001_fischerei_enum_values.sql)
|
|
-- =====================================================
|
|
|
|
-- =====================================================
|
|
-- 2. association_types (Verbandsarten)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.association_types (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
description text,
|
|
sort_order integer NOT NULL DEFAULT 0,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_association_types_account ON public.association_types(account_id);
|
|
|
|
ALTER TABLE public.association_types ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.association_types FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.association_types TO authenticated;
|
|
GRANT ALL ON public.association_types TO service_role;
|
|
|
|
CREATE POLICY association_types_select ON public.association_types FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account(account_id));
|
|
CREATE POLICY association_types_mutate ON public.association_types FOR ALL TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
|
|
-- =====================================================
|
|
-- 3. member_clubs (Mitgliedsvereine)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.member_clubs (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
|
|
|
-- Identity
|
|
name text NOT NULL,
|
|
short_name text,
|
|
club_number text,
|
|
association_type_id uuid REFERENCES public.association_types(id) ON DELETE SET NULL,
|
|
|
|
-- Contact
|
|
address_street text,
|
|
address_zip text,
|
|
address_city text,
|
|
phone text,
|
|
email text,
|
|
website text,
|
|
|
|
-- Bank details
|
|
iban text,
|
|
bic text,
|
|
account_holder text,
|
|
|
|
-- Stats
|
|
member_count integer NOT NULL DEFAULT 0,
|
|
youth_count integer NOT NULL DEFAULT 0,
|
|
founding_year integer,
|
|
|
|
-- Flags
|
|
is_active boolean NOT NULL DEFAULT true,
|
|
is_archived boolean NOT NULL DEFAULT false,
|
|
|
|
-- Meta
|
|
notes text,
|
|
custom_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_member_clubs_account ON public.member_clubs(account_id);
|
|
CREATE INDEX ix_member_clubs_name ON public.member_clubs(account_id, name);
|
|
CREATE INDEX ix_member_clubs_type ON public.member_clubs(account_id, association_type_id);
|
|
CREATE INDEX ix_member_clubs_active ON public.member_clubs(account_id, is_active);
|
|
CREATE INDEX ix_member_clubs_archived ON public.member_clubs(account_id, is_archived);
|
|
|
|
ALTER TABLE public.member_clubs ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.member_clubs FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.member_clubs TO authenticated;
|
|
GRANT ALL ON public.member_clubs TO service_role;
|
|
|
|
CREATE POLICY member_clubs_select ON public.member_clubs FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account(account_id));
|
|
CREATE POLICY member_clubs_insert ON public.member_clubs FOR INSERT TO authenticated
|
|
WITH CHECK (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
CREATE POLICY member_clubs_update ON public.member_clubs FOR UPDATE TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
CREATE POLICY member_clubs_delete ON public.member_clubs FOR DELETE TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
|
|
CREATE TRIGGER trg_member_clubs_updated_at
|
|
BEFORE UPDATE ON public.member_clubs
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
|
|
|
-- =====================================================
|
|
-- 4. club_roles (Vereinsfunktionen)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.club_roles (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
description text,
|
|
sort_order integer NOT NULL DEFAULT 0,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_club_roles_account ON public.club_roles(account_id);
|
|
|
|
ALTER TABLE public.club_roles ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.club_roles FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.club_roles TO authenticated;
|
|
GRANT ALL ON public.club_roles TO service_role;
|
|
|
|
CREATE POLICY club_roles_select ON public.club_roles FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account(account_id));
|
|
CREATE POLICY club_roles_mutate ON public.club_roles FOR ALL TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
|
|
-- =====================================================
|
|
-- 5. club_contacts (Ansprechpartner)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.club_contacts (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
club_id uuid NOT NULL REFERENCES public.member_clubs(id) ON DELETE CASCADE,
|
|
role_id uuid REFERENCES public.club_roles(id) ON DELETE SET NULL,
|
|
|
|
-- Person info
|
|
first_name text NOT NULL,
|
|
last_name text NOT NULL,
|
|
email text,
|
|
phone text,
|
|
mobile text,
|
|
|
|
-- Period
|
|
valid_from date,
|
|
valid_until date,
|
|
is_active boolean NOT NULL DEFAULT true,
|
|
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_club_contacts_club ON public.club_contacts(club_id);
|
|
CREATE INDEX ix_club_contacts_role ON public.club_contacts(role_id);
|
|
|
|
ALTER TABLE public.club_contacts ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.club_contacts FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.club_contacts TO authenticated;
|
|
GRANT ALL ON public.club_contacts TO service_role;
|
|
|
|
CREATE POLICY club_contacts_select ON public.club_contacts FOR SELECT TO authenticated
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.member_clubs mc
|
|
WHERE mc.id = club_contacts.club_id
|
|
AND public.has_role_on_account(mc.account_id)
|
|
));
|
|
CREATE POLICY club_contacts_mutate ON public.club_contacts FOR ALL TO authenticated
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.member_clubs mc
|
|
WHERE mc.id = club_contacts.club_id
|
|
AND public.has_permission(auth.uid(), mc.account_id, 'verband.write'::public.app_permissions)
|
|
));
|
|
|
|
CREATE TRIGGER trg_club_contacts_updated_at
|
|
BEFORE UPDATE ON public.club_contacts
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
|
|
|
-- =====================================================
|
|
-- 6. club_fee_types (Beitragsarten)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.club_fee_types (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
description text,
|
|
default_amount numeric(10,2) NOT NULL DEFAULT 0,
|
|
is_per_member boolean NOT NULL DEFAULT false,
|
|
is_active boolean NOT NULL DEFAULT true,
|
|
sort_order integer NOT NULL DEFAULT 0,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_club_fee_types_account ON public.club_fee_types(account_id);
|
|
|
|
ALTER TABLE public.club_fee_types ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.club_fee_types FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.club_fee_types TO authenticated;
|
|
GRANT ALL ON public.club_fee_types TO service_role;
|
|
|
|
CREATE POLICY club_fee_types_select ON public.club_fee_types FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account(account_id));
|
|
CREATE POLICY club_fee_types_mutate ON public.club_fee_types FOR ALL TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
|
|
-- =====================================================
|
|
-- 7. club_fee_billings (Beitragsabrechnungen)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.club_fee_billings (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
club_id uuid NOT NULL REFERENCES public.member_clubs(id) ON DELETE CASCADE,
|
|
fee_type_id uuid NOT NULL REFERENCES public.club_fee_types(id) ON DELETE RESTRICT,
|
|
|
|
-- Billing period
|
|
billing_year integer NOT NULL,
|
|
billing_period text DEFAULT 'annual'
|
|
CHECK (billing_period IN ('annual', 'semi_annual', 'quarterly')),
|
|
|
|
-- Amounts
|
|
amount numeric(10,2) NOT NULL DEFAULT 0,
|
|
member_count_at_billing integer,
|
|
|
|
-- Payment tracking
|
|
status text NOT NULL DEFAULT 'open'
|
|
CHECK (status IN ('open', 'invoiced', 'paid', 'overdue', 'cancelled')),
|
|
invoice_number text,
|
|
invoice_date date,
|
|
paid_date date,
|
|
paid_amount numeric(10,2),
|
|
|
|
remarks text,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_club_fee_billings_club ON public.club_fee_billings(club_id);
|
|
CREATE INDEX ix_club_fee_billings_fee_type ON public.club_fee_billings(fee_type_id);
|
|
CREATE INDEX ix_club_fee_billings_year ON public.club_fee_billings(billing_year);
|
|
CREATE INDEX ix_club_fee_billings_status ON public.club_fee_billings(status);
|
|
|
|
ALTER TABLE public.club_fee_billings ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.club_fee_billings FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.club_fee_billings TO authenticated;
|
|
GRANT ALL ON public.club_fee_billings TO service_role;
|
|
|
|
CREATE POLICY club_fee_billings_select ON public.club_fee_billings FOR SELECT TO authenticated
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.member_clubs mc
|
|
WHERE mc.id = club_fee_billings.club_id
|
|
AND public.has_role_on_account(mc.account_id)
|
|
));
|
|
CREATE POLICY club_fee_billings_mutate ON public.club_fee_billings FOR ALL TO authenticated
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.member_clubs mc
|
|
WHERE mc.id = club_fee_billings.club_id
|
|
AND public.has_permission(auth.uid(), mc.account_id, 'verband.write'::public.app_permissions)
|
|
));
|
|
|
|
CREATE TRIGGER trg_club_fee_billings_updated_at
|
|
BEFORE UPDATE ON public.club_fee_billings
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
|
|
|
-- =====================================================
|
|
-- 8. club_notes (Vereinsnotizen)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.club_notes (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
club_id uuid NOT NULL REFERENCES public.member_clubs(id) ON DELETE CASCADE,
|
|
title text,
|
|
content text NOT NULL,
|
|
note_date date NOT NULL DEFAULT current_date,
|
|
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_club_notes_club ON public.club_notes(club_id);
|
|
CREATE INDEX ix_club_notes_date ON public.club_notes(note_date DESC);
|
|
|
|
ALTER TABLE public.club_notes ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.club_notes FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.club_notes TO authenticated;
|
|
GRANT ALL ON public.club_notes TO service_role;
|
|
|
|
CREATE POLICY club_notes_select ON public.club_notes FOR SELECT TO authenticated
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.member_clubs mc
|
|
WHERE mc.id = club_notes.club_id
|
|
AND public.has_role_on_account(mc.account_id)
|
|
));
|
|
CREATE POLICY club_notes_mutate ON public.club_notes FOR ALL TO authenticated
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.member_clubs mc
|
|
WHERE mc.id = club_notes.club_id
|
|
AND public.has_permission(auth.uid(), mc.account_id, 'verband.write'::public.app_permissions)
|
|
));
|
|
|
|
-- =====================================================
|
|
-- 9. association_history (Verbandschronik)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.association_history (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
|
club_id uuid REFERENCES public.member_clubs(id) ON DELETE SET NULL,
|
|
|
|
-- Event
|
|
event_type text NOT NULL DEFAULT 'note'
|
|
CHECK (event_type IN ('joined', 'left', 'name_change', 'merge', 'split', 'status_change', 'note')),
|
|
event_date date NOT NULL DEFAULT current_date,
|
|
description text NOT NULL,
|
|
|
|
-- Metadata
|
|
old_value text,
|
|
new_value text,
|
|
|
|
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX ix_association_history_account ON public.association_history(account_id);
|
|
CREATE INDEX ix_association_history_club ON public.association_history(club_id);
|
|
CREATE INDEX ix_association_history_date ON public.association_history(event_date DESC);
|
|
CREATE INDEX ix_association_history_type ON public.association_history(event_type);
|
|
|
|
ALTER TABLE public.association_history ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.association_history FROM authenticated, service_role;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.association_history TO authenticated;
|
|
GRANT ALL ON public.association_history TO service_role;
|
|
|
|
CREATE POLICY association_history_select ON public.association_history FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account(account_id));
|
|
CREATE POLICY association_history_insert ON public.association_history FOR INSERT TO authenticated
|
|
WITH CHECK (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
CREATE POLICY association_history_update ON public.association_history FOR UPDATE TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
CREATE POLICY association_history_delete ON public.association_history FOR DELETE TO authenticated
|
|
USING (public.has_permission(auth.uid(), account_id, 'verband.write'::public.app_permissions));
|
|
|
|
-- =====================================================
|
|
-- 10. Dashboard stats RPC
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION public.get_verband_dashboard_stats(p_account_id uuid)
|
|
RETURNS TABLE(
|
|
total_clubs bigint,
|
|
active_clubs bigint,
|
|
total_members bigint,
|
|
total_youth bigint,
|
|
open_fees bigint,
|
|
open_fees_amount numeric
|
|
)
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
(SELECT COUNT(*) FROM public.member_clubs WHERE account_id = p_account_id AND is_archived = false)::bigint AS total_clubs,
|
|
(SELECT COUNT(*) FROM public.member_clubs WHERE account_id = p_account_id AND is_active = true AND is_archived = false)::bigint AS active_clubs,
|
|
(SELECT COALESCE(SUM(member_count), 0) FROM public.member_clubs WHERE account_id = p_account_id AND is_archived = false)::bigint AS total_members,
|
|
(SELECT COALESCE(SUM(youth_count), 0) FROM public.member_clubs WHERE account_id = p_account_id AND is_archived = false)::bigint AS total_youth,
|
|
(SELECT COUNT(*) FROM public.club_fee_billings cfb JOIN public.member_clubs mc ON mc.id = cfb.club_id WHERE mc.account_id = p_account_id AND cfb.status IN ('open', 'overdue'))::bigint AS open_fees,
|
|
(SELECT COALESCE(SUM(cfb.amount), 0) FROM public.club_fee_billings cfb JOIN public.member_clubs mc ON mc.id = cfb.club_id WHERE mc.account_id = p_account_id AND cfb.status IN ('open', 'overdue'))::numeric AS open_fees_amount;
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.get_verband_dashboard_stats(uuid) TO authenticated, service_role;
|