-- Organigramm-Modul + Modul-Toggle. -- ==================================================================== -- Generisches Datenmodell, das verschiedene Compliance-Regelwerke -- (TSM, ISO 9001, DSGVO, …) abbilden kann. Stellen werden von Personen -- getrennt, damit Vakanzen ("N.N.") und Personalwechsel ohne Layout- -- Bruch funktionieren. -- -- Tabellen: -- org_units — Sparte/Bereich (Strom, Gas, Finanzen, …) -- positions — Stelle in der Hierarchie -- position_assignments — Person ⇄ Stelle, mit Zeitraum -- deputies — Stelle ⇄ Stelle (Vertretung), mit Zeitraum -- external_parties — Fremdfirma / Behörde / Dienstleister -- service_relations — Externe ⇄ Pflichtbereich -- -- Erweiterung an site_settings: -- enabled_modules — text[]: aktivierte Produkt-Module -- organigram_visibility — public | internal | trusted -- -- RLS-Modell: Tabellen sind RLS-aktiv, KEINE anon-Policies. Reads -- laufen ausschließlich über service_role aus der App. Die Sichtbarkeits- -- Logik (public/internal/trusted) prüft der App-Code, nicht die DB. -- -------------------------------------------------------------------- -- 1) Module-Toggle + Organigramm-Sichtbarkeit auf site_settings -- -------------------------------------------------------------------- alter table public.site_settings add column if not exists enabled_modules text[] not null default array['business_cards']::text[]; alter table public.site_settings add column if not exists organigram_visibility text not null default 'internal'; -- Sichtbarkeitswerte einschränken (idempotent) alter table public.site_settings drop constraint if exists site_settings_organigram_visibility_check; alter table public.site_settings add constraint site_settings_organigram_visibility_check check (organigram_visibility in ('public', 'internal', 'trusted')); -- -------------------------------------------------------------------- -- 2) org_units — Sparte / Bereich (Hierarchie auch innerhalb möglich) -- -------------------------------------------------------------------- create table if not exists public.org_units ( id uuid primary key default gen_random_uuid(), parent_id uuid references public.org_units(id) on delete restrict, slug text not null unique, name text not null, description text, sort_order integer not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint org_units_no_self_parent check (parent_id is null or parent_id <> id) ); create index if not exists org_units_parent_id_idx on public.org_units (parent_id); -- -------------------------------------------------------------------- -- 3) positions — Stelle (entkoppelt von Person) -- -------------------------------------------------------------------- create table if not exists public.positions ( id uuid primary key default gen_random_uuid(), parent_id uuid references public.positions(id) on delete restrict, org_unit_id uuid references public.org_units(id) on delete set null, name text not null, short_name text, description text, -- Anzeigewert wenn keine Person zugewiesen ist (Default: 'N.N.'). vacant_label text not null default 'N.N.', sort_order integer not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint positions_no_self_parent check (parent_id is null or parent_id <> id) ); create index if not exists positions_parent_id_idx on public.positions (parent_id); create index if not exists positions_org_unit_id_idx on public.positions (org_unit_id); -- -------------------------------------------------------------------- -- 4) position_assignments — Person sitzt auf Stelle (mit Zeitraum) -- -------------------------------------------------------------------- create table if not exists public.position_assignments ( id uuid primary key default gen_random_uuid(), employee_id uuid not null references public.employees(id) on delete cascade, position_id uuid not null references public.positions(id) on delete cascade, -- Optionaler Zusatz, z.B. "kommissarisch", "i.V." role_label text, is_primary boolean not null default false, valid_from date not null default current_date, valid_to date, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint position_assignments_valid_range check (valid_to is null or valid_to >= valid_from) ); create index if not exists position_assignments_employee_id_idx on public.position_assignments (employee_id); create index if not exists position_assignments_position_id_idx on public.position_assignments (position_id); -- Schnelle "wer ist gerade aktiv auf Stelle X"-Queries create index if not exists position_assignments_active_idx on public.position_assignments (position_id, valid_from, valid_to); -- -------------------------------------------------------------------- -- 5) deputies — Stelle wird vertreten von Stelle (mit Zeitraum) -- -------------------------------------------------------------------- create table if not exists public.deputies ( id uuid primary key default gen_random_uuid(), position_id uuid not null references public.positions(id) on delete cascade, deputy_position_id uuid not null references public.positions(id) on delete cascade, -- Reihenfolge bei mehreren Vertretungen (1. Vertretung, 2. Vertretung, …) sort_order integer not null default 0, valid_from date not null default current_date, valid_to date, created_at timestamptz not null default now(), constraint deputies_no_self check (position_id <> deputy_position_id), constraint deputies_valid_range check (valid_to is null or valid_to >= valid_from) ); create index if not exists deputies_position_id_idx on public.deputies (position_id); create index if not exists deputies_deputy_position_id_idx on public.deputies (deputy_position_id); -- -------------------------------------------------------------------- -- 6) external_parties — Fremdfirma / Behörde / Dienstleister -- -------------------------------------------------------------------- create table if not exists public.external_parties ( id uuid primary key default gen_random_uuid(), name text not null, -- Art der Partei: dienstleister, behoerde, kooperationspartner, sonstige kind text not null default 'dienstleister', contact_name text, contact_email text, contact_phone text, website text, address text, notes text, sort_order integer not null default 0, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint external_parties_kind_check check (kind in ('dienstleister', 'behoerde', 'kooperationspartner', 'sonstige')) ); -- -------------------------------------------------------------------- -- 7) service_relations — Externe deckt Pflichtbereich ab -- -------------------------------------------------------------------- create table if not exists public.service_relations ( id uuid primary key default gen_random_uuid(), external_party_id uuid not null references public.external_parties(id) on delete cascade, -- Polymorphe Referenz: scope_kind = 'org_unit' | 'position' | 'global' scope_kind text not null, scope_id uuid, -- Klartext, z.B. "Brandschutzbeauftragter", "Betriebsarzt", "Personalwesen" service_label text not null, description text, valid_from date not null default current_date, valid_to date, created_at timestamptz not null default now(), constraint service_relations_scope_kind_check check (scope_kind in ('org_unit', 'position', 'global')), constraint service_relations_scope_id_consistency check ( (scope_kind = 'global' and scope_id is null) or (scope_kind in ('org_unit', 'position') and scope_id is not null) ), constraint service_relations_valid_range check (valid_to is null or valid_to >= valid_from) ); create index if not exists service_relations_external_party_id_idx on public.service_relations (external_party_id); create index if not exists service_relations_scope_idx on public.service_relations (scope_kind, scope_id); -- -------------------------------------------------------------------- -- 8) Trigger: updated_at automatisch aktualisieren -- -------------------------------------------------------------------- create or replace function public.set_updated_at() returns trigger as $$ begin new.updated_at = now(); return new; end; $$ language plpgsql; drop trigger if exists trg_org_units_updated_at on public.org_units; create trigger trg_org_units_updated_at before update on public.org_units for each row execute function public.set_updated_at(); drop trigger if exists trg_positions_updated_at on public.positions; create trigger trg_positions_updated_at before update on public.positions for each row execute function public.set_updated_at(); drop trigger if exists trg_position_assignments_updated_at on public.position_assignments; create trigger trg_position_assignments_updated_at before update on public.position_assignments for each row execute function public.set_updated_at(); drop trigger if exists trg_external_parties_updated_at on public.external_parties; create trigger trg_external_parties_updated_at before update on public.external_parties for each row execute function public.set_updated_at(); -- -------------------------------------------------------------------- -- 9) RLS — alle neuen Tabellen sind defensive -- -------------------------------------------------------------------- -- Reads laufen ausschließlich über service_role (Admin + öffentliche -- Org-Page checken Visibility selbst). KEINE anon-Policies. alter table public.org_units enable row level security; alter table public.positions enable row level security; alter table public.position_assignments enable row level security; alter table public.deputies enable row level security; alter table public.external_parties enable row level security; alter table public.service_relations enable row level security;