Let an absence be taken back, in the right order
A long-term absence recorded by mistake could only be undone by booking a second event on top of it — leaving two entries in the file, the first of which never happened. Karenz and Rückkehr can now be deleted and corrected like the other entries. For that to restore anything, the two operations first had to start recording what they overwrote. start_karenz and record_karenz_return now keep before/after the way change_employee_data does: status, kind of absence, start, planned return — and for a return also employment type, hours and the part-time variant. Without that there is nothing to revert to, only a sentence. The ordering rule HR asked for is enforced in the database, not just in the UI: an absence cannot be deleted while a later return exists. A return standing on its own would be a return from nothing, and the person's status would derive from an entry whose starting point had been deleted. Delete the return first and the absence frees up. Rehearsed end to end on real data: absence recorded, return recorded on reduced hours; deleting the absence refused; deleting the return put the person back on Karenz with the original hours and the part-time variant cleared; deleting the absence then put them back to Aktiv with no trace. Rows written before today carry no before/after and stay untouchable, with the reason they already gave. Planned absences are refused too — they have their own operation, and their fields have no place in a pending payload, which is why app_feld_karte carries a null group for them rather than a plausible-looking wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,9 @@ export function HistorieTab({ history, employeeId }: { history: HistoryRow[]; em
|
|||||||
{history.map((h) => {
|
{history.map((h) => {
|
||||||
const isFuture = h.event_date > today;
|
const isFuture = h.event_date > today;
|
||||||
const changes = h.changes ?? [];
|
const changes = h.changes ?? [];
|
||||||
const urteil = darfKorrigiertWerden(h, today);
|
// Die ganze Historie mitgeben: ob eine Abwesenheit gelöscht werden
|
||||||
|
// darf, hängt daran, ob es eine spätere Rückkehr gibt.
|
||||||
|
const urteil = darfKorrigiertWerden(h, today, history);
|
||||||
return (
|
return (
|
||||||
<li key={h.id} className="py-3">
|
<li key={h.id} className="py-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -21,14 +21,22 @@ type Eintrag = {
|
|||||||
changes: AuditChange[] | null;
|
changes: AuditChange[] | null;
|
||||||
/** Der geplante Vorgang, falls die Änderung noch nicht wirksam ist. */
|
/** Der geplante Vorgang, falls die Änderung noch nicht wirksam ist. */
|
||||||
pending_id?: string | null;
|
pending_id?: string | null;
|
||||||
|
/** Für die Reihenfolge bei gleichem Datum. */
|
||||||
|
created_at?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function darfKorrigiertWerden(eintrag: Eintrag, heute: string): KorrekturUrteil {
|
/** Die Vorgänge, die sich zurücknehmen und berichtigen lassen. */
|
||||||
|
const KORRIGIERBAR: HistoryEventType[] = ["Stammdatenänderung", "Vertragsänderung", "Karenz", "Rückkehr"];
|
||||||
|
|
||||||
|
/** Abwesenheit und Rückkehr haben eigene Vorgänge, auch für die Zukunft. */
|
||||||
|
const NUR_WIRKSAM: HistoryEventType[] = ["Karenz", "Rückkehr"];
|
||||||
|
|
||||||
|
export function darfKorrigiertWerden(eintrag: Eintrag, heute: string, alle: Eintrag[] = []): KorrekturUrteil {
|
||||||
if (eintrag.event_type === "Eintritt") {
|
if (eintrag.event_type === "Eintritt") {
|
||||||
return { erlaubt: false, grund: "Der Eintritt ist der Anfang der Zeitleiste und bleibt." };
|
return { erlaubt: false, grund: "Der Eintritt ist der Anfang der Zeitleiste und bleibt." };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eintrag.event_type !== "Stammdatenänderung" && eintrag.event_type !== "Vertragsänderung") {
|
if (!KORRIGIERBAR.includes(eintrag.event_type)) {
|
||||||
return {
|
return {
|
||||||
erlaubt: false,
|
erlaubt: false,
|
||||||
grund:
|
grund:
|
||||||
@@ -37,6 +45,24 @@ export function darfKorrigiertWerden(eintrag: Eintrag, heute: string): Korrektur
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Die Reihenfolge zählt: eine Rückkehr setzt eine Abwesenheit voraus.
|
||||||
|
// Bliebe sie stehen, während die Abwesenheit verschwindet, stünde in der
|
||||||
|
// Akte eine Rückkehr aus dem Nichts — und der Status ergäbe sich aus einem
|
||||||
|
// Eintrag, dessen Ausgangslage gelöscht ist.
|
||||||
|
if (eintrag.event_type === "Karenz" && alle.some((h) => h.event_type === "Rückkehr" && spaeter(h, eintrag))) {
|
||||||
|
return {
|
||||||
|
erlaubt: false,
|
||||||
|
grund: "Zu dieser Abwesenheit gibt es eine Rückkehr. Sie muss zuerst gelöscht werden.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NUR_WIRKSAM.includes(eintrag.event_type) && eintrag.event_date > heute) {
|
||||||
|
return {
|
||||||
|
erlaubt: false,
|
||||||
|
grund: "Diese Abwesenheit ist noch nicht wirksam. Sie muss über den Vorgang selbst abgebrochen werden.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Noch nicht wirksam: das geht, aber nur mit Bezug auf den geplanten
|
// Noch nicht wirksam: das geht, aber nur mit Bezug auf den geplanten
|
||||||
// Vorgang. Zeilen aus der Zeit vor dieser Verknüpfung haben keinen — sie
|
// Vorgang. Zeilen aus der Zeit vor dieser Verknüpfung haben keinen — sie
|
||||||
// liessen sich nur über Person und Datum zuordnen, und das ist nicht
|
// liessen sich nur über Person und Datum zuordnen, und das ist nicht
|
||||||
@@ -60,6 +86,12 @@ export function darfKorrigiertWerden(eintrag: Eintrag, heute: string): Korrektur
|
|||||||
return { erlaubt: true };
|
return { erlaubt: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Später im Sinne der Anzeige: erst das Datum, dann die Erfassungszeit. */
|
||||||
|
function spaeter(a: Eintrag, b: Eintrag): boolean {
|
||||||
|
if (a.event_date !== b.event_date) return a.event_date > b.event_date;
|
||||||
|
return (a.created_at ?? "") > (b.created_at ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Was das Löschen bewirken würde: je Feld entweder Zurücksetzen oder nicht,
|
* Was das Löschen bewirken würde: je Feld entweder Zurücksetzen oder nicht,
|
||||||
* weil ein späterer Eintrag dasselbe Feld angefasst hat.
|
* weil ein späterer Eintrag dasselbe Feld angefasst hat.
|
||||||
|
|||||||
607
supabase/migrations/20260814180000_karenz_ruecknahme.sql
Normal file
607
supabase/migrations/20260814180000_karenz_ruecknahme.sql
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
-- Abwesenheit und Rückkehr lassen sich zurücknehmen und berichtigen.
|
||||||
|
--
|
||||||
|
-- Bisher endete beides bei Stammdaten- und Vertragsänderungen. Eine
|
||||||
|
-- versehentlich erfasste Langzeitabwesenheit liess sich nur durch eine zweite
|
||||||
|
-- Buchung wieder loswerden — und dann standen zwei Einträge in der Akte, von
|
||||||
|
-- denen der erste nie stattgefunden hat.
|
||||||
|
--
|
||||||
|
-- Damit das Zurücknehmen den Stand wirklich wiederherstellt, halten
|
||||||
|
-- start_karenz und record_karenz_return jetzt ihre Vorher-Werte fest, so wie
|
||||||
|
-- change_employee_data es tut: Status, Art der Abwesenheit, Beginn, geplante
|
||||||
|
-- Rückkehr — bei der Rückkehr zusätzlich Beschäftigungsausmaß, Stunden und
|
||||||
|
-- Teilzeitvariante. Erst damit hat die vorhandene Rücknahme etwas, worauf sie
|
||||||
|
-- zurücksetzen kann.
|
||||||
|
--
|
||||||
|
-- ═══ Die Reihenfolge ═══
|
||||||
|
--
|
||||||
|
-- Eine Rückkehr setzt eine Abwesenheit voraus. Bliebe sie stehen, während die
|
||||||
|
-- Abwesenheit verschwindet, stünde in der Akte eine Rückkehr aus dem Nichts,
|
||||||
|
-- und der Status ergäbe sich aus einem Eintrag, dessen Ausgangslage gelöscht
|
||||||
|
-- ist. Deshalb: **eine Abwesenheit lässt sich erst löschen, wenn ihre
|
||||||
|
-- Rückkehr gelöscht ist.** Die Prüfung liegt in der Datenbank, nicht nur in
|
||||||
|
-- der Oberfläche.
|
||||||
|
--
|
||||||
|
-- Zeilen von vor dieser Migration tragen keine Vorher-Werte und bleiben
|
||||||
|
-- deshalb unantastbar — mit derselben Meldung wie bisher.
|
||||||
|
|
||||||
|
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'),
|
||||||
|
-- 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. Wer sie in einer
|
||||||
|
-- geplanten Änderung setzen wollte, hätte keinen Ort dafür — deshalb
|
||||||
|
-- weisen delete_/update_history_entry solche Einträge in der Zukunft
|
||||||
|
-- ab, statt an einer fehlenden Gruppe zu scheitern.
|
||||||
|
'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.start_karenz(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_start_date date := (payload->>'karenz_start_date')::date;
|
||||||
|
v_absence_type text := nullif(payload->>'absence_type', '');
|
||||||
|
v_name text;
|
||||||
|
v_old employees%rowtype;
|
||||||
|
-- Ohne Vorher-Werte liesse sich eine irrtümlich erfasste Abwesenheit
|
||||||
|
-- nicht zurücknehmen: es stünde nirgends, was vorher galt.
|
||||||
|
v_changes jsonb := '[]'::jsonb;
|
||||||
|
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_changes := app_aenderung(v_changes, 'Status', v_old.status::text,
|
||||||
|
case when v_start_date <= current_date then 'Karenz' else v_old.status::text end);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Art der Abwesenheit', v_old.absence_type, v_absence_type);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Abwesend ab', v_old.karenz_start_date::text, v_start_date::text);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Geplante Rückkehr', v_old.karenz_return_date::text, payload->>'planned_return_date');
|
||||||
|
|
||||||
|
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, changes)
|
||||||
|
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,
|
||||||
|
v_changes);
|
||||||
|
|
||||||
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||||
|
values (app_current_user_id(), current_actor_name(), 'Karenz', v_name, v_employee_id,
|
||||||
|
coalesce(v_absence_type, 'Langzeitabwesenheit') || ', geplante Rückkehr ' || (payload->>'planned_return_date'));
|
||||||
|
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;
|
||||||
|
v_old employees%rowtype;
|
||||||
|
-- Wie bei der Abwesenheit: ohne Vorher-Werte liesse sich eine
|
||||||
|
-- irrtümlich erfasste Rückkehr nicht zurücknehmen.
|
||||||
|
v_changes jsonb := '[]'::jsonb;
|
||||||
|
-- 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 * into v_old from employees where id = v_employee_id;
|
||||||
|
v_name := v_old.first_name || ' ' || v_old.last_name;
|
||||||
|
v_karenz_start := v_old.karenz_start_date;
|
||||||
|
v_absence_type := v_old.absence_type;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if v_return_date <= current_date then
|
||||||
|
v_changes := app_aenderung(v_changes, 'Status', v_old.status::text, 'Aktiv');
|
||||||
|
v_changes := app_aenderung(v_changes, 'Art der Abwesenheit', v_old.absence_type, null);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Abwesend ab', v_old.karenz_start_date::text, null);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Geplante Rückkehr', v_old.karenz_return_date::text, null);
|
||||||
|
if v_employment_type is not null then
|
||||||
|
v_changes := app_aenderung(v_changes, 'Beschäftigungsausmaß', v_old.employment_type::text, v_employment_type::text);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Wochenstunden', v_old.weekly_hours::text, v_weekly_hours::text);
|
||||||
|
end if;
|
||||||
|
if payload->>'employment_mode' = 'Teilzeit' then
|
||||||
|
v_changes := app_aenderung(v_changes, 'Teilzeitvariante', v_old.teilzeit_art, v_grund);
|
||||||
|
v_changes := app_aenderung(v_changes, 'Teilzeit bis', v_old.teilzeit_bis::text,
|
||||||
|
case when v_grund is not null then nullif(payload->>'teilzeit_bis', '') else null end);
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into employee_history (employee_id, event_date, event_type, description, changes)
|
||||||
|
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,
|
||||||
|
v_changes);
|
||||||
|
|
||||||
|
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$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.delete_history_entry(payload jsonb)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
declare
|
||||||
|
v_id uuid := (payload->>'history_id')::uuid;
|
||||||
|
v_eintrag employee_history%rowtype;
|
||||||
|
v_name text;
|
||||||
|
v_karte constant jsonb := app_feld_karte();
|
||||||
|
v_aenderung jsonb;
|
||||||
|
v_feld text;
|
||||||
|
v_wert text;
|
||||||
|
v_spalte text;
|
||||||
|
v_typ text;
|
||||||
|
v_gruppe text;
|
||||||
|
v_spaeter boolean;
|
||||||
|
v_zurueckgesetzt jsonb := '[]'::jsonb;
|
||||||
|
v_setz text[] := '{}';
|
||||||
|
v_plan pending_org_changes%rowtype;
|
||||||
|
v_neuer_payload jsonb;
|
||||||
|
v_leer boolean;
|
||||||
|
begin
|
||||||
|
perform require_hr_admin();
|
||||||
|
|
||||||
|
select * into v_eintrag from employee_history where id = v_id;
|
||||||
|
if not found then
|
||||||
|
raise exception 'Historieneintrag nicht gefunden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.event_type = 'Eintritt' then
|
||||||
|
raise exception 'Der Eintritt lässt sich nicht löschen — er ist der Anfang der Zeitleiste.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.event_type not in ('Stammdatenänderung', 'Vertragsänderung', 'Karenz', 'Rückkehr') then
|
||||||
|
raise exception 'Dieser Vorgang lässt sich hier nicht zurücknehmen. Für % gibt es den passenden Weg.', v_eintrag.event_type;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Die Reihenfolge zählt: eine Rückkehr setzt eine Abwesenheit voraus.
|
||||||
|
-- Bliebe sie stehen, während die Abwesenheit verschwindet, stünde in der
|
||||||
|
-- Akte eine Rückkehr aus dem Nichts — und der Status ergäbe sich aus
|
||||||
|
-- einem Eintrag, dessen Ausgangslage gelöscht ist.
|
||||||
|
if v_eintrag.event_type = 'Karenz' and exists (
|
||||||
|
select 1 from employee_history h
|
||||||
|
where h.employee_id = v_eintrag.employee_id
|
||||||
|
and h.event_type = 'Rückkehr'
|
||||||
|
and (h.event_date, h.created_at) > (v_eintrag.event_date, v_eintrag.created_at)
|
||||||
|
) then
|
||||||
|
raise exception 'Zu dieser Abwesenheit gibt es eine Rückkehr. Sie muss zuerst gelöscht werden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Abwesenheit und Rückkehr haben eigene Vorgänge; in einer geplanten
|
||||||
|
-- Änderung haben ihre Felder keinen Ort (siehe app_feld_karte).
|
||||||
|
if v_eintrag.event_type in ('Karenz', 'Rückkehr') and v_eintrag.event_date > current_date then
|
||||||
|
raise exception 'Diese Abwesenheit ist noch nicht wirksam. Sie muss über den Vorgang selbst abgebrochen werden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.changes is null or jsonb_array_length(v_eintrag.changes) = 0 then
|
||||||
|
raise exception 'Zu diesem Eintrag sind keine Feldwerte erfasst — es gibt nichts, worauf zurückgesetzt werden könnte.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select first_name || ' ' || last_name into v_name from employees where id = v_eintrag.employee_id;
|
||||||
|
|
||||||
|
-- ── Noch nicht wirksam: die geplante Änderung entschärfen ──────────
|
||||||
|
if v_eintrag.event_date > current_date then
|
||||||
|
if v_eintrag.pending_id is null then
|
||||||
|
raise exception 'Zu dieser geplanten Änderung ist kein Vorgang hinterlegt. Sie stammt aus der Zeit vor dieser Verknüpfung und lässt sich hier nicht abbrechen.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select * into v_plan from pending_org_changes where id = v_eintrag.pending_id for update;
|
||||||
|
if not found or v_plan.status <> 'pending' then
|
||||||
|
raise exception 'Der geplante Vorgang läuft nicht mehr — er wurde bereits angewendet oder abgebrochen.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_neuer_payload := v_plan.payload;
|
||||||
|
for v_aenderung in select * from jsonb_array_elements(v_eintrag.changes) loop
|
||||||
|
v_feld := v_aenderung->>'feld';
|
||||||
|
if not v_karte ? v_feld then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
v_spalte := v_karte->v_feld->>0;
|
||||||
|
v_gruppe := v_karte->v_feld->>2;
|
||||||
|
if v_neuer_payload ? v_gruppe then
|
||||||
|
v_neuer_payload := jsonb_set(v_neuer_payload, array[v_gruppe], (v_neuer_payload->v_gruppe) - v_spalte);
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
v_leer := coalesce(jsonb_array_length(
|
||||||
|
(select jsonb_agg(k) from jsonb_object_keys(coalesce(v_neuer_payload->'person', '{}'::jsonb)) k)), 0) = 0
|
||||||
|
and coalesce(jsonb_array_length(
|
||||||
|
(select jsonb_agg(k) from jsonb_object_keys(coalesce(v_neuer_payload->'contract', '{}'::jsonb)) k)), 0) = 0
|
||||||
|
and coalesce(jsonb_array_length(
|
||||||
|
(select jsonb_agg(k) from jsonb_object_keys(coalesce(v_neuer_payload->'role', '{}'::jsonb)) k)), 0) = 0;
|
||||||
|
|
||||||
|
if v_leer then
|
||||||
|
update pending_org_changes set status = 'cancelled' where id = v_plan.id;
|
||||||
|
else
|
||||||
|
update pending_org_changes set payload = v_neuer_payload where id = v_plan.id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
delete from employee_history where id = v_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(), 'Geplante Änderung abgebrochen', v_name, v_eintrag.employee_id,
|
||||||
|
v_eintrag.event_type || ' zum ' || v_eintrag.event_date || ' abgebrochen: ' || app_aenderungsfelder(v_eintrag.changes) ||
|
||||||
|
case when v_leer then ' (der Vorgang entfällt ganz)' else ' (der Vorgang läuft mit den übrigen Feldern weiter)' end,
|
||||||
|
v_eintrag.changes);
|
||||||
|
return;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- ── Bereits wirksam: Feld für Feld zurücksetzen ────────────────────
|
||||||
|
for v_aenderung in select * from jsonb_array_elements(v_eintrag.changes) loop
|
||||||
|
v_feld := v_aenderung->>'feld';
|
||||||
|
|
||||||
|
if not v_karte ? v_feld then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from employee_history h,
|
||||||
|
lateral jsonb_array_elements(coalesce(h.changes, '[]'::jsonb)) a
|
||||||
|
where h.employee_id = v_eintrag.employee_id
|
||||||
|
and h.id <> v_eintrag.id
|
||||||
|
and a->>'feld' = v_feld
|
||||||
|
and (h.event_date, h.created_at) > (v_eintrag.event_date, v_eintrag.created_at)
|
||||||
|
) into v_spaeter;
|
||||||
|
|
||||||
|
if v_spaeter then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_spalte := v_karte->v_feld->>0;
|
||||||
|
v_typ := v_karte->v_feld->>1;
|
||||||
|
v_wert := v_aenderung->>'vorher';
|
||||||
|
|
||||||
|
if v_typ = 'liste' then
|
||||||
|
v_setz := v_setz || format('%I = coalesce(string_to_array(%L, '', ''), ''{}'')', v_spalte, nullif(v_wert, ''));
|
||||||
|
else
|
||||||
|
v_setz := v_setz || format('%I = %L::%s', v_spalte, nullif(v_wert, ''), v_typ);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_zurueckgesetzt := v_zurueckgesetzt || jsonb_build_object(
|
||||||
|
'feld', v_feld,
|
||||||
|
'vorher', v_aenderung->>'nachher',
|
||||||
|
'nachher', v_wert
|
||||||
|
);
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
-- Alles in einem UPDATE: chk_weekly_hours verknüpft Beschäftigungsausmaß
|
||||||
|
-- und Wochenstunden, und zwischen zwei getrennten Anweisungen stünde
|
||||||
|
-- zwangsläufig ein Zwischenstand, den die Bedingung verbietet.
|
||||||
|
if array_length(v_setz, 1) > 0 then
|
||||||
|
begin
|
||||||
|
execute format('update employees set %s where id = %L', array_to_string(v_setz, ', '), v_eintrag.employee_id);
|
||||||
|
exception when check_violation then
|
||||||
|
raise exception 'Zurücksetzen nicht möglich: die Werte von damals passen nicht mehr zum heutigen Stand (%). Vermutlich wurde ein zusammengehörendes Feld später einzeln geändert.', sqlerrm;
|
||||||
|
end;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
delete from employee_history where id = v_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(), 'Historieneintrag gelöscht', v_name, v_eintrag.employee_id,
|
||||||
|
v_eintrag.event_type || ' vom ' || v_eintrag.event_date ||
|
||||||
|
case when jsonb_array_length(v_zurueckgesetzt) = 0
|
||||||
|
then ' gelöscht; keine Werte zurückgesetzt (spätere Änderungen gelten)'
|
||||||
|
else ' gelöscht und zurückgesetzt: ' || app_aenderungsfelder(v_zurueckgesetzt) end,
|
||||||
|
v_zurueckgesetzt);
|
||||||
|
end;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.update_history_entry(payload jsonb)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
declare
|
||||||
|
v_id uuid := (payload->>'history_id')::uuid;
|
||||||
|
v_eintrag employee_history%rowtype;
|
||||||
|
v_datum date;
|
||||||
|
v_name text;
|
||||||
|
v_karte constant jsonb := app_feld_karte();
|
||||||
|
v_alt jsonb;
|
||||||
|
v_feld text;
|
||||||
|
v_neuer_wert text;
|
||||||
|
v_neu jsonb := '[]'::jsonb;
|
||||||
|
v_korrektur jsonb := '[]'::jsonb;
|
||||||
|
v_setz text[] := '{}';
|
||||||
|
v_spalte text;
|
||||||
|
v_typ text;
|
||||||
|
v_gruppe text;
|
||||||
|
v_gueltig text;
|
||||||
|
v_plan pending_org_changes%rowtype;
|
||||||
|
v_neuer_payload jsonb;
|
||||||
|
v_war_zukunft boolean;
|
||||||
|
begin
|
||||||
|
perform require_hr_admin();
|
||||||
|
|
||||||
|
select * into v_eintrag from employee_history where id = v_id;
|
||||||
|
if not found then
|
||||||
|
raise exception 'Historieneintrag nicht gefunden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.event_type = 'Eintritt' then
|
||||||
|
raise exception 'Der Eintritt lässt sich hier nicht berichtigen.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.event_type not in ('Stammdatenänderung', 'Vertragsänderung', 'Karenz', 'Rückkehr') then
|
||||||
|
raise exception 'Dieser Vorgang lässt sich hier nicht berichtigen. Für % gibt es den passenden Weg.', v_eintrag.event_type;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.event_type in ('Karenz', 'Rückkehr') and v_eintrag.event_date > current_date then
|
||||||
|
raise exception 'Diese Abwesenheit ist noch nicht wirksam. Sie muss über den Vorgang selbst berichtigt werden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_eintrag.changes is null or jsonb_array_length(v_eintrag.changes) = 0 then
|
||||||
|
raise exception 'Zu diesem Eintrag sind keine Feldwerte erfasst — es gibt nichts zu berichtigen.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_war_zukunft := v_eintrag.event_date > current_date;
|
||||||
|
v_datum := coalesce(nullif(payload->>'event_date', '')::date, v_eintrag.event_date);
|
||||||
|
|
||||||
|
-- Ein Eintrag bleibt auf seiner Seite der Gegenwart. Beides zu erlauben
|
||||||
|
-- hiesse, eine gelaufene Änderung in eine geplante zu verwandeln (oder
|
||||||
|
-- umgekehrt) — dann müssten Stammdaten und payload gegenläufig angepasst
|
||||||
|
-- werden, und dafür gibt es die fachlichen Vorgänge.
|
||||||
|
if v_war_zukunft and v_datum <= current_date then
|
||||||
|
raise exception 'Eine geplante Änderung lässt sich hier nicht vorziehen. Dafür ist „Daten ändern" der richtige Weg.';
|
||||||
|
end if;
|
||||||
|
if not v_war_zukunft and v_datum > current_date then
|
||||||
|
raise exception 'Eine bereits wirksame Änderung lässt sich nicht in die Zukunft verschieben.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select first_name || ' ' || last_name into v_name from employees where id = v_eintrag.employee_id;
|
||||||
|
|
||||||
|
-- Neue Werteliste bauen: Vorher bleibt, Nachher darf ersetzt werden.
|
||||||
|
for v_alt in select * from jsonb_array_elements(v_eintrag.changes) loop
|
||||||
|
v_feld := v_alt->>'feld';
|
||||||
|
select w->>'nachher' into v_neuer_wert
|
||||||
|
from jsonb_array_elements(coalesce(payload->'werte', '[]'::jsonb)) w
|
||||||
|
where w->>'feld' = v_feld;
|
||||||
|
|
||||||
|
if v_neuer_wert is null then
|
||||||
|
v_neu := v_neu || v_alt;
|
||||||
|
else
|
||||||
|
v_neu := v_neu || jsonb_build_object('feld', v_feld, 'vorher', v_alt->>'vorher', 'nachher', nullif(v_neuer_wert, ''));
|
||||||
|
if coalesce(v_alt->>'nachher', '') is distinct from coalesce(nullif(v_neuer_wert, ''), '') then
|
||||||
|
v_korrektur := v_korrektur || jsonb_build_object('feld', v_feld, 'vorher', v_alt->>'nachher', 'nachher', nullif(v_neuer_wert, ''));
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
if jsonb_array_length(v_korrektur) = 0 and v_datum = v_eintrag.event_date then
|
||||||
|
raise exception 'Nichts geändert.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
update employee_history
|
||||||
|
set changes = v_neu,
|
||||||
|
event_date = v_datum,
|
||||||
|
description = 'Geänderte Felder: ' || app_aenderungsfelder(v_neu) || ', wirksam ab ' || v_datum
|
||||||
|
where id = v_id;
|
||||||
|
|
||||||
|
-- ── Noch nicht wirksam: den geplanten Vorgang nachziehen ───────────
|
||||||
|
if v_war_zukunft then
|
||||||
|
if v_eintrag.pending_id is null then
|
||||||
|
raise exception 'Zu dieser geplanten Änderung ist kein Vorgang hinterlegt. Sie stammt aus der Zeit vor dieser Verknüpfung und lässt sich hier nicht berichtigen.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select * into v_plan from pending_org_changes where id = v_eintrag.pending_id for update;
|
||||||
|
if not found or v_plan.status <> 'pending' then
|
||||||
|
raise exception 'Der geplante Vorgang läuft nicht mehr — er wurde bereits angewendet oder abgebrochen.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_neuer_payload := jsonb_set(v_plan.payload, '{effective_date}', to_jsonb(v_datum::text));
|
||||||
|
for v_alt in select * from jsonb_array_elements(v_neu) loop
|
||||||
|
v_feld := v_alt->>'feld';
|
||||||
|
if not v_karte ? v_feld then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
v_spalte := v_karte->v_feld->>0;
|
||||||
|
v_typ := v_karte->v_feld->>1;
|
||||||
|
v_gruppe := v_karte->v_feld->>2;
|
||||||
|
if not v_neuer_payload ? v_gruppe then
|
||||||
|
v_neuer_payload := jsonb_set(v_neuer_payload, array[v_gruppe], '{}'::jsonb);
|
||||||
|
end if;
|
||||||
|
v_neuer_payload := jsonb_set(
|
||||||
|
v_neuer_payload,
|
||||||
|
array[v_gruppe, v_spalte],
|
||||||
|
case
|
||||||
|
when v_alt->>'nachher' is null then 'null'::jsonb
|
||||||
|
when v_typ = 'liste' then to_jsonb(string_to_array(v_alt->>'nachher', ', '))
|
||||||
|
when v_typ = 'boolean' then to_jsonb((v_alt->>'nachher')::boolean)
|
||||||
|
when v_typ = 'numeric' then to_jsonb((v_alt->>'nachher')::numeric)
|
||||||
|
else to_jsonb(v_alt->>'nachher')
|
||||||
|
end,
|
||||||
|
true);
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
update pending_org_changes
|
||||||
|
set payload = v_neuer_payload, effective_date = v_datum
|
||||||
|
where id = v_plan.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(), 'Geplante Änderung berichtigt', v_name, v_eintrag.employee_id,
|
||||||
|
v_eintrag.event_type || ' zum ' || v_eintrag.event_date ||
|
||||||
|
case when v_datum <> v_eintrag.event_date then ' auf ' || v_datum || ' verschoben' else '' end ||
|
||||||
|
case when jsonb_array_length(v_korrektur) > 0 then '; berichtigt: ' || app_aenderungsfelder(v_korrektur) else '' end,
|
||||||
|
v_korrektur);
|
||||||
|
return;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- ── Bereits wirksam: Stammdaten nachziehen ─────────────────────────
|
||||||
|
for v_feld in select distinct e->>'feld' from jsonb_array_elements(v_neu) e loop
|
||||||
|
if not v_karte ? v_feld then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select a->>'nachher' into v_gueltig
|
||||||
|
from employee_history h,
|
||||||
|
lateral jsonb_array_elements(coalesce(h.changes, '[]'::jsonb)) a
|
||||||
|
where h.employee_id = v_eintrag.employee_id
|
||||||
|
and a->>'feld' = v_feld
|
||||||
|
and h.event_date <= current_date
|
||||||
|
order by h.event_date desc, h.created_at desc
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
v_spalte := v_karte->v_feld->>0;
|
||||||
|
v_typ := v_karte->v_feld->>1;
|
||||||
|
|
||||||
|
if v_typ = 'liste' then
|
||||||
|
v_setz := v_setz || format('%I = coalesce(string_to_array(%L, '', ''), ''{}'')', v_spalte, nullif(v_gueltig, ''));
|
||||||
|
else
|
||||||
|
v_setz := v_setz || format('%I = %L::%s', v_spalte, nullif(v_gueltig, ''), v_typ);
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
if array_length(v_setz, 1) > 0 then
|
||||||
|
begin
|
||||||
|
execute format('update employees set %s where id = %L', array_to_string(v_setz, ', '), v_eintrag.employee_id);
|
||||||
|
exception when check_violation then
|
||||||
|
raise exception 'Der berichtigte Wert passt nicht zum übrigen Stand (%). Zusammengehörende Felder — etwa Beschäftigungsausmaß und Wochenstunden — müssen gemeinsam stimmen.', sqlerrm;
|
||||||
|
end;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
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(), 'Historieneintrag berichtigt', v_name, v_eintrag.employee_id,
|
||||||
|
v_eintrag.event_type || ' vom ' || v_eintrag.event_date ||
|
||||||
|
case when v_datum <> v_eintrag.event_date then ' auf ' || v_datum || ' umdatiert' else '' end ||
|
||||||
|
case when jsonb_array_length(v_korrektur) > 0
|
||||||
|
then '; berichtigt: ' || app_aenderungsfelder(v_korrektur) else '' end,
|
||||||
|
v_korrektur);
|
||||||
|
end;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
|
||||||
|
-- Selbstprüfung.
|
||||||
|
do $$
|
||||||
|
declare
|
||||||
|
v_start text := pg_get_functiondef('public.start_karenz(jsonb)'::regprocedure);
|
||||||
|
v_ret text := pg_get_functiondef('public.record_karenz_return(jsonb)'::regprocedure);
|
||||||
|
v_del text := pg_get_functiondef('public.delete_history_entry(jsonb)'::regprocedure);
|
||||||
|
v_upd text := pg_get_functiondef('public.update_history_entry(jsonb)'::regprocedure);
|
||||||
|
begin
|
||||||
|
if v_start not like '%description, changes%' then
|
||||||
|
raise exception 'start_karenz hält keine Vorher-Werte fest';
|
||||||
|
end if;
|
||||||
|
if v_ret not like '%description, changes%' then
|
||||||
|
raise exception 'record_karenz_return hält keine Vorher-Werte fest';
|
||||||
|
end if;
|
||||||
|
if v_del not like '%muss zuerst gelöscht werden%' then
|
||||||
|
raise exception 'Die Reihenfolgeregel fehlt';
|
||||||
|
end if;
|
||||||
|
if v_upd not like '%''Karenz'', ''Rückkehr''%' then
|
||||||
|
raise exception 'update_history_entry lässt die zwei Typen nicht zu';
|
||||||
|
end if;
|
||||||
|
if not (app_feld_karte() ? 'Art der Abwesenheit' and app_feld_karte() ? 'Status') then
|
||||||
|
raise exception 'Die Feldtabelle kennt die Felder der Abwesenheit nicht';
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$$;
|
||||||
@@ -30,7 +30,7 @@ describe("darfKorrigiertWerden", () => {
|
|||||||
expect(u.erlaubt === false && u.grund).toMatch(/Anfang der Zeitleiste/);
|
expect(u.erlaubt === false && u.grund).toMatch(/Anfang der Zeitleiste/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each<HistoryEventType>(["Versetzung", "Beförderung", "Karenz", "Rückkehr", "Austritt", "Wiedereintritt", "Reorganisation"])(
|
it.each<HistoryEventType>(["Versetzung", "Beförderung", "Austritt", "Wiedereintritt", "Reorganisation"])(
|
||||||
"verweist bei %s auf den fachlichen Vorgang",
|
"verweist bei %s auf den fachlichen Vorgang",
|
||||||
(typ) => {
|
(typ) => {
|
||||||
const u = darfKorrigiertWerden(eintrag({ event_type: typ }), HEUTE);
|
const u = darfKorrigiertWerden(eintrag({ event_type: typ }), HEUTE);
|
||||||
@@ -39,6 +39,14 @@ describe("darfKorrigiertWerden", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("lässt Abwesenheit und Rückkehr zu", () => {
|
||||||
|
// Eine versehentlich erfasste Abwesenheit war sonst nur durch eine zweite
|
||||||
|
// Buchung loszuwerden — und dann stünden zwei Einträge in der Akte, von
|
||||||
|
// denen der erste nie stattgefunden hat.
|
||||||
|
expect(darfKorrigiertWerden(eintrag({ event_type: "Karenz" }), HEUTE).erlaubt).toBe(true);
|
||||||
|
expect(darfKorrigiertWerden(eintrag({ event_type: "Rückkehr" }), HEUTE).erlaubt).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("lässt Zukünftiges zu, sobald der geplante Vorgang bekannt ist", () => {
|
it("lässt Zukünftiges zu, sobald der geplante Vorgang bekannt ist", () => {
|
||||||
// Das ist der Sinn von pending_id: ohne Bezug liesse sich die geplante
|
// Das ist der Sinn von pending_id: ohne Bezug liesse sich die geplante
|
||||||
// Änderung nur über Person und Datum finden, und das ist nicht eindeutig.
|
// Änderung nur über Person und Datum finden, und das ist nicht eindeutig.
|
||||||
@@ -113,3 +121,38 @@ describe("loeschVorschau", () => {
|
|||||||
expect(v).toMatchObject({ feld: "Adresse", von: "Endstrasse 9", auf: "Zwischenweg 1" });
|
expect(v).toMatchObject({ feld: "Adresse", von: "Endstrasse 9", auf: "Zwischenweg 1" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Die Reihenfolge zählt: eine Rückkehr setzt eine Abwesenheit voraus. Bliebe
|
||||||
|
// sie stehen, während die Abwesenheit verschwindet, stünde in der Akte eine
|
||||||
|
// Rückkehr aus dem Nichts — und der Status ergäbe sich aus einem Eintrag,
|
||||||
|
// dessen Ausgangslage gelöscht ist.
|
||||||
|
describe("Reihenfolge von Abwesenheit und Rückkehr", () => {
|
||||||
|
const karenz = eintrag({ id: "k", event_type: "Karenz", event_date: "2026-07-15", created_at: "2026-07-15T09:00:00.000Z" });
|
||||||
|
const rueckkehr = eintrag({ id: "r", event_type: "Rückkehr", event_date: "2026-08-10", created_at: "2026-08-10T09:00:00.000Z" });
|
||||||
|
|
||||||
|
it("verweigert die Abwesenheit, solange die Rückkehr steht", () => {
|
||||||
|
const u = darfKorrigiertWerden(karenz, HEUTE, [karenz, rueckkehr]);
|
||||||
|
expect(u.erlaubt).toBe(false);
|
||||||
|
expect(u.erlaubt === false && u.grund).toMatch(/Rückkehr.*zuerst gelöscht/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lässt die Rückkehr jederzeit zurücknehmen — sie ist das obere Ende", () => {
|
||||||
|
expect(darfKorrigiertWerden(rueckkehr, HEUTE, [karenz, rueckkehr]).erlaubt).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gibt die Abwesenheit frei, sobald die Rückkehr weg ist", () => {
|
||||||
|
expect(darfKorrigiertWerden(karenz, HEUTE, [karenz]).erlaubt).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stört sich nicht an einer früheren Rückkehr aus einer anderen Abwesenheit", () => {
|
||||||
|
const frueher = eintrag({ id: "r0", event_type: "Rückkehr", event_date: "2026-01-05", created_at: "2026-01-05T09:00:00.000Z" });
|
||||||
|
expect(darfKorrigiertWerden(karenz, HEUTE, [frueher, karenz]).erlaubt).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lässt eine noch nicht wirksame Abwesenheit in Ruhe", () => {
|
||||||
|
const geplant = eintrag({ event_type: "Karenz", event_date: "2099-01-01", pending_id: "p1" });
|
||||||
|
const u = darfKorrigiertWerden(geplant, HEUTE, [geplant]);
|
||||||
|
expect(u.erlaubt).toBe(false);
|
||||||
|
expect(u.erlaubt === false && u.grund).toMatch(/noch nicht wirksam/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user