diff --git a/actions/employees.ts b/actions/employees.ts index 3670532..a9ac923 100644 --- a/actions/employees.ts +++ b/actions/employees.ts @@ -138,6 +138,13 @@ export async function terminateEmployee(payload: { employee_id: string; exit_date: string; exit_reason: string; + /** + * Freiwillig oder unfreiwillig — leer heisst „nicht erfasst". + * + * Eigene Angabe und nicht aus `exit_reason` abgeleitet: die einvernehmliche + * Auflösung kann von beiden Seiten ausgehen (siehe lib/beendigung.ts). + */ + austrittsart?: string; note?: string; }): Promise { // No Show ist kein Austritt im gewohnten Sinn — die Person hat nie diff --git a/app/api/export/employees/route.ts b/app/api/export/employees/route.ts index 0d6ce2e..f752d7d 100644 --- a/app/api/export/employees/route.ts +++ b/app/api/export/employees/route.ts @@ -189,6 +189,9 @@ function employeeExportColumns( { header: "Eintrittsdatum", get: (e) => e.entry_date, kind: "date" }, { header: "Austrittsdatum", get: (e) => e.exit_date, kind: "date" }, { header: "Austrittsgrund", get: (e) => e.exit_reason }, + // Eigene Spalte neben dem Grund: aus ihm folgt sie nicht. Leer heisst + // „nicht erfasst" und nicht „weder noch". + { header: "Freiwillig/unfreiwillig", get: (e) => e.austrittsart ?? "" }, { header: "Abwesenheit ab", get: (e) => e.karenz_start_date, kind: "date" }, { header: "Rückkehr geplant", get: (e) => e.karenz_return_date, kind: "date" }, { header: "Anzahl Angehörige", get: (e) => dependentsCounts.get(e.id) ?? 0 }, diff --git a/components/employees/panels/TerminatePanel.tsx b/components/employees/panels/TerminatePanel.tsx index 004b1c0..7e18339 100644 --- a/components/employees/panels/TerminatePanel.tsx +++ b/components/employees/panels/TerminatePanel.tsx @@ -7,15 +7,8 @@ import { Button } from "@/components/ui/Button"; import { SelectField, TextField, TextareaField } from "@/components/ui/Field"; import { SlideOver } from "@/components/ui/SlideOver"; import { useToast } from "@/components/ui/Toast"; -import { - AUSTRITTSART_LABELS, - BEENDIGUNG_NO_SHOW, - BEENDIGUNGSART_WERTE, - austrittsartVon, - beendigungsartenZu, - type Austrittsart, -} from "@/lib/beendigung"; -import type { Database, EmploymentStatus } from "@/lib/types"; +import { AUSTRITTSART_LABELS, AUSTRITTSARTEN, BEENDIGUNG_NO_SHOW, BEENDIGUNGSART_WERTE } from "@/lib/beendigung"; +import type { Austrittsart, Database, EmploymentStatus } from "@/lib/types"; import { fmtDate, fmtName } from "@/lib/format"; type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; @@ -41,11 +34,9 @@ export function TerminatePanel({ open, onClose, employee, status, directReportCo // aus. Die Vorbelegung nimmt den wahrscheinlichen Fall vorweg, ohne die // übrigen zu verstellen. const [reason, setReason] = useState(status === "Geplant" ? NO_SHOW : BEENDIGUNGSART_WERTE[0]); - // „Alle" statt einer Vorauswahl: die Einschränkung ist eine Hilfe beim - // Suchen, keine Pflichtangabe. Wer die gewünschte Art auswendig kennt, - // soll sie unmittelbar wählen können. + // Leer als Vorbelegung: die Angabe ist freiwillig, und eine vorausgewählte + // Seite wäre eine Behauptung, die niemand getroffen hat. const [austrittsart, setAustrittsart] = useState(""); - const waehlbar = beendigungsartenZu(austrittsart || null); const [note, setNote] = useState(""); const [pending, setPending] = useState(false); @@ -66,6 +57,9 @@ export function TerminatePanel({ open, onClose, employee, status, directReportCo employee_id: employee.id, exit_date: wirksamesDatum, exit_reason: reason, + // Leer statt undefined: die SQL-Funktion macht daraus null, und null + // heisst „nicht erfasst" — das ist der ehrliche Wert. + austrittsart, note, }); setPending(false); @@ -120,36 +114,38 @@ export function TerminatePanel({ open, onClose, employee, status, directReportCo {directReportCount} direkte Berichte werden automatisch der nächsthöheren Führungskraft zugeordnet. )} - {/* Die Austrittsart wählt nicht mit, sie engt ein. Gespeichert wird - allein die Beendigungsart; auf wessen Betreiben beendet wurde, - leitet lib/beendigung.ts daraus ab. Als zweites freies Feld liesse - sich „Entlassung, freiwillig" erfassen, und zwei Auswertungen über - dieselbe Person kämen zu verschiedenen Ergebnissen. */} - { - const neu = (v || "") as Austrittsart | ""; - setAustrittsart(neu); - // Passt der bisher gewählte Grund nicht mehr zur Einschränkung, - // wandert die Auswahl auf den ersten passenden. Stehenzulassen, - // was in der Liste nicht mehr sichtbar ist, wäre ein Formular, - // das etwas anderes absendet, als es zeigt. - const erlaubt = beendigungsartenZu(neu || null); - if (!erlaubt.includes(reason)) setReason(erlaubt[0]); - }} - options={[ - { value: "", label: "Alle Beendigungsarten" }, - ...(Object.keys(AUSTRITTSART_LABELS) as Austrittsart[]).map((a) => ({ value: a, label: AUSTRITTSART_LABELS[a] })), - ]} - hint="Schränkt die Auswahl darunter ein. Gespeichert wird die Beendigungsart." - /> + {/* ── Zwei Angaben, die einander nicht bestimmen ────────────── + Erst die Beendigungsart, darunter der Anstoss. Beide Listen sind + vollständig und keine schränkt die andere ein. + + Der erste Entwurf hatte es umgekehrt: oben die Austrittsart, und + sie filterte die Beendigungsarten darunter, weil der Anstoss aus + der Art ableitbar schien. In Österreich ist er das nicht — die + einvernehmliche Auflösung ist der Regelfall und kann von beiden + Seiten ausgehen. Die Begründung steht in lib/beendigung.ts. + + Unter der Beendigungsart stand ausserdem der abgeleitete Anstoss + als Hinweis. Er erschien von selbst und sah aus wie ein Fehler des + Formulars; er ist weg. */} ({ value: r, label: r === NO_SHOW ? "No Show (nicht angetreten)" : r }))} - hint={austrittsartVon(reason) ? AUSTRITTSART_LABELS[austrittsartVon(reason)!] : undefined} + options={BEENDIGUNGSART_WERTE.map((r) => ({ value: r, label: r === NO_SHOW ? "No Show (nicht angetreten)" : r }))} + /> + setAustrittsart((v || "") as Austrittsart | "")} + options={[ + // „Nicht erfasst" als Vorbelegung und nicht eine der beiden + // Seiten: ein Befristungsablauf geschieht auf niemandes + // Betreiben, und ein Nichtantritt ist gar kein Austritt. Eine + // erzwungene Antwort wäre dort eine erfundene Zahl. + { value: "", label: "Nicht erfasst" }, + ...AUSTRITTSARTEN.map((a) => ({ value: a, label: AUSTRITTSART_LABELS[a] })), + ]} + hint="Für die Fluktuationsauswertung. Lässt sich aus der Beendigungsart nicht ableiten." /> >'employee_id')::uuid; + v_exit date := (payload->>'exit_date')::date; + v_name text; + -- „No Show" ist kein gewöhnlicher Austritt: die Person hat nie + -- angefangen. Deshalb hängt an diesem einen Grund anderes Verhalten. + v_no_show boolean := coalesce(payload->>'exit_reason', '') = 'No Show'; + v_entry date; + v_art text := nullif(payload->>'austrittsart', ''); +begin + perform require_hr_admin(); + select first_name || ' ' || last_name, entry_date into v_name, v_entry + from employees where id = v_employee_id; + + -- Wer nie angetreten ist, tritt am Tag seines Eintritts wieder aus. + -- Damit gibt es keinen einzigen Tag, an dem die Person beschäftigt war: + -- die Statusableitung verlangt exit_date > Stichtag, um jemanden als + -- beschäftigt zu zählen, und das ist hier nie erfüllt. „Nie aktiv" ist + -- damit keine zusätzliche Regel, sondern folgt aus dem Datum. + if v_no_show then + v_exit := v_entry; + end if; + + update employees set + -- Bei einem Nichtantritt sofort, auch wenn der Eintritt noch in der + -- Zukunft lag: sonst bliebe in der Spalte auf Dauer „Geplant" stehen, + -- denn es gibt keinen Lauf, der sie später nachzieht. + status = case when v_no_show or v_exit <= current_date then 'Ausgetreten' else status end, + exit_date = v_exit, + exit_reason = payload->>'exit_reason', + austrittsart = v_art + 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. + if v_no_show then + -- Die Planstelle war nie besetzt. Sie auf [Eintritt, Eintritt) zu + -- schliessen ginge nicht — chk_assignment_range verlangt ein echtes + -- Intervall. Die Zuordnung wird deshalb entfernt: die Stelle ist wieder + -- frei, und es steht nirgends, jemand hätte sie je innegehabt. + delete from position_assignments + where employee_id = v_employee_id and valid_to is null; + else + update position_assignments set valid_to = v_exit + where employee_id = v_employee_id and valid_to is null; + end if; + + insert into employee_history (employee_id, event_date, event_type, description) + values (v_employee_id, v_exit, 'Austritt', + case when v_no_show + then 'Kein Antritt am ' || v_entry || ' (No Show)' + else 'Austritt (' || coalesce(payload->>'exit_reason', '-') || ')' + || case when v_art is not null then ', ' || v_art else '' end end); + + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details) + values (app_current_user_id(), current_actor_name(), 'Austritt', v_name, v_employee_id, + case when v_no_show + then 'Kein Antritt am ' || v_entry || ' (No Show)' + else 'Austritt am ' || v_exit || case when v_art is not null then ', ' || v_art else '' end end); +end; +$function$; + + +-- Eine Wiedereinstellung räumt den Austritt weg — und damit auch die +-- Austrittsart. Ohne das bliebe an einer wieder aktiven Person stehen, sie +-- sei freiwillig gegangen, und chk_austrittsart_nur_mit_austritt wiese den +-- Vorgang ohnehin zurück, weil exit_date dabei auf null geht. +-- +-- Ausgeschrieben statt über pg_get_functiondef gepatcht: die beiden +-- Umstellungen, die diese Funktion seit ihrer Entstehung erfahren hat +-- (app_current_user_id statt auth.uid, fester search_path), sind hier +-- übernommen. Wer das nachliest, sieht die ganze Funktion und nicht eine +-- Kette von Ersetzungen. +CREATE OR REPLACE FUNCTION public.rehire_employee(payload jsonb) + RETURNS void + LANGUAGE plpgsql + SET search_path TO 'public', 'pg_temp' +AS $function$ +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, + austrittsart = 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 (app_current_user_id(), current_actor_name(), 'Wiedereinstellung', v_name, v_employee_id, + 'Wiedereintritt am ' || v_date); +end; +$function$; + +grant execute on function public.terminate_employee(payload jsonb) to alpenwerk_app; +grant execute on function public.rehire_employee(payload jsonb) to alpenwerk_app; + + +-- Selbstprüfung. +do $$ +declare + v_term text := pg_get_functiondef('public.terminate_employee(jsonb)'::regprocedure); + v_re text := pg_get_functiondef('public.rehire_employee(jsonb)'::regprocedure); + v_bedingung text; +begin + if not exists ( + select 1 from information_schema.columns + where table_schema = 'public' and table_name = 'employees' and column_name = 'austrittsart' + ) then + raise exception 'Die Spalte austrittsart fehlt'; + end if; + + if v_term not like '%austrittsart%' then + raise exception 'terminate_employee schreibt die Austrittsart nicht'; + end if; + if v_re not like '%austrittsart = null%' then + raise exception 'rehire_employee räumt die Austrittsart nicht weg'; + end if; + + -- Die beiden Umstellungen, die diese Funktion schon hinter sich hatte, + -- dürfen beim Neuschreiben nicht verlorengehen. Genau das ist der Fehler, + -- den ein „create or replace" aus einer alten Vorlage leise macht. + if v_re like '%auth.uid()%' then + raise exception 'rehire_employee ruft wieder auth.uid() — die Anwendungsrolle darf das Schema auth nicht'; + end if; + if position('SET search_path' in v_re) = 0 then + raise exception 'rehire_employee hat keinen festen search_path mehr'; + end if; + if position('SET search_path' in v_term) = 0 then + raise exception 'terminate_employee hat keinen festen search_path mehr'; + end if; + + -- Der Nichtantritt muss erhalten bleiben: er hängt an dieser Funktion und + -- wäre beim Übertragen leicht zu verlieren. + if v_term not like '%v_no_show%' or v_term not like '%delete from position_assignments%' then + raise exception 'terminate_employee kennt den Nichtantritt nicht mehr'; + end if; + + foreach v_bedingung in array array['chk_austrittsart', 'chk_austrittsart_nur_mit_austritt'] loop + if not exists (select 1 from pg_constraint where conname = v_bedingung) then + raise exception 'Die Bedingung % fehlt', v_bedingung; + end if; + end loop; +end +$$; diff --git a/lib/beendigung.ts b/lib/beendigung.ts index 2b2f172..69c1e1d 100644 --- a/lib/beendigung.ts +++ b/lib/beendigung.ts @@ -1,86 +1,59 @@ +import type { Austrittsart } from "./types"; + // Wie ein Dienstverhältnis endet — und auf wessen Betreiben. // -// Die Liste stand bisher in components/employees/panels/TerminatePanel.tsx und -// damit an genau einer Stelle: dem Formular. Der Berichtemanager konnte -// deshalb nach dem Ereignis „Austritt" fragen, aber nicht danach, *warum* — -// und die Frage, die im Personalwesen zuerst kommt, ist die nach der -// Fluktuation: wie viele sind gegangen, wie vielen wurde gekündigt. +// Zwei Angaben, die **unabhängig** voneinander erhoben werden. Das ist die +// Korrektur einer früheren Entscheidung, und der Grund gehört hierher, damit +// sie niemand versehentlich rückgängig macht: // -// ── Warum die Zuordnung abgeleitet und nicht gespeichert wird ──────── +// Wir hatten „freiwillig / unfreiwillig" aus der Beendigungsart abgeleitet — +// Kündigung AN gilt als freiwillig, Kündigung AG als unfreiwillig, und so +// fort. Das schien sauberer, weil es zwei Felder ausschliesst, die einander +// widersprechen können. // -// Aus dem Workshop kam „ein weiteres Dropdown: freiwilliger / unfreiwilliger -// Austritt". Als zweites, frei wählbares Feld liesse es sich dem Grund -// widersprechen — „Entlassung, freiwillig" wäre eine erfassbare Eingabe, und -// zwei Auswertungen über dieselbe Person kämen zu verschiedenen Ergebnissen. -// Auf wessen Betreiben beendet wird, steckt bereits in der Beendigungsart; -// das Dropdown schränkt deshalb die Auswahl ein, statt eine zweite Wahrheit -// daneben zu stellen. Gespeichert wird weiterhin nur `exit_reason`. +// In Österreich stimmt es nicht. Der Kunde hat es am 17.09.2026 am Beispiel +// erklärt: die einvernehmliche Auflösung ist hier der Regelfall und sagt über +// den Anstoss nichts aus. Sie kann von der Person ausgehen — „ich möchte +// kündigen", worauf einvernehmlich aufgelöst wird, damit das AMS zahlt. Sie +// kann vom Dienstgeber ausgehen — „ich will die Trennung, dafür gibt es eine +// Abfindung". Dieselbe Beendigungsart, zwei gegensätzliche Antworten. // -// ── Warum es drei Gruppen sind und nicht zwei ──────────────────────── +// Was sich nicht aus den Daten ergibt, muss erhoben werden. Die Ableitung +// hätte die Hälfte der Fälle still falsch einsortiert. // -// Zwei Gruppen decken die Liste nicht ab. Ein Befristungsablauf geschieht auf -// niemandes Betreiben — er war von Anfang an vereinbart. Ein Nichtantritt ist -// kein Austritt, sondern ein Verhältnis, das nie begonnen hat. Beide einer der -// beiden Seiten zuzuschlagen würde jede Fluktuationsquote verfälschen, und -// zwar in eine Richtung, die niemand mehr nachvollziehen kann. Sie stehen -// deshalb als „Sonstige" für sich. -// -// Offen und im Begleitschreiben an den Kunden gestellt: ob die -// einvernehmliche Auflösung — hier als freiwillig geführt, weil sie die -// Zustimmung der Person voraussetzt — dort richtig steht. In der Praxis geht -// ihr oft ein Wunsch des Dienstgebers voraus. +// Deshalb gibt es hier auch keine Zuordnung mehr von Art zu Grund und keine +// eingeschränkte Auswahl: die Beendigungsart bietet immer alle Werte an. export const BEENDIGUNG_NO_SHOW = "No Show"; -export type Austrittsart = "freiwillig" | "unfreiwillig" | "sonstige"; +/** + * Die Beendigungsarten. + * + * „No Show" steht am Ende und getrennt: es ist kein Austritt im gewohnten + * Sinn, sondern der Fall, dass jemand nie angetreten ist. + */ +export const BEENDIGUNGSART_WERTE: readonly string[] = [ + "Einvernehmliche Auflösung", + "Kündigung AN", + "Kündigung AG", + "Befristungsablauf", + "Pensionierung", + "Entlassung", + // Aus dem Workshop nachgetragen: in der Probezeit kann jede Seite ohne + // Frist und ohne Grund beenden; welche es war, ist die Unterscheidung, die + // sonst verlorenginge. + "Dienstnehmerseitige Beendigung in Probezeit", + "Dienstgeberseitige Beendigung in Probezeit", + BEENDIGUNG_NO_SHOW, +] as const; export const AUSTRITTSART_LABELS: Record = { freiwillig: "Freiwilliger Austritt", unfreiwillig: "Unfreiwilliger Austritt", - sonstige: "Sonstige Beendigung", }; -export type Beendigungsart = { - /** Der gespeicherte Wert in `employees.exit_reason`. */ - wert: string; - art: Austrittsart; -}; +export const AUSTRITTSARTEN: readonly Austrittsart[] = ["freiwillig", "unfreiwillig"] as const; -export const BEENDIGUNGSARTEN: readonly Beendigungsart[] = [ - { wert: "Einvernehmliche Auflösung", art: "freiwillig" }, - { wert: "Kündigung AN", art: "freiwillig" }, - { wert: "Pensionierung", art: "freiwillig" }, - // Aus dem Workshop nachgetragen (Anforderung 1). In der Probezeit kann jede - // Seite ohne Frist und ohne Grund beenden; welche es war, ist genau die - // Unterscheidung, die sonst verlorenginge. - { wert: "Dienstnehmerseitige Beendigung in Probezeit", art: "freiwillig" }, - { wert: "Kündigung AG", art: "unfreiwillig" }, - { wert: "Entlassung", art: "unfreiwillig" }, - { wert: "Dienstgeberseitige Beendigung in Probezeit", art: "unfreiwillig" }, - { wert: "Befristungsablauf", art: "sonstige" }, - // „No Show" steht am Ende und getrennt: es ist kein Austritt im gewohnten - // Sinn, sondern der Fall, dass jemand nie angetreten ist. - { wert: BEENDIGUNG_NO_SHOW, art: "sonstige" }, -] as const; - -export const BEENDIGUNGSART_WERTE: readonly string[] = BEENDIGUNGSARTEN.map((b) => b.wert); - -const ART_ZU_WERT = new Map(BEENDIGUNGSARTEN.map((b) => [b.wert, b.art] as const)); - -/** - * Auf wessen Betreiben beendet wurde — oder null, wenn der gespeicherte Grund - * in keiner der Gruppen steht. - * - * Null ist kein Sonderfall, sondern der Normalfall für Altdaten: `exit_reason` - * ist eine Textspalte ohne Prüfung, und der Massenimport schreibt hinein, was - * in der Datei stand. Wer hier auf eine Gruppe rät, ordnet fremde Schreibweisen - * still der falschen zu. - */ -export function austrittsartVon(grund: string | null | undefined): Austrittsart | null { - return grund ? (ART_ZU_WERT.get(grund) ?? null) : null; -} - -/** Die Beendigungsarten einer Gruppe — für die abhängige Auswahl im Formular. */ -export function beendigungsartenZu(art: Austrittsart | null): readonly string[] { - return art ? BEENDIGUNGSART_WERTE.filter((w) => ART_ZU_WERT.get(w) === art) : BEENDIGUNGSART_WERTE; +export function istAustrittsart(wert: string | null | undefined): wert is Austrittsart { + return AUSTRITTSARTEN.includes(wert as Austrittsart); } diff --git a/lib/report-criteria.ts b/lib/report-criteria.ts index cbcf850..b09b06e 100644 --- a/lib/report-criteria.ts +++ b/lib/report-criteria.ts @@ -1,5 +1,5 @@ import { ABSENCE_TYPES } from "./absence"; -import { AUSTRITTSART_LABELS, BEENDIGUNGSART_WERTE, type Austrittsart } from "./beendigung"; +import { AUSTRITTSART_LABELS, AUSTRITTSARTEN, BEENDIGUNGSART_WERTE } from "./beendigung"; import { KUENDIGUNGSSCHUTZ_GRUENDE } from "./kuendigungsschutz"; import { MITARBEITERARTEN } from "./mitarbeiterart"; import { parseIsoDateParam } from "./reports"; @@ -72,11 +72,14 @@ export const AUSWAHL_KRITERIEN: readonly AuswahlKriterium[] = [ // Grund. Die Austrittsart (freiwillig/unfreiwillig) steht daneben und wird // aus derselben Angabe abgeleitet, nicht zweitgespeichert. { key: "beendigung", label: "Beendigungsart", alle: "Alle Beendigungsarten", optionen: werte(...BEENDIGUNGSART_WERTE) }, + // Eigene Spalte, nicht aus der Beendigungsart abgeleitet — siehe + // lib/beendigung.ts. Der Wert steht in der Datenbank genauso da; nur die + // Beschriftung kommt von hier. { key: "austrittsart", - label: "Austrittsart", + label: "Freiwillig oder unfreiwillig", alle: "Alle Austrittsarten", - optionen: (Object.keys(AUSTRITTSART_LABELS) as Austrittsart[]).map((a) => ({ wert: a, label: AUSTRITTSART_LABELS[a] })), + optionen: AUSTRITTSARTEN.map((a) => ({ wert: a, label: AUSTRITTSART_LABELS[a] })), }, { key: "teilzeitArt", diff --git a/lib/reports-data.ts b/lib/reports-data.ts index 83fd8a3..6c1dc2d 100644 --- a/lib/reports-data.ts +++ b/lib/reports-data.ts @@ -3,7 +3,7 @@ import type { Schema } from "./db/schema"; import type { Tx } from "./db"; import { ancestorsOf, loadOrgMaps, subtreeOf, type OrgMaps } from "./org"; import { loadPlacements } from "./placement"; -import { beendigungsartenZu, type Austrittsart } from "./beendigung"; +import type { Austrittsart } from "./types"; import { LEERE_CRITERIA, passtImSpeicher, type Criteria } from "./report-criteria"; import { deriveStatusAsOf, @@ -73,14 +73,10 @@ export function applyCriteria( if (a.absence) q = q.where("absence_type", "=", a.absence); if (a.schutzGrund) q = q.where("kuendigungsschutz_grund", "=", a.schutzGrund as KuendigungsschutzGrund); if (a.beendigung) q = q.where("exit_reason", "=", a.beendigung); - // Die Austrittsart ist keine Spalte, sondern eine Einteilung der - // Beendigungsarten (lib/beendigung.ts). Aus ihr wird deshalb eine - // Wertemenge statt eines Vergleichs — dieselbe Zuordnung, die auch das - // Formular verwendet, nur als `in`. - if (a.austrittsart) { - const werte = beendigungsartenZu(a.austrittsart as Austrittsart); - q = werte.length > 0 ? q.where("exit_reason", "in", werte) : q; - } + // Seit 20260917100000 eine eigene Spalte. Vorher wurde sie aus der + // Beendigungsart abgeleitet und der Filter zu einer Wertemenge aufgelöst — + // das ordnete die einvernehmlichen Auflösungen still einer Seite zu. + if (a.austrittsart) q = q.where("austrittsart", "=", a.austrittsart as Austrittsart); const j = c.jaNein; if (j.dienstwagen) q = q.where("has_dienstwagen", "=", j.dienstwagen === "ja"); diff --git a/lib/types.ts b/lib/types.ts index 42e15a8..132f17d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -39,6 +39,16 @@ export type DienstwagenArt = "Verbrenner" | "Elektro"; */ export type TeilzeitArt = "Bildungsteilzeit" | "Elternteilzeit" | "Pflegeteilzeit" | "Wiedereingliederungsteilzeit"; +/** + * Auf wessen Betreiben ein Dienstverhältnis endet. + * + * Eigene Angabe und **nicht** aus der Beendigungsart abgeleitet: die in + * Österreich übliche einvernehmliche Auflösung kann von beiden Seiten + * ausgehen und sagt über den Anstoss nichts aus. Die Begründung steht + * ausführlich in lib/beendigung.ts und in Migration 20260917100000. + */ +export type Austrittsart = "freiwillig" | "unfreiwillig"; + /** * Form der Beschäftigung — zweite Achse neben `worker_type`. * @@ -208,6 +218,8 @@ export type Database = { entry_date: string; exit_date: string | null; exit_reason: string | null; + /** Freiwillig oder unfreiwillig — eigene Angabe, nur mit einem Austritt erlaubt. */ + austrittsart: Austrittsart | null; karenz_start_date: string | null; karenz_return_date: string | null; absence_type: string | null; @@ -283,6 +295,7 @@ export type Database = { entry_date: string; exit_date?: string | null; exit_reason?: string | null; + austrittsart?: Austrittsart | null; karenz_start_date?: string | null; karenz_return_date?: string | null; absence_type?: string | null;