/* * ------------------------------------------------------- * 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 -- (Moved to 20260411900001_fischerei_enum_values.sql — Postgres requires -- new enum values to be committed in a separate transaction) -- ===================================================== -- ===================================================== -- 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;