feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Major changes: - Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI - Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions - Sitzungsprotokolle module: meeting protocols, agenda items, task tracking - Verbandsverwaltung module: federation management, member clubs, contacts, fees - Per-account module activation via Modules page toggle - Site Builder: live CMS data in Puck blocks (courses, events, membership registration) - Public registration APIs: course signup, event registration, membership application - Document generation: PDF member cards, Excel reports, HTML labels - Landing page: real Com.BISS content (no filler text) - UX audit fixes: AccountNotFound component, shared status badges, confirm dialog, pagination, duplicate heading removal, emoji→badge replacement, a11y fixes - QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
This commit is contained in:
862
apps/web/supabase/migrations/20260412000001_fischerei.sql
Normal file
862
apps/web/supabase/migrations/20260412000001_fischerei.sql
Normal file
@@ -0,0 +1,862 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Fischerei (Fishing Association Management) Schema
|
||||
* Waters, species, stocking, leases, catch books,
|
||||
* catches, permits, inspectors, competitions
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. Enums
|
||||
-- =====================================================
|
||||
|
||||
CREATE TYPE public.water_type AS ENUM(
|
||||
'fluss', 'bach', 'see', 'teich', 'weiher', 'kanal', 'stausee', 'baggersee', 'sonstige'
|
||||
);
|
||||
|
||||
CREATE TYPE public.fish_age_class AS ENUM(
|
||||
'brut', 'soemmerlinge', 'einsoemmerig', 'zweisoemmerig', 'dreisoemmerig', 'vorgestreckt', 'setzlinge', 'laichfische', 'sonstige'
|
||||
);
|
||||
|
||||
CREATE TYPE public.catch_book_status AS ENUM(
|
||||
'offen', 'eingereicht', 'geprueft', 'akzeptiert', 'abgelehnt'
|
||||
);
|
||||
|
||||
CREATE TYPE public.catch_book_verification AS ENUM(
|
||||
'sehrgut', 'gut', 'ok', 'schlecht', 'falsch', 'leer'
|
||||
);
|
||||
|
||||
CREATE TYPE public.lease_payment_method AS ENUM(
|
||||
'bar', 'lastschrift', 'ueberweisung'
|
||||
);
|
||||
|
||||
CREATE TYPE public.fish_gender AS ENUM(
|
||||
'maennlich', 'weiblich', 'unbekannt'
|
||||
);
|
||||
|
||||
CREATE TYPE public.fish_size_category AS ENUM(
|
||||
'gross', 'mittel', 'klein'
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. Extend app_permissions
|
||||
-- =====================================================
|
||||
|
||||
ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'fischerei.read';
|
||||
ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'fischerei.write';
|
||||
|
||||
-- =====================================================
|
||||
-- 3. cost_centers (shared, may already exist)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.cost_centers (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
code text,
|
||||
description text,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_cost_centers_account ON public.cost_centers(account_id);
|
||||
ALTER TABLE public.cost_centers ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.cost_centers FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.cost_centers TO authenticated;
|
||||
GRANT ALL ON public.cost_centers TO service_role;
|
||||
|
||||
CREATE POLICY cost_centers_select ON public.cost_centers FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY cost_centers_mutate ON public.cost_centers FOR ALL TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'finance.write'::public.app_permissions));
|
||||
|
||||
-- =====================================================
|
||||
-- 4. fish_suppliers
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.fish_suppliers (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
contact_person text,
|
||||
phone text,
|
||||
email text,
|
||||
address text,
|
||||
notes text,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_fish_suppliers_account ON public.fish_suppliers(account_id);
|
||||
ALTER TABLE public.fish_suppliers ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.fish_suppliers FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.fish_suppliers TO authenticated;
|
||||
GRANT ALL ON public.fish_suppliers TO service_role;
|
||||
|
||||
CREATE POLICY fish_suppliers_select ON public.fish_suppliers FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY fish_suppliers_insert ON public.fish_suppliers FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fish_suppliers_update ON public.fish_suppliers FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fish_suppliers_delete ON public.fish_suppliers FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_fish_suppliers_updated_at
|
||||
BEFORE UPDATE ON public.fish_suppliers
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 5. waters (ve_gewaesser)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.waters (
|
||||
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,
|
||||
water_type public.water_type NOT NULL DEFAULT 'sonstige',
|
||||
description text,
|
||||
|
||||
-- Dimensions
|
||||
surface_area_ha numeric(10,2),
|
||||
length_m numeric(10,0),
|
||||
width_m numeric(10,2),
|
||||
avg_depth_m numeric(6,2),
|
||||
max_depth_m numeric(6,2),
|
||||
|
||||
-- Geography
|
||||
outflow text,
|
||||
location text,
|
||||
classification_order integer,
|
||||
county text,
|
||||
geo_lat numeric(10,7),
|
||||
geo_lng numeric(10,7),
|
||||
|
||||
-- LFV (Landesfischereiverband)
|
||||
lfv_number text,
|
||||
lfv_name text,
|
||||
|
||||
-- Cost allocation
|
||||
cost_share_ds numeric(5,2) DEFAULT 0,
|
||||
cost_share_kalk numeric(5,2) DEFAULT 0,
|
||||
|
||||
-- Electrofishing
|
||||
electrofishing_permit_requested boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- HejFish integration
|
||||
hejfish_id text,
|
||||
|
||||
-- References
|
||||
cost_center_id uuid REFERENCES public.cost_centers(id) ON DELETE SET NULL,
|
||||
|
||||
-- Flags
|
||||
is_archived boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Meta
|
||||
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_waters_account ON public.waters(account_id);
|
||||
CREATE INDEX ix_waters_name ON public.waters(account_id, name);
|
||||
CREATE INDEX ix_waters_type ON public.waters(account_id, water_type);
|
||||
CREATE INDEX ix_waters_archived ON public.waters(account_id, is_archived);
|
||||
|
||||
ALTER TABLE public.waters ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.waters FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.waters TO authenticated;
|
||||
GRANT ALL ON public.waters TO service_role;
|
||||
|
||||
CREATE POLICY waters_select ON public.waters FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY waters_insert ON public.waters FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY waters_update ON public.waters FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY waters_delete ON public.waters FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_waters_updated_at
|
||||
BEFORE UPDATE ON public.waters
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 6. fish_species (ve_fischarten)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.fish_species (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
|
||||
-- Names
|
||||
name text NOT NULL,
|
||||
name_latin text,
|
||||
name_local text,
|
||||
|
||||
-- Status
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
|
||||
-- Biometrics
|
||||
max_age_years integer,
|
||||
max_weight_kg numeric(8,2),
|
||||
max_length_cm numeric(6,1),
|
||||
|
||||
-- Protection (Schonmaß / Schonzeit)
|
||||
protected_min_size_cm numeric(5,1),
|
||||
protection_period_start text, -- MM.DD format
|
||||
protection_period_end text, -- MM.DD format
|
||||
|
||||
-- Spawning season (Sonderschonzeit / SZG)
|
||||
spawning_season_start text, -- MM.DD format
|
||||
spawning_season_end text, -- MM.DD format
|
||||
has_special_spawning_season boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Condition factor (K-Faktor)
|
||||
k_factor_avg numeric(6,3),
|
||||
k_factor_min numeric(6,3),
|
||||
k_factor_max numeric(6,3),
|
||||
|
||||
-- Quotas
|
||||
price_per_unit numeric(8,2),
|
||||
max_catch_per_day integer,
|
||||
max_catch_per_year integer,
|
||||
|
||||
-- Recording
|
||||
individual_recording boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Meta
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_fish_species_account ON public.fish_species(account_id);
|
||||
CREATE INDEX ix_fish_species_name ON public.fish_species(account_id, name);
|
||||
CREATE INDEX ix_fish_species_active ON public.fish_species(account_id, is_active);
|
||||
|
||||
ALTER TABLE public.fish_species ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.fish_species FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.fish_species TO authenticated;
|
||||
GRANT ALL ON public.fish_species TO service_role;
|
||||
|
||||
CREATE POLICY fish_species_select ON public.fish_species FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY fish_species_insert ON public.fish_species FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fish_species_update ON public.fish_species FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fish_species_delete ON public.fish_species FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_fish_species_updated_at
|
||||
BEFORE UPDATE ON public.fish_species
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 7. water_species_rules (ve_gewaesser_fischart)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.water_species_rules (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
water_id uuid NOT NULL REFERENCES public.waters(id) ON DELETE CASCADE,
|
||||
species_id uuid NOT NULL REFERENCES public.fish_species(id) ON DELETE CASCADE,
|
||||
|
||||
-- Overrides (null = use species default)
|
||||
min_size_cm numeric(5,1),
|
||||
protection_period_start text,
|
||||
protection_period_end text,
|
||||
max_catch_per_day integer,
|
||||
max_catch_per_year integer,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(water_id, species_id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_water_species_rules_water ON public.water_species_rules(water_id);
|
||||
CREATE INDEX ix_water_species_rules_species ON public.water_species_rules(species_id);
|
||||
|
||||
ALTER TABLE public.water_species_rules ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.water_species_rules FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.water_species_rules TO authenticated;
|
||||
GRANT ALL ON public.water_species_rules TO service_role;
|
||||
|
||||
CREATE POLICY water_species_rules_select ON public.water_species_rules FOR SELECT TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.waters w WHERE w.id = water_species_rules.water_id AND public.has_role_on_account(w.account_id)));
|
||||
CREATE POLICY water_species_rules_mutate ON public.water_species_rules FOR ALL TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.waters w WHERE w.id = water_species_rules.water_id AND public.has_permission(auth.uid(), w.account_id, 'fischerei.write'::public.app_permissions)));
|
||||
|
||||
-- =====================================================
|
||||
-- 8. fish_stocking (ve_besatz)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.fish_stocking (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
water_id uuid NOT NULL REFERENCES public.waters(id) ON DELETE CASCADE,
|
||||
species_id uuid NOT NULL REFERENCES public.fish_species(id) ON DELETE RESTRICT,
|
||||
|
||||
stocking_date date NOT NULL,
|
||||
quantity integer NOT NULL DEFAULT 0,
|
||||
weight_kg numeric(8,2),
|
||||
age_class public.fish_age_class NOT NULL DEFAULT 'sonstige',
|
||||
cost_euros numeric(10,2),
|
||||
supplier_id uuid REFERENCES public.fish_suppliers(id) ON DELETE SET NULL,
|
||||
remarks text,
|
||||
|
||||
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_fish_stocking_account ON public.fish_stocking(account_id);
|
||||
CREATE INDEX ix_fish_stocking_water ON public.fish_stocking(water_id);
|
||||
CREATE INDEX ix_fish_stocking_species ON public.fish_stocking(species_id);
|
||||
CREATE INDEX ix_fish_stocking_date ON public.fish_stocking(stocking_date DESC);
|
||||
|
||||
ALTER TABLE public.fish_stocking ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.fish_stocking FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.fish_stocking TO authenticated;
|
||||
GRANT ALL ON public.fish_stocking TO service_role;
|
||||
|
||||
CREATE POLICY fish_stocking_select ON public.fish_stocking FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY fish_stocking_insert ON public.fish_stocking FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fish_stocking_update ON public.fish_stocking FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fish_stocking_delete ON public.fish_stocking FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_fish_stocking_updated_at
|
||||
BEFORE UPDATE ON public.fish_stocking
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 9. fishing_leases (ve_pachten)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.fishing_leases (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
water_id uuid NOT NULL REFERENCES public.waters(id) ON DELETE CASCADE,
|
||||
|
||||
-- Lessor
|
||||
lessor_name text NOT NULL,
|
||||
lessor_address text,
|
||||
lessor_phone text,
|
||||
lessor_email text,
|
||||
|
||||
-- Lease terms
|
||||
start_date date NOT NULL,
|
||||
end_date date,
|
||||
duration_years integer,
|
||||
initial_amount numeric(10,2) NOT NULL DEFAULT 0,
|
||||
fixed_annual_increase numeric(10,2) DEFAULT 0,
|
||||
percentage_annual_increase numeric(5,2) DEFAULT 0,
|
||||
|
||||
-- Payment
|
||||
payment_method public.lease_payment_method DEFAULT 'ueberweisung',
|
||||
account_holder text,
|
||||
iban text,
|
||||
bic text,
|
||||
|
||||
-- Location details
|
||||
location_details text,
|
||||
special_agreements text,
|
||||
|
||||
-- Status
|
||||
is_archived boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Meta
|
||||
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_fishing_leases_account ON public.fishing_leases(account_id);
|
||||
CREATE INDEX ix_fishing_leases_water ON public.fishing_leases(water_id);
|
||||
CREATE INDEX ix_fishing_leases_dates ON public.fishing_leases(start_date, end_date);
|
||||
|
||||
ALTER TABLE public.fishing_leases ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.fishing_leases FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.fishing_leases TO authenticated;
|
||||
GRANT ALL ON public.fishing_leases TO service_role;
|
||||
|
||||
CREATE POLICY fishing_leases_select ON public.fishing_leases FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY fishing_leases_insert ON public.fishing_leases FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fishing_leases_update ON public.fishing_leases FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY fishing_leases_delete ON public.fishing_leases FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_fishing_leases_updated_at
|
||||
BEFORE UPDATE ON public.fishing_leases
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 10. fishing_permits (ve_gewaesserkarten)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.fishing_permits (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
short_code text,
|
||||
primary_water_id uuid REFERENCES public.waters(id) ON DELETE SET NULL,
|
||||
total_quantity integer,
|
||||
cost_center_id uuid REFERENCES public.cost_centers(id) ON DELETE SET NULL,
|
||||
hejfish_id text,
|
||||
is_for_sale boolean NOT NULL DEFAULT true,
|
||||
is_archived boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_fishing_permits_account ON public.fishing_permits(account_id);
|
||||
ALTER TABLE public.fishing_permits ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.fishing_permits FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.fishing_permits TO authenticated;
|
||||
GRANT ALL ON public.fishing_permits TO service_role;
|
||||
|
||||
CREATE POLICY fishing_permits_select ON public.fishing_permits FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY fishing_permits_mutate ON public.fishing_permits FOR ALL TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_fishing_permits_updated_at
|
||||
BEFORE UPDATE ON public.fishing_permits
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 11. permit_quotas (ve_gewaesserk_kontingent)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.permit_quotas (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
permit_id uuid NOT NULL REFERENCES public.fishing_permits(id) ON DELETE CASCADE,
|
||||
business_year integer NOT NULL,
|
||||
category_name text,
|
||||
quota_quantity integer NOT NULL DEFAULT 0,
|
||||
conversion_factor numeric(5,2) DEFAULT 1.00,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_permit_quotas_permit ON public.permit_quotas(permit_id);
|
||||
ALTER TABLE public.permit_quotas ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.permit_quotas FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.permit_quotas TO authenticated;
|
||||
GRANT ALL ON public.permit_quotas TO service_role;
|
||||
|
||||
CREATE POLICY permit_quotas_select ON public.permit_quotas FOR SELECT TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.fishing_permits p WHERE p.id = permit_quotas.permit_id AND public.has_role_on_account(p.account_id)));
|
||||
CREATE POLICY permit_quotas_mutate ON public.permit_quotas FOR ALL TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.fishing_permits p WHERE p.id = permit_quotas.permit_id AND public.has_permission(auth.uid(), p.account_id, 'fischerei.write'::public.app_permissions)));
|
||||
|
||||
-- =====================================================
|
||||
-- 12. catch_books (ve_mitglieder_fangbuch)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.catch_books (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
||||
year integer NOT NULL,
|
||||
|
||||
-- Denormalized for reporting
|
||||
member_name text,
|
||||
member_birth_date date,
|
||||
|
||||
-- Usage
|
||||
fishing_days_count integer NOT NULL DEFAULT 0,
|
||||
total_fish_caught integer NOT NULL DEFAULT 0,
|
||||
|
||||
-- Associated permits
|
||||
card_numbers text,
|
||||
is_fly_fisher boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Verification workflow
|
||||
status public.catch_book_status NOT NULL DEFAULT 'offen',
|
||||
verification public.catch_book_verification,
|
||||
is_checked boolean NOT NULL DEFAULT false,
|
||||
is_submitted boolean NOT NULL DEFAULT false,
|
||||
submitted_at timestamptz,
|
||||
|
||||
-- HejFish
|
||||
is_hejfish boolean NOT NULL DEFAULT false,
|
||||
is_empty boolean NOT NULL DEFAULT false,
|
||||
not_fished boolean NOT NULL DEFAULT false,
|
||||
|
||||
remarks text,
|
||||
|
||||
-- Meta
|
||||
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(),
|
||||
|
||||
UNIQUE(account_id, member_id, year)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_catch_books_account ON public.catch_books(account_id);
|
||||
CREATE INDEX ix_catch_books_member ON public.catch_books(member_id);
|
||||
CREATE INDEX ix_catch_books_year ON public.catch_books(account_id, year);
|
||||
CREATE INDEX ix_catch_books_status ON public.catch_books(account_id, status);
|
||||
|
||||
ALTER TABLE public.catch_books ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.catch_books FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.catch_books TO authenticated;
|
||||
GRANT ALL ON public.catch_books TO service_role;
|
||||
|
||||
CREATE POLICY catch_books_select ON public.catch_books FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY catch_books_insert ON public.catch_books FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY catch_books_update ON public.catch_books FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY catch_books_delete ON public.catch_books FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_catch_books_updated_at
|
||||
BEFORE UPDATE ON public.catch_books
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 13. catches (ve_faenge)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.catches (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
catch_book_id uuid NOT NULL REFERENCES public.catch_books(id) ON DELETE CASCADE,
|
||||
species_id uuid NOT NULL REFERENCES public.fish_species(id) ON DELETE RESTRICT,
|
||||
water_id uuid REFERENCES public.waters(id) ON DELETE SET NULL,
|
||||
member_id uuid REFERENCES public.members(id) ON DELETE SET NULL,
|
||||
|
||||
catch_date date NOT NULL,
|
||||
quantity integer NOT NULL DEFAULT 1,
|
||||
length_cm numeric(5,1),
|
||||
weight_g numeric(8,0),
|
||||
|
||||
-- Size category
|
||||
size_category public.fish_size_category,
|
||||
gender public.fish_gender,
|
||||
|
||||
-- Flags
|
||||
is_empty_entry boolean NOT NULL DEFAULT false,
|
||||
has_error boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- HejFish
|
||||
hejfish_id text,
|
||||
|
||||
-- Competition link
|
||||
competition_id uuid,
|
||||
competition_participant_id uuid,
|
||||
|
||||
-- Permit
|
||||
permit_id uuid REFERENCES public.fishing_permits(id) ON DELETE SET NULL,
|
||||
|
||||
remarks text,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- K-factor computed: weight_g / (length_cm^3) * 100000
|
||||
-- This is done application-side, not as a generated column, because
|
||||
-- it requires both non-null values and the formula is specific to fisheries.
|
||||
|
||||
CREATE INDEX ix_catches_catch_book ON public.catches(catch_book_id);
|
||||
CREATE INDEX ix_catches_species ON public.catches(species_id);
|
||||
CREATE INDEX ix_catches_water ON public.catches(water_id);
|
||||
CREATE INDEX ix_catches_member ON public.catches(member_id);
|
||||
CREATE INDEX ix_catches_date ON public.catches(catch_date DESC);
|
||||
|
||||
ALTER TABLE public.catches ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.catches FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.catches TO authenticated;
|
||||
GRANT ALL ON public.catches TO service_role;
|
||||
|
||||
CREATE POLICY catches_select ON public.catches FOR SELECT TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.catch_books cb WHERE cb.id = catches.catch_book_id AND public.has_role_on_account(cb.account_id)));
|
||||
CREATE POLICY catches_mutate ON public.catches FOR ALL TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.catch_books cb WHERE cb.id = catches.catch_book_id AND public.has_permission(auth.uid(), cb.account_id, 'fischerei.write'::public.app_permissions)));
|
||||
|
||||
-- =====================================================
|
||||
-- 14. water_inspectors (ve_kontrolleur_gewaesser)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.water_inspectors (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
water_id uuid NOT NULL REFERENCES public.waters(id) ON DELETE CASCADE,
|
||||
member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
||||
assignment_start date NOT NULL DEFAULT current_date,
|
||||
assignment_end date,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(water_id, member_id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_water_inspectors_account ON public.water_inspectors(account_id);
|
||||
CREATE INDEX ix_water_inspectors_water ON public.water_inspectors(water_id);
|
||||
CREATE INDEX ix_water_inspectors_member ON public.water_inspectors(member_id);
|
||||
|
||||
ALTER TABLE public.water_inspectors ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.water_inspectors FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.water_inspectors TO authenticated;
|
||||
GRANT ALL ON public.water_inspectors TO service_role;
|
||||
|
||||
CREATE POLICY water_inspectors_select ON public.water_inspectors FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY water_inspectors_mutate ON public.water_inspectors FOR ALL TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
-- =====================================================
|
||||
-- 15. competition_categories (ve_fanglisten_rubriken)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.competition_categories (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_competition_categories_account ON public.competition_categories(account_id);
|
||||
ALTER TABLE public.competition_categories ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.competition_categories FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.competition_categories TO authenticated;
|
||||
GRANT ALL ON public.competition_categories TO service_role;
|
||||
|
||||
CREATE POLICY competition_categories_select ON public.competition_categories FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY competition_categories_mutate ON public.competition_categories FOR ALL TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
-- =====================================================
|
||||
-- 16. competitions (ve_fanglisten)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.competitions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
event_id uuid REFERENCES public.events(id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
competition_date date NOT NULL,
|
||||
permit_id uuid REFERENCES public.fishing_permits(id) ON DELETE SET NULL,
|
||||
water_id uuid REFERENCES public.waters(id) ON DELETE SET NULL,
|
||||
max_participants integer,
|
||||
|
||||
-- Scoring flags
|
||||
score_by_count boolean NOT NULL DEFAULT false,
|
||||
score_by_heaviest boolean NOT NULL DEFAULT false,
|
||||
score_by_total_weight boolean NOT NULL DEFAULT true,
|
||||
score_by_longest boolean NOT NULL DEFAULT false,
|
||||
score_by_total_length boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Separation
|
||||
separate_member_guest_scoring boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Result counts
|
||||
result_count_weight integer DEFAULT 3,
|
||||
result_count_length integer DEFAULT 3,
|
||||
result_count_count integer DEFAULT 3,
|
||||
|
||||
-- Meta
|
||||
created_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_competitions_account ON public.competitions(account_id);
|
||||
CREATE INDEX ix_competitions_date ON public.competitions(competition_date DESC);
|
||||
|
||||
ALTER TABLE public.competitions ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.competitions FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.competitions TO authenticated;
|
||||
GRANT ALL ON public.competitions TO service_role;
|
||||
|
||||
CREATE POLICY competitions_select ON public.competitions FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY competitions_insert ON public.competitions FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY competitions_update ON public.competitions FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
CREATE POLICY competitions_delete ON public.competitions FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_competitions_updated_at
|
||||
BEFORE UPDATE ON public.competitions
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 17. competition_participants (ve_fanglisten_teilnehmer)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.competition_participants (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
competition_id uuid NOT NULL REFERENCES public.competitions(id) ON DELETE CASCADE,
|
||||
member_id uuid REFERENCES public.members(id) ON DELETE SET NULL,
|
||||
category_id uuid REFERENCES public.competition_categories(id) ON DELETE SET NULL,
|
||||
|
||||
-- Guest/external participant info
|
||||
participant_name text NOT NULL,
|
||||
birth_date date,
|
||||
address text,
|
||||
phone text,
|
||||
email text,
|
||||
|
||||
-- Status
|
||||
participated boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Results (computed from catches)
|
||||
total_catch_count integer NOT NULL DEFAULT 0,
|
||||
total_weight_g integer NOT NULL DEFAULT 0,
|
||||
total_length_cm numeric(8,1) NOT NULL DEFAULT 0,
|
||||
heaviest_catch_g integer NOT NULL DEFAULT 0,
|
||||
longest_catch_cm numeric(5,1) NOT NULL DEFAULT 0,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_competition_participants_competition ON public.competition_participants(competition_id);
|
||||
CREATE INDEX ix_competition_participants_member ON public.competition_participants(member_id);
|
||||
|
||||
ALTER TABLE public.competition_participants ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.competition_participants FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.competition_participants TO authenticated;
|
||||
GRANT ALL ON public.competition_participants TO service_role;
|
||||
|
||||
CREATE POLICY competition_participants_select ON public.competition_participants FOR SELECT TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.competitions c WHERE c.id = competition_participants.competition_id AND public.has_role_on_account(c.account_id)));
|
||||
CREATE POLICY competition_participants_mutate ON public.competition_participants FOR ALL TO authenticated
|
||||
USING (EXISTS (SELECT 1 FROM public.competitions c WHERE c.id = competition_participants.competition_id AND public.has_permission(auth.uid(), c.account_id, 'fischerei.write'::public.app_permissions)));
|
||||
|
||||
-- Add FK from catches to competitions
|
||||
ALTER TABLE public.catches
|
||||
ADD CONSTRAINT fk_catches_competition FOREIGN KEY (competition_id) REFERENCES public.competitions(id) ON DELETE SET NULL;
|
||||
ALTER TABLE public.catches
|
||||
ADD CONSTRAINT fk_catches_competition_participant FOREIGN KEY (competition_participant_id) REFERENCES public.competition_participants(id) ON DELETE SET NULL;
|
||||
|
||||
-- =====================================================
|
||||
-- 18. hejfish_sync (integration state tracking)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.hejfish_sync (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
entity_type text NOT NULL, -- 'water', 'species', 'member', 'catch'
|
||||
local_id uuid NOT NULL,
|
||||
hejfish_id text NOT NULL,
|
||||
last_synced_at timestamptz NOT NULL DEFAULT now(),
|
||||
sync_data jsonb,
|
||||
UNIQUE(account_id, entity_type, local_id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_hejfish_sync_account ON public.hejfish_sync(account_id);
|
||||
CREATE INDEX ix_hejfish_sync_entity ON public.hejfish_sync(entity_type, local_id);
|
||||
|
||||
ALTER TABLE public.hejfish_sync ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.hejfish_sync FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.hejfish_sync TO authenticated;
|
||||
GRANT ALL ON public.hejfish_sync TO service_role;
|
||||
|
||||
CREATE POLICY hejfish_sync_select ON public.hejfish_sync FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY hejfish_sync_mutate ON public.hejfish_sync FOR ALL TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'fischerei.write'::public.app_permissions));
|
||||
|
||||
-- =====================================================
|
||||
-- 19. Aggregate function for catch statistics
|
||||
-- =====================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_catch_statistics(
|
||||
p_account_id uuid,
|
||||
p_year integer DEFAULT NULL,
|
||||
p_water_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE(
|
||||
species_id uuid,
|
||||
species_name text,
|
||||
total_count bigint,
|
||||
total_weight_kg numeric,
|
||||
avg_length_cm numeric,
|
||||
avg_weight_g numeric,
|
||||
avg_k_factor numeric
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
c.species_id,
|
||||
fs.name AS species_name,
|
||||
COUNT(*)::bigint AS total_count,
|
||||
ROUND(COALESCE(SUM(c.weight_g) / 1000.0, 0), 2) AS total_weight_kg,
|
||||
ROUND(AVG(c.length_cm), 1) AS avg_length_cm,
|
||||
ROUND(AVG(c.weight_g), 0) AS avg_weight_g,
|
||||
ROUND(AVG(
|
||||
CASE WHEN c.length_cm > 0 AND c.weight_g > 0
|
||||
THEN c.weight_g / POWER(c.length_cm, 3) * 100000
|
||||
ELSE NULL END
|
||||
), 3) AS avg_k_factor
|
||||
FROM public.catches c
|
||||
JOIN public.catch_books cb ON cb.id = c.catch_book_id
|
||||
JOIN public.fish_species fs ON fs.id = c.species_id
|
||||
WHERE cb.account_id = p_account_id
|
||||
AND (p_year IS NULL OR cb.year = p_year)
|
||||
AND (p_water_id IS NULL OR c.water_id = p_water_id)
|
||||
AND c.is_empty_entry = false
|
||||
GROUP BY c.species_id, fs.name
|
||||
ORDER BY total_count DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_catch_statistics(uuid, integer, uuid)
|
||||
TO authenticated, service_role;
|
||||
|
||||
-- =====================================================
|
||||
-- 20. Function to compute lease amount for a given year
|
||||
-- =====================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.compute_lease_amount(
|
||||
p_initial_amount numeric,
|
||||
p_fixed_increase numeric,
|
||||
p_percentage_increase numeric,
|
||||
p_start_year integer,
|
||||
p_target_year integer
|
||||
)
|
||||
RETURNS numeric
|
||||
LANGUAGE plpgsql
|
||||
IMMUTABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_amount numeric;
|
||||
v_year_offset integer;
|
||||
BEGIN
|
||||
v_year_offset := p_target_year - p_start_year;
|
||||
IF v_year_offset <= 0 THEN
|
||||
RETURN p_initial_amount;
|
||||
END IF;
|
||||
|
||||
IF p_percentage_increase > 0 THEN
|
||||
v_amount := p_initial_amount * POWER(1 + p_percentage_increase / 100.0, v_year_offset);
|
||||
ELSE
|
||||
v_amount := p_initial_amount + (p_fixed_increase * v_year_offset);
|
||||
END IF;
|
||||
|
||||
RETURN ROUND(v_amount, 2);
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.compute_lease_amount(numeric, numeric, numeric, integer, integer)
|
||||
TO authenticated, service_role;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Seed fischerei permissions for existing roles
|
||||
|
||||
INSERT INTO public.role_permissions (role, permission) VALUES
|
||||
('owner', 'fischerei.read'),
|
||||
('owner', 'fischerei.write'),
|
||||
('member', 'fischerei.read')
|
||||
ON CONFLICT (role, permission) DO NOTHING;
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Sitzungsprotokolle (Meeting Protocols) Schema
|
||||
* Meeting minutes, agenda items, tasks, attachments
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. Extend app_permissions
|
||||
-- =====================================================
|
||||
|
||||
ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'meetings.read';
|
||||
ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'meetings.write';
|
||||
|
||||
-- =====================================================
|
||||
-- 2. Enums
|
||||
-- =====================================================
|
||||
|
||||
CREATE TYPE public.meeting_item_status AS ENUM(
|
||||
'offen',
|
||||
'in_bearbeitung',
|
||||
'erledigt',
|
||||
'vertagt'
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 3. meeting_protocols
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.meeting_protocols (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identity
|
||||
title text NOT NULL,
|
||||
protocol_number text,
|
||||
|
||||
-- Schedule
|
||||
meeting_date date NOT NULL,
|
||||
meeting_time time,
|
||||
end_time time,
|
||||
location text,
|
||||
|
||||
-- Participants
|
||||
chair text,
|
||||
recorder text,
|
||||
attendees jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
absent jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
|
||||
-- Content
|
||||
summary text,
|
||||
|
||||
-- Status workflow
|
||||
status text NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'review', 'approved', 'archived')),
|
||||
|
||||
-- Follow-up
|
||||
next_meeting_date date,
|
||||
|
||||
-- Flags
|
||||
is_archived boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Meta
|
||||
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_meeting_protocols_account ON public.meeting_protocols(account_id);
|
||||
CREATE INDEX ix_meeting_protocols_date ON public.meeting_protocols(account_id, meeting_date DESC);
|
||||
CREATE INDEX ix_meeting_protocols_status ON public.meeting_protocols(account_id, status);
|
||||
CREATE INDEX ix_meeting_protocols_archived ON public.meeting_protocols(account_id, is_archived);
|
||||
|
||||
ALTER TABLE public.meeting_protocols ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.meeting_protocols FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.meeting_protocols TO authenticated;
|
||||
GRANT ALL ON public.meeting_protocols TO service_role;
|
||||
|
||||
CREATE POLICY meeting_protocols_select ON public.meeting_protocols FOR SELECT TO authenticated
|
||||
USING (public.has_role_on_account(account_id));
|
||||
CREATE POLICY meeting_protocols_insert ON public.meeting_protocols FOR INSERT TO authenticated
|
||||
WITH CHECK (public.has_permission(auth.uid(), account_id, 'meetings.write'::public.app_permissions));
|
||||
CREATE POLICY meeting_protocols_update ON public.meeting_protocols FOR UPDATE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'meetings.write'::public.app_permissions));
|
||||
CREATE POLICY meeting_protocols_delete ON public.meeting_protocols FOR DELETE TO authenticated
|
||||
USING (public.has_permission(auth.uid(), account_id, 'meetings.write'::public.app_permissions));
|
||||
|
||||
CREATE TRIGGER trg_meeting_protocols_updated_at
|
||||
BEFORE UPDATE ON public.meeting_protocols
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 4. meeting_protocol_items (agenda items / TOPs)
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.meeting_protocol_items (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
protocol_id uuid NOT NULL REFERENCES public.meeting_protocols(id) ON DELETE CASCADE,
|
||||
|
||||
-- Ordering
|
||||
item_number integer NOT NULL DEFAULT 0,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
|
||||
-- Content
|
||||
title text NOT NULL,
|
||||
content text,
|
||||
|
||||
-- Classification
|
||||
item_type text NOT NULL DEFAULT 'information'
|
||||
CHECK (item_type IN ('information', 'discussion', 'decision', 'task')),
|
||||
|
||||
-- Decision tracking
|
||||
decision_text text,
|
||||
|
||||
-- Task tracking
|
||||
status public.meeting_item_status NOT NULL DEFAULT 'offen',
|
||||
responsible_person text,
|
||||
due_date date,
|
||||
|
||||
-- Meta
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_meeting_protocol_items_protocol ON public.meeting_protocol_items(protocol_id);
|
||||
CREATE INDEX ix_meeting_protocol_items_status ON public.meeting_protocol_items(status);
|
||||
CREATE INDEX ix_meeting_protocol_items_type ON public.meeting_protocol_items(item_type);
|
||||
CREATE INDEX ix_meeting_protocol_items_due ON public.meeting_protocol_items(due_date)
|
||||
WHERE due_date IS NOT NULL AND status != 'erledigt';
|
||||
|
||||
ALTER TABLE public.meeting_protocol_items ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.meeting_protocol_items FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.meeting_protocol_items TO authenticated;
|
||||
GRANT ALL ON public.meeting_protocol_items TO service_role;
|
||||
|
||||
CREATE POLICY meeting_protocol_items_select ON public.meeting_protocol_items FOR SELECT TO authenticated
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM public.meeting_protocols mp
|
||||
WHERE mp.id = meeting_protocol_items.protocol_id
|
||||
AND public.has_role_on_account(mp.account_id)
|
||||
));
|
||||
CREATE POLICY meeting_protocol_items_mutate ON public.meeting_protocol_items FOR ALL TO authenticated
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM public.meeting_protocols mp
|
||||
WHERE mp.id = meeting_protocol_items.protocol_id
|
||||
AND public.has_permission(auth.uid(), mp.account_id, 'meetings.write'::public.app_permissions)
|
||||
));
|
||||
|
||||
CREATE TRIGGER trg_meeting_protocol_items_updated_at
|
||||
BEFORE UPDATE ON public.meeting_protocol_items
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 5. meeting_protocol_attachments
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.meeting_protocol_attachments (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
protocol_id uuid NOT NULL REFERENCES public.meeting_protocols(id) ON DELETE CASCADE,
|
||||
item_id uuid REFERENCES public.meeting_protocol_items(id) ON DELETE SET NULL,
|
||||
|
||||
-- File info
|
||||
file_name text NOT NULL,
|
||||
file_path text NOT NULL,
|
||||
file_size bigint,
|
||||
content_type text,
|
||||
|
||||
-- Meta
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_meeting_protocol_attachments_protocol ON public.meeting_protocol_attachments(protocol_id);
|
||||
CREATE INDEX ix_meeting_protocol_attachments_item ON public.meeting_protocol_attachments(item_id)
|
||||
WHERE item_id IS NOT NULL;
|
||||
|
||||
ALTER TABLE public.meeting_protocol_attachments ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.meeting_protocol_attachments FROM authenticated, service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.meeting_protocol_attachments TO authenticated;
|
||||
GRANT ALL ON public.meeting_protocol_attachments TO service_role;
|
||||
|
||||
CREATE POLICY meeting_protocol_attachments_select ON public.meeting_protocol_attachments FOR SELECT TO authenticated
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM public.meeting_protocols mp
|
||||
WHERE mp.id = meeting_protocol_attachments.protocol_id
|
||||
AND public.has_role_on_account(mp.account_id)
|
||||
));
|
||||
CREATE POLICY meeting_protocol_attachments_mutate ON public.meeting_protocol_attachments FOR ALL TO authenticated
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM public.meeting_protocols mp
|
||||
WHERE mp.id = meeting_protocol_attachments.protocol_id
|
||||
AND public.has_permission(auth.uid(), mp.account_id, 'meetings.write'::public.app_permissions)
|
||||
));
|
||||
|
||||
-- =====================================================
|
||||
-- 6. View: open_meeting_tasks
|
||||
-- =====================================================
|
||||
|
||||
CREATE OR REPLACE VIEW public.open_meeting_tasks AS
|
||||
SELECT
|
||||
mpi.id AS item_id,
|
||||
mpi.protocol_id,
|
||||
mp.account_id,
|
||||
mp.title AS protocol_title,
|
||||
mp.meeting_date,
|
||||
mpi.item_number,
|
||||
mpi.title AS task_title,
|
||||
mpi.content AS task_description,
|
||||
mpi.responsible_person,
|
||||
mpi.due_date,
|
||||
mpi.status,
|
||||
CASE
|
||||
WHEN mpi.due_date < current_date AND mpi.status != 'erledigt' THEN true
|
||||
ELSE false
|
||||
END AS is_overdue
|
||||
FROM public.meeting_protocol_items mpi
|
||||
JOIN public.meeting_protocols mp ON mp.id = mpi.protocol_id
|
||||
WHERE mpi.item_type = 'task'
|
||||
AND mpi.status != 'erledigt';
|
||||
|
||||
-- Grant access to the view (RLS on underlying tables still applies)
|
||||
GRANT SELECT ON public.open_meeting_tasks TO authenticated;
|
||||
GRANT SELECT ON public.open_meeting_tasks TO service_role;
|
||||
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Verbandsverwaltung (Association Management) Schema
|
||||
* Association types, member clubs, contacts, fees,
|
||||
* billing, notes, history
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. Extend app_permissions
|
||||
-- =====================================================
|
||||
|
||||
ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'verband.read';
|
||||
ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'verband.write';
|
||||
|
||||
-- =====================================================
|
||||
-- 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;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Seed meetings + verband permissions for existing roles
|
||||
|
||||
INSERT INTO public.role_permissions (role, permission) VALUES
|
||||
('owner', 'meetings.read'),
|
||||
('owner', 'meetings.write'),
|
||||
('member', 'meetings.read'),
|
||||
('owner', 'verband.read'),
|
||||
('owner', 'verband.write'),
|
||||
('member', 'verband.read')
|
||||
ON CONFLICT (role, permission) DO NOTHING;
|
||||
Reference in New Issue
Block a user