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>
133 lines
5.5 KiB
PL/PgSQL
133 lines
5.5 KiB
PL/PgSQL
-- Eine Planstelle ändern.
|
|
--
|
|
-- Bisher liess sie sich nur anlegen und löschen. Ein Tippfehler in der
|
|
-- Tätigkeit oder ein falsches Gültigkeitsdatum bedeutete: löschen und neu —
|
|
-- mit neuer Planstellennummer. Die Nummer steht aber in Stellenausschreibungen
|
|
-- und Budgets, und die Protokollspur reisst ab.
|
|
--
|
|
-- Was hier bewusst **nicht** geht, steht als Sperre drin, nicht als
|
|
-- Anmerkung — siehe die drei Prüfungen unten.
|
|
create or replace function update_position(payload jsonb)
|
|
returns void
|
|
language plpgsql
|
|
set search_path = public, pg_temp
|
|
as $function$
|
|
declare
|
|
v_id uuid := (payload->>'position_id')::uuid;
|
|
v_alt om_positions%rowtype;
|
|
v_alt_titel text;
|
|
v_alt_einheit text;
|
|
v_org_unit_id uuid;
|
|
v_job_title text := nullif(trim(payload->>'job_title'), '');
|
|
v_is_chief boolean;
|
|
v_valid_from date;
|
|
v_valid_to date;
|
|
v_job_id uuid;
|
|
v_unit_name text;
|
|
v_besetzt boolean;
|
|
v_changes jsonb := '[]'::jsonb;
|
|
begin
|
|
perform require_hr_admin();
|
|
|
|
select * into v_alt from om_positions where id = v_id;
|
|
if v_alt.id is null then
|
|
raise exception 'Die Planstelle existiert nicht.';
|
|
end if;
|
|
|
|
select title into v_alt_titel from jobs where id = v_alt.job_id;
|
|
select name into v_alt_einheit from org_units where id = v_alt.org_unit_id;
|
|
|
|
-- Fehlende Schlüssel heissen „unverändert", nicht „leeren".
|
|
v_org_unit_id := coalesce(nullif(payload->>'org_unit_id','')::uuid, v_alt.org_unit_id);
|
|
v_job_title := coalesce(v_job_title, v_alt_titel);
|
|
v_is_chief := coalesce((payload->>'is_chief')::boolean, v_alt.is_chief);
|
|
v_valid_from := coalesce(nullif(payload->>'valid_from','')::date, v_alt.valid_from);
|
|
v_valid_to := case when payload ? 'valid_to' then nullif(payload->>'valid_to','')::date else v_alt.valid_to end;
|
|
|
|
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_valid_to is not null and v_valid_to < v_valid_from then
|
|
raise exception 'Das Ende der Gültigkeit liegt vor ihrem Beginn.';
|
|
end if;
|
|
|
|
select exists (
|
|
select 1 from position_assignments
|
|
where position_id = v_id and (valid_to is null or valid_to > current_date)
|
|
) into v_besetzt;
|
|
|
|
-- (1) Die Einheit einer vergebenen Planstelle zu wechseln wäre eine
|
|
-- Versetzung — mit allem, was dazugehört: Historie, Berichtslinie,
|
|
-- Protokoll. Das gehört in transfer_employee und nicht hierher, sonst
|
|
-- wandert jemand lautlos in eine andere Abteilung.
|
|
if v_besetzt and v_org_unit_id is distinct from v_alt.org_unit_id then
|
|
raise exception 'Diese Planstelle ist vergeben. Für einen Wechsel der Einheit die Versetzung benutzen.';
|
|
end if;
|
|
|
|
-- (2) Ein Ende, während noch jemand darauf sitzt, hinterlässt eine
|
|
-- Besetzung ohne Planstelle.
|
|
if v_besetzt and v_valid_to is not null then
|
|
raise exception 'Diese Planstelle ist vergeben und kann kein Ende der Gültigkeit bekommen.';
|
|
end if;
|
|
|
|
-- (3) Je Einheit nur eine gültige Leitung. Der Unique-Index fängt das auch,
|
|
-- 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 and id <> v_id
|
|
) then
|
|
raise exception 'Für % besteht bereits eine Leitungsplanstelle.', v_unit_name;
|
|
end if;
|
|
|
|
-- Gleiche Tätigkeit, ein Katalogeintrag — dieselbe Regel wie beim Anlegen.
|
|
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;
|
|
|
|
v_changes := app_aenderung(v_changes, 'Tätigkeit', v_alt_titel, v_job_title);
|
|
v_changes := app_aenderung(v_changes, 'Organisationseinheit', v_alt_einheit, v_unit_name);
|
|
v_changes := app_aenderung(v_changes, 'Leitungsplanstelle', v_alt.is_chief::text, v_is_chief::text);
|
|
v_changes := app_aenderung(v_changes, 'Gültig ab', v_alt.valid_from::text, v_valid_from::text);
|
|
v_changes := app_aenderung(v_changes, 'Gültig bis', v_alt.valid_to::text, v_valid_to::text);
|
|
|
|
-- Nichts geändert ist ein Ergebnis, kein Erfolg.
|
|
--
|
|
-- Vorher kehrte die Funktion hier stumm zurück, und die Oberfläche meldete
|
|
-- „Planstelle geändert." Wer etwas eingetragen hatte, das unterwegs
|
|
-- verworfen wurde — etwa das Leitungshäkchen, das bei bereits vergebener
|
|
-- Leitung nicht durchkommt —, sah eine Erfolgsmeldung und eine unveränderte
|
|
-- Liste. Das ist genau die Rückmeldung, die einen suchen lässt.
|
|
if jsonb_array_length(v_changes) = 0 then
|
|
raise exception 'Es wurde nichts geändert.';
|
|
end if;
|
|
|
|
update om_positions
|
|
set org_unit_id = v_org_unit_id,
|
|
job_id = v_job_id,
|
|
is_chief = v_is_chief,
|
|
valid_from = v_valid_from,
|
|
valid_to = v_valid_to
|
|
where id = v_id;
|
|
|
|
insert into audit_log (actor_user_id, actor_name, action, target_label, details, changes)
|
|
values (app_current_user_id(), current_actor_name(), 'Planstelle geändert',
|
|
v_job_title || ' (' || v_unit_name || ')',
|
|
app_aenderungsfelder(v_changes), v_changes);
|
|
end;
|
|
$function$;
|
|
|
|
do $$
|
|
declare r text;
|
|
begin
|
|
foreach r in array array['anon', 'authenticated', 'service_role', 'alpenwerk_app'] loop
|
|
if exists (select 1 from pg_roles where rolname = r) then
|
|
execute format('grant execute on function update_position(jsonb) to %I', r);
|
|
end if;
|
|
end loop;
|
|
end;
|
|
$$;
|