Files
Florian WildeandClaude Opus 5 e503aafc18 Phase 2: Wartungs- und Pruefmanagement
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>
2026-09-03 12:07:11 +02:00

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();
});
});