-- Organisationsmanagement nach SAP-OM-Vorbild. -- -- Bisher: drei feste Tabellen (divisions -> departments -> teams) und -- Personen, die direkt daran hängen (employees.division_id/team_id) mit -- einer frei gepflegten manager_id. Damit ist die Hierarchie in ihrer Tiefe -- fest verdrahtet — eine Abteilungsleitung liess sich nicht abbilden, ohne -- das Schema zu ändern, und ein Team direkt unter einem Bereich gar nicht. -- -- SAP OM löst das über wenige Objekttypen und Verknüpfungen dazwischen: -- -- O Organisationseinheit org_units (rekursiv über parent_id) -- C Stelle / Job jobs (Katalog) -- S Planstelle positions (gehört zu genau einer O) -- P Person employees (besetzt eine S) -- -- A003 "gehört zu" positions.org_unit_id -- A012 "ist Leiter von" positions.is_chief -- A008 "Inhaber ist" position_assignments -- -- Die Berichtslinie wird daraus abgeleitet statt gepflegt, siehe die -- folgende Migration. Ebenen sind nur noch ein Etikett (unit_type), keine -- Struktur — eine fünfte Ebene ist damit eine Datenfrage, keine Migration. -- ── O: Organisationseinheit ──────────────────────────────────────── create type org_unit_type as enum ('Gesellschaft', 'Bereich', 'Abteilung', 'Team'); create table org_units ( id uuid primary key default gen_random_uuid(), org_number text not null unique, name text not null, -- Die Hierarchie selbst. Null nur für die Wurzel. parent_id uuid references org_units(id), -- Nur Beschriftung und Nummernkreis-Konvention; die Struktur steckt in -- parent_id. Eine Abteilung unter einer Abteilung wäre technisch möglich -- und ist bewusst nicht verboten. unit_type org_unit_type not null, valid_from date not null default current_date, valid_to date, created_at timestamptz not null default now(), constraint chk_org_unit_range check (valid_to is null or valid_to > valid_from), constraint chk_org_unit_not_own_parent check (parent_id is null or parent_id <> id) ); create index on org_units (parent_id); create index on org_units (unit_type); -- Genau eine Wurzel: ohne das kann ein Fehlgriff beim Import einen zweiten -- Baum aufmachen, und die Ableitung der Berichtslinie liefe ins Leere. create unique index org_units_single_root on org_units ((parent_id is null)) where parent_id is null; comment on table org_units is 'SAP-OM-Objekttyp O. Rekursiv über parent_id; unit_type ist nur ein Etikett.'; -- ── C: Stelle / Job-Katalog ──────────────────────────────────────── -- Trennt die Tätigkeitsbeschreibung von der einzelnen Planstelle: viele -- Planstellen teilen sich einen Job. Bisher war job_title Freitext je -- Person, weshalb "Schlosser:in" und "Schlosser" nebeneinander existieren -- konnten und keine Auswertung über Tätigkeiten möglich war. create table jobs ( id uuid primary key default gen_random_uuid(), code text not null unique, title text not null unique, created_at timestamptz not null default now() ); comment on table jobs is 'SAP-OM-Objekttyp C. Katalog der Tätigkeiten; Planstellen verweisen darauf.'; -- ── S: Planstelle ────────────────────────────────────────────────── -- Anders als die bisherige positions-Tabelle, die nur *offene* Stellen -- führte: hier bekommt jede Person eine Planstelle. Eine offene Stelle ist -- schlicht eine Planstelle ohne laufende Besetzung — Vakanz ist damit eine -- Eigenschaft der Planstelle, kein eigenes Objekt. create table om_positions ( id uuid primary key default gen_random_uuid(), position_number text not null unique, org_unit_id uuid not null references org_units(id), job_id uuid not null references jobs(id), -- A012 "ist Leiter von": diese Planstelle führt ihre Organisationseinheit. is_chief boolean not null default false, valid_from date not null default current_date, valid_to date, created_at timestamptz not null default now(), constraint chk_om_position_range check (valid_to is null or valid_to > valid_from) ); create index on om_positions (org_unit_id); create index on om_positions (job_id); -- Höchstens eine Leitungsplanstelle je Einheit, solange sie gültig ist. create unique index om_positions_one_chief on om_positions (org_unit_id) where is_chief and valid_to is null; comment on table om_positions is 'SAP-OM-Objekttyp S. is_chief entspricht der Verknüpfung A012 "ist Leiter von".'; -- ── A008: Person besetzt Planstelle ──────────────────────────────── create table position_assignments ( id uuid primary key default gen_random_uuid(), position_id uuid not null references om_positions(id) on delete cascade, employee_id uuid not null references employees(id) on delete cascade, valid_from date not null, valid_to date, created_at timestamptz not null default now(), constraint chk_assignment_range check (valid_to is null or valid_to > valid_from) ); create index on position_assignments (position_id); create index on position_assignments (employee_id); -- Eine Planstelle ist zu einem Zeitpunkt von höchstens einer Person besetzt, -- und eine Person hat höchstens eine laufende Planstelle. Beides sind die -- Invarianten, auf die sich die Ableitung der Berichtslinie stützt. create unique index position_assignments_one_holder on position_assignments (position_id) where valid_to is null; create unique index position_assignments_one_position on position_assignments (employee_id) where valid_to is null; comment on table position_assignments is 'SAP-OM-Verknüpfung A008 "Inhaber ist", zeitabhängig.'; -- ── RLS, wie bei allen anderen Tabellen ──────────────────────────── alter table org_units enable row level security; alter table jobs enable row level security; alter table om_positions enable row level security; alter table position_assignments enable row level security; create policy "org_units_hr_all" on org_units for all using (is_hr_user()) with check (is_hr_user()); create policy "jobs_hr_all" on jobs for all using (is_hr_user()) with check (is_hr_user()); create policy "om_positions_hr_all" on om_positions for all using (is_hr_user()) with check (is_hr_user()); create policy "position_assignments_hr_all" on position_assignments for all using (is_hr_user()) with check (is_hr_user()); grant all on table org_units, jobs, om_positions, position_assignments to anon, authenticated, service_role;