F.24 -- eine Planstelle, auf die eine Versetzung vorgemerkt ist, galt als frei. Die Vormerkung steht in pending_org_changes, "besetzt?" wurde allein an position_assignments gefragt. Auffallen wuerde es erst in der Nacht des Wirksamkeitstages: dort legt der Nachtlauf die zweite Besetzung an und laeuft in den Teilindex, der genau eine laufende Besetzung je Planstelle zulaesst. Er arbeitet in einer Transaktion ueber alle faelligen Vorgaenge -- eine einzige solche Buchung haette ihn vollstaendig zum Stehen gebracht, auch fuer alle anderen. Neue Auskunft planstelle_vorgemerkt, abgefragt in hire_employee, rehire_employee, transfer_employee und promote_employee, mit Datum in der Meldung. Die eigene Vormerkung zaehlt nicht als Hindernis. D.06 -- die Struktursicht zeigte Einheiten, die es am gewaehlten Stichtag noch nicht gab: org_units wurde ungefiltert gelesen, waehrend Planstellen und Besetzungen daneben auf den Stichtag eingeschraenkt waren. Die Druckansicht filterte schon immer richtig; jetzt tun es alle drei Stellen. H.09 -- der Kuendigungsschutz stand in der Akte nur als "bis TT.MM.JJJJ", obwohl Personenkreis, Beginn, Ende und die ganze Begunstigung erfasst werden. Der Personenkreis ist die eigentliche Auskunft. Die Behinderung bekommt eine eigene Zeile: sie fuehrt oft zu Kuendigungsschutz, ist aber ein eigener Bescheid. Elternteilzeit und Wiedereingliederungsteilzeit sind jetzt auch bei der Stundenaenderung waehlbar. Sie standen nur bei der Rueckkehr aus einer Abwesenheit, mit der Begruendung, dass sie typischerweise dann beginnen -- typischerweise ist aber nicht immer. Die Ueberschneidung der beiden Listen ist damit gewollt; der Test prueft nicht mehr auf Partition, sondern darauf, dass keine Teilzeit an keinem der beiden Wege haengt.
122 lines
5.4 KiB
TypeScript
122 lines
5.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { ABSENCE_TYPES, absenceLabel, isAbsenceType, RUECKKEHR_GRUENDE, statusLabel, STUNDEN_GRUENDE } from "@/lib/absence";
|
|
|
|
// The stored employment status is still 'Karenz' — renaming the enum value
|
|
// would mean rewriting every stored function that spells it. The rename
|
|
// therefore lives entirely in this mapping, which makes it the one thing
|
|
// standing between the database value and what an HR user reads.
|
|
describe("statusLabel", () => {
|
|
it("renders the stored 'Karenz' value as Langzeitabwesenheit", () => {
|
|
expect(statusLabel("Karenz")).toBe("Langzeitabwesenheit");
|
|
});
|
|
|
|
it("leaves the other statuses alone", () => {
|
|
expect(statusLabel("Aktiv")).toBe("Aktiv");
|
|
expect(statusLabel("Geplant")).toBe("Geplant");
|
|
expect(statusLabel("Ausgetreten")).toBe("Ausgetreten");
|
|
});
|
|
});
|
|
|
|
describe("absenceLabel", () => {
|
|
it("shows the specific kind when one is recorded", () => {
|
|
expect(absenceLabel("Karenz", "Bildungskarenz")).toBe("Bildungskarenz");
|
|
expect(absenceLabel("Karenz", "Präsenzdienst")).toBe("Präsenzdienst");
|
|
});
|
|
|
|
it("falls back to the generic name for absences recorded before the field existed", () => {
|
|
expect(absenceLabel("Karenz", null)).toBe("Langzeitabwesenheit");
|
|
expect(absenceLabel("Karenz", "")).toBe("Langzeitabwesenheit");
|
|
});
|
|
|
|
it("ignores a type that is not on the list rather than displaying it", () => {
|
|
// The column has a check constraint, but the value arrives here as a
|
|
// plain string — echoing an unknown one would put unvalidated text in
|
|
// the status chip.
|
|
expect(absenceLabel("Karenz", "Ausgedachte Karenz")).toBe("Langzeitabwesenheit");
|
|
});
|
|
|
|
it("ignores the absence type entirely for any other status", () => {
|
|
expect(absenceLabel("Aktiv", "Sabbatical")).toBe("Aktiv");
|
|
expect(absenceLabel("Ausgetreten", "Papamonat")).toBe("Ausgetreten");
|
|
});
|
|
});
|
|
|
|
describe("ABSENCE_TYPES", () => {
|
|
it("holds the ten kinds, in the order they are offered", () => {
|
|
// Waren dreizehn. Die vier Teilzeiten sind dorthin gewandert, wo sie
|
|
// hingehören — siehe unten.
|
|
expect(ABSENCE_TYPES).toHaveLength(10);
|
|
expect(ABSENCE_TYPES[0]).toBe("Wochenhilfe (Mutterschutz)");
|
|
expect(ABSENCE_TYPES.at(-1)).toBe("Sabbatical");
|
|
});
|
|
|
|
it("guards every entry with isAbsenceType", () => {
|
|
for (const t of ABSENCE_TYPES) expect(isAbsenceType(t)).toBe(true);
|
|
expect(isAbsenceType("Karenz")).toBe(false);
|
|
expect(isAbsenceType(null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Eine Teilzeit ist keine Abwesenheit. Wer in Bildungs-, Pflege-, Eltern-
|
|
// oder Wiedereingliederungsteilzeit als „langzeitabwesend" geführt wurde,
|
|
// verschwand aus dem Bestand, verlor die Berichtslinie an eine Vertretung und
|
|
// zählte in Auswertungen nicht mehr mit — obwohl er jede Woche im Haus war,
|
|
// nur kürzer.
|
|
const TEILZEITEN = ["Bildungsteilzeit", "Elternteilzeit", "Wiedereingliederungsteilzeit", "Pflegeteilzeit"];
|
|
|
|
describe("Teilzeiten sind keine Abwesenheiten", () => {
|
|
it("führt keine Teilzeit mehr als Abwesenheit", () => {
|
|
for (const t of TEILZEITEN) {
|
|
expect(ABSENCE_TYPES).not.toContain(t);
|
|
expect(isAbsenceType(t)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("behält die Karenzen — sie setzen aus, statt zu reduzieren", () => {
|
|
// Die Wörter ähneln sich, die Sachverhalte nicht: Bildungskarenz ist
|
|
// eine Abwesenheit, Bildungsteilzeit sind weniger Stunden.
|
|
for (const t of ["Elternkarenz (inkl. Väterkarenz)", "Bildungskarenz", "Pflegekarenz"]) {
|
|
expect(ABSENCE_TYPES).toContain(t);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Gründe für weniger Stunden", () => {
|
|
it("bietet bei der Rückkehr die zwei an, die dort hingehören", () => {
|
|
expect([...RUECKKEHR_GRUENDE]).toEqual(["Wiedereingliederungsteilzeit", "Elternteilzeit"]);
|
|
});
|
|
|
|
it("bietet bei der Stundenänderung die übrigen an, mit dem Normalfall zuerst", () => {
|
|
expect(STUNDEN_GRUENDE[0]).toBe("Vertragliche Stundenänderung");
|
|
expect([...STUNDEN_GRUENDE]).toContain("Bildungsteilzeit");
|
|
expect([...STUNDEN_GRUENDE]).toContain("Pflegeteilzeit");
|
|
});
|
|
|
|
it("bietet alle vier Teilzeiten bei der Stundenänderung an", () => {
|
|
// Bis zum 29.09. standen Eltern- und Wiedereingliederungsteilzeit nur bei
|
|
// der Rückkehr — mit der Begründung, dass sie typischerweise dann
|
|
// beginnen. Typischerweise ist nicht immer, und wer sie ohne
|
|
// vorangegangene Abwesenheit vereinbart, fand sie nirgends. Gemeldet von
|
|
// Lara: „Teilzeitvariante Elternteilzeit fehlt".
|
|
for (const teilzeit of TEILZEITEN) {
|
|
expect([...STUNDEN_GRUENDE], `„${teilzeit}" fehlt bei der Stundenänderung`).toContain(teilzeit);
|
|
}
|
|
});
|
|
|
|
it("überschneidet sich dabei bewusst mit der Rückkehr", () => {
|
|
// Die Überschneidung ist kein Versehen: es ist derselbe Wert in derselben
|
|
// Spalte, nur aus zwei Anlässen erfasst. Was es nicht geben darf, ist eine
|
|
// Teilzeit, die an *keinem* der beiden Wege hängt.
|
|
const beide = TEILZEITEN.filter(
|
|
(t) => ([...RUECKKEHR_GRUENDE] as string[]).includes(t) && ([...STUNDEN_GRUENDE] as string[]).includes(t)
|
|
);
|
|
expect(new Set(beide)).toEqual(new Set(RUECKKEHR_GRUENDE));
|
|
});
|
|
|
|
it("führt den Normalfall nicht als Teilzeit", () => {
|
|
// „Vertragliche Stundenänderung" ist keine Variante, sondern ihr Fehlen:
|
|
// sie landet als null in teilzeit_art.
|
|
expect([...TEILZEITEN]).not.toContain("Vertragliche Stundenänderung");
|
|
});
|
|
});
|