Remove Supabase
The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
539
db/migrations/20260727120200_om_cutover.sql
Normal file
539
db/migrations/20260727120200_om_cutover.sql
Normal file
@@ -0,0 +1,539 @@
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user