From 6ab78d4b272ead168b3ccf2165fe4781a8532d83 Mon Sep 17 00:00:00 2001 From: Andrei Laas Date: Thu, 17 Sep 2026 12:10:50 +0200 Subject: [PATCH] Uebernahme: extern auf intern ist ein eigenes Ereignis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aus dem Gespraech vom 17.09.2026. Migrationen 20260917110000 (Enum-Wert) und 20260917120000 (Logik) — getrennt, weil ein in derselben Transaktion angelegter Enum-Wert dort noch nicht benutzt werden darf. Die Besetzungsart (extern/intern) wurde bisher nur bei der Neueinstellung gesetzt und war danach unerreichbar. Eine Angabe, die mit dem ersten Tag erstarrt, obwohl gerade ihr Wechsel der Vorgang ist, um den es geht. Sie steht jetzt in "Daten aendern". Wechselt sie von Extern auf Intern, entsteht das Ereignis "Uebernahme" — in der Personalakte und im Protokoll, und im Berichtemanager als Ereignistyp auswertbar. Der umgekehrte Weg bekommt ausdruecklich keines: eine Uebernahme zurueckzunehmen gibt es fachlich nicht. Zwei Eintraege und nicht einer: die Vertragsaenderung haelt fest, dass ein Feld sich geaendert hat und worauf (und laesst sich darueber zuruecknehmen), das Ereignis, dass dieser Wechsel eine Uebernahme war. Nur das zweite laesst sich zaehlen. Eine auf spaeter datierte Uebernahme erzeugt das Ereignis sofort mit pending_id, wie die Vertragsaenderung daneben; der Nachtlauf schreibt am Stichtag nur noch die Spalte. Die Selbstpruefung haelt fest, dass er kein zweites Ereignis schreibt — sonst staende die Uebernahme doppelt in der Akte und jede Auswertung zaehlte sie zweimal. Im Assistenten erscheint das Feld nicht: dort steht die Besetzungsart im Schritt "Position", und zweimal danach zu fragen waere eine Einladung, zwei verschiedene Antworten zu geben. --- actions/employees.ts | 3 + app/api/export/employees/route.ts | 2 +- components/employees/RoleEmploymentFields.tsx | 34 +- .../employees/panels/DatenAendernPanel.tsx | 5 + .../20260917110000_ereignis_uebernahme.sql | 55 ++ .../20260917120000_uebernahme_schreiben.sql | 570 ++++++++++++++++++ lib/colors.ts | 3 + lib/reports.ts | 1 + lib/types.ts | 6 +- 9 files changed, 676 insertions(+), 3 deletions(-) create mode 100644 db/migrations/20260917110000_ereignis_uebernahme.sql create mode 100644 db/migrations/20260917120000_uebernahme_schreiben.sql diff --git a/actions/employees.ts b/actions/employees.ts index a9ac923..6718b27 100644 --- a/actions/employees.ts +++ b/actions/employees.ts @@ -247,6 +247,9 @@ export async function changeEmployeeData(payload: { effective_date: string; person: Record; contract: Record; + // `role` trägt seit 20260917120000 auch `source`. Wechselt sie von + // „Extern" auf „Intern", schreibt change_employee_data zusätzlich das + // Ereignis „Übernahme" — der umgekehrte Weg erzeugt keines. role: Record; }): Promise { return callRpc("change_employee_data", payload, [`/employees/${payload.employee_id}`, "/employees"]); diff --git a/app/api/export/employees/route.ts b/app/api/export/employees/route.ts index f752d7d..8319b77 100644 --- a/app/api/export/employees/route.ts +++ b/app/api/export/employees/route.ts @@ -11,7 +11,7 @@ import { deriveStatusAsOf, parseIsoDateParam, parseStatuses, type OrgLookups } f import { applyCriteria, loadDependentsCounts, loadOrgLookups, type ReportFilters } from "@/lib/reports-data"; import { requireHrUser } from "@/lib/auth/require-hr"; import { withUser } from "@/lib/db"; -import type { Database, Weekday } from "@/lib/types"; +import type { Database } from "@/lib/types"; // Die Rohzeile plus die Einordnung, die nicht mehr auf ihr steht: sie kommt // über die Planstelle und die abgeleitete Berichtslinie. diff --git a/components/employees/RoleEmploymentFields.tsx b/components/employees/RoleEmploymentFields.tsx index 08e3aa6..e52b43a 100644 --- a/components/employees/RoleEmploymentFields.tsx +++ b/components/employees/RoleEmploymentFields.tsx @@ -8,7 +8,7 @@ import { } from "@/lib/kuendigungsschutz"; import { MITARBEITERARTEN } from "@/lib/mitarbeiterart"; import { sortiereWochentage, WOCHENTAGE } from "@/lib/wochentage"; -import type { CollectiveAgreement, DienstwagenArt, Mitarbeiterart, Weekday, WorkerType } from "@/lib/types"; +import type { CollectiveAgreement, DienstwagenArt, Mitarbeiterart, SourceType, Weekday, WorkerType } from "@/lib/types"; export type RoleEmploymentValue = { @@ -19,6 +19,21 @@ export type RoleEmploymentValue = { * von beidem sagen. */ mitarbeiterart: Mitarbeiterart; + /** + * Extern oder intern besetzt. + * + * Wurde bisher nur bei der Neueinstellung gesetzt und war danach + * unerreichbar — eine Angabe, die mit dem ersten Tag erstarrt, obwohl + * gerade ihr Wechsel der Vorgang ist, um den es geht: von extern auf + * intern ist eine **Übernahme** und wird als eigenes Ereignis protokolliert + * (Migration 20260917120000). + * + * Nicht gesetzt heisst: dieses Formular fragt die Besetzungsart nicht. So + * ist es im Einstellungsassistenten — dort steht sie im Schritt „Position", + * wo sie hingehört, und zweimal danach zu fragen wäre eine Einladung, zwei + * verschiedene Antworten zu geben. + */ + source?: SourceType; collectiveAgreement: CollectiveAgreement; workDays: Weekday[]; isBetriebsrat: boolean; @@ -110,6 +125,23 @@ export function RoleEmploymentFields({ value, onChange }: { value: RoleEmploymen onChange={(v) => onChange({ mitarbeiterart: v as Mitarbeiterart })} options={MITARBEITERARTEN.map((a) => ({ value: a, label: a }))} /> + {/* Der Wechsel von extern auf intern ist eine Übernahme und wird als + eigenes Ereignis protokolliert — der Hinweis steht am Feld, damit + niemand ihn erst im Protokoll entdeckt. Der umgekehrte Weg + erzeugt nichts; das ist Absicht und keine Lücke. */} + {value.source !== undefined && ( + onChange({ source: v as SourceType })} + options={[ + { value: "Extern", label: "Extern" }, + { value: "Intern", label: "Intern" }, + ]} + hint={value.source === "Extern" ? "Ein Wechsel auf „Intern“ wird als Übernahme protokolliert." : undefined} + /> + )} ({ workerType: employee.worker_type ?? "Angestellte:r", mitarbeiterart: employee.mitarbeiterart ?? MITARBEITERART_STANDARD, + // Hier gesetzt und im Assistenten nicht: dort steht die Besetzungsart im + // Schritt „Position". Ihr Wechsel von extern auf intern ist eine + // Übernahme und wird als eigenes Ereignis protokolliert. + source: employee.source ?? "Extern", collectiveAgreement: employee.collective_agreement ?? "Handel", workDays: employee.work_days ?? ["Mo", "Di", "Mi", "Do", "Fr"], isBetriebsrat: employee.is_betriebsrat ?? false, @@ -175,6 +179,7 @@ export function DatenAendernPanel({ role: { worker_type: role.workerType, mitarbeiterart: role.mitarbeiterart, + source: role.source, collective_agreement: role.collectiveAgreement, work_days: role.workDays, is_betriebsrat: role.isBetriebsrat, diff --git a/db/migrations/20260917110000_ereignis_uebernahme.sql b/db/migrations/20260917110000_ereignis_uebernahme.sql new file mode 100644 index 0000000..73b1efb --- /dev/null +++ b/db/migrations/20260917110000_ereignis_uebernahme.sql @@ -0,0 +1,55 @@ +-- „Übernahme" als eigenes Ereignis. +-- +-- Aus dem Gespräch vom 17.09.2026: wenn eine Person von `Extern` auf `Intern` +-- wechselt, ist das eine Übernahme — ein Vorgang wie Neueinstellung, +-- Wiedereinstellung oder Versetzung, und er soll als solcher im Protokoll +-- stehen und im Berichtemanager auswertbar sein. +-- +-- Der umgekehrte Weg (Intern auf Extern) bekommt ausdrücklich **kein** +-- Ereignis: eine Übernahme zurückzunehmen gibt es fachlich nicht, und ein +-- Ereignis dafür wäre eine Erfindung. +-- +-- ═══ Warum das hier allein steht ═══════════════════════════════════ +-- +-- Dieselbe Begründung wie bei Migration 20260910140000 (Lehrling): +-- scripts/migrate.mjs fährt jede Migration in einer Transaktion. Seit +-- PostgreSQL 12 darf `add value` darin stehen — **benutzen** lässt sich der +-- neue Wert in derselben Transaktion aber nicht. Die Logik, die ihn schreibt, +-- steht deshalb in der nächsten Datei; hier ist der Wert nach dem Commit +-- verfügbar. +-- +-- Und wie dort gilt: ein Enum-Wert lässt sich nicht wieder entfernen. In +-- dieser Datei steht darum nichts weiter, was man gleichzeitig zurücknehmen +-- wollen könnte. + +alter type history_event_type add value if not exists 'Übernahme'; + +-- ═══ Gegenprobe ═══════════════════════════════════════════════════ +-- +-- Über pg_enum, wo die Beschriftung schlichter Text ist: ein Vergleich gegen +-- 'Übernahme'::history_event_type scheiterte in derselben Transaktion mit +-- „unsafe use of new value". +do $$ +declare + anzahl int; +begin + if not exists ( + select 1 + from pg_enum e + join pg_type t on t.oid = e.enumtypid + where t.typname = 'history_event_type' and e.enumlabel = 'Übernahme' + ) then + raise exception 'history_event_type kennt Übernahme nicht.'; + end if; + + -- Die elf bisherigen Werte müssen stehen bleiben. Ein Enum, das statt + -- zwölf Werten nur noch den neuen trägt, hätte jede Historienzeile + -- unlesbar gemacht. + select count(*) into anzahl + from pg_enum e + join pg_type t on t.oid = e.enumtypid + where t.typname = 'history_event_type'; + if anzahl <> 12 then + raise exception 'history_event_type hat % Werte, erwartet werden 12.', anzahl; + end if; +end $$; diff --git a/db/migrations/20260917120000_uebernahme_schreiben.sql b/db/migrations/20260917120000_uebernahme_schreiben.sql new file mode 100644 index 0000000..de469ee --- /dev/null +++ b/db/migrations/20260917120000_uebernahme_schreiben.sql @@ -0,0 +1,570 @@ +-- Die Übernahme schreiben: Extern auf Intern ist ein eigenes Ereignis. +-- +-- Fortsetzung von 20260917110000, das den Enum-Wert angelegt hat — benutzen +-- liess er sich dort nicht, weil eine Migration in einer Transaktion läuft. +-- +-- ═══ Was sich ändert ═══════════════════════════════════════════════ +-- +-- * `source` (Extern/Intern) lässt sich jetzt über „Daten ändern" +-- bearbeiten. Bisher wurde die Angabe **nur** bei der Neueinstellung +-- gesetzt und war danach unerreichbar — was sie zu einer Angabe machte, +-- die mit dem ersten Tag erstarrt, obwohl genau ihr Wechsel der Vorgang +-- ist, um den es hier geht. +-- * Wechselt sie von 'Extern' auf 'Intern', entsteht zusätzlich ein +-- Ereignis „Übernahme" in der Personalakte und im Protokoll. +-- * Der umgekehrte Weg bekommt ausdrücklich keines: eine Übernahme +-- zurückzunehmen gibt es fachlich nicht. +-- +-- ═══ Warum zwei Einträge und nicht einer ═══════════════════════════ +-- +-- Eine Übernahme erzeugt beides: den Feldwechsel in der Vertragsänderung +-- (mit Vorher und Nachher, über app_feld_karte korrigierbar) und daneben das +-- Ereignis. Das sieht nach Doppelung aus, ist aber zweierlei: der eine +-- Eintrag hält fest, **dass sich ein Feld geändert hat, und auf welchen +-- Wert**, der andere, **dass dieser Wechsel eine Übernahme war**. Nur der +-- zweite lässt sich im Berichtemanager zählen, und nur der erste lässt sich +-- zurücknehmen. +-- +-- ═══ Eine auf später datierte Übernahme ═══════════════════════════ +-- +-- Das Ereignis entsteht **sofort** und trägt `pending_id` — genau wie die +-- Vertragsänderung daneben. Die Akte zeigt es damit als anstehend, und der +-- Nachtlauf schreibt am Stichtag nur noch die Spalte. +-- +-- Er schreibt dort ausdrücklich *kein* zweites Ereignis. Das wäre der +-- naheliegende Fehler: die Übernahme stünde zweimal in der Akte, einmal zum +-- Zeitpunkt der Erfassung und einmal zum Stichtag, und jede Auswertung +-- zählte sie doppelt. + +CREATE OR REPLACE FUNCTION public.app_feld_karte() + RETURNS jsonb + LANGUAGE sql + IMMUTABLE + SET search_path TO 'public', 'pg_temp' +AS $function$ + select jsonb_build_object( + 'Vorname', jsonb_build_array('first_name', 'text', 'person'), + 'Nachname', jsonb_build_array('last_name', 'text', 'person'), + 'Geschlecht', jsonb_build_array('gender', 'gender_type', 'person'), + 'Geburtsdatum', jsonb_build_array('birth_date', 'date', 'person'), + 'SV-Nummer', jsonb_build_array('sv_nummer', 'text', 'person'), + 'Staatsbürgerschaft', jsonb_build_array('nationality', 'text', 'person'), + 'Adresse', jsonb_build_array('address', 'text', 'person'), + 'Postleitzahl', jsonb_build_array('postal_code', 'text', 'person'), + 'Ort', jsonb_build_array('city', 'text', 'person'), + 'Land', jsonb_build_array('address_country', 'text', 'person'), + 'E-Mail', jsonb_build_array('email', 'text', 'person'), + 'Telefon', jsonb_build_array('phone', 'text', 'person'), + 'Notfallkontakt', jsonb_build_array('emergency_contact_name', 'text', 'person'), + 'Notfallkontakt Telefon', jsonb_build_array('emergency_contact_phone', 'text', 'person'), + 'Notfallkontakt Verhältnis', jsonb_build_array('emergency_contact_relation', 'text', 'person'), + 'Aufenthaltstitel', jsonb_build_array('hat_aufenthaltstitel', 'boolean', 'person'), + 'Aufenthaltstitel bis', jsonb_build_array('aufenthaltstitel_bis', 'date', 'person'), + 'Titel (vorangestellt)', jsonb_build_array('title_prefix', 'liste', 'person'), + 'Titel (nachgestellt)', jsonb_build_array('title_suffix', 'liste', 'person'), + 'Beschäftigungsausmaß', jsonb_build_array('employment_type', 'employment_type', 'contract'), + 'Wochenstunden', jsonb_build_array('weekly_hours', 'numeric', 'contract'), + 'Vertragsart', jsonb_build_array('contract_type', 'contract_type', 'contract'), + 'Befristet bis', jsonb_build_array('contract_end_date', 'date', 'contract'), + 'Angestellte:r/Arbeiter:in', jsonb_build_array('worker_type', 'worker_type', 'role'), + 'Mitarbeiterart', jsonb_build_array('mitarbeiterart', 'text', 'role'), + 'Besetzungsart', jsonb_build_array('source', 'source_type', 'role'), + 'Kollektivvertrag', jsonb_build_array('collective_agreement', 'collective_agreement', 'role'), + 'Arbeitstage', jsonb_build_array('work_days', 'liste', 'role'), + 'Betriebsrat', jsonb_build_array('is_betriebsrat', 'boolean', 'role'), + 'Dienstwagen', jsonb_build_array('has_dienstwagen', 'boolean', 'role'), + 'Laterale Führung', jsonb_build_array('is_laterale_fuehrung', 'boolean', 'role'), + 'C-Level', jsonb_build_array('is_c_level', 'boolean', 'role'), + 'Dienstwagen Antrieb', jsonb_build_array('dienstwagen_art', 'text', 'role'), + 'Besonderer Kündigungsschutz', jsonb_build_array('has_kuendigungsschutz', 'boolean', 'role'), + 'Kündigungsschutz Personenkreis', jsonb_build_array('kuendigungsschutz_grund', 'text', 'role'), + 'Kündigungsschutz ab', jsonb_build_array('kuendigungsschutz_ab', 'date', 'role'), + 'Kündigungsschutz bis', jsonb_build_array('kuendigungsschutz_bis', 'date', 'role'), + 'Begünstigt behindert', jsonb_build_array('ist_beguenstigt_behindert', 'boolean', 'role'), + -- 'integer' und nicht 'numeric': der Typ geht in update_history_entry + -- unmittelbar als Umwandlung ins SQL (`%L::%s`). 'numeric' liefe über + -- eine zweite, stillschweigende Umwandlung auf die Spalte und rundete + -- dabei — der Grad ist eine ganze Zahl aus einem Bescheid. + 'Grad der Behinderung', jsonb_build_array('behinderung_grad', 'integer', 'role'), + 'Behinderung ab', jsonb_build_array('behinderung_ab', 'date', 'role'), + 'Behinderung bis', jsonb_build_array('behinderung_bis', 'date', 'role'), + 'Teilzeitvariante', jsonb_build_array('teilzeit_art', 'text', 'role'), + 'Teilzeit bis', jsonb_build_array('teilzeit_bis', 'date', 'role'), + -- Die Felder der Abwesenheit. Dritter Eintrag null: sie gehören zu + -- keiner Gruppe im payload einer geplanten Änderung, weil Abwesenheit + -- und Rückkehr ihre eigenen Vorgänge haben. + 'Status', jsonb_build_array('status', 'employment_status', null), + 'Art der Abwesenheit', jsonb_build_array('absence_type', 'text', null), + 'Abwesend ab', jsonb_build_array('karenz_start_date', 'date', null), + 'Geplante Rückkehr', jsonb_build_array('karenz_return_date', 'date', null) + ); +$function$; + + +CREATE OR REPLACE FUNCTION public.change_employee_data(payload jsonb) + RETURNS void + LANGUAGE plpgsql + SET search_path TO 'public', 'pg_temp' +AS $function$ +declare + v_employee_id uuid := (payload->>'employee_id')::uuid; + v_effective_date date := coalesce(nullif(payload->>'effective_date', '')::date, current_date); + v_old employees%rowtype; + v_name text; + v_person_changes jsonb := '[]'::jsonb; + v_contract_changes jsonb := '[]'::jsonb; + v_person jsonb := payload->'person'; + v_contract jsonb := payload->'contract'; + v_role jsonb := payload->'role'; + v_immediate boolean; + v_new_work_days text[]; + v_new_title_prefix text[]; + v_new_title_suffix text[]; + v_pending_id uuid; +begin + perform require_hr_admin(); + select * into v_old from employees where id = v_employee_id; + v_name := v_old.first_name || ' ' || v_old.last_name; + v_immediate := v_effective_date <= current_date; + + -- Der `?`-Test bleibt: ein fehlender Schlüssel heisst „nicht übermittelt", + -- nicht „geleert". Ohne ihn würde jedes nicht gesendete Feld als Änderung + -- auf null gemeldet. + if v_person ? 'first_name' then v_person_changes := app_aenderung(v_person_changes, 'Vorname', v_old.first_name, v_person->>'first_name'); end if; + if v_person ? 'last_name' then v_person_changes := app_aenderung(v_person_changes, 'Nachname', v_old.last_name, v_person->>'last_name'); end if; + if v_person ? 'gender' then v_person_changes := app_aenderung(v_person_changes, 'Geschlecht', v_old.gender::text, v_person->>'gender'); end if; + -- Datumswerte über ::date::text vergleichen, damit „2026-8-3" und + -- „2026-08-03" nicht als Änderung gelten. + if v_person ? 'birth_date' then v_person_changes := app_aenderung(v_person_changes, 'Geburtsdatum', v_old.birth_date::text, (nullif(v_person->>'birth_date','')::date)::text); end if; + if v_person ? 'sv_nummer' then v_person_changes := app_aenderung(v_person_changes, 'SV-Nummer', v_old.sv_nummer, v_person->>'sv_nummer'); end if; + if v_person ? 'nationality' then v_person_changes := app_aenderung(v_person_changes, 'Staatsbürgerschaft', v_old.nationality, v_person->>'nationality'); end if; + if v_person ? 'address' then v_person_changes := app_aenderung(v_person_changes, 'Adresse', v_old.address, v_person->>'address'); end if; + if v_person ? 'postal_code' then v_person_changes := app_aenderung(v_person_changes, 'Postleitzahl', v_old.postal_code, v_person->>'postal_code'); end if; + if v_person ? 'city' then v_person_changes := app_aenderung(v_person_changes, 'Ort', v_old.city, v_person->>'city'); end if; + if v_person ? 'address_country' then v_person_changes := app_aenderung(v_person_changes, 'Land', v_old.address_country, v_person->>'address_country'); end if; + if v_person ? 'email' then v_person_changes := app_aenderung(v_person_changes, 'E-Mail', v_old.email, v_person->>'email'); end if; + if v_person ? 'phone' then v_person_changes := app_aenderung(v_person_changes, 'Telefon', v_old.phone, v_person->>'phone'); end if; + if v_person ? 'emergency_contact_name' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt', v_old.emergency_contact_name, v_person->>'emergency_contact_name'); end if; + if v_person ? 'emergency_contact_phone' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Telefon', v_old.emergency_contact_phone, v_person->>'emergency_contact_phone'); end if; + if v_person ? 'emergency_contact_relation' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Verhältnis', v_old.emergency_contact_relation, v_person->>'emergency_contact_relation'); end if; + if v_person ? 'hat_aufenthaltstitel' then v_person_changes := app_aenderung(v_person_changes, 'Aufenthaltstitel', v_old.hat_aufenthaltstitel::text, v_person->>'hat_aufenthaltstitel'); end if; + if v_person ? 'aufenthaltstitel_bis' then v_person_changes := app_aenderung(v_person_changes, 'Aufenthaltstitel bis', v_old.aufenthaltstitel_bis::text, (nullif(v_person->>'aufenthaltstitel_bis','')::date)::text); end if; + + if v_person ? 'title_prefix' then + v_new_title_prefix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_prefix') elem), '{}'); + v_person_changes := app_aenderung(v_person_changes, 'Titel (vorangestellt)', + array_to_string(v_old.title_prefix, ', '), array_to_string(v_new_title_prefix, ', ')); + end if; + if v_person ? 'title_suffix' then + v_new_title_suffix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_suffix') elem), '{}'); + v_person_changes := app_aenderung(v_person_changes, 'Titel (nachgestellt)', + array_to_string(v_old.title_suffix, ', '), array_to_string(v_new_title_suffix, ', ')); + end if; + + if v_contract ? 'employment_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Beschäftigungsausmaß', v_old.employment_type::text, v_contract->>'employment_type'); end if; + -- Über ::numeric::text, damit „38.50" und „38.5" gleich zählen. + if v_contract ? 'weekly_hours' then v_contract_changes := app_aenderung(v_contract_changes, 'Wochenstunden', v_old.weekly_hours::text, (nullif(v_contract->>'weekly_hours','')::numeric)::text); end if; + if v_contract ? 'contract_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Vertragsart', v_old.contract_type::text, v_contract->>'contract_type'); end if; + if v_contract ? 'contract_end_date' then v_contract_changes := app_aenderung(v_contract_changes, 'Befristet bis', v_old.contract_end_date::text, (nullif(v_contract->>'contract_end_date','')::date)::text); end if; + + if v_role ? 'worker_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Angestellte:r/Arbeiter:in', v_old.worker_type::text, v_role->>'worker_type'); end if; + if v_role ? 'mitarbeiterart' then v_contract_changes := app_aenderung(v_contract_changes, 'Mitarbeiterart', v_old.mitarbeiterart, v_role->>'mitarbeiterart'); end if; + if v_role ? 'source' then v_contract_changes := app_aenderung(v_contract_changes, 'Besetzungsart', v_old.source::text, v_role->>'source'); end if; + if v_role ? 'collective_agreement' then v_contract_changes := app_aenderung(v_contract_changes, 'Kollektivvertrag', v_old.collective_agreement::text, v_role->>'collective_agreement'); end if; + if v_role ? 'work_days' then + v_new_work_days := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}'); + v_contract_changes := app_aenderung(v_contract_changes, 'Arbeitstage', + array_to_string(v_old.work_days, ', '), array_to_string(v_new_work_days, ', ')); + end if; + if v_role ? 'is_betriebsrat' then v_contract_changes := app_aenderung(v_contract_changes, 'Betriebsrat', v_old.is_betriebsrat::text, v_role->>'is_betriebsrat'); end if; + if v_role ? 'has_dienstwagen' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen', v_old.has_dienstwagen::text, v_role->>'has_dienstwagen'); end if; + if v_role ? 'is_laterale_fuehrung' then v_contract_changes := app_aenderung(v_contract_changes, 'Laterale Führung', v_old.is_laterale_fuehrung::text, v_role->>'is_laterale_fuehrung'); end if; + if v_role ? 'is_c_level' then v_contract_changes := app_aenderung(v_contract_changes, 'C-Level', v_old.is_c_level::text, v_role->>'is_c_level'); end if; + if v_role ? 'has_kuendigungsschutz' then v_contract_changes := app_aenderung(v_contract_changes, 'Besonderer Kündigungsschutz', v_old.has_kuendigungsschutz::text, v_role->>'has_kuendigungsschutz'); end if; + if v_role ? 'kuendigungsschutz_grund' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz Personenkreis', v_old.kuendigungsschutz_grund, nullif(v_role->>'kuendigungsschutz_grund', '')); end if; + if v_role ? 'kuendigungsschutz_ab' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz ab', v_old.kuendigungsschutz_ab::text, (nullif(v_role->>'kuendigungsschutz_ab','')::date)::text); end if; + if v_role ? 'kuendigungsschutz_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz bis', v_old.kuendigungsschutz_bis::text, (nullif(v_role->>'kuendigungsschutz_bis','')::date)::text); end if; + if v_role ? 'ist_beguenstigt_behindert' then v_contract_changes := app_aenderung(v_contract_changes, 'Begünstigt behindert', v_old.ist_beguenstigt_behindert::text, v_role->>'ist_beguenstigt_behindert'); end if; + if v_role ? 'behinderung_grad' then v_contract_changes := app_aenderung(v_contract_changes, 'Grad der Behinderung', v_old.behinderung_grad::text, (nullif(v_role->>'behinderung_grad','')::integer)::text); end if; + if v_role ? 'behinderung_ab' then v_contract_changes := app_aenderung(v_contract_changes, 'Behinderung ab', v_old.behinderung_ab::text, (nullif(v_role->>'behinderung_ab','')::date)::text); end if; + if v_role ? 'behinderung_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Behinderung bis', v_old.behinderung_bis::text, (nullif(v_role->>'behinderung_bis','')::date)::text); end if; + if v_role ? 'teilzeit_art' then v_contract_changes := app_aenderung(v_contract_changes, 'Teilzeitvariante', v_old.teilzeit_art, nullif(v_role->>'teilzeit_art', '')); end if; + if v_role ? 'teilzeit_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Teilzeit bis', v_old.teilzeit_bis::text, (nullif(v_role->>'teilzeit_bis','')::date)::text); end if; + if v_role ? 'dienstwagen_art' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen Antrieb', v_old.dienstwagen_art, nullif(v_role->>'dienstwagen_art', '')); end if; + + if v_immediate then + update employees set + first_name = coalesce(v_person->>'first_name', first_name), + last_name = coalesce(v_person->>'last_name', last_name), + gender = coalesce((v_person->>'gender')::gender_type, gender), + birth_date = coalesce((v_person->>'birth_date')::date, birth_date), + sv_nummer = coalesce(v_person->>'sv_nummer', sv_nummer), + nationality = coalesce(v_person->>'nationality', nationality), + address = coalesce(v_person->>'address', address), + postal_code = coalesce(v_person->>'postal_code', postal_code), + city = coalesce(v_person->>'city', city), + address_country = coalesce(v_person->>'address_country', address_country), + email = coalesce(v_person->>'email', email), + phone = coalesce(v_person->>'phone', phone), + emergency_contact_name = case when v_person ? 'emergency_contact_name' then nullif(v_person->>'emergency_contact_name', '') else emergency_contact_name end, + emergency_contact_phone = case when v_person ? 'emergency_contact_phone' then nullif(v_person->>'emergency_contact_phone', '') else emergency_contact_phone end, + emergency_contact_relation = case when v_person ? 'emergency_contact_relation' then nullif(v_person->>'emergency_contact_relation', '') else emergency_contact_relation end, + hat_aufenthaltstitel = coalesce((v_person->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel), + -- Fällt der Titel weg, fällt das Datum mit. Sonst bliebe ein + -- Enddatum ohne Titel stehen, und chk_aufenthaltstitel_bis liesse + -- die ganze Änderung scheitern statt das Offensichtliche zu tun. + aufenthaltstitel_bis = case + when coalesce((v_person->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel) then + case when v_person ? 'aufenthaltstitel_bis' + then nullif(v_person->>'aufenthaltstitel_bis','')::date + else aufenthaltstitel_bis end + else null + end, + title_prefix = case when v_person ? 'title_prefix' then v_new_title_prefix else title_prefix end, + title_suffix = case when v_person ? 'title_suffix' then v_new_title_suffix else title_suffix end, + employment_type = coalesce((v_contract->>'employment_type')::employment_type, employment_type), + weekly_hours = coalesce((v_contract->>'weekly_hours')::numeric, weekly_hours), + contract_type = coalesce((v_contract->>'contract_type')::contract_type, contract_type), + contract_end_date = case when v_contract ? 'contract_end_date' then nullif(v_contract->>'contract_end_date','')::date else contract_end_date end, + worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type), + -- Ohne nullif: die Spalte ist NOT NULL, ein leerer Text waere kein + -- geloeschter Wert, sondern ein Wert, den chk_mitarbeiterart abweist. + mitarbeiterart = coalesce(nullif(v_role->>'mitarbeiterart', ''), mitarbeiterart), + source = coalesce((nullif(v_role->>'source', ''))::source_type, source), + collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement), + work_days = case when v_role ? 'work_days' then v_new_work_days else work_days end, + is_betriebsrat = coalesce((v_role->>'is_betriebsrat')::boolean, is_betriebsrat), + has_dienstwagen = coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen), + is_laterale_fuehrung = coalesce((v_role->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung), + is_c_level = coalesce((v_role->>'is_c_level')::boolean, is_c_level), + has_kuendigungsschutz = coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz), + teilzeit_art = case when v_role ? 'teilzeit_art' then nullif(v_role->>'teilzeit_art', '') else teilzeit_art end, + -- Ohne Variante kein Enddatum: chk_teilzeit_bis verlangt es so, und + -- ein Datum ohne Sache wäre ein Rest, den niemand mehr deutet. + teilzeit_bis = case + when coalesce(nullif(v_role->>'teilzeit_art', ''), case when v_role ? 'teilzeit_art' then null else teilzeit_art end) is null then null + when v_role ? 'teilzeit_bis' then nullif(v_role->>'teilzeit_bis','')::date + else teilzeit_bis + end, + -- Fällt der Schutz weg, fallen Grund und beide Daten mit. Andernfalls + -- bliebe ein Rest ohne Bezug stehen — die Bedingungen verbieten das, + -- und der Vorgang schlüge fehl, statt das Offensichtliche zu tun. + kuendigungsschutz_grund = case + when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then + case when v_role ? 'kuendigungsschutz_grund' + then nullif(v_role->>'kuendigungsschutz_grund','') + else kuendigungsschutz_grund end + else null + end, + kuendigungsschutz_ab = case + when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then + case when v_role ? 'kuendigungsschutz_ab' + then nullif(v_role->>'kuendigungsschutz_ab','')::date + else kuendigungsschutz_ab end + else null + end, + kuendigungsschutz_bis = case + when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then + case when v_role ? 'kuendigungsschutz_bis' + then nullif(v_role->>'kuendigungsschutz_bis','')::date + else kuendigungsschutz_bis end + else null + end, + ist_beguenstigt_behindert = coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert), + behinderung_grad = case + when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then + case when v_role ? 'behinderung_grad' then nullif(v_role->>'behinderung_grad','')::integer else behinderung_grad end + else null + end, + behinderung_ab = case + when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then + case when v_role ? 'behinderung_ab' then nullif(v_role->>'behinderung_ab','')::date else behinderung_ab end + else null + end, + behinderung_bis = case + when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then + case when v_role ? 'behinderung_bis' then nullif(v_role->>'behinderung_bis','')::date else behinderung_bis end + else null + end, + dienstwagen_art = case + when coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen) then + coalesce(nullif(v_role->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner') + else null + end + where id = v_employee_id; + elsif jsonb_array_length(v_person_changes) > 0 or jsonb_array_length(v_contract_changes) > 0 then + insert into pending_org_changes (employee_id, change_type, effective_date, payload) + values (v_employee_id, 'contract_change', v_effective_date, payload) + returning id into v_pending_id; + end if; + + if jsonb_array_length(v_person_changes) > 0 then + insert into employee_history (employee_id, event_date, event_type, description, changes, pending_id) + values (v_employee_id, v_effective_date, 'Stammdatenänderung', + 'Geänderte Felder: ' || app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes, v_pending_id); + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes) + values (app_current_user_id(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id, + app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes); + end if; + + if jsonb_array_length(v_contract_changes) > 0 then + insert into employee_history (employee_id, event_date, event_type, description, changes, pending_id) + values (v_employee_id, v_effective_date, 'Vertragsänderung', + 'Geänderte Felder: ' || app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes, v_pending_id); + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes) + values (app_current_user_id(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id, + app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes); + end if; + + -- ── Die Übernahme ──────────────────────────────────────────────── + -- + -- Nur in dieser einen Richtung. Wer von extern nach intern wechselt, wird + -- übernommen; der umgekehrte Weg ist kein Vorgang, den es fachlich gibt. + -- + -- Das Ereignis entsteht **zusätzlich** zur Vertragsänderung darüber und + -- nicht an ihrer Stelle: die eine hält fest, dass ein Feld sich geändert + -- hat und worauf (und lässt sich darüber zurücknehmen), das andere, dass + -- dieser Wechsel eine Übernahme war — und nur das lässt sich zählen. + -- + -- `v_effective_date`, nicht `now()`: eine auf später datierte Übernahme + -- gehört in die Akte auf den Tag, an dem sie gilt. Bei einer sofortigen + -- ist das heute. + if v_role ? 'source' and v_old.source::text = 'Extern' and v_role->>'source' = 'Intern' then + insert into employee_history (employee_id, event_date, event_type, description, pending_id) + values (v_employee_id, v_effective_date, 'Übernahme', + 'Übernahme von extern auf intern, wirksam ab ' || v_effective_date, v_pending_id); + + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details) + values (app_current_user_id(), current_actor_name(), 'Übernahme', v_name, v_employee_id, + 'Von extern auf intern, wirksam ab ' || v_effective_date); + end if; +end; +$function$; + + +-- ═══ Der Nachtlauf holt nach, was er bisher liegenliess ═══ +-- +-- Eine auf später datierte Änderung landet als 'contract_change' in +-- pending_org_changes; apply_due_pending_changes schreibt sie am Stichtag in +-- die Zeile. Bisher schrieb es dabei **nur** Person und Vertrag zurück — die +-- ganze Gruppe `role` fehlte. Betriebsrat, Dienstwagen, Kollektivvertrag, +-- Arbeitstage, Teilzeitvariante und der Kündigungsschutz wurden also erfasst, +-- in der Historie vermerkt, als Vertragsänderung protokolliert — und am +-- Stichtag nicht angewendet. Sichtbar wurde das nie: der Eintrag in der Akte +-- steht da, nur die Spalte blieb alt. +-- +-- Das ist älter als diese Migration und gehört streng genommen nicht zu ihr. +-- Die neuen Felder erben den Fehler aber unmittelbar, und ihn nur für sie zu +-- beheben hiesse, zwei Klassen von Feldern mit zwei Verhaltensweisen zu +-- hinterlassen. Deshalb hier vollständig. +-- +-- Die Reihenfolge der Zuweisungen entspricht der in change_employee_data; +-- dieselben Kopplungen (fällt das Kennzeichen, fällt das Datum) gelten hier +-- genauso, sonst wiese eine Bedingung den Nachtlauf zurück. +CREATE OR REPLACE FUNCTION public.apply_due_pending_changes() + RETURNS integer + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'pg_temp' +AS $function$ +declare + v_rec record; + v_count integer := 0; + v_role jsonb; +begin + for v_rec in + select * from pending_org_changes + where status = 'pending' and effective_date <= current_date + order by effective_date, created_at + loop + if v_rec.change_type = 'transfer' then + update employees set job_title = coalesce(v_rec.payload->>'new_title', job_title) + where id = v_rec.employee_id; + + elsif v_rec.change_type = 'promotion' then + update employees set + job_title = coalesce(v_rec.payload->>'new_title', job_title), + paygrade = coalesce((v_rec.payload->>'new_paygrade')::paygrade_type, paygrade) + where id = v_rec.employee_id; + + elsif v_rec.change_type = 'karenz_start' then + update employees set + status = 'Karenz', + karenz_return_date = (v_rec.payload->>'planned_return_date')::date, + absence_type = coalesce(nullif(v_rec.payload->>'absence_type', ''), absence_type) + where id = v_rec.employee_id; + + elsif v_rec.change_type = 'karenz_return' then + update employees set + status = 'Aktiv', + karenz_return_date = null, + karenz_start_date = null, + absence_type = null, + employment_type = coalesce((v_rec.payload->>'employment_type')::employment_type, employment_type), + weekly_hours = coalesce((v_rec.payload->>'weekly_hours')::numeric, weekly_hours), + -- Auch bei einer *geplanten* Rückkehr: sonst käme jemand am + -- Stichtag mit reduzierten Stunden zurück, und der Grund dafür + -- wäre verschwunden. + teilzeit_art = case when v_rec.payload ? 'teilzeit_art' + then nullif(v_rec.payload->>'teilzeit_art', '') else teilzeit_art end, + teilzeit_bis = case when v_rec.payload ? 'teilzeit_art' + then nullif(v_rec.payload->>'teilzeit_bis', '')::date else teilzeit_bis end + where id = v_rec.employee_id; + + elsif v_rec.change_type = 'contract_change' then + v_role := v_rec.payload->'role'; + update employees set + first_name = coalesce(v_rec.payload->'person'->>'first_name', first_name), + last_name = coalesce(v_rec.payload->'person'->>'last_name', last_name), + gender = coalesce((v_rec.payload->'person'->>'gender')::gender_type, gender), + birth_date = coalesce((v_rec.payload->'person'->>'birth_date')::date, birth_date), + sv_nummer = coalesce(v_rec.payload->'person'->>'sv_nummer', sv_nummer), + nationality = coalesce(v_rec.payload->'person'->>'nationality', nationality), + address = coalesce(v_rec.payload->'person'->>'address', address), + postal_code = coalesce(v_rec.payload->'person'->>'postal_code', postal_code), + city = coalesce(v_rec.payload->'person'->>'city', city), + address_country = coalesce(v_rec.payload->'person'->>'address_country', address_country), + email = coalesce(v_rec.payload->'person'->>'email', email), + phone = coalesce(v_rec.payload->'person'->>'phone', phone), + employment_type = coalesce((v_rec.payload->'contract'->>'employment_type')::employment_type, employment_type), + weekly_hours = coalesce((v_rec.payload->'contract'->>'weekly_hours')::numeric, weekly_hours), + contract_type = coalesce((v_rec.payload->'contract'->>'contract_type')::contract_type, contract_type), + contract_end_date = case when v_rec.payload->'contract' ? 'contract_end_date' + then nullif(v_rec.payload->'contract'->>'contract_end_date','')::date + else contract_end_date end, + worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type), + mitarbeiterart = coalesce(nullif(v_role->>'mitarbeiterart', ''), mitarbeiterart), + source = coalesce((nullif(v_role->>'source', ''))::source_type, source), + collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement), + work_days = case when v_role ? 'work_days' + then coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}') + else work_days end, + is_betriebsrat = coalesce((v_role->>'is_betriebsrat')::boolean, is_betriebsrat), + has_dienstwagen = coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen), + is_laterale_fuehrung = coalesce((v_role->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung), + is_c_level = coalesce((v_role->>'is_c_level')::boolean, is_c_level), + dienstwagen_art = case + when coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen) then + coalesce(nullif(v_role->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner') + else null + end, + teilzeit_art = case when v_role ? 'teilzeit_art' then nullif(v_role->>'teilzeit_art', '') else teilzeit_art end, + teilzeit_bis = case + when coalesce(nullif(v_role->>'teilzeit_art', ''), case when v_role ? 'teilzeit_art' then null else teilzeit_art end) is null then null + when v_role ? 'teilzeit_bis' then nullif(v_role->>'teilzeit_bis','')::date + else teilzeit_bis + end, + hat_aufenthaltstitel = coalesce((v_rec.payload->'person'->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel), + aufenthaltstitel_bis = case + when coalesce((v_rec.payload->'person'->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel) then + case when v_rec.payload->'person' ? 'aufenthaltstitel_bis' + then nullif(v_rec.payload->'person'->>'aufenthaltstitel_bis','')::date + else aufenthaltstitel_bis end + else null + end, + has_kuendigungsschutz = coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz), + kuendigungsschutz_grund = case + when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then + case when v_role ? 'kuendigungsschutz_grund' then nullif(v_role->>'kuendigungsschutz_grund','') else kuendigungsschutz_grund end + else null + end, + kuendigungsschutz_ab = case + when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then + case when v_role ? 'kuendigungsschutz_ab' then nullif(v_role->>'kuendigungsschutz_ab','')::date else kuendigungsschutz_ab end + else null + end, + kuendigungsschutz_bis = case + when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then + case when v_role ? 'kuendigungsschutz_bis' then nullif(v_role->>'kuendigungsschutz_bis','')::date else kuendigungsschutz_bis end + else null + end, + ist_beguenstigt_behindert = coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert), + behinderung_grad = case + when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then + case when v_role ? 'behinderung_grad' then nullif(v_role->>'behinderung_grad','')::integer else behinderung_grad end + else null + end, + behinderung_ab = case + when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then + case when v_role ? 'behinderung_ab' then nullif(v_role->>'behinderung_ab','')::date else behinderung_ab end + else null + end, + behinderung_bis = case + when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then + case when v_role ? 'behinderung_bis' then nullif(v_role->>'behinderung_bis','')::date else behinderung_bis end + else null + end + where id = v_rec.employee_id; + + elsif v_rec.change_type = 'dependent_add' then + insert into employee_dependents (employee_id, first_name, last_name, relationship, sv_nummer, birth_date) + values (v_rec.employee_id, v_rec.payload->>'first_name', v_rec.payload->>'last_name', + (v_rec.payload->>'relationship')::text, + nullif(v_rec.payload->>'sv_nummer', ''), (v_rec.payload->>'birth_date')::date); + + elsif v_rec.change_type = 'dependent_remove' then + delete from employee_dependents where id = (v_rec.payload->>'dependent_id')::uuid; + end if; + + update pending_org_changes set status = 'applied', applied_at = now() where id = v_rec.id; + v_count := v_count + 1; + end loop; + + return v_count; +end; +$function$; + +grant execute on function public.apply_due_pending_changes() to alpenwerk_app; +grant execute on function public.change_employee_data(payload jsonb) to alpenwerk_app; + + +-- Selbstprüfung. +do $$ +declare + v_chg text := pg_get_functiondef('public.change_employee_data(jsonb)'::regprocedure); + v_lauf text := pg_get_functiondef('public.apply_due_pending_changes()'::regprocedure); + v_feld text; +begin + -- Der Wechsel lässt sich erfassen … + if v_chg not like '%source = coalesce%' then + raise exception 'change_employee_data schreibt die Besetzungsart nicht'; + end if; + if v_lauf not like '%source = coalesce%' then + raise exception 'Der Nachtlauf wendet die Besetzungsart nicht an'; + end if; + + -- … und er erzeugt das Ereignis, aber nur in einer Richtung. + if v_chg not like '%Übernahme%' then + raise exception 'change_employee_data schreibt kein Ereignis Übernahme'; + end if; + if v_lauf like '%''Übernahme''%' then + raise exception 'Der Nachtlauf schreibt ein zweites Ereignis Übernahme — die Übernahme stünde doppelt in der Akte'; + end if; + + if not (app_feld_karte() ? 'Besetzungsart') then + raise exception 'Die Feldtabelle kennt die Besetzungsart nicht — dann liesse sich ein Eintrag dazu nicht berichtigen'; + end if; + if app_feld_karte()->'Besetzungsart'->>2 <> 'role' then + raise exception 'Die Gruppe im payload stimmt bei der Besetzungsart nicht'; + end if; + + -- Die Felder der vorangegangenen Migrationen: die vier Funktionen werden + -- erneut vollständig geschrieben, und eines beim Übertragen zu verlieren + -- wäre ein Fehler, der nirgends auffiele. + foreach v_feld in array array[ + 'mitarbeiterart', 'kuendigungsschutz_grund', 'kuendigungsschutz_ab', + 'ist_beguenstigt_behindert', 'behinderung_grad', 'behinderung_ab', 'behinderung_bis' + ] loop + if v_chg not like '%' || v_feld || '%' then + raise exception 'change_employee_data schreibt % nicht mehr', v_feld; + end if; + if v_lauf not like '%' || v_feld || '%' then + raise exception 'Der Nachtlauf wendet % nicht mehr an', v_feld; + end if; + end loop; + + if v_lauf not like '%is_betriebsrat%' or v_lauf not like '%collective_agreement%' then + raise exception 'Der Nachtlauf wendet die Rollenfelder nicht mehr an'; + end if; +end +$$; \ No newline at end of file diff --git a/lib/colors.ts b/lib/colors.ts index c93e8fb..a480840 100644 --- a/lib/colors.ts +++ b/lib/colors.ts @@ -86,6 +86,9 @@ export const EVENT_CATEGORY: Record = { // Begruendung samt Messung steht bei ANSTEHEND_STYLES. Rückkehr: "purple", Versetzung: "info", + // Wie die Versetzung: eine Bewegung innerhalb des Hauses, kein Anfang und + // kein Ende. Gruen waere es nicht — die Person war schon da. + Übernahme: "info", Beförderung: "purple", Reorganisation: "purple", Karenz: "warning", diff --git a/lib/reports.ts b/lib/reports.ts index 513d923..3d1e47e 100644 --- a/lib/reports.ts +++ b/lib/reports.ts @@ -391,6 +391,7 @@ export const EVENT_TYPE_LABELS: Record = { Reorganisation: "Reorganisation", Gehaltsanpassung: "Gehaltsanpassung", Rückkehr: "Rückkehr aus Langzeitabwesenheit", + Übernahme: "Übernahme (extern → intern)", }; // ── Was im Berichtemanager zur Auswahl steht ──────────────────────── diff --git a/lib/types.ts b/lib/types.ts index 132f17d..598b0f3 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -133,7 +133,11 @@ export type HistoryEventType = | "Wiedereintritt" | "Reorganisation" | "Gehaltsanpassung" - | "Rückkehr"; + | "Rückkehr" + // Wechsel von extern auf intern. Eigenes Ereignis auf Wunsch des Kunden — + // ein Vorgang wie Neueinstellung oder Versetzung, nicht bloss ein + // geändertes Feld. Nur in dieser Richtung (Migration 20260917110000). + | "Übernahme"; export type PendingChangeType = | "transfer" | "promotion"