import { describe, expect, it } from "vitest"; import { darfKorrigiertWerden, loeschVorschau } from "@/lib/history"; import type { AuditChange, HistoryEventType } from "@/lib/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(["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("bricht eine noch nicht wirksame Abwesenheit ab, wenn ihr Vorgang bekannt ist", () => { // Vorher hiess es hier „Sie muss über den Vorgang selbst abgebrochen // werden" — nur gab es diesen Weg nirgends. Die Zeile stand in der Akte, // der Vorgang lief weiter, und niemand konnte beides aufhalten. const geplant = eintrag({ event_type: "Karenz", event_date: "2099-01-01", pending_id: "p1", changes: null }); expect(darfKorrigiertWerden(geplant, HEUTE, [geplant]).erlaubt).toBe(true); }); it("bleibt bei einer geplanten Abwesenheit ohne hinterlegten Vorgang stehen", () => { // Ohne Verweis liesse sich nur über Person und Datum raten, welcher // Vorgang gemeint ist — und geraten wird beim Abbrechen nicht. const geplant = eintrag({ event_type: "Karenz", event_date: "2099-01-01", pending_id: null, changes: null }); const u = darfKorrigiertWerden(geplant, HEUTE, [geplant]); expect(u.erlaubt).toBe(false); expect(u.erlaubt === false && u.grund).toMatch(/kein Vorgang hinterlegt/); }); it("verlangt bei einer bereits wirksamen Abwesenheit weiterhin Feldwerte", () => { // Rückwärts geht es nur, wenn irgendwo steht, was vorher galt. const alt = eintrag({ event_type: "Karenz", event_date: "2026-07-15", changes: null }); const u = darfKorrigiertWerden(alt, HEUTE, [alt]); expect(u.erlaubt).toBe(false); expect(u.erlaubt === false && u.grund).toMatch(/keine Feldwerte/); }); it("hält die Reihenfolge auch bei geplanten Einträgen ein", () => { const geplanteKarenz = eintrag({ id: "k2", event_type: "Karenz", event_date: "2099-01-01", pending_id: "p1", changes: null }); const geplanteRueckkehr = eintrag({ id: "r2", event_type: "Rückkehr", event_date: "2099-06-01", pending_id: "p2", changes: null }); const u = darfKorrigiertWerden(geplanteKarenz, HEUTE, [geplanteKarenz, geplanteRueckkehr]); expect(u.erlaubt).toBe(false); expect(u.erlaubt === false && u.grund).toMatch(/zuerst gelöscht/); }); });