Make the part-time arrangement a state you can report on
Last step moved the four part-time arrangements out of the absence list and recorded the reason in the history text. That answered "what happened" but not "who is in one right now", and the profile showed nothing at all. So it becomes a real field: teilzeit_art, with an optional end date. The objection I raised then still holds — a state goes stale, because nobody goes back to note when a Bildungsteilzeit ended. teilzeit_bis is the answer to it: with an end date a report decides for itself what is still running instead of trusting that someone maintained the row. Left empty it means "open end", which is an honest thing to say. It runs through the ordinary change machinery rather than beside it. It sits in app_feld_karte, so it shows up in the history as a field with before and after, and can be corrected there like any other. The description suffix from last step is gone — writing the same thing twice is how two versions start disagreeing. Reporting: filter by variant, by "in one at all", and by when it ends; group headcount by variant, where the absence of one reads "Keine" rather than a dash, because in a report that is an answer and not a gap. Plus columns in the export and the import. One gap found while rehearsing, and only because the probe happened to pick a return date in the future: a scheduled return carries its payload through pending_org_changes, and that payload did not include the variant. Someone would have come back on reduced hours in April with the reason gone. The daily run now carries it too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
350
supabase/migrations/20260814160000_teilzeit_art.sql
Normal file
350
supabase/migrations/20260814160000_teilzeit_art.sql
Normal file
@@ -0,0 +1,350 @@
|
||||
-- Die Teilzeitvariante als Zustand, nicht nur als Notiz.
|
||||
--
|
||||
-- Beim letzten Schritt sind die vier Teilzeiten aus der Abwesenheitsliste
|
||||
-- gewandert und wurden mit der Änderung festgehalten — im Beschreibungstext
|
||||
-- der Historie. Damit liess sich nachlesen, *dass* jemand in Bildungsteilzeit
|
||||
-- ging, aber nicht auswerten, wer gerade in einer ist, und am Profil stand es
|
||||
-- nirgends.
|
||||
--
|
||||
-- Also ein richtiges Feld: teilzeit_art, dazu ein freiwilliges Enddatum.
|
||||
--
|
||||
-- Der Einwand von damals — ein Zustand veraltet, weil niemand nachträgt, wann
|
||||
-- eine Bildungsteilzeit endet — bleibt richtig und ist der Grund für
|
||||
-- teilzeit_bis. Mit einem Enddatum kann eine Auswertung selbst entscheiden,
|
||||
-- was noch läuft, statt sich auf gepflegte Daten zu verlassen. Bleibt das
|
||||
-- Datum leer, heisst das „Ende offen", und das ist eine ehrliche Aussage.
|
||||
--
|
||||
-- Geführt wird das Feld über den gewöhnlichen Weg der Änderungen: es steht in
|
||||
-- app_feld_karte, taucht in der Historie als Feld mit Vorher/Nachher auf und
|
||||
-- lässt sich dort berichtigen wie jedes andere. Der Anhang am
|
||||
-- Beschreibungstext aus dem letzten Schritt entfällt dafür — zweimal
|
||||
-- dasselbe zu schreiben lädt nur dazu ein, dass die zwei Fassungen
|
||||
-- auseinanderlaufen.
|
||||
|
||||
alter table employees
|
||||
add column if not exists teilzeit_art text,
|
||||
add column if not exists teilzeit_bis date;
|
||||
|
||||
comment on column employees.teilzeit_art is
|
||||
'Bildungs-, Eltern-, Pflege- oder Wiedereingliederungsteilzeit; null bei einer gewöhnlichen vertraglichen Stundenregelung. Keine Abwesenheit — die Person arbeitet, nur kürzer.';
|
||||
comment on column employees.teilzeit_bis is
|
||||
'Ende der Teilzeit, falls bekannt. Freiwillig; leer heisst „Ende offen". Erlaubt Auswertungen darüber, was noch läuft, ohne auf nachgepflegte Daten angewiesen zu sein.';
|
||||
|
||||
alter table employees drop constraint if exists chk_teilzeit_art;
|
||||
alter table employees add constraint chk_teilzeit_art
|
||||
check (teilzeit_art is null or teilzeit_art in
|
||||
('Bildungsteilzeit', 'Elternteilzeit', 'Pflegeteilzeit', 'Wiedereingliederungsteilzeit'));
|
||||
|
||||
alter table employees drop constraint if exists chk_teilzeit_bis;
|
||||
alter table employees add constraint chk_teilzeit_bis
|
||||
check (teilzeit_bis is null or teilzeit_art is not null);
|
||||
|
||||
comment on constraint chk_teilzeit_bis on employees is
|
||||
'Ein Enddatum ohne Variante wäre ein Rest ohne Bezug. Umgekehrt ist eine Variante ohne Enddatum ausdrücklich erlaubt — nicht jede Teilzeit hat ein bekanntes Ende.';
|
||||
|
||||
create index if not exists idx_employees_teilzeit on employees (teilzeit_art) where teilzeit_art is not null;
|
||||
|
||||
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'),
|
||||
'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'),
|
||||
'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 bis', jsonb_build_array('kuendigungsschutz_bis', 'date', 'role'),
|
||||
'Teilzeitvariante', jsonb_build_array('teilzeit_art', 'text', 'role'),
|
||||
'Teilzeit bis', jsonb_build_array('teilzeit_bis', 'date', 'role')
|
||||
);
|
||||
$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[];
|
||||
-- Die Teilzeitvariante ist jetzt ein Feld an der Person (teilzeit_art)
|
||||
-- und läuft über die gewöhnliche Änderungsliste. Der frühere Anhang am
|
||||
-- Beschreibungstext ist damit weg — zweimal dasselbe zu schreiben lädt
|
||||
-- nur dazu ein, dass die zwei Fassungen auseinanderlaufen.
|
||||
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 ? '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 ? '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_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 ? '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,
|
||||
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),
|
||||
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),
|
||||
-- Fällt der Schutz weg, fällt das Datum mit. Andernfalls bliebe ein
|
||||
-- Enddatum ohne Schutz stehen — die Bedingung verbietet das, und der
|
||||
-- Vorgang schlüge fehl, statt das Offensichtliche zu tun.
|
||||
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,
|
||||
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,
|
||||
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;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.record_karenz_return(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_return_date date := (payload->>'return_date')::date;
|
||||
v_name text;
|
||||
v_employment_type employment_type;
|
||||
v_weekly_hours numeric;
|
||||
v_karenz_start date;
|
||||
v_absence_type text;
|
||||
-- Warum jemand mit weniger Stunden zurückkommt: Wiedereingliederungs-
|
||||
-- oder Elternteilzeit. Nur bedeutsam, wenn überhaupt reduziert wird.
|
||||
v_grund text := nullif(payload->>'reduction_reason', '');
|
||||
begin
|
||||
perform require_hr_admin();
|
||||
select first_name || ' ' || last_name, karenz_start_date, absence_type
|
||||
into v_name, v_karenz_start, v_absence_type
|
||||
from employees where id = v_employee_id;
|
||||
|
||||
if v_karenz_start is not null and v_return_date <= v_karenz_start then
|
||||
raise exception 'Das Rückkehrdatum muss nach dem Beginn der Langzeitabwesenheit (%) liegen.', v_karenz_start;
|
||||
end if;
|
||||
|
||||
if payload->>'employment_mode' = 'Vollzeit' then
|
||||
v_employment_type := 'Vollzeit'; v_weekly_hours := 38.5;
|
||||
elsif payload->>'employment_mode' = 'Teilzeit' then
|
||||
v_employment_type := 'Teilzeit'; v_weekly_hours := (payload->>'weekly_hours')::numeric;
|
||||
end if;
|
||||
|
||||
if v_return_date <= current_date then
|
||||
-- Keine Manager-Nachführung mehr nötig: wer aus der Abwesenheit
|
||||
-- zurückkehrt, ist wieder anwesend, und die abgeleitete Berichtslinie
|
||||
-- fällt automatisch von der Vertretung auf ihn zurück.
|
||||
update employees set
|
||||
status = 'Aktiv',
|
||||
karenz_return_date = null,
|
||||
karenz_start_date = null,
|
||||
absence_type = null,
|
||||
employment_type = coalesce(v_employment_type, employment_type),
|
||||
weekly_hours = coalesce(v_weekly_hours, weekly_hours),
|
||||
-- Kehrt jemand reduziert zurück, ist der Grund dafür ein Zustand,
|
||||
-- kein Einmalereignis: danach lässt sich auswerten, wer gerade in
|
||||
-- Eltern- oder Wiedereingliederungsteilzeit ist.
|
||||
teilzeit_art = case when payload->>'employment_mode' = 'Teilzeit' then v_grund else teilzeit_art end,
|
||||
teilzeit_bis = case
|
||||
when payload->>'employment_mode' = 'Teilzeit' and v_grund is not null
|
||||
then nullif(payload->>'teilzeit_bis', '')::date
|
||||
when payload->>'employment_mode' = 'Teilzeit' then null
|
||||
else teilzeit_bis end
|
||||
where id = v_employee_id;
|
||||
else
|
||||
update employees set karenz_return_date = v_return_date where id = v_employee_id;
|
||||
insert into pending_org_changes (employee_id, change_type, effective_date, payload)
|
||||
values (v_employee_id, 'karenz_return', v_return_date,
|
||||
jsonb_build_object('employment_type', v_employment_type, 'weekly_hours', v_weekly_hours,
|
||||
'teilzeit_art', v_grund, 'teilzeit_bis', nullif(payload->>'teilzeit_bis', '')));
|
||||
end if;
|
||||
|
||||
insert into employee_history (employee_id, event_date, event_type, description)
|
||||
values (v_employee_id, v_return_date, 'Rückkehr',
|
||||
'Rückkehr aus ' || coalesce(v_absence_type, 'Langzeitabwesenheit') || ' am ' || v_return_date
|
||||
|| case when payload->>'employment_mode' = 'Teilzeit'
|
||||
then ', reduziert auf ' || (payload->>'weekly_hours') || ' h'
|
||||
|| coalesce(' (' || v_grund || ')', '')
|
||||
else '' end);
|
||||
|
||||
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||
values (app_current_user_id(), current_actor_name(), 'Rückkehr', v_name, v_employee_id, 'Rückkehr am ' || v_return_date || coalesce(' — ' || v_grund, ''));
|
||||
end;
|
||||
$function$;
|
||||
|
||||
|
||||
-- Selbstprüfung.
|
||||
do $$
|
||||
declare
|
||||
v_chg text := pg_get_functiondef('public.change_employee_data(jsonb)'::regprocedure);
|
||||
v_ret text := pg_get_functiondef('public.record_karenz_return(jsonb)'::regprocedure);
|
||||
begin
|
||||
if not (app_feld_karte() ? 'Teilzeitvariante' and app_feld_karte() ? 'Teilzeit bis') then
|
||||
raise exception 'Die Feldtabelle kennt die Teilzeitvariante nicht';
|
||||
end if;
|
||||
if v_chg not like '%teilzeit_art%' then
|
||||
raise exception 'change_employee_data schreibt die Teilzeitvariante nicht';
|
||||
end if;
|
||||
if v_chg like '%hours_reason%' then
|
||||
raise exception 'Der alte Anhang am Beschreibungstext steht noch drin';
|
||||
end if;
|
||||
if v_ret not like '%teilzeit_art%' then
|
||||
raise exception 'record_karenz_return schreibt die Teilzeitvariante nicht';
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
Reference in New Issue
Block a user