Give exits their own checklist, next to the entry one
The offboarding list was a checkbox fieldset inside the exit panel — four items, never sent anywhere. Nothing in terminateEmployee's payload carried them; ticking a box there recorded exactly nothing. It's replaced with the same kind of list the entries got: its own tab, appearing the moment an exit is recorded, with one item per row, a comment on each, and — unlike the fieldset — a record of who touched it and when. The eleven items come from the same printed sheet as the entry list. Nine are plain checkboxes. Two are text fields under "Vermerke": remaining vacation and the balance transferred for payout — the sheet names "Überleitung Salden für Auszahlung" twice, once as a task to do and once as the actual figure, and those are genuinely two different questions, kept as two items. Where the sheet still says "GKK" rather than today's "ÖGK", it's left as written — that's the name the process runs under internally, not a typo. No Show gets no list. Never having worked a single day, there's no IT access to revoke, no GKK registration to undo, no Dienstzettel to collect — an empty checklist there would be a label with nothing behind it. Both the tab and the auto-creation on exit check for this specifically, not just the "Ausgetreten" status that No Show shares with a real exit. Rehiring the same person hides the tab again — the data stays, since it happened, but a checklist for someone currently working has nothing to point at. The engine (what counts as done, how progress is computed) moved into lib/checklist.ts so onboarding and offboarding can't drift into two different ideas of "done" the way two independent copies eventually do; lib/onboarding.ts and lib/offboarding.ts bind it to their own item list, and the tab UI is a single ChecklistPanel bound the same way. Checked against the real database: a real exit creates all eleven items in the same transaction as the exit itself; a No Show creates none; rehiring flips the tab off while the old answers stay queryable. One false alarm during that check turned out to be the user's own clicks on a real employee's onboarding list, made in the browser while trying the earlier feature — left untouched, not test debris. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
148
supabase/migrations/20260818100000_offboarding_checkliste.sql
Normal file
148
supabase/migrations/20260818100000_offboarding_checkliste.sql
Normal file
@@ -0,0 +1,148 @@
|
||||
-- Offboarding-Checkliste je Person
|
||||
--
|
||||
-- Dieselbe Sache wie onboarding_tasks (20260817100000), für den anderen Weg:
|
||||
-- je Person und Punkt eine Zeile, der Schlüssel als Text statt als Fremd-
|
||||
-- schlüssel, damit ein später gestrichener Punkt seine alten Antworten
|
||||
-- stehen lässt statt sie mitzureissen. Die Begründung im Einzelnen steht bei
|
||||
-- der Onboarding-Migration; hier nur, was abweicht.
|
||||
--
|
||||
-- Eine eigene Tabelle statt einer gemeinsamen mit Spalte „Richtung": beide
|
||||
-- Listen haben eigene Punkte (lib/onboarding.ts, lib/offboarding.ts), einen
|
||||
-- eigenen Anlass (Ein-, Aus- oder Wiedereintritt) und eigene Protokollzeilen.
|
||||
-- Eine gemeinsame Tabelle würde das mit einer Fallunterscheidung nachbauen,
|
||||
-- die schon in zwei Tabellen und zwei Funktionen so nicht existiert.
|
||||
|
||||
create table if not exists offboarding_tasks (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
employee_id uuid not null references employees(id) on delete cascade,
|
||||
item_key text not null,
|
||||
erledigt boolean not null default false,
|
||||
wert text,
|
||||
kommentar text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
updated_by uuid references profiles(id) on delete set null,
|
||||
updated_by_name text,
|
||||
constraint offboarding_tasks_eine_zeile unique (employee_id, item_key)
|
||||
);
|
||||
|
||||
create index if not exists offboarding_tasks_employee_id_idx on offboarding_tasks (employee_id);
|
||||
create index if not exists offboarding_tasks_offen_idx on offboarding_tasks (employee_id) where not erledigt;
|
||||
|
||||
drop policy if exists offboarding_tasks_hr_all on offboarding_tasks;
|
||||
create policy offboarding_tasks_hr_all on offboarding_tasks
|
||||
for all using (is_hr_user()) with check (is_hr_user());
|
||||
|
||||
|
||||
-- ── Einen Punkt festhalten ─────────────────────────────────────────────
|
||||
-- Wortgleich mit set_onboarding_task, nur auf die andere Tabelle bezogen.
|
||||
create or replace function set_offboarding_task(payload jsonb)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path to 'public', 'pg_temp'
|
||||
as $function$
|
||||
declare
|
||||
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
||||
v_item_key text := nullif(trim(payload->>'item_key'), '');
|
||||
v_name text;
|
||||
v_vorher offboarding_tasks%rowtype;
|
||||
v_erledigt boolean;
|
||||
v_wert text;
|
||||
v_kommentar text;
|
||||
begin
|
||||
perform require_hr_admin();
|
||||
|
||||
if v_item_key is null then
|
||||
raise exception 'Es wurde kein Punkt angegeben.';
|
||||
end if;
|
||||
|
||||
select first_name || ' ' || last_name into v_name from employees where id = v_employee_id;
|
||||
if v_name is null then
|
||||
raise exception 'Die Person existiert nicht.';
|
||||
end if;
|
||||
|
||||
select * into v_vorher from offboarding_tasks
|
||||
where employee_id = v_employee_id and item_key = v_item_key for update;
|
||||
|
||||
v_erledigt := case when payload ? 'erledigt' then (payload->>'erledigt')::boolean
|
||||
else coalesce(v_vorher.erledigt, false) end;
|
||||
v_wert := case when payload ? 'wert' then nullif(trim(payload->>'wert'), '') else v_vorher.wert end;
|
||||
v_kommentar := case when payload ? 'kommentar' then nullif(trim(payload->>'kommentar'), '')
|
||||
else v_vorher.kommentar end;
|
||||
|
||||
insert into offboarding_tasks (employee_id, item_key, erledigt, wert, kommentar, updated_by, updated_by_name)
|
||||
values (v_employee_id, v_item_key, v_erledigt, v_wert, v_kommentar,
|
||||
app_current_user_id(), current_actor_name())
|
||||
on conflict (employee_id, item_key) do update
|
||||
set erledigt = excluded.erledigt,
|
||||
wert = excluded.wert,
|
||||
kommentar = excluded.kommentar,
|
||||
updated_at = now(),
|
||||
updated_by = excluded.updated_by,
|
||||
updated_by_name = excluded.updated_by_name;
|
||||
|
||||
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||
values (app_current_user_id(), current_actor_name(), 'Offboarding-Punkt', v_name, v_employee_id,
|
||||
v_item_key || ': ' ||
|
||||
case when v_wert is not null then v_wert
|
||||
when v_erledigt then 'erledigt'
|
||||
else 'offen' end ||
|
||||
coalesce(' — ' || v_kommentar, ''));
|
||||
end;
|
||||
$function$;
|
||||
|
||||
revoke all on function set_offboarding_task(jsonb) from public;
|
||||
grant execute on function set_offboarding_task(jsonb) to public;
|
||||
|
||||
|
||||
-- ── Die Liste anlegen ──────────────────────────────────────────────────
|
||||
--
|
||||
-- Anders als beim Onboarding steht dieser Aufruf nicht für jeden Austritt in
|
||||
-- derselben Transaktion, die den Status setzt: bei einem No Show — wer nie
|
||||
-- angetreten ist — gibt es nichts offzuboarden, es war nie ein IT-Zugang
|
||||
-- eingerichtet, keine GKK-Anmeldung, kein Dienstzettel. Diese Funktion prüft
|
||||
-- das nicht selbst; die aufrufende Seite (actions/employees.ts) entscheidet,
|
||||
-- wann sie gerufen wird. Sie existiert trotzdem als eigene, aufrufbare
|
||||
-- Funktion — für den Nachtrag bei einem Austritt von vor dieser Liste.
|
||||
create or replace function start_offboarding(payload jsonb)
|
||||
returns integer
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path to 'public', 'pg_temp'
|
||||
as $function$
|
||||
declare
|
||||
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
||||
v_keys jsonb := coalesce(payload->'item_keys', '[]'::jsonb);
|
||||
v_name text;
|
||||
v_angelegt integer;
|
||||
begin
|
||||
perform require_hr_admin();
|
||||
|
||||
select first_name || ' ' || last_name into v_name from employees where id = v_employee_id;
|
||||
if v_name is null then
|
||||
raise exception 'Die Person existiert nicht.';
|
||||
end if;
|
||||
if jsonb_array_length(v_keys) = 0 then
|
||||
raise exception 'Es wurden keine Punkte übergeben.';
|
||||
end if;
|
||||
|
||||
insert into offboarding_tasks (employee_id, item_key, updated_by, updated_by_name)
|
||||
select v_employee_id, k, app_current_user_id(), current_actor_name()
|
||||
from jsonb_array_elements_text(v_keys) k
|
||||
on conflict (employee_id, item_key) do nothing;
|
||||
|
||||
get diagnostics v_angelegt = row_count;
|
||||
|
||||
if v_angelegt > 0 then
|
||||
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||
values (app_current_user_id(), current_actor_name(), 'Offboarding-Checkliste angelegt', v_name, v_employee_id,
|
||||
v_angelegt || ' Punkte');
|
||||
end if;
|
||||
|
||||
return v_angelegt;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
revoke all on function start_offboarding(jsonb) from public;
|
||||
grant execute on function start_offboarding(jsonb) to public;
|
||||
Reference in New Issue
Block a user