Wartungsvorlagen mit Checklisten, Prueftermine aus einem naechtlichen Lauf, Prueffristen-Dashboard, Pruefwizard mit Foto und Unterschrift, unveraenderliche Protokolle als PDF, Maengel-Workflow, Rueckrufe und Lebensdauerueberwachung. Elf neue Tabellen, alle mit organisationId und Row Level Security. Die Unveraenderbarkeit abgeschlossener Protokolle setzt ein Datenbanktrigger durch, nicht der Anwendungscode: eine Korrektur erzeugt eine neue Pruefung mit Verweis auf die alte, beide bleiben lesbar. 79 neue Tests (47 Unit, 10 gegen eine echte Datenbank, 22 E2E). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
5.5 KiB
TypeScript
151 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
erinnerungsstufe,
|
|
fensterGrenzen,
|
|
innerhalbPlanungshorizont,
|
|
naechsteFaelligkeit,
|
|
protokollNummer,
|
|
protokollNummerLesen,
|
|
terminFenster,
|
|
} from "@/lib/wartung/fristen";
|
|
|
|
/**
|
|
* Fristenlogik der Wartung.
|
|
*
|
|
* Alle Tests rechnen mit einem festen "heute", nicht mit der Systemuhr. Sonst
|
|
* wären sie am 31. Dezember rot und am 1. Januar wieder grün — und niemand
|
|
* wüsste, warum.
|
|
*/
|
|
|
|
/** Ein Kalendertag als UTC-Mitternacht, so wie er aus einer DATE-Spalte kommt. */
|
|
function tag(jahr: number, monat: number, tagImMonat: number): Date {
|
|
return new Date(Date.UTC(jahr, monat - 1, tagImMonat));
|
|
}
|
|
|
|
/** Mittags, damit die Zeitzonenumrechnung nicht zufällig aufgeht. */
|
|
const HEUTE = new Date("2026-06-15T12:00:00.000Z");
|
|
|
|
describe("terminFenster", () => {
|
|
it("erkennt Überfälligkeit ab dem Vortag", () => {
|
|
expect(terminFenster(tag(2026, 6, 14), HEUTE)).toBe("ueberfaellig");
|
|
expect(terminFenster(tag(2026, 1, 1), HEUTE)).toBe("ueberfaellig");
|
|
});
|
|
|
|
it("zählt den heutigen Tag zur laufenden Woche, nicht zur Überfälligkeit", () => {
|
|
expect(terminFenster(tag(2026, 6, 15), HEUTE)).toBe("diese_woche");
|
|
});
|
|
|
|
it("staffelt Woche, Monat und Quartal", () => {
|
|
expect(terminFenster(tag(2026, 6, 22), HEUTE)).toBe("diese_woche");
|
|
expect(terminFenster(tag(2026, 6, 23), HEUTE)).toBe("dieser_monat");
|
|
expect(terminFenster(tag(2026, 7, 15), HEUTE)).toBe("dieser_monat");
|
|
expect(terminFenster(tag(2026, 7, 16), HEUTE)).toBe("naechste_90_tage");
|
|
expect(terminFenster(tag(2026, 9, 13), HEUTE)).toBe("naechste_90_tage");
|
|
expect(terminFenster(tag(2026, 9, 14), HEUTE)).toBe("spaeter");
|
|
});
|
|
});
|
|
|
|
describe("fensterGrenzen", () => {
|
|
it("liefert die Grenzen als UTC-Kalendertage", () => {
|
|
const grenzen = fensterGrenzen(HEUTE);
|
|
|
|
expect(grenzen.heute.toISOString()).toBe("2026-06-15T00:00:00.000Z");
|
|
expect(grenzen.inSiebenTagen.toISOString()).toBe("2026-06-22T00:00:00.000Z");
|
|
expect(grenzen.inDreissigTagen.toISOString()).toBe("2026-07-15T00:00:00.000Z");
|
|
expect(grenzen.inNeunzigTagen.toISOString()).toBe("2026-09-13T00:00:00.000Z");
|
|
});
|
|
|
|
it("rechnet auch kurz nach Mitternacht Berliner Zeit den richtigen Tag", () => {
|
|
// 00:30 Uhr in Berlin ist nach UTC noch der Vortag.
|
|
const kurzNachMitternacht = new Date("2026-06-15T22:30:00.000Z");
|
|
expect(fensterGrenzen(kurzNachMitternacht).heute.toISOString()).toBe(
|
|
"2026-06-16T00:00:00.000Z",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("naechsteFaelligkeit", () => {
|
|
it("rechnet ab der letzten Prüfung", () => {
|
|
const faellig = naechsteFaelligkeit({
|
|
letztePruefung: tag(2026, 3, 10),
|
|
intervallMonate: 12,
|
|
});
|
|
expect(faellig?.toISOString()).toBe("2027-03-10T00:00:00.000Z");
|
|
});
|
|
|
|
it("nimmt ersatzweise die Indienststellung", () => {
|
|
const faellig = naechsteFaelligkeit({
|
|
indienststellung: tag(2024, 5, 1),
|
|
intervallMonate: 6,
|
|
});
|
|
expect(faellig?.toISOString()).toBe("2024-11-01T00:00:00.000Z");
|
|
});
|
|
|
|
it("klemmt überzählige Tage auf das Monatsende", () => {
|
|
const faellig = naechsteFaelligkeit({
|
|
letztePruefung: tag(2026, 1, 31),
|
|
intervallMonate: 1,
|
|
});
|
|
expect(faellig?.toISOString()).toBe("2026-02-28T00:00:00.000Z");
|
|
});
|
|
|
|
it("gibt ohne Anhaltspunkt oder Intervall nichts zurück", () => {
|
|
expect(naechsteFaelligkeit({ intervallMonate: 12 })).toBeNull();
|
|
expect(
|
|
naechsteFaelligkeit({ letztePruefung: tag(2026, 1, 1), intervallMonate: 0 }),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("erinnerungsstufe", () => {
|
|
it("meldet Überfälligkeit", () => {
|
|
expect(erinnerungsstufe(tag(2026, 6, 14), 30, HEUTE)).toBe("UEBERFAELLIG");
|
|
});
|
|
|
|
it("meldet den Fälligkeitstag selbst", () => {
|
|
expect(erinnerungsstufe(tag(2026, 6, 15), 30, HEUTE)).toBe("T_0");
|
|
});
|
|
|
|
it("staffelt die Vorwarnungen", () => {
|
|
expect(erinnerungsstufe(tag(2026, 6, 20), 30, HEUTE)).toBe("T_7");
|
|
expect(erinnerungsstufe(tag(2026, 7, 5), 30, HEUTE)).toBe("T_30");
|
|
expect(erinnerungsstufe(tag(2026, 8, 1), 30, HEUTE)).toBeNull();
|
|
});
|
|
|
|
it("richtet die erste Stufe nach der Vorwarnzeit der Vorlage", () => {
|
|
// 60 Tage Vorwarnzeit: ein Termin in 45 Tagen wird schon erinnert.
|
|
expect(erinnerungsstufe(tag(2026, 7, 30), 60, HEUTE)).toBe("T_30");
|
|
// Mit der Standardvorwarnzeit dagegen noch nicht.
|
|
expect(erinnerungsstufe(tag(2026, 7, 30), 30, HEUTE)).toBeNull();
|
|
});
|
|
|
|
it("erinnert auch bei spät angelegten Terminen — kleiner-gleich statt genau", () => {
|
|
// Der Tag T-30 ist längst vorbei, die erste Erinnerung darf trotzdem raus.
|
|
expect(erinnerungsstufe(tag(2026, 6, 30), 30, HEUTE)).toBe("T_30");
|
|
});
|
|
});
|
|
|
|
describe("innerhalbPlanungshorizont", () => {
|
|
it("plant nur, was in den Horizont fällt", () => {
|
|
expect(innerhalbPlanungshorizont(tag(2026, 9, 1), 180, HEUTE)).toBe(true);
|
|
expect(innerhalbPlanungshorizont(tag(2027, 6, 1), 180, HEUTE)).toBe(false);
|
|
});
|
|
|
|
it("bezieht Vergangenes immer ein — Überfälliges muss geplant bleiben", () => {
|
|
expect(innerhalbPlanungshorizont(tag(2025, 1, 1), 180, HEUTE)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Protokollnummern", () => {
|
|
it("füllt die laufende Nummer auf vier Stellen", () => {
|
|
expect(protokollNummer(2026, 1)).toBe("2026-0001");
|
|
expect(protokollNummer(2026, 137)).toBe("2026-0137");
|
|
});
|
|
|
|
it("liest die Nummer wieder auseinander", () => {
|
|
expect(protokollNummerLesen("2026-0042")).toEqual({ jahr: 2026, laufend: 42 });
|
|
expect(protokollNummerLesen("kaputt")).toBeNull();
|
|
expect(protokollNummerLesen(null)).toBeNull();
|
|
});
|
|
});
|