Rename Karenz to Langzeitabwesenheit and record its type
Karenz was doing duty as the name for every kind of extended absence, but the cases behave differently in payroll and reporting — Wochenhilfe, a Präsenzdienst, a long sick leave and a sabbatical are not the same thing. The concept is now called Langzeitabwesenheit and carries which kind it is. - employees.absence_type, constrained to the thirteen kinds. start_karenz stores it on both paths (written straight away, or parked in the pending_org_changes payload when the absence starts later); record_karenz_return and the karenz_return branch of apply_due_pending_changes clear it, so a returned employee does not keep looking like they are still away. It also reaches employee_history, the audit log and the employee export. - The status enum value stays 'Karenz'. Postgres can rename an enum value in place, but every stored function body that spells it would then reference a value that no longer exists — a dozen functions across fifteen migrations, rewritten for a label. The mapping lives in lib/absence.ts instead, which is the single place the UI reads the display name from. - Where a kind is recorded the chip shows it — "Bildungskarenz" says more than "Langzeitabwesenheit". Absences predating the field have none and fall back to the generic name rather than to a guess, and a value outside the list is dropped rather than echoed into the UI. - The export prints the display name, not the raw enum: a payroll hand-off reading "Karenz" for what the app calls Langzeitabwesenheit only causes questions. Audit filter options keep their stored values and change only their labels. - The seed spreads the twelve absences across the kinds; all of them being Karenz would leave any breakdown by kind invisible.
This commit is contained in:
266
supabase/migrations/20260726120000_absence_type.sql
Normal file
266
supabase/migrations/20260726120000_absence_type.sql
Normal file
@@ -0,0 +1,266 @@
|
||||
-- Long-term absence gets a type.
|
||||
--
|
||||
-- "Karenz" was being used as the name for every kind of extended absence,
|
||||
-- but the cases behave differently in payroll and reporting: Wochenhilfe,
|
||||
-- Präsenz-/Zivildienst, a long sick leave and a sabbatical are not the same
|
||||
-- thing. The UI now calls the concept "Langzeitabwesenheit" and asks which
|
||||
-- kind it is.
|
||||
--
|
||||
-- Deliberately NOT renaming the 'Karenz' enum value in employment_status or
|
||||
-- history_event_type. Postgres can rename an enum value in place, but every
|
||||
-- stored function body that spells 'Karenz' would then reference a value
|
||||
-- that no longer exists — that is a dozen functions across fifteen
|
||||
-- migrations, all of them rewritten for a label change. The name is a
|
||||
-- presentation concern, so it is mapped to "Langzeitabwesenheit" in one
|
||||
-- place in the UI (lib/absence.ts) and the stored value stays put.
|
||||
|
||||
alter table employees add column if not exists absence_type text;
|
||||
|
||||
alter table employees drop constraint if exists chk_absence_type;
|
||||
alter table employees add constraint chk_absence_type check (
|
||||
absence_type is null or absence_type in (
|
||||
'Wochenhilfe (Mutterschutz)',
|
||||
'Elternkarenz (inkl. Väterkarenz)',
|
||||
'Papamonat',
|
||||
'Bildungskarenz',
|
||||
'Bildungsteilzeit',
|
||||
'Präsenzdienst',
|
||||
'Zivildienst',
|
||||
'Langer Krankenstand',
|
||||
'Wiedereingliederungsteilzeit',
|
||||
'Pflegekarenz',
|
||||
'Pflegeteilzeit',
|
||||
'Familienhospizkarenz',
|
||||
'Sabbatical'
|
||||
)
|
||||
);
|
||||
|
||||
comment on column employees.absence_type is
|
||||
'Art der laufenden Langzeitabwesenheit; null, wenn keine besteht. Wird bei der Rückkehr geleert.';
|
||||
|
||||
-- Existing absences predate the field and their kind was never recorded, so
|
||||
-- they stay null rather than being guessed at. The UI shows them as
|
||||
-- "Langzeitabwesenheit" without a type until someone sets one.
|
||||
|
||||
-- ── start_karenz ───────────────────────────────────────────────────
|
||||
-- Unchanged apart from carrying absence_type through both paths: written
|
||||
-- straight away when the absence has already begun, or parked in the
|
||||
-- pending_org_changes payload when it starts later.
|
||||
create or replace function start_karenz(payload jsonb)
|
||||
returns void language plpgsql as $$
|
||||
declare
|
||||
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
||||
v_start_date date := (payload->>'karenz_start_date')::date;
|
||||
v_absence_type text := nullif(payload->>'absence_type', '');
|
||||
v_name text;
|
||||
begin
|
||||
perform require_hr_admin();
|
||||
select first_name || ' ' || last_name into v_name from employees where id = v_employee_id;
|
||||
|
||||
if v_start_date <= current_date then
|
||||
update employees set status = 'Karenz', karenz_start_date = v_start_date,
|
||||
karenz_return_date = (payload->>'planned_return_date')::date,
|
||||
absence_type = v_absence_type
|
||||
where id = v_employee_id;
|
||||
else
|
||||
update employees set karenz_start_date = v_start_date where id = v_employee_id;
|
||||
insert into pending_org_changes (employee_id, change_type, effective_date, payload)
|
||||
values (v_employee_id, 'karenz_start', v_start_date,
|
||||
jsonb_build_object('planned_return_date', payload->>'planned_return_date', 'absence_type', v_absence_type));
|
||||
end if;
|
||||
|
||||
insert into employee_history (employee_id, event_date, event_type, description)
|
||||
values (v_employee_id, v_start_date, 'Karenz',
|
||||
coalesce(v_absence_type, 'Langzeitabwesenheit') || ', geplante Rückkehr am ' || (payload->>'planned_return_date') ||
|
||||
case when payload->>'note' is not null and payload->>'note' <> '' then ' — ' || (payload->>'note') else '' end);
|
||||
|
||||
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||
values (auth.uid(), current_actor_name(), 'Karenz', v_name, v_employee_id,
|
||||
coalesce(v_absence_type, 'Langzeitabwesenheit') || ', geplante Rückkehr ' || (payload->>'planned_return_date'));
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ── record_karenz_return ───────────────────────────────────────────
|
||||
-- Clears absence_type alongside the dates: the absence is over, so leaving
|
||||
-- its kind behind would make a returned employee look like they were still
|
||||
-- on one.
|
||||
create or replace function record_karenz_return(payload jsonb)
|
||||
returns void language plpgsql as $$
|
||||
declare
|
||||
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
||||
v_return_date date := (payload->>'return_date')::date;
|
||||
v_name text;
|
||||
v_team_id uuid;
|
||||
v_division_id uuid;
|
||||
v_is_lead boolean;
|
||||
v_manager uuid;
|
||||
v_employment_type employment_type;
|
||||
v_weekly_hours numeric;
|
||||
v_karenz_start date;
|
||||
v_absence_type text;
|
||||
begin
|
||||
perform require_hr_admin();
|
||||
select first_name || ' ' || last_name, team_id, division_id, is_lead, karenz_start_date, absence_type
|
||||
into v_name, v_team_id, v_division_id, v_is_lead, 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
|
||||
v_manager := resolve_manager_for(v_team_id, v_is_lead, v_division_id);
|
||||
update employees set
|
||||
status = 'Aktiv',
|
||||
karenz_return_date = null,
|
||||
karenz_start_date = null,
|
||||
absence_type = null,
|
||||
manager_id = v_manager,
|
||||
employment_type = coalesce(v_employment_type, employment_type),
|
||||
weekly_hours = coalesce(v_weekly_hours, weekly_hours)
|
||||
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));
|
||||
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 ' || (payload->>'return_date'));
|
||||
|
||||
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||
values (auth.uid(), current_actor_name(), 'Rückkehr', v_name, v_employee_id, 'Rückkehr am ' || (payload->>'return_date'));
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ── apply_due_pending_changes ──────────────────────────────────────
|
||||
-- Only the two karenz branches change; the rest of the body is carried over
|
||||
-- unchanged from 20260718160000_dependents_effective_dating.sql.
|
||||
create or replace function apply_due_pending_changes()
|
||||
returns int language plpgsql security definer set search_path = public as $$
|
||||
declare
|
||||
v_rec record;
|
||||
v_count int := 0;
|
||||
v_manager uuid;
|
||||
v_team_id uuid;
|
||||
v_division_id uuid;
|
||||
v_is_lead boolean;
|
||||
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
|
||||
select is_lead into v_is_lead from employees where id = v_rec.employee_id;
|
||||
select division_id into v_division_id from teams t
|
||||
join departments d on d.id = t.department_id
|
||||
where t.id = (v_rec.payload->>'new_team_id')::uuid;
|
||||
v_manager := resolve_manager_for((v_rec.payload->>'new_team_id')::uuid, v_is_lead, v_division_id);
|
||||
update employees set
|
||||
team_id = (v_rec.payload->>'new_team_id')::uuid,
|
||||
manager_id = v_manager,
|
||||
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
|
||||
select team_id, division_id, is_lead into v_team_id, v_division_id, v_is_lead
|
||||
from employees where id = v_rec.employee_id;
|
||||
v_manager := resolve_manager_for(v_team_id, v_is_lead, v_division_id);
|
||||
update employees set
|
||||
status = 'Aktiv',
|
||||
karenz_return_date = null,
|
||||
karenz_start_date = null,
|
||||
absence_type = null,
|
||||
manager_id = v_manager,
|
||||
employment_type = coalesce((v_rec.payload->>'employment_type')::employment_type, employment_type),
|
||||
weekly_hours = coalesce((v_rec.payload->>'weekly_hours')::numeric, weekly_hours)
|
||||
where id = v_rec.employee_id;
|
||||
|
||||
elsif v_rec.change_type = 'contract_change' then
|
||||
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),
|
||||
title_prefix = coalesce(
|
||||
case when v_rec.payload->'person' ? 'title_prefix'
|
||||
then array(select jsonb_array_elements_text(v_rec.payload->'person'->'title_prefix')) end, title_prefix),
|
||||
title_suffix = coalesce(
|
||||
case when v_rec.payload->'person' ? 'title_suffix'
|
||||
then array(select jsonb_array_elements_text(v_rec.payload->'person'->'title_suffix')) end, title_suffix),
|
||||
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_rec.payload->'role'->>'worker_type')::worker_type, worker_type),
|
||||
collective_agreement = coalesce((v_rec.payload->'role'->>'collective_agreement')::collective_agreement, collective_agreement),
|
||||
work_days = coalesce(
|
||||
case when v_rec.payload->'role' ? 'work_days'
|
||||
then array(select jsonb_array_elements_text(v_rec.payload->'role'->'work_days'))::weekday[] end, work_days),
|
||||
is_betriebsrat = coalesce((v_rec.payload->'role'->>'is_betriebsrat')::boolean, is_betriebsrat),
|
||||
has_dienstwagen = coalesce((v_rec.payload->'role'->>'has_dienstwagen')::boolean, has_dienstwagen),
|
||||
is_laterale_fuehrung = coalesce((v_rec.payload->'role'->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung),
|
||||
is_c_level = coalesce((v_rec.payload->'role'->>'is_c_level')::boolean, is_c_level)
|
||||
where id = v_rec.employee_id;
|
||||
|
||||
elsif v_rec.change_type = 'reorg' then
|
||||
select is_lead into v_is_lead from employees where id = v_rec.employee_id;
|
||||
select division_id into v_division_id from teams t
|
||||
join departments d on d.id = t.department_id
|
||||
where t.id = (v_rec.payload->>'target_team_id')::uuid;
|
||||
v_manager := resolve_manager_for((v_rec.payload->>'target_team_id')::uuid, v_is_lead, v_division_id);
|
||||
update employees set team_id = (v_rec.payload->>'target_team_id')::uuid, manager_id = v_manager
|
||||
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')::relationship_type,
|
||||
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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user