Files
alpenwerk-hr/tests/unit/offboarding.test.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

124 lines
5.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
OFFBOARDING_GRUPPEN,
OFFBOARDING_PUNKTE,
fortschritt,
gehoertOffboarding,
istErledigt,
punktZu,
type AufgabenStand,
} from "@/lib/offboarding";
// Vom selben Blatt Papier wie die Onboarding-Liste, für den anderen Weg. Die
// Mechanik selbst (was als erledigt zählt, wie der Fortschritt entsteht) ist
// mit lib/onboarding.ts geteilt (lib/checklist.ts) und dort schon geprüft —
// hier geht es um das, was an dieser Liste besonders ist.
function stand(teil: Partial<AufgabenStand> & { item_key: string }): AufgabenStand {
return { erledigt: false, wert: null, kommentar: null, updated_at: null, updated_by_name: null, ...teil };
}
describe("Das Verzeichnis", () => {
it("hat eindeutige Schlüssel — sie stehen in den Daten", () => {
const keys = OFFBOARDING_PUNKTE.map((p) => p.key);
expect(new Set(keys).size).toBe(keys.length);
});
it("bildet jeden Punkt aus dem Blatt ab", () => {
// 11 vom Blatt, dazu „Firmenfahrzeug" aus dem Workshop.
expect(OFFBOARDING_PUNKTE).toHaveLength(12);
for (const key of [
"firmenfahrzeug",
"br_verstaendigung",
"kuendigungsschreiben",
"bestaetigung_kdn_bei",
"lgv_austritt",
"gkk_abmeldung",
"if6020_austritt",
"endabrechnung",
"offboarding_gestartet",
]) {
expect(punktZu(key), key).toBeDefined();
}
});
it("führt die beiden Vermerke als Text, nicht als Haken", () => {
// Ein Resturlaubssaldo abgehakt wäre wertlos — die Zahl selbst ist der Zweck.
expect(punktZu("resturlaub")?.art).toBe("text");
expect(punktZu("ueberleitung_salden_betrag")?.art).toBe("text");
});
it("unterscheidet die Aufgabe von ihrem Betrag", () => {
// Das Blatt nennt „Überleitung Salden für Auszahlung" zweimal: einmal als
// abzuhakende Aufgabe, einmal als Feld für den tatsächlichen Betrag. Zwei
// verschiedene Fragen, zwei verschiedene Punkte.
expect(punktZu("ueberleitung_salden")?.art).toBe("haken");
expect(punktZu("ueberleitung_salden_betrag")?.art).toBe("text");
expect(punktZu("ueberleitung_salden")?.key).not.toBe(punktZu("ueberleitung_salden_betrag")?.key);
});
it("führt keinen Ja/Nein-Punkt — anders als beim Onboarding", () => {
// Auf diesem Blatt gibt es keine Stelle mit zwei Kästchen nebeneinander.
expect(OFFBOARDING_PUNKTE.some((p) => p.art === "janein")).toBe(false);
});
it("gruppiert vollständig — kein Punkt fällt zwischen die Gruppen", () => {
const ausGruppen = OFFBOARDING_GRUPPEN.flatMap((g) => g.punkte.map((p) => p.key));
expect(new Set(ausGruppen)).toEqual(new Set(OFFBOARDING_PUNKTE.map((p) => p.key)));
});
});
describe("fortschritt", () => {
it("ist auf einer leeren Liste vollständig offen", () => {
const f = fortschritt([]);
expect(f.erledigt).toBe(0);
expect(f.gesamt).toBe(OFFBOARDING_PUNKTE.length);
});
it("zählt Haken und Text zusammen", () => {
const f = fortschritt([
stand({ item_key: "endabrechnung", erledigt: true }),
stand({ item_key: "resturlaub", wert: "12 Tage" }),
]);
expect(f.erledigt).toBe(2);
expect(f.offen).not.toContain("Endabrechnung");
});
});
describe("istErledigt", () => {
it("verlangt bei einem Haken die ausdrückliche Markierung", () => {
const punkt = punktZu("gkk_abmeldung")!;
expect(istErledigt(punkt, stand({ item_key: punkt.key, erledigt: true }))).toBe(true);
expect(istErledigt(punkt, stand({ item_key: punkt.key }))).toBe(false);
expect(istErledigt(punkt, undefined)).toBe(false);
});
});
describe("gehoertOffboarding", () => {
// Geprüft wird exit_date, nicht status: eine künftige Beendigung setzt
// exit_date sofort, aber status erst am Stichtag selbst. Bis dahin steht
// die Person weiterhin als „Aktiv" — mit status als Bedingung bliebe der
// Reiter bis zum letzten Arbeitstag unsichtbar, genau dann, wenn er
// gebraucht wird.
it("bejaht bei einem bereits wirksamen Austritt", () => {
expect(gehoertOffboarding({ exit_date: "2026-08-01", exit_reason: "Kündigung AN" })).toBe(true);
});
it("bejaht schon bei einem erst künftig wirksamen Austritt", () => {
expect(gehoertOffboarding({ exit_date: "2099-01-01", exit_reason: "Kündigung AN" })).toBe(true);
});
it("verneint bei No Show, obwohl exit_date ebenso gesetzt ist", () => {
// Nie einen Tag gearbeitet: kein IT-Zugang, keine GKK-Anmeldung, kein
// Dienstzettel. Der Reiter wäre ein Etikett ohne Inhalt.
expect(gehoertOffboarding({ exit_date: "2026-08-01", exit_reason: "No Show" })).toBe(false);
});
it("verneint ohne Austritt — auch nach einer Wiedereinstellung, die exit_date zurücksetzt", () => {
// Beides führt auf dieselbe Datenlage: wer nie ausgeschieden ist, und wer
// wiedereingestellt wurde (rehire_employee setzt exit_date und
// exit_reason ausdrücklich auf null), sehen für diese Regel gleich aus.
expect(gehoertOffboarding({ exit_date: null, exit_reason: null })).toBe(false);
});
});