Eine Organisationseinheit aus dem Organigramm heraus anlegen
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m30s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m22s

Bisher gab es dafuer nur den Import. Jetzt sitzt auf jeder Einheit im
Organigramm -- in der Grafik wie in der Liste -- ein Plus, das eine
untergeordnete Einheit anlegt, wahlweise gleich mit Leitungsplanstelle.

Bewusst nur dieser eine Fall. Umbenennen, verschieben und schliessen fehlen
nicht aus Zeitmangel: org_units.parent_id traegt kein Datum, ein Verschieben
aenderte damit auch jede Auswertung auf einen vergangenen Stichtag, und der
fruehere Stand waere danach nirgends mehr ablesbar. Anlegen stellt diese Frage
nicht, weil vorher nichts da war -- und es kann dabei auch kein Kreis
entstehen, was hier mehr wiegt als es klingt: die rekursive Abfrage in
om_reporting_lines hat weder Tiefenbegrenzung noch Kreiserkennung.

Die Leitungsplanstelle entsteht ueber create_position statt durch eine zweite
Fassung derselben Logik -- dort haengen Jobkatalog, Nummernvergabe und die
Pruefung "je Einheit genau eine Leitung".

Die Orgnummer wird eingetragen, nicht vergeben. Vorgeschlagen wird die naechste
freie Nummer der bestehenden Reihe, und nur dann, wenn sich im Bestand genau
eine Systematik ablesen laesst -- eine plausibel aussehende, aber erfundene
Nummer prueft niemand nach.

Das Datum kommt nicht aus dem Stichtag der Ansicht. Wer sich die Struktur zum
letzten Jahresende ansieht und auf Plus drueckt, will in aller Regel eine
Einheit von heute anlegen.
This commit is contained in:
2026-09-28 16:28:33 +02:00
parent 8b7e32f14a
commit a6b6a7d67c
11 changed files with 547 additions and 9 deletions

View File

@@ -0,0 +1,156 @@
-- Eine Organisationseinheit aus der Anwendung heraus anlegen
--
-- Bisher gab es dafür nur den Import — und für alles andere an einer Einheit
-- (umbenennen, verschieben, schliessen) gibt es weiterhin nur die Datenbank.
-- Das hier ist bewusst nur der eine Fall: **eine neue Einheit unter einer
-- bestehenden**.
--
-- Warum gerade der: er ist der einzige, der nichts umschreibt. Die neue
-- Einheit ist immer ein Blatt, also
--
-- * kann kein Kreis entstehen — und das ist keine Kleinigkeit: die
-- rekursive Abfrage `ancestry` in om_reporting_lines hat weder
-- Tiefenbegrenzung noch Kreiserkennung, ein Kreis in parent_id würde dort
-- nicht zu einem falschen Ergebnis führen, sondern zu keinem;
-- * wird keine Vergangenheit verändert. org_units.parent_id trägt kein
-- Datum: wer eine bestehende Einheit umhängt, ändert damit auch jede
-- Auswertung auf einen vergangenen Stichtag, und der frühere Stand ist
-- danach nirgends mehr ablesbar. Beim Anlegen stellt sich die Frage
-- nicht, weil vorher nichts da war.
--
-- Das Verschieben bestehender Einheiten braucht deshalb eine Entscheidung
-- (Vergangenheit überschreiben oder die Aufhängung datieren) und steht
-- absichtlich noch nicht hier.
create or replace function create_org_unit(payload jsonb)
returns uuid language plpgsql
set search_path = public, pg_temp
as $$
declare
v_parent_id uuid := (payload->>'parent_id')::uuid;
v_org_number text := nullif(trim(payload->>'org_number'), '');
v_name text := nullif(trim(payload->>'name'), '');
v_unit_type text := nullif(trim(payload->>'unit_type'), '');
v_valid_from date := coalesce(nullif(payload->>'valid_from','')::date, current_date);
v_leitung text := nullif(trim(payload->>'leitung_taetigkeit'), '');
v_parent record;
v_id uuid;
begin
perform require_hr_admin();
if v_parent_id is null then
-- Eine Einheit ohne Übergeordnete wäre eine zweite Wurzel. Es gibt genau
-- eine, und sie entsteht nicht hier.
raise exception 'Es muss eine übergeordnete Einheit angegeben werden.';
end if;
select id, org_number, name, valid_from, valid_to
into v_parent
from org_units
where id = v_parent_id;
if v_parent.id is null then
raise exception 'Die übergeordnete Einheit existiert nicht.';
end if;
if v_org_number is null then
raise exception 'Es muss eine Orgnummer angegeben werden.';
end if;
if v_name is null then
raise exception 'Es muss eine Bezeichnung angegeben werden.';
end if;
if v_unit_type is null then
raise exception 'Es muss eine Art angegeben werden.';
end if;
-- Die Gesellschaft ist das Ganze und steht über allem. Als Kind einer
-- anderen Einheit wäre sie ein Etikett, das seiner Stelle im Baum
-- widerspricht — und genau daran ist in diesem Projekt schon dreimal Code
-- zerbrochen, der aus dem Etikett auf die Ebene geschlossen hat.
if v_unit_type = 'Gesellschaft' then
raise exception 'Die Gesellschaft ist die Wurzel und kann nicht unter einer anderen Einheit stehen.';
end if;
-- Der eindeutige Index würde das ebenfalls abfangen, aber mit einer
-- Meldung, die in der Oberfläche nichts erklärt.
if exists (select 1 from org_units where org_number = v_org_number) then
raise exception 'Die Orgnummer % ist bereits vergeben.', v_org_number;
end if;
-- Eine Einheit, die früher beginnt als die über ihr, hängt an einem Tag im
-- Baum, an dem es diesen Baum noch nicht gab. Auf einen solchen Stichtag
-- ausgewertet stünde sie ohne Vorfahren da.
if v_valid_from < v_parent.valid_from then
raise exception 'Die übergeordnete Einheit gilt erst ab %. Ein früherer Beginn ist nicht möglich.', v_parent.valid_from;
end if;
if v_parent.valid_to is not null and v_valid_from >= v_parent.valid_to then
raise exception 'Die übergeordnete Einheit gilt nur bis %.', v_parent.valid_to;
end if;
insert into org_units (org_number, name, parent_id, unit_type, valid_from)
values (v_org_number, v_name, v_parent_id, v_unit_type::org_unit_type, v_valid_from)
returning id into v_id;
insert into audit_log (actor_user_id, actor_name, action, target_label, details)
values (app_current_user_id(), current_actor_name(), 'Organisationseinheit angelegt',
v_org_number || ' · ' || v_name,
'Unter ' || v_parent.org_number || ' · ' || v_parent.name ||
', Art ' || v_unit_type || ', gültig ab ' || v_valid_from ||
case when v_leitung is not null then ', mit Leitungsplanstelle „' || v_leitung || '“' else '' end);
-- Die Leitungsplanstelle entsteht über create_position und nicht durch eine
-- zweite Fassung derselben Logik. Dort hängen der Jobkatalog (gleiche
-- Tätigkeit = ein Eintrag), die Nummernvergabe und die Prüfung „je Einheit
-- genau eine Leitung“ — nachgebaut liefe das hier beim ersten Zusatz
-- auseinander. Sie schreibt ihren eigenen Protokolleintrag.
if v_leitung is not null then
perform create_position(jsonb_build_object(
'org_unit_id', v_id,
'job_title', v_leitung,
'is_chief', true,
'valid_from', v_valid_from
));
end if;
return v_id;
end;
$$;
comment on function create_org_unit(jsonb) is
'Legt eine Organisationseinheit unter einer bestehenden an, wahlweise gleich mit Leitungsplanstelle. Nur anlegen: umbenennen, verschieben und schliessen gibt es bewusst nicht, weil parent_id kein Datum trägt.';
grant execute on function create_org_unit(jsonb) to alpenwerk_app;
-- Selbstprüfung.
do $$
declare
v_def text;
begin
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'create_org_unit' and p.prokind = 'f';
if v_def is null then
raise exception 'create_org_unit wurde nicht angelegt';
end if;
if v_def not like '%require_hr_admin()%' then
raise exception 'create_org_unit prüft die Rechte nicht';
end if;
-- Ohne search_path liesse sich der Funktion über einen eigenen Schemapfad
-- eine andere Tabelle unterschieben. Hauskonvention für jede Funktion.
if v_def not like '%search_path%' then
raise exception 'create_org_unit hat keinen festen search_path';
end if;
if v_def not like '%audit_log%' then
raise exception 'create_org_unit schreibt nichts ins Protokoll';
end if;
-- Nicht nachgebaut, sondern aufgerufen — siehe den Kommentar dort.
if v_def not like '%create_position(%' then
raise exception 'create_org_unit legt die Leitungsplanstelle nicht über create_position an';
end if;
-- Ohne Übergeordnete entstünde eine zweite Wurzel.
if v_def not like '%parent_id%' then
raise exception 'create_org_unit kennt die übergeordnete Einheit nicht';
end if;
end
$$;