Files
2026-06-25 19:54:40 +02:00

234 lines
10 KiB
PL/PgSQL

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