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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f866ac35c
commit
e503aafc18
@@ -0,0 +1,150 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user