Alle Daten gelöscht und neu aufgebaut: 60 Organisationseinheiten, 133 Jobs,
823 Planstellen, 852 Personen, 852 Besetzungen. Die Anmeldekonten bleiben
stehen — ein Seed, der sich selbst aus der Anwendung aussperrt, ist keiner.
Der Baum kommt aus buildOrg(); der Seed entscheidet nur noch, wer welche
Planstelle besetzt. Damit fällt die halbe Datei weg: keine division_id,
team_id, manager_id, org_level, is_lead mehr auf der Person.
Zwei Dinge, die das Altmodell nicht abbilden konnte, stehen jetzt bewusst in
den Daten:
- Vakanz ist eine Planstelle ohne laufende Besetzung, keine eigene Tabelle.
14 Planstellen sind heute unbesetzt, drei davon mit einem Eintritt in der
Zukunft — die Besetzung beginnt später, die Planstelle existiert schon.
- Ausgetretene sind Vorgänger:innen auf heute besetzten Planstellen, nicht
Karteileichen an einem Team. Vorher liessen sie deren Planstellen als
vakant erscheinen.
Drei Teamleitungen sind unbesetzt und zwei langzeitabwesend, damit die
Hochroll-Regel überhaupt Daten hat: 76 der 809 Berichtslinien weichen von der
formalen ab. Genau eine Person hat keine Vorgesetzte, die Geschäftsführung.
Beim ersten scharfen Lauf hat der SVNR-Trigger mitten im Einfügen abgebrochen,
mit bereits geleerter Datenbank. Ursache war nicht die Prüfziffer, sondern
isoDate(): es ging über toISOString(), während makeSvNummer die lokalen
Datumsteile liest. In Österreich verschiebt das jedes Datum um einen Tag — das
gespeicherte Geburtsdatum passte nicht mehr zu dem in der SV-Nummer codierten.
isoDate rechnet jetzt lokal, wie der Rest des Seeds auch.
Damit so etwas nicht wieder erst die Datenbank leerräumt: pruefeInvarianten()
läuft *vor* dem Löschen und prüft, was sonst erst die Unique-Indizes und
Trigger abfangen — doppelte Besetzungen, überlappende Historie, Ereignisse
nach dem Austritt, und jede SV-Nummer gegen ihr Geburtsdatum. Mit --dry-run
schreibt der Seed gar nichts und meldet nur, was entstehen würde.
540 lines
24 KiB
PL/PgSQL
540 lines
24 KiB
PL/PgSQL
-- Umstieg auf das SAP-OM-Modell: Altbestand überführen, Altmodell entfernen.
|
|
--
|
|
-- Läuft nach 20260727120000 (Tabellen) und 20260727120100 (Berichtslinie).
|
|
--
|
|
-- Warum ein Schnitt und keine schrittweise Migration: Sobald eine
|
|
-- Abteilungsleitung besetzt ist, liefert das alte resolve_manager_for()
|
|
-- falsche Ergebnisse. Es sucht die Bereichsleitung über
|
|
-- "division_id = X and team_id is null and org_level = 1" — eine
|
|
-- Abteilungsleitung erfüllt dieselbe Bedingung, und das LIMIT 1 greift dann
|
|
-- willkürlich eine von beiden.
|
|
--
|
|
-- Der Bestand wird überführt, nicht gelöscht. Direkt nach dem Ausführen ist
|
|
-- das Organigramm gefüllt.
|
|
--
|
|
-- Die Abteilungsleitungen entstehen als *unbesetzte* Planstellen: es gibt
|
|
-- niemanden, der sie innehat, und erfundene Zuordnungen wären schlechter als
|
|
-- eine sichtbare Lücke. Die Aufwärtsregel überspringt sie, bis sie besetzt
|
|
-- sind — die Berichtslinie bleibt durchgängig.
|
|
|
|
begin;
|
|
|
|
-- ═══ 1. Organisationseinheiten ═══════════════════════════════════
|
|
|
|
-- Wurzel. Die bisherige Pseudo-Division "Geschäftsführung" wird sie, damit
|
|
-- die Personen, die daran hingen, ihre Einheit behalten.
|
|
insert into org_units (id, org_number, name, parent_id, unit_type, valid_from)
|
|
select id, '10000000', 'Alpenwerk Industrie GmbH', null, 'Gesellschaft', '2000-01-01'
|
|
from divisions where name = 'Geschäftsführung';
|
|
|
|
-- Falls es sie nicht gab, eine neue Wurzel anlegen.
|
|
insert into org_units (org_number, name, parent_id, unit_type, valid_from)
|
|
select '10000000', 'Alpenwerk Industrie GmbH', null, 'Gesellschaft', '2000-01-01'
|
|
where not exists (select 1 from org_units where unit_type = 'Gesellschaft');
|
|
|
|
insert into org_units (id, org_number, name, parent_id, unit_type, valid_from)
|
|
select d.id, d.org_number, d.name,
|
|
(select id from org_units where unit_type = 'Gesellschaft'),
|
|
'Bereich', '2000-01-01'
|
|
from divisions d
|
|
where d.name <> 'Geschäftsführung';
|
|
|
|
insert into org_units (id, org_number, name, parent_id, unit_type, valid_from)
|
|
select dep.id, dep.org_number, dep.name,
|
|
coalesce((select u.id from org_units u where u.id = dep.division_id),
|
|
(select id from org_units where unit_type = 'Gesellschaft')),
|
|
'Abteilung', '2000-01-01'
|
|
from departments dep;
|
|
|
|
insert into org_units (id, org_number, name, parent_id, unit_type, valid_from)
|
|
select t.id, t.org_number, t.name, t.department_id, 'Team', '2000-01-01'
|
|
from teams t;
|
|
|
|
-- ═══ 2. Job-Katalog ══════════════════════════════════════════════
|
|
-- Vollständig *vor* den Planstellen, die darauf verweisen. Enthält auch die
|
|
-- Titel der neuen Abteilungsleitungen.
|
|
insert into jobs (code, title)
|
|
select 'J' || lpad(row_number() over (order by title)::text, 4, '0'), title
|
|
from (
|
|
select distinct job_title as title from employees where job_title is not null
|
|
union
|
|
select distinct title from positions where title is not null
|
|
union
|
|
select distinct 'Abteilungsleitung ' || name from org_units where unit_type = 'Abteilung'
|
|
) t
|
|
on conflict (title) do nothing;
|
|
|
|
-- ═══ 3. Planstellen und Besetzungen ══════════════════════════════
|
|
|
|
-- Die Zuordnung Person -> Planstelle wird einmal festgelegt und dann von
|
|
-- beiden Inserts benutzt. Zwei unabhängig berechnete Fensterfunktionen
|
|
-- wären hier die klassische Fehlerquelle: sie sehen gleich aus und ordnen
|
|
-- doch verschieden.
|
|
-- Kein "on commit drop": im SQL-Editor hängt es vom Transaktionsverhalten
|
|
-- ab, wann das greift, und eine zu früh verschwundene Zuordnungstabelle
|
|
-- wäre schwer zu diagnostizieren. Wird am Ende explizit entfernt.
|
|
create temporary table om_pos_map (
|
|
employee_id uuid primary key,
|
|
position_id uuid not null default gen_random_uuid(),
|
|
seq bigint
|
|
);
|
|
|
|
insert into om_pos_map (employee_id, seq)
|
|
select id, row_number() over (order by org_level, personnel_number) from employees;
|
|
|
|
insert into om_positions (id, position_number, org_unit_id, job_id, is_chief, valid_from)
|
|
select
|
|
m.position_id,
|
|
'6' || lpad(m.seq::text, 7, '0'),
|
|
case
|
|
when e.org_level = 0 then (select id from org_units where unit_type = 'Gesellschaft')
|
|
when e.org_level = 1 then coalesce(e.division_id, (select id from org_units where unit_type = 'Gesellschaft'))
|
|
else coalesce(e.team_id, (select id from org_units where unit_type = 'Gesellschaft'))
|
|
end,
|
|
(select j.id from jobs j where j.title = e.job_title),
|
|
(e.org_level <= 1 or (e.org_level = 2 and e.is_lead)),
|
|
e.entry_date
|
|
from employees e
|
|
join om_pos_map m on m.employee_id = e.id;
|
|
|
|
-- Wer ausgetreten ist, hat eine beendete Besetzung: die Planstelle ist
|
|
-- wieder frei, die Historie bleibt.
|
|
--
|
|
-- greatest(): employees erlaubt exit_date = entry_date, die Prüfregel auf
|
|
-- position_assignments verlangt aber valid_to > valid_from. Ein
|
|
-- gleichtägiger Ein- und Austritt würde die Migration sonst abbrechen.
|
|
insert into position_assignments (position_id, employee_id, valid_from, valid_to)
|
|
select m.position_id, e.id, e.entry_date,
|
|
case when e.exit_date is null then null else greatest(e.exit_date, e.entry_date + 1) end
|
|
from employees e
|
|
join om_pos_map m on m.employee_id = e.id;
|
|
|
|
-- Unbesetzte Abteilungsleitungen — die Ebene, die im Altmodell fehlte.
|
|
insert into om_positions (position_number, org_unit_id, job_id, is_chief, valid_from)
|
|
select '69' || lpad(row_number() over (order by u.org_number)::text, 6, '0'),
|
|
u.id,
|
|
(select id from jobs where title = 'Abteilungsleitung ' || u.name),
|
|
true,
|
|
current_date
|
|
from org_units u
|
|
where u.unit_type = 'Abteilung'
|
|
and not exists (select 1 from om_positions p where p.org_unit_id = u.id and p.is_chief);
|
|
|
|
-- Bisher offene Stellen werden unbesetzte Planstellen. is_chief nur, wenn
|
|
-- die Einheit noch keine Leitung hat — der Unique-Index liesse es sonst
|
|
-- ohnehin nicht zu.
|
|
insert into om_positions (position_number, org_unit_id, job_id, is_chief, valid_from)
|
|
select p.position_number, p.team_id,
|
|
(select id from jobs j where j.title = p.title),
|
|
p.is_lead and not exists (select 1 from om_positions o where o.org_unit_id = p.team_id and o.is_chief),
|
|
p.valid_from
|
|
from positions p
|
|
where p.status = 'open'
|
|
and exists (select 1 from org_units u where u.id = p.team_id);
|
|
|
|
-- Abbruch, bevor das Altmodell fällt: lieber eine gescheiterte Migration
|
|
-- als ein halb überführter Bestand ohne Rückweg.
|
|
do $$
|
|
declare v_fehlend int;
|
|
begin
|
|
select count(*) into v_fehlend
|
|
from employees e
|
|
where not exists (select 1 from position_assignments pa where pa.employee_id = e.id);
|
|
if v_fehlend > 0 then
|
|
raise exception 'Abbruch: % Mitarbeitende ohne Planstelle.', v_fehlend;
|
|
end if;
|
|
|
|
select count(*) into v_fehlend from om_positions where job_id is null;
|
|
if v_fehlend > 0 then
|
|
raise exception 'Abbruch: % Planstellen ohne Job.', v_fehlend;
|
|
end if;
|
|
|
|
select count(*) into v_fehlend
|
|
from org_units u
|
|
where u.parent_id is null and u.unit_type <> 'Gesellschaft';
|
|
if v_fehlend > 0 then
|
|
raise exception 'Abbruch: % Einheiten ohne Elternteil.', v_fehlend;
|
|
end if;
|
|
end $$;
|
|
|
|
drop table om_pos_map;
|
|
|
|
-- ═══ 4. Altmodell entfernen ══════════════════════════════════════
|
|
|
|
-- Vorgemerkte Änderungen verweisen über team_id auf das Altmodell. Sie sind
|
|
-- transient; halb übersetzt wären sie schlimmer als verworfen.
|
|
delete from pending_org_changes where status = 'pending';
|
|
|
|
drop trigger if exists trg_track_employee_assignment on employees;
|
|
drop function if exists fn_track_employee_assignment();
|
|
drop table if exists employee_assignments;
|
|
|
|
-- Leitet division_id aus team_id ab und haengt damit an einer Spalte, die
|
|
-- gleich faellt. Im neuen Modell uebernimmt die Einheit der Planstelle
|
|
-- diese Rolle, der Trigger wird ersatzlos entfernt.
|
|
drop trigger if exists trg_employees_set_org_unit on employees;
|
|
drop function if exists fn_set_employee_org_unit();
|
|
|
|
-- Die View selektiert team_id/division_id/manager_id/org_level/is_lead und
|
|
-- blockiert damit das Entfernen dieser Spalten. Sie wird von der Anwendung
|
|
-- nirgends benutzt und ersatzlos entfernt; die Ableitung über
|
|
-- om_reporting_lines() tritt an ihre Stelle.
|
|
drop view if exists employees_directory;
|
|
|
|
-- Funktionen auf den Alt-Spalten. Die weiterhin benötigten werden in
|
|
-- Abschnitt 5 neu angelegt; Reorganisation und Ausschreibung folgen mit der
|
|
-- Umstellung der Oberfläche.
|
|
drop function if exists resolve_manager_for(uuid, boolean, uuid);
|
|
drop function if exists staff_position_internally(jsonb);
|
|
drop function if exists create_position(jsonb);
|
|
drop function if exists delete_position(uuid);
|
|
drop function if exists apply_reorg(jsonb);
|
|
drop function if exists undo_reorg(uuid);
|
|
drop function if exists hire_employee(jsonb);
|
|
drop function if exists terminate_employee(jsonb);
|
|
drop function if exists transfer_employee(jsonb);
|
|
drop function if exists rehire_employee(jsonb);
|
|
drop function if exists record_karenz_return(jsonb);
|
|
drop function if exists apply_due_pending_changes();
|
|
|
|
alter table employees
|
|
drop column if exists division_id,
|
|
drop column if exists team_id,
|
|
drop column if exists manager_id,
|
|
drop column if exists org_level,
|
|
drop column if exists is_lead;
|
|
|
|
drop table if exists positions;
|
|
drop table if exists teams;
|
|
drop table if exists departments;
|
|
drop table if exists divisions;
|
|
|
|
-- Mit der positions-Tabelle verschwindet ihr Trigger, nicht aber dessen
|
|
-- Funktion und die Nummernvergabe, die nur als Spaltenvorgabe dort benutzt
|
|
-- wurde. om_positions vergibt seine Nummern selbst.
|
|
drop function if exists fn_set_position_org_unit();
|
|
drop function if exists generate_position_number();
|
|
|
|
-- ═══ 5. Mutationen im neuen Modell ═══════════════════════════════
|
|
-- Die Berichtslinie wird nicht mehr mitgeschrieben, sondern abgeleitet.
|
|
-- Das entfernt aus jeder dieser Funktionen die Manager-Nachführung — beim
|
|
-- Austritt etwa entfällt das Umhängen der direkten Berichte vollständig,
|
|
-- weil sie ohnehin auf die nächste besetzte Ebene hochrutschen.
|
|
|
|
create or replace function hire_employee(payload jsonb)
|
|
returns uuid language plpgsql as $$
|
|
declare
|
|
v_id uuid;
|
|
v_position_id uuid := (payload->>'position_id')::uuid;
|
|
v_entry date := (payload->>'entry_date')::date;
|
|
v_besetzt uuid;
|
|
begin
|
|
perform require_hr_admin();
|
|
|
|
if v_position_id is null then
|
|
raise exception 'Es muss eine Planstelle angegeben werden.';
|
|
end if;
|
|
|
|
select pa.employee_id into v_besetzt
|
|
from position_assignments pa
|
|
where pa.position_id = v_position_id and pa.valid_to is null;
|
|
if v_besetzt is not null then
|
|
raise exception 'Diese Planstelle ist bereits besetzt.';
|
|
end if;
|
|
|
|
insert into employees (
|
|
first_name, last_name, gender, birth_date, sv_nummer, nationality, email, phone,
|
|
address, postal_code, city, address_country, location_id, job_title,
|
|
employment_type, weekly_hours, contract_type, contract_end_date, paygrade,
|
|
source, status, entry_date, title_prefix, title_suffix,
|
|
worker_type, collective_agreement, work_days,
|
|
is_betriebsrat, has_dienstwagen, is_laterale_fuehrung, is_c_level
|
|
)
|
|
values (
|
|
payload->>'first_name', payload->>'last_name', (payload->>'gender')::gender_type,
|
|
(payload->>'birth_date')::date, payload->>'sv_nummer',
|
|
coalesce(payload->>'nationality', 'Österreich'), payload->>'email', payload->>'phone',
|
|
payload->>'address', payload->>'postal_code', payload->>'city',
|
|
coalesce(payload->>'address_country', 'Österreich'),
|
|
(payload->>'location_id')::uuid,
|
|
(select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = v_position_id),
|
|
coalesce((payload->>'employment_type')::employment_type, 'Vollzeit'),
|
|
coalesce((payload->>'weekly_hours')::numeric, 38.5),
|
|
coalesce((payload->>'contract_type')::contract_type, 'unbefristet'),
|
|
nullif(payload->>'contract_end_date', '')::date,
|
|
coalesce((payload->>'paygrade')::paygrade_type, 'B'),
|
|
coalesce((payload->>'source')::source_type, 'Extern'),
|
|
case when v_entry > current_date then 'Geplant' else 'Aktiv' end::employment_status,
|
|
v_entry,
|
|
coalesce(array(select jsonb_array_elements_text(payload->'title_prefix')), '{}'),
|
|
coalesce(array(select jsonb_array_elements_text(payload->'title_suffix')), '{}'),
|
|
coalesce((payload->>'worker_type')::worker_type, 'Angestellte:r'),
|
|
coalesce((payload->>'collective_agreement')::collective_agreement, 'Süßwaren'),
|
|
coalesce(array(select jsonb_array_elements_text(payload->'work_days'))::weekday[], '{Mo,Di,Mi,Do,Fr}'),
|
|
coalesce((payload->>'is_betriebsrat')::boolean, false),
|
|
coalesce((payload->>'has_dienstwagen')::boolean, false),
|
|
coalesce((payload->>'is_laterale_fuehrung')::boolean, false),
|
|
coalesce((payload->>'is_c_level')::boolean, false)
|
|
)
|
|
returning id into v_id;
|
|
|
|
insert into position_assignments (position_id, employee_id, valid_from)
|
|
values (v_position_id, v_id, v_entry);
|
|
|
|
insert into employee_history (employee_id, event_date, event_type, description)
|
|
values (v_id, v_entry, 'Eintritt', 'Eintritt 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 (auth.uid(), current_actor_name(), 'Neueinstellung',
|
|
payload->>'first_name' || ' ' || payload->>'last_name', v_id, 'Eintritt am ' || v_entry);
|
|
|
|
return v_id;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function terminate_employee(payload jsonb)
|
|
returns void language plpgsql as $$
|
|
declare
|
|
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
|
v_exit date := (payload->>'exit_date')::date;
|
|
v_name text;
|
|
begin
|
|
perform require_hr_admin();
|
|
select first_name || ' ' || last_name into v_name from employees where id = v_employee_id;
|
|
|
|
update employees set
|
|
status = case when v_exit <= current_date then 'Ausgetreten' else status end,
|
|
exit_date = v_exit,
|
|
exit_reason = payload->>'exit_reason'
|
|
where id = v_employee_id;
|
|
|
|
-- Die Planstelle wird frei. Direkte Berichte müssen nicht umgehängt
|
|
-- werden: die Berichtslinie wird abgeleitet und rutscht von selbst auf
|
|
-- die nächste besetzte Ebene.
|
|
update position_assignments set valid_to = v_exit
|
|
where employee_id = v_employee_id and valid_to is null;
|
|
|
|
insert into employee_history (employee_id, event_date, event_type, description)
|
|
values (v_employee_id, v_exit, 'Austritt', 'Austritt (' || coalesce(payload->>'exit_reason', '-') || ')');
|
|
|
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
|
values (auth.uid(), current_actor_name(), 'Austritt', v_name, v_employee_id, 'Austritt am ' || v_exit);
|
|
end;
|
|
$$;
|
|
|
|
-- Versetzung ist im OM-Modell ein Wechsel der Planstelle: die alte
|
|
-- Besetzung endet, die neue beginnt. Bereich, Abteilung und Team ergeben
|
|
-- sich aus der Einheit der Zielplanstelle und werden nicht mehr mitgeführt.
|
|
create or replace function transfer_employee(payload jsonb)
|
|
returns void language plpgsql as $$
|
|
declare
|
|
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
|
v_target_position uuid := (payload->>'target_position_id')::uuid;
|
|
v_effective date := coalesce(nullif(payload->>'effective_date','')::date, current_date);
|
|
v_name text;
|
|
v_besetzt uuid;
|
|
begin
|
|
perform require_hr_admin();
|
|
select first_name || ' ' || last_name into v_name from employees where id = v_employee_id;
|
|
|
|
select pa.employee_id into v_besetzt
|
|
from position_assignments pa
|
|
where pa.position_id = v_target_position and pa.valid_to is null;
|
|
if v_besetzt is not null and v_besetzt <> v_employee_id then
|
|
raise exception 'Die Zielplanstelle ist bereits besetzt.';
|
|
end if;
|
|
|
|
if v_effective <= current_date then
|
|
update position_assignments set valid_to = v_effective
|
|
where employee_id = v_employee_id and valid_to is null;
|
|
insert into position_assignments (position_id, employee_id, valid_from)
|
|
values (v_target_position, v_employee_id, v_effective);
|
|
update employees set job_title =
|
|
(select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = v_target_position)
|
|
where id = v_employee_id;
|
|
else
|
|
insert into pending_org_changes (employee_id, change_type, effective_date, payload)
|
|
values (v_employee_id, 'transfer', v_effective,
|
|
jsonb_build_object('target_position_id', v_target_position));
|
|
end if;
|
|
|
|
insert into employee_history (employee_id, event_date, event_type, description)
|
|
values (v_employee_id, v_effective, 'Versetzung', 'Versetzung auf Planstelle ' ||
|
|
(select position_number from om_positions where id = v_target_position));
|
|
|
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
|
values (auth.uid(), current_actor_name(), 'Versetzung', v_name, v_employee_id, 'Wirksam ab ' || v_effective);
|
|
end;
|
|
$$;
|
|
|
|
create or replace function rehire_employee(payload jsonb)
|
|
returns void language plpgsql as $$
|
|
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;
|
|
begin
|
|
perform require_hr_admin();
|
|
select first_name || ' ' || last_name into v_name from employees where id = v_employee_id;
|
|
|
|
if v_position_id is null then
|
|
raise exception 'Für die Wiedereinstellung muss eine Planstelle angegeben werden.';
|
|
end if;
|
|
|
|
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
|
|
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);
|
|
|
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
|
values (auth.uid(), current_actor_name(), 'Wiedereinstellung', v_name, v_employee_id, 'Wiedereintritt am ' || v_date);
|
|
end;
|
|
$$;
|
|
|
|
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_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, 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)
|
|
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 ' || v_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 ' || v_return_date);
|
|
end;
|
|
$$;
|
|
|
|
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;
|
|
begin
|
|
for v_rec in
|
|
select * from pending_org_changes
|
|
where status = 'pending' and effective_date <= current_date
|
|
order by effective_date, created_at
|
|
loop
|
|
if v_rec.change_type = 'transfer' then
|
|
update position_assignments set valid_to = v_rec.effective_date
|
|
where employee_id = v_rec.employee_id and valid_to is null;
|
|
insert into position_assignments (position_id, employee_id, valid_from)
|
|
values ((v_rec.payload->>'target_position_id')::uuid, v_rec.employee_id, v_rec.effective_date);
|
|
update employees set job_title = (
|
|
select j.title from om_positions p join jobs j on j.id = p.job_id
|
|
where p.id = (v_rec.payload->>'target_position_id')::uuid
|
|
) where id = v_rec.employee_id;
|
|
|
|
elsif v_rec.change_type = 'promotion' then
|
|
update employees set
|
|
job_title = coalesce(v_rec.payload->>'new_title', job_title),
|
|
paygrade = coalesce((v_rec.payload->>'new_paygrade')::paygrade_type, paygrade)
|
|
where id = v_rec.employee_id;
|
|
|
|
elsif v_rec.change_type = 'karenz_start' then
|
|
update employees set
|
|
status = 'Karenz',
|
|
karenz_return_date = (v_rec.payload->>'planned_return_date')::date,
|
|
absence_type = coalesce(nullif(v_rec.payload->>'absence_type', ''), absence_type)
|
|
where id = v_rec.employee_id;
|
|
|
|
elsif v_rec.change_type = 'karenz_return' then
|
|
update employees set
|
|
status = 'Aktiv',
|
|
karenz_return_date = null,
|
|
karenz_start_date = null,
|
|
absence_type = null,
|
|
employment_type = coalesce((v_rec.payload->>'employment_type')::employment_type, employment_type),
|
|
weekly_hours = coalesce((v_rec.payload->>'weekly_hours')::numeric, weekly_hours)
|
|
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),
|
|
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)
|
|
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;
|
|
$$;
|
|
|
|
commit;
|