-- Der Wiedereintritt nimmt die Stammdaten mit. -- -- Aus dem Gespräch vom 17.09.2026: der Knopf „Wiedereintritt" soll den -- Einstellungsassistenten öffnen, **vorbefüllt** mit dem, was zu der Person -- bekannt ist. Durchklicken, ändern, bestätigen — und danach ist sie aktiv -- oder geplant, je nach Datum. -- -- ═══ Warum die Funktion das Ganze übernimmt ════════════════════════ -- -- Bisher nahm rehire_employee drei Angaben: Person, Datum, Planstelle. Alles -- andere blieb stehen, wie es beim Austritt war. Nach zwei Jahren Abwesenheit -- ist das selten noch richtig — die Anschrift, die Wochenstunden, der -- Kollektivvertrag, oft auch der Name. -- -- Der naheliegende Weg wäre gewesen, danach ein change_employee_data -- hinterherzuschicken. Er ist falsch: zwei Aufrufe sind zwei Transaktionen, -- und scheitert die zweite, steht die Person wieder im Dienst — mit den Daten -- von damals und ohne dass jemand es merkt. Ausserdem entstünde neben dem -- Wiedereintritt eine Vertragsänderung am selben Tag, die niemand vorgenommen -- hat. -- -- Deshalb nimmt die Funktion den ganzen Satz entgegen. Ein Wiedereintritt ist -- ein Vorgang, kein Paar von Vorgängen. -- -- ═══ Warum jedes Feld einzeln mit coalesce ═════════════════════════ -- -- Fehlt ein Schlüssel im payload, bleibt der bestehende Wert stehen. Das ist -- nicht Bequemlichkeit, sondern Abwärtskompatibilität: der alte, schlanke -- Dialog schickt weiterhin nur Datum und Planstelle, und er muss weiter -- funktionieren — sonst wäre diese Migration ein Bruch für jeden Aufruf, den -- ich hier nicht vor Augen habe. -- -- Die Personalnummer steht ausdrücklich **nicht** im payload. Es ist dieselbe -- Person; sie behält ihre Nummer, und sie hier änderbar zu machen hiesse, -- einen zweiten Weg zu schaffen, auf dem die Nummern von Loga und Interflex -- auseinanderlaufen können. CREATE OR REPLACE FUNCTION public.rehire_employee(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_date date := (payload->>'rehire_date')::date; v_position_id uuid := (payload->>'position_id')::uuid; v_name text; v_alt employees%rowtype; v_besetzt uuid; v_schutz boolean; v_behindert boolean; begin perform require_hr_admin(); select * into v_alt from employees where id = v_employee_id; if v_alt.id is null then raise exception 'Die Person existiert nicht.'; end if; v_name := v_alt.first_name || ' ' || v_alt.last_name; if v_position_id is null then raise exception 'Für die Wiedereinstellung muss eine Planstelle angegeben werden.'; end if; -- Dieselben Prüfungen wie bei einer Neueinstellung. Sie fehlten hier, und -- das hiess: eine Wiedereinstellung auf eine besetzte oder noch nicht -- gültige Planstelle scheiterte erst an einem Index oder gar nicht. declare v_ab date; v_bis date; begin select valid_from, valid_to into v_ab, v_bis from om_positions where id = v_position_id; if v_ab is null then raise exception 'Die Planstelle existiert nicht.'; end if; if v_date < v_ab then raise exception 'Die Planstelle gilt erst ab %. Ein Eintritt am % ist darauf nicht möglich.', v_ab, v_date; end if; if v_bis is not null and v_date >= v_bis then raise exception 'Die Planstelle gilt nur bis %. Ein Eintritt am % ist darauf nicht möglich.', v_bis, v_date; end if; end; select pa.employee_id into v_besetzt from position_assignments pa where pa.position_id = v_position_id and (pa.valid_to is null or pa.valid_to > v_date); if v_besetzt is not null then raise exception 'Diese Planstelle ist bereits besetzt.'; end if; -- Die beiden Kennzeichen zuerst: an ihnen hängen Felder, die ohne sie nicht -- dastehen dürfen (chk_kuendigungsschutz_grund, chk_behinderung). v_schutz := coalesce((payload->>'has_kuendigungsschutz')::boolean, v_alt.has_kuendigungsschutz); v_behindert := coalesce((payload->>'ist_beguenstigt_behindert')::boolean, v_alt.ist_beguenstigt_behindert); update employees set status = case when v_date <= current_date then 'Aktiv' else 'Geplant' end, entry_date = v_date, exit_date = null, exit_reason = null, austrittsart = null, first_name = coalesce(payload->>'first_name', first_name), last_name = coalesce(payload->>'last_name', last_name), gender = coalesce((payload->>'gender')::gender_type, gender), birth_date = coalesce((payload->>'birth_date')::date, birth_date), sv_nummer = case when payload ? 'sv_nummer' then nullif(payload->>'sv_nummer', '') else sv_nummer end, nationality = coalesce(payload->>'nationality', nationality), email = case when payload ? 'email' then nullif(payload->>'email', '') else email end, phone = case when payload ? 'phone' then nullif(payload->>'phone', '') else phone end, address = coalesce(payload->>'address', address), postal_code = coalesce(payload->>'postal_code', postal_code), city = coalesce(payload->>'city', city), address_country = coalesce(payload->>'address_country', address_country), location_id = coalesce((payload->>'location_id')::uuid, location_id), title_prefix = case when payload ? 'title_prefix' then coalesce(array(select jsonb_array_elements_text(payload->'title_prefix')), '{}') else title_prefix end, title_suffix = case when payload ? 'title_suffix' then coalesce(array(select jsonb_array_elements_text(payload->'title_suffix')), '{}') else title_suffix end, emergency_contact_name = case when payload ? 'emergency_contact_name' then nullif(payload->>'emergency_contact_name', '') else emergency_contact_name end, emergency_contact_phone = case when payload ? 'emergency_contact_phone' then nullif(payload->>'emergency_contact_phone', '') else emergency_contact_phone end, emergency_contact_relation = case when payload ? 'emergency_contact_relation' then nullif(payload->>'emergency_contact_relation', '') else emergency_contact_relation end, hat_aufenthaltstitel = coalesce((payload->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel), aufenthaltstitel_bis = case when coalesce((payload->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel) then case when payload ? 'aufenthaltstitel_bis' then nullif(payload->>'aufenthaltstitel_bis', '')::date else aufenthaltstitel_bis end else null end, -- Die Tätigkeit kommt von der Planstelle, nicht aus dem payload: sie -- steht am Platz, den die Person einnimmt, und nicht an ihr. job_title = (select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = v_position_id), employment_type = coalesce((payload->>'employment_type')::employment_type, employment_type), weekly_hours = coalesce((payload->>'weekly_hours')::numeric, weekly_hours), contract_type = coalesce((payload->>'contract_type')::contract_type, contract_type), contract_end_date = case when payload ? 'contract_end_date' then nullif(payload->>'contract_end_date', '')::date else contract_end_date end, paygrade = coalesce((payload->>'paygrade')::paygrade_type, paygrade), source = coalesce((payload->>'source')::source_type, source), worker_type = coalesce((payload->>'worker_type')::worker_type, worker_type), mitarbeiterart = coalesce(nullif(payload->>'mitarbeiterart', ''), mitarbeiterart), collective_agreement = coalesce((payload->>'collective_agreement')::collective_agreement, collective_agreement), work_days = case when payload ? 'work_days' then coalesce(nullif(array(select jsonb_array_elements_text(payload->'work_days'))::text[], '{}'), work_days) else work_days end, is_betriebsrat = coalesce((payload->>'is_betriebsrat')::boolean, is_betriebsrat), has_dienstwagen = coalesce((payload->>'has_dienstwagen')::boolean, has_dienstwagen), dienstwagen_art = case when coalesce((payload->>'has_dienstwagen')::boolean, has_dienstwagen) then coalesce(nullif(payload->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner') else null end, is_laterale_fuehrung = coalesce((payload->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung), is_c_level = coalesce((payload->>'is_c_level')::boolean, is_c_level), has_kuendigungsschutz = v_schutz, kuendigungsschutz_grund = case when v_schutz then case when payload ? 'kuendigungsschutz_grund' then nullif(payload->>'kuendigungsschutz_grund', '') else kuendigungsschutz_grund end else null end, kuendigungsschutz_ab = case when v_schutz then case when payload ? 'kuendigungsschutz_ab' then nullif(payload->>'kuendigungsschutz_ab', '')::date else kuendigungsschutz_ab end else null end, kuendigungsschutz_bis = case when v_schutz then case when payload ? 'kuendigungsschutz_bis' then nullif(payload->>'kuendigungsschutz_bis', '')::date else kuendigungsschutz_bis end else null end, ist_beguenstigt_behindert = v_behindert, behinderung_grad = case when v_behindert then case when payload ? 'behinderung_grad' then nullif(payload->>'behinderung_grad', '')::integer else behinderung_grad end else null end, behinderung_ab = case when v_behindert then case when payload ? 'behinderung_ab' then nullif(payload->>'behinderung_ab', '')::date else behinderung_ab end else null end, behinderung_bis = case when v_behindert then case when payload ? 'behinderung_bis' then nullif(payload->>'behinderung_bis', '')::date else behinderung_bis end else null end where id = v_employee_id; insert into position_assignments (position_id, employee_id, valid_from) values (v_position_id, v_employee_id, v_date); insert into employee_history (employee_id, event_date, event_type, description) values (v_employee_id, v_date, 'Wiedereintritt', 'Wiedereinstellung zum ' || v_date || ' auf Planstelle ' || (select position_number from om_positions where id = v_position_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(), 'Wiedereinstellung', v_name, v_employee_id, 'Wiedereintritt am ' || v_date); end; $function$; grant execute on function public.rehire_employee(payload jsonb) to alpenwerk_app; -- Selbstprüfung. do $$ declare v_re text := pg_get_functiondef('public.rehire_employee(jsonb)'::regprocedure); v_feld text; begin -- Was der Wiedereintritt schon vorher tat, muss er weiter tun. foreach v_feld in array array['exit_date = null', 'exit_reason = null', 'austrittsart = null', 'position_assignments', 'Wiedereintritt', 'Wiedereinstellung'] loop if v_re not like '%' || v_feld || '%' then raise exception 'rehire_employee macht % nicht mehr', v_feld; end if; end loop; -- Und was neu dazukommt. foreach v_feld in array array['first_name', 'weekly_hours', 'collective_agreement', 'work_days', 'kuendigungsschutz_grund', 'behinderung_grad', 'mitarbeiterart', 'source'] loop if v_re not like '%' || v_feld || '%' then raise exception 'rehire_employee nimmt % nicht entgegen', v_feld; end if; end loop; -- Die Personalnummer bleibt aussen vor: es ist dieselbe Person, und ein -- zweiter Weg, sie zu ändern, hiesse ein zweiter Weg, sie von Loga und -- Interflex abweichen zu lassen. if v_re like '%personnel_number%' then raise exception 'rehire_employee fasst die Personalnummer an'; end if; if v_re like '%auth.uid()%' then raise exception 'rehire_employee ruft wieder auth.uid()'; end if; if position('SET search_path' in v_re) = 0 then raise exception 'rehire_employee hat keinen festen search_path mehr'; end if; end $$;