-- Reste des Altmodells entfernen und die Planstellenpflege im OM-Modell -- nachziehen. -- -- Die Cut-over-Migration hat die Funktionen des Altmodells mit ihren damals -- bekannten Signaturen entfernt. Ein Teil davon existierte zusätzlich in -- einer jsonb-Variante und ist deshalb stehen geblieben — sichtbar daran, -- dass delete_position und undo_reorg weiterhin in der PostgREST-Schnittstelle -- auftauchen, obwohl die Tabellen, auf denen sie arbeiten, weg sind. Ein -- Aufruf würde erst zur Laufzeit scheitern. -- ═══ 1. Übriggebliebene Funktionen des Altmodells ════════════════ drop function if exists create_position(jsonb); drop function if exists delete_position(jsonb); drop function if exists delete_position(uuid); drop function if exists staff_position_internally(jsonb); drop function if exists apply_reorg(jsonb); drop function if exists undo_reorg(jsonb); drop function if exists undo_reorg(uuid); -- ═══ 2. Reorganisations-Werkbank ═════════════════════════════════ -- Sie hat Teams und Abteilungen zwischen Bereichen verschoben — Objekte, die -- es nicht mehr gibt. Im OM-Modell ist eine Reorganisation das Umhängen von -- org_units.parent_id und braucht kein eigenes Szenario-Modell mehr. alter table employee_history drop column if exists reorg_scenario_id; alter table pending_org_changes drop column if exists reorg_scenario_id; drop table if exists reorg_moves; drop table if exists reorg_scenarios; -- ═══ 3. Planstellen pflegen ══════════════════════════════════════ -- Die alte positions-Tabelle führte nur *offene* Stellen und war damit ein -- eigenes Objekt neben der Person. Im OM-Modell hat jede Person eine -- Planstelle, und eine offene Stelle ist schlicht eine unbesetzte. Anlegen -- und Schliessen sind deshalb Operationen auf om_positions. create or replace function next_position_number() returns text language sql stable as $$ select '6' || lpad((coalesce(max(substring(position_number from 2)::bigint), 0) + 1)::text, 7, '0') from om_positions where position_number ~ '^6[0-9]{7}$'; $$; comment on function next_position_number() is 'Nächste freie Planstellennummer im Nummernkreis 6xxxxxxx.'; create or replace function create_position(payload jsonb) returns uuid language plpgsql as $$ declare v_org_unit_id uuid := (payload->>'org_unit_id')::uuid; v_job_title text := nullif(trim(payload->>'job_title'), ''); v_is_chief boolean := coalesce((payload->>'is_chief')::boolean, false); v_valid_from date := coalesce(nullif(payload->>'valid_from','')::date, current_date); v_job_id uuid; v_position_id uuid; v_unit_name text; begin perform require_hr_admin(); select name into v_unit_name from org_units where id = v_org_unit_id; if v_unit_name is null then raise exception 'Die Organisationseinheit existiert nicht.'; end if; if v_job_title is null then raise exception 'Es muss eine Tätigkeit angegeben werden.'; end if; -- Der Unique-Index würde das ebenfalls abfangen, aber mit einer Meldung, -- die in der Oberfläche nichts erklärt. if v_is_chief and exists ( select 1 from om_positions where org_unit_id = v_org_unit_id and is_chief and valid_to is null ) then raise exception 'Für % besteht bereits eine Leitungsplanstelle.', v_unit_name; end if; -- Gleiche Tätigkeit, ein Katalogeintrag: sonst stehen "Schlosser:in" und -- "Schlosser" nebeneinander und jede Auswertung nach Tätigkeit ist wertlos. select id into v_job_id from jobs where lower(title) = lower(v_job_title); if v_job_id is null then insert into jobs (code, title) values ('J' || lpad((select count(*) + 1 from jobs)::text, 4, '0'), v_job_title) returning id into v_job_id; end if; insert into om_positions (position_number, org_unit_id, job_id, is_chief, valid_from) values (next_position_number(), v_org_unit_id, v_job_id, v_is_chief, v_valid_from) returning id into v_position_id; insert into audit_log (actor_user_id, actor_name, action, target_label, details) values (auth.uid(), current_actor_name(), 'Planstelle angelegt', v_job_title || ' (' || v_unit_name || ')', 'Gültig ab ' || v_valid_from || case when v_is_chief then ', Leitung' else '' end); return v_position_id; end; $$; create or replace function delete_position(payload jsonb) returns void language plpgsql as $$ declare v_position_id uuid := (payload->>'position_id')::uuid; v_label text; v_hat_historie boolean; begin perform require_hr_admin(); select j.title || ' (' || u.name || ')' into v_label from om_positions p join jobs j on j.id = p.job_id join org_units u on u.id = p.org_unit_id where p.id = v_position_id; if v_label is null then raise exception 'Die Planstelle existiert nicht.'; end if; if exists (select 1 from position_assignments where position_id = v_position_id and valid_to is null) then raise exception 'Die Planstelle ist besetzt und kann nicht entfernt werden.'; end if; select exists (select 1 from position_assignments where position_id = v_position_id) into v_hat_historie; -- Eine Planstelle, auf der einmal jemand sass, wird geschlossen statt -- gelöscht: sonst verschwindet mit ihr die Besetzungshistorie, und in der -- Personalakte klafft eine Lücke. if v_hat_historie then update om_positions set valid_to = current_date where id = v_position_id; else delete from om_positions where id = v_position_id; end if; insert into audit_log (actor_user_id, actor_name, action, target_label, details) values (auth.uid(), current_actor_name(), case when v_hat_historie then 'Planstelle geschlossen' else 'Planstelle gelöscht' end, v_label, null); end; $$; grant execute on function next_position_number() to anon, authenticated, service_role; grant execute on function create_position(jsonb) to anon, authenticated, service_role; grant execute on function delete_position(jsonb) to anon, authenticated, service_role;