A long-term absence recorded by mistake could only be undone by booking a second event on top of it — leaving two entries in the file, the first of which never happened. Karenz and Rückkehr can now be deleted and corrected like the other entries. For that to restore anything, the two operations first had to start recording what they overwrote. start_karenz and record_karenz_return now keep before/after the way change_employee_data does: status, kind of absence, start, planned return — and for a return also employment type, hours and the part-time variant. Without that there is nothing to revert to, only a sentence. The ordering rule HR asked for is enforced in the database, not just in the UI: an absence cannot be deleted while a later return exists. A return standing on its own would be a return from nothing, and the person's status would derive from an entry whose starting point had been deleted. Delete the return first and the absence frees up. Rehearsed end to end on real data: absence recorded, return recorded on reduced hours; deleting the absence refused; deleting the return put the person back on Karenz with the original hours and the part-time variant cleared; deleting the absence then put them back to Aktiv with no trace. Rows written before today carry no before/after and stay untouchable, with the reason they already gave. Planned absences are refused too — they have their own operation, and their fields have no place in a pending payload, which is why app_feld_karte carries a null group for them rather than a plausible-looking wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
6.9 KiB
TypeScript
159 lines
6.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { darfKorrigiertWerden, loeschVorschau } from "@/lib/history";
|
|
import type { AuditChange, HistoryEventType } from "@/lib/supabase/types";
|
|
|
|
const HEUTE = "2026-08-13";
|
|
|
|
function eintrag(teil: Partial<{ id: string; event_type: HistoryEventType; event_date: string; created_at: string; changes: AuditChange[] | null; pending_id: string | null }> = {}) {
|
|
return {
|
|
id: "h1",
|
|
event_type: "Stammdatenänderung" as HistoryEventType,
|
|
event_date: "2026-08-10",
|
|
created_at: "2026-08-10T09:00:00.000Z",
|
|
changes: [{ feld: "Adresse", vorher: "Feldweg 66", nachher: "Bahnhofstrasse 5" }] as AuditChange[] | null,
|
|
...teil,
|
|
};
|
|
}
|
|
|
|
describe("darfKorrigiertWerden", () => {
|
|
it("lässt eine wirksame Stammdatenänderung mit Werten zu", () => {
|
|
expect(darfKorrigiertWerden(eintrag(), HEUTE)).toEqual({ erlaubt: true });
|
|
});
|
|
|
|
it("lässt eine Vertragsänderung ebenso zu", () => {
|
|
expect(darfKorrigiertWerden(eintrag({ event_type: "Vertragsänderung" }), HEUTE).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("schützt den Eintritt", () => {
|
|
const u = darfKorrigiertWerden(eintrag({ event_type: "Eintritt", changes: null }), HEUTE);
|
|
expect(u.erlaubt).toBe(false);
|
|
expect(u.erlaubt === false && u.grund).toMatch(/Anfang der Zeitleiste/);
|
|
});
|
|
|
|
it.each<HistoryEventType>(["Versetzung", "Beförderung", "Austritt", "Wiedereintritt", "Reorganisation"])(
|
|
"verweist bei %s auf den fachlichen Vorgang",
|
|
(typ) => {
|
|
const u = darfKorrigiertWerden(eintrag({ event_type: typ }), HEUTE);
|
|
expect(u.erlaubt).toBe(false);
|
|
expect(u.erlaubt === false && u.grund).toMatch(/passenden Vorgang|Planstellen oder den Status/);
|
|
}
|
|
);
|
|
|
|
it("lässt Abwesenheit und Rückkehr zu", () => {
|
|
// Eine versehentlich erfasste Abwesenheit war sonst nur durch eine zweite
|
|
// Buchung loszuwerden — und dann stünden zwei Einträge in der Akte, von
|
|
// denen der erste nie stattgefunden hat.
|
|
expect(darfKorrigiertWerden(eintrag({ event_type: "Karenz" }), HEUTE).erlaubt).toBe(true);
|
|
expect(darfKorrigiertWerden(eintrag({ event_type: "Rückkehr" }), HEUTE).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("lässt Zukünftiges zu, sobald der geplante Vorgang bekannt ist", () => {
|
|
// Das ist der Sinn von pending_id: ohne Bezug liesse sich die geplante
|
|
// Änderung nur über Person und Datum finden, und das ist nicht eindeutig.
|
|
expect(darfKorrigiertWerden(eintrag({ event_date: "2026-09-01", pending_id: "p1" }), HEUTE).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("weist Zukünftiges ohne Bezug zum Vorgang ab", () => {
|
|
const u = darfKorrigiertWerden(eintrag({ event_date: "2026-09-01", pending_id: null }), HEUTE);
|
|
expect(u.erlaubt).toBe(false);
|
|
expect(u.erlaubt === false && u.grund).toMatch(/kein Vorgang hinterlegt/);
|
|
});
|
|
|
|
it("zählt den heutigen Tag als wirksam", () => {
|
|
expect(darfKorrigiertWerden(eintrag({ event_date: HEUTE }), HEUTE).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("weist Einträge ohne Feldwerte ab", () => {
|
|
for (const ohne of [null, []]) {
|
|
const u = darfKorrigiertWerden(eintrag({ changes: ohne }), HEUTE);
|
|
expect(u.erlaubt).toBe(false);
|
|
expect(u.erlaubt === false && u.grund).toMatch(/keine Feldwerte/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("loeschVorschau", () => {
|
|
const alt = eintrag({
|
|
id: "alt",
|
|
event_date: "2026-08-01",
|
|
created_at: "2026-08-01T09:00:00.000Z",
|
|
changes: [
|
|
{ feld: "Adresse", vorher: "Feldweg 66", nachher: "Zwischenweg 1" },
|
|
{ feld: "Telefon", vorher: "0664 000", nachher: "0664 111" },
|
|
],
|
|
});
|
|
const neu = eintrag({
|
|
id: "neu",
|
|
event_date: "2026-08-10",
|
|
created_at: "2026-08-10T09:00:00.000Z",
|
|
changes: [{ feld: "Adresse", vorher: "Zwischenweg 1", nachher: "Endstrasse 9" }],
|
|
});
|
|
|
|
it("lässt ein Feld stehen, das später erneut geändert wurde", () => {
|
|
// Das ist die Regel, um die es geht: die letztgültige Änderung schlägt.
|
|
const v = loeschVorschau(alt, [alt, neu]);
|
|
expect(v.find((x) => x.feld === "Adresse")!.bleibt).toBe(true);
|
|
});
|
|
|
|
it("setzt ein Feld zurück, das seither niemand angefasst hat", () => {
|
|
const v = loeschVorschau(alt, [alt, neu]);
|
|
const tel = v.find((x) => x.feld === "Telefon")!;
|
|
expect(tel.bleibt).toBe(false);
|
|
expect(tel.auf).toBe("0664 000");
|
|
});
|
|
|
|
it("setzt beim neuesten Eintrag alles zurück", () => {
|
|
const v = loeschVorschau(neu, [alt, neu]);
|
|
expect(v.every((x) => !x.bleibt)).toBe(true);
|
|
expect(v[0].auf).toBe("Zwischenweg 1");
|
|
});
|
|
|
|
it("unterscheidet zwei Einträge am selben Tag über die Erfassungszeit", () => {
|
|
const frueh = eintrag({ id: "a", event_date: HEUTE, created_at: `${HEUTE}T08:00:00.000Z` });
|
|
const spaet = eintrag({ id: "b", event_date: HEUTE, created_at: `${HEUTE}T16:00:00.000Z` });
|
|
expect(loeschVorschau(frueh, [frueh, spaet])[0].bleibt).toBe(true);
|
|
expect(loeschVorschau(spaet, [frueh, spaet])[0].bleibt).toBe(false);
|
|
});
|
|
|
|
it("nennt die Richtung so, wie sie im Dialog steht", () => {
|
|
// von = was jetzt drinsteht, auf = worauf zurückgesetzt wird.
|
|
const [v] = loeschVorschau(neu, [neu]);
|
|
expect(v).toMatchObject({ feld: "Adresse", von: "Endstrasse 9", auf: "Zwischenweg 1" });
|
|
});
|
|
});
|
|
|
|
// Die Reihenfolge zählt: eine Rückkehr setzt eine Abwesenheit voraus. Bliebe
|
|
// sie stehen, während die Abwesenheit verschwindet, stünde in der Akte eine
|
|
// Rückkehr aus dem Nichts — und der Status ergäbe sich aus einem Eintrag,
|
|
// dessen Ausgangslage gelöscht ist.
|
|
describe("Reihenfolge von Abwesenheit und Rückkehr", () => {
|
|
const karenz = eintrag({ id: "k", event_type: "Karenz", event_date: "2026-07-15", created_at: "2026-07-15T09:00:00.000Z" });
|
|
const rueckkehr = eintrag({ id: "r", event_type: "Rückkehr", event_date: "2026-08-10", created_at: "2026-08-10T09:00:00.000Z" });
|
|
|
|
it("verweigert die Abwesenheit, solange die Rückkehr steht", () => {
|
|
const u = darfKorrigiertWerden(karenz, HEUTE, [karenz, rueckkehr]);
|
|
expect(u.erlaubt).toBe(false);
|
|
expect(u.erlaubt === false && u.grund).toMatch(/Rückkehr.*zuerst gelöscht/);
|
|
});
|
|
|
|
it("lässt die Rückkehr jederzeit zurücknehmen — sie ist das obere Ende", () => {
|
|
expect(darfKorrigiertWerden(rueckkehr, HEUTE, [karenz, rueckkehr]).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("gibt die Abwesenheit frei, sobald die Rückkehr weg ist", () => {
|
|
expect(darfKorrigiertWerden(karenz, HEUTE, [karenz]).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("stört sich nicht an einer früheren Rückkehr aus einer anderen Abwesenheit", () => {
|
|
const frueher = eintrag({ id: "r0", event_type: "Rückkehr", event_date: "2026-01-05", created_at: "2026-01-05T09:00:00.000Z" });
|
|
expect(darfKorrigiertWerden(karenz, HEUTE, [frueher, karenz]).erlaubt).toBe(true);
|
|
});
|
|
|
|
it("lässt eine noch nicht wirksame Abwesenheit in Ruhe", () => {
|
|
const geplant = eintrag({ event_type: "Karenz", event_date: "2099-01-01", pending_id: "p1" });
|
|
const u = darfKorrigiertWerden(geplant, HEUTE, [geplant]);
|
|
expect(u.erlaubt).toBe(false);
|
|
expect(u.erlaubt === false && u.grund).toMatch(/noch nicht wirksam/);
|
|
});
|
|
});
|