Eine geleerte freiwillige Angabe ist null, nicht die leere Zeichenkette
Beim Speichern in "Daten aendern" brach es ab, sobald bei einer zweiten Person das Feld "E-Mail (privat)" leer blieb: duplicate key value violates unique constraint "employees_email_key". email war bis 20260811140000 NOT NULL -- jede Person hatte eine, das Feld war nie leer. Seither ist die Angabe freiwillig, aber change_employee_data schrieb weiter `coalesce(v_person->>'email', email)`. Ein leeres Formularfeld kommt als "" an, und "" ist nicht null: es landete als leere Zeichenkette in der Spalte. Beim ersten Mal ging das gut, beim zweiten schlug der eindeutige Index zu -- "" ist gleich "", waehrend null nie gleich null ist. Aufgefallen ist es erst jetzt, weil die Spieldaten durchweg Adressen trugen. Die 784 uebernommenen Personen tragen keine. Alle freiwilligen Textfelder bekommen deshalb dieselbe Form wie der Notfallkontakt und die Firmen-E-Mail: `case when ? then nullif(…, '')`. Fehlt der Schluessel, bleibt der alte Wert; steht er leer da, wird das Feld geleert -- die Absicht, die jemand ausdrueckt, wenn er eine Angabe herausloescht. first_name, last_name, gender, birth_date und nationality bleiben ausgenommen, sie sind NOT NULL. Der Nachtlauf bekommt dieselbe Behandlung, sonst liefe eine auf spaeter datierte Aenderung in denselben Index -- um drei Uhr frueh und ohne jemanden, dem die Meldung angezeigt wuerde. Was bereits als "" in der Datenbank steht, raeumt die Migration auf; sonst blockierte diese eine Zeile weiterhin jede weitere Person ohne Adresse.
This commit is contained in:
501
db/migrations/20260923160000_leere_angaben_sind_null.sql
Normal file
501
db/migrations/20260923160000_leere_angaben_sind_null.sql
Normal file
@@ -0,0 +1,501 @@
|
||||
-- Eine geleerte freiwillige Angabe ist null, nicht die leere Zeichenkette.
|
||||
--
|
||||
-- Gemeldet als Fehlermeldung beim Speichern in „Daten ändern":
|
||||
-- `duplicate key value violates unique constraint "employees_email_key"`,
|
||||
-- sobald bei einer zweiten Person das Feld „E-Mail (privat)" leer blieb.
|
||||
--
|
||||
-- ═══ Warum das erst jetzt auffiel ══════════════════════════════════
|
||||
--
|
||||
-- `email` war bis 20260811140000 NOT NULL: jede Person hatte eine, das Feld
|
||||
-- im Formular war nie leer. Seither ist die Angabe freiwillig — aber
|
||||
-- change_employee_data schrieb weiter `coalesce(v_person->>'email', email)`.
|
||||
-- Ein leeres Formularfeld kommt als "" an, und "" ist nicht null: es landete
|
||||
-- als leere Zeichenkette in der Spalte. Beim ersten Mal ging das gut, beim
|
||||
-- zweiten schlug der eindeutige Index zu, denn "" ist gleich "" — während
|
||||
-- null in Postgres nie gleich null ist und deshalb beliebig oft vorkommen
|
||||
-- darf.
|
||||
--
|
||||
-- Vorher fiel es niemandem auf, weil die Spieldaten durchweg Adressen
|
||||
-- trugen. Die 784 übernommenen Personen tragen keine, und damit ist jede
|
||||
-- zweite Änderung an irgendeiner von ihnen darauf gelaufen.
|
||||
--
|
||||
-- ═══ Was sich ändert ═══════════════════════════════════════════════
|
||||
--
|
||||
-- Dieselbe Form wie beim Notfallkontakt und bei der Firmen-E-Mail:
|
||||
-- `case when ? then nullif(…, '') else … end`. Der Unterschied zu coalesce
|
||||
-- ist beides Mal wichtig:
|
||||
--
|
||||
-- * fehlt der Schlüssel, bleibt der alte Wert — wie bisher;
|
||||
-- * steht er mit leerem Wert da, wird das Feld **geleert** statt auf ""
|
||||
-- gesetzt. Das ist die Absicht, die jemand im Formular ausdrückt, wenn
|
||||
-- er eine Angabe herauslöscht.
|
||||
--
|
||||
-- Betroffen sind nur die freiwilligen Textfelder. first_name, last_name,
|
||||
-- gender, birth_date und nationality sind NOT NULL; dort wäre nullif ein
|
||||
-- Fehler und keine Verbesserung.
|
||||
--
|
||||
-- Der Nachtlauf bekommt dieselbe Behandlung: eine auf später datierte
|
||||
-- Änderung liefe sonst in denselben Index, nur um drei Uhr früh und ohne
|
||||
-- jemanden, dem die Meldung angezeigt würde.
|
||||
|
||||
-- Was schon als leere Zeichenkette dasteht, wird aufgeräumt. Andernfalls
|
||||
-- blockierte die eine bereits gespeicherte "" weiterhin jede weitere Person
|
||||
-- ohne private Adresse.
|
||||
update employees set
|
||||
sv_nummer = nullif(sv_nummer, ''),
|
||||
address = nullif(address, ''),
|
||||
postal_code = nullif(postal_code, ''),
|
||||
city = nullif(city, ''),
|
||||
address_country = nullif(address_country, ''),
|
||||
email = nullif(email, ''),
|
||||
phone = nullif(phone, ''),
|
||||
company_email = nullif(company_email, '')
|
||||
where sv_nummer = '' or address = '' or postal_code = '' or city = ''
|
||||
or address_country = '' or email = '' or phone = '' or company_email = '';
|
||||
|
||||
|
||||
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 ? 'company_email' then v_person_changes := app_aenderung(v_person_changes, 'Firmen-E-Mail', v_old.company_email, v_person->>'company_email'); 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 = case when v_person ? 'sv_nummer' then nullif(v_person->>'sv_nummer', '') else sv_nummer end,
|
||||
nationality = coalesce(v_person->>'nationality', nationality),
|
||||
address = case when v_person ? 'address' then nullif(v_person->>'address', '') else address end,
|
||||
postal_code = case when v_person ? 'postal_code' then nullif(v_person->>'postal_code', '') else postal_code end,
|
||||
city = case when v_person ? 'city' then nullif(v_person->>'city', '') else city end,
|
||||
address_country = case when v_person ? 'address_country' then nullif(v_person->>'address_country', '') else address_country end,
|
||||
email = case when v_person ? 'email' then nullif(v_person->>'email', '') else email end,
|
||||
phone = case when v_person ? 'phone' then nullif(v_person->>'phone', '') else phone end,
|
||||
company_email = case when v_person ? 'company_email' then nullif(v_person->>'company_email', '') else company_email end,
|
||||
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 = case when v_rec.payload->'person' ? 'sv_nummer' then nullif(v_rec.payload->'person'->>'sv_nummer', '') else sv_nummer end,
|
||||
nationality = coalesce(v_rec.payload->'person'->>'nationality', nationality),
|
||||
address = case when v_rec.payload->'person' ? 'address' then nullif(v_rec.payload->'person'->>'address', '') else address end,
|
||||
postal_code = case when v_rec.payload->'person' ? 'postal_code' then nullif(v_rec.payload->'person'->>'postal_code', '') else postal_code end,
|
||||
city = case when v_rec.payload->'person' ? 'city' then nullif(v_rec.payload->'person'->>'city', '') else city end,
|
||||
address_country = case when v_rec.payload->'person' ? 'address_country' then nullif(v_rec.payload->'person'->>'address_country', '') else address_country end,
|
||||
email = case when v_rec.payload->'person' ? 'email' then nullif(v_rec.payload->'person'->>'email', '') else email end,
|
||||
phone = case when v_rec.payload->'person' ? 'phone' then nullif(v_rec.payload->'person'->>'phone', '') else phone end,
|
||||
company_email = case when v_rec.payload->'person' ? 'company_email' then nullif(v_rec.payload->'person'->>'company_email', '') else company_email end,
|
||||
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_leer int;
|
||||
begin
|
||||
-- Kein coalesce mehr auf den freiwilligen Feldern: genau das schrieb "".
|
||||
if v_chg like '%coalesce(v_person->>''email''%' then
|
||||
raise exception 'change_employee_data schreibt die E-Mail weiter mit coalesce — eine geleerte Angabe wuerde "" statt null.';
|
||||
end if;
|
||||
if v_lauf like '%coalesce(v_rec.payload->''person''->>''email''%' then
|
||||
raise exception 'Der Nachtlauf schreibt die E-Mail weiter mit coalesce.';
|
||||
end if;
|
||||
|
||||
if v_chg not like '%nullif(v_person->>''phone''%' then
|
||||
raise exception 'change_employee_data schreibt die Telefonnummer nicht mit nullif.';
|
||||
end if;
|
||||
|
||||
select count(*) into v_leer from employees
|
||||
where sv_nummer = '' or address = '' or postal_code = '' or city = ''
|
||||
or address_country = '' or email = '' or phone = '' or company_email = '';
|
||||
if v_leer > 0 then
|
||||
raise exception 'Es stehen noch % Zeilen mit leeren Zeichenketten in freiwilligen Feldern.', v_leer;
|
||||
end if;
|
||||
end $$;
|
||||
Reference in New Issue
Block a user