Files
alpenwerk-hr/lib/beendigung.ts
Andrei Laas 8d0c9b4b65 Workshop-Anforderungen, erster Teil: was ohne Migration geht
Anforderung 1 — Freiwilliger vs unfreiwilliger Austritt. Die Liste der
Beendigungsarten zieht aus TerminatePanel.tsx nach lib/beendigung.ts um: der
Berichtemanager braucht sie ebenso, und zwei Listen liefen auseinander. Zwei
neue Arten (Beendigung in der Probezeit, je Seite). Auf wessen Betreiben
beendet wurde, wird aus der Art **abgeleitet** und nicht daneben gespeichert
— als zweites freies Feld liesse sich "Entlassung, freiwillig" erfassen. Das
Dropdown im Formular schraenkt die Auswahl darunter ein.

Drei Gruppen statt zwei: Befristungsablauf geschieht auf niemandes
Betreiben, ein Nichtantritt ist kein Austritt. Beide einer Seite
zuzuschlagen wuerde jede Fluktuationsquote verfaelschen.

Anforderung 2 — Namensfilter in "Anstehend", ab neun Eintraegen.

Anforderung 3 — die zwei Unterschriftenfelder im gedruckten Blatt sind weg;
"Firmenfahrzeug" steht in beiden Checklisten. has_dienstwagen sagt, ob eines
zusteht, nicht ob es uebergeben wurde.

Anforderung 4 — "+794 weitere" ist ein Knopf geworden; die Namen waren
vorher nur ueber den Export erreichbar. Stammdatenaenderung und
Gehaltsanpassung stehen nicht mehr zur Auswahl: die eine entsteht bei jeder
geaenderten Telefonnummer, die andere ist ein totes Ereignis, seit das
Gehalt in Loga liegt. Neu ist der Untertyp — Beendigungsart beim Austritt,
Art der Abwesenheit bei der Langzeitabwesenheit, im Bericht und im Export.

Anforderung 10 — zwei Kacheln. "Aktives Dienstverhaeltnis" ist nicht
dasselbe wie "Aktive Mitarbeiter:innen": dort steht, wer heute arbeitet,
hier, mit wem ein Vertrag laeuft. Sichtbar waren 806 und 10, addieren musste
man selbst.
2026-09-15 22:35:47 +02:00

87 lines
4.2 KiB
TypeScript

// 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.
//
// ── Warum die Zuordnung abgeleitet und nicht gespeichert wird ────────
//
// 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`.
//
// ── Warum es drei Gruppen sind und nicht zwei ────────────────────────
//
// 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.
export const BEENDIGUNG_NO_SHOW = "No Show";
export type Austrittsart = "freiwillig" | "unfreiwillig" | "sonstige";
export const AUSTRITTSART_LABELS: Record<Austrittsart, string> = {
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 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;
}