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>
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
fruehesteFaelligkeit,
|
|
zuPlanendeTermine,
|
|
type PlanungsGeraet,
|
|
} from "@/lib/wartung/planung";
|
|
|
|
/**
|
|
* Terminplanung des nächtlichen Laufs.
|
|
*
|
|
* Der Lauf entscheidet, was im Fristen-Dashboard steht. Ein Fehler hier fällt
|
|
* niemandem auf — bis eine Prüfung fehlt, die hätte stattfinden müssen.
|
|
*/
|
|
|
|
const HEUTE = new Date("2026-06-15T02:15:00.000Z");
|
|
|
|
function tag(jahr: number, monat: number, tagImMonat: number): Date {
|
|
return new Date(Date.UTC(jahr, monat - 1, tagImMonat));
|
|
}
|
|
|
|
const VORLAGE = { id: "v1", intervallMonate: 12 };
|
|
|
|
function geraet(teil: Partial<PlanungsGeraet> = {}): PlanungsGeraet {
|
|
return {
|
|
id: "g1",
|
|
inventarnummer: "INV-0001",
|
|
status: "EINSATZBEREIT",
|
|
hatOffenenTermin: false,
|
|
...teil,
|
|
};
|
|
}
|
|
|
|
describe("zuPlanendeTermine", () => {
|
|
it("rechnet ab der letzten Prüfung nach dieser Vorlage", () => {
|
|
const geplant = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ letztePruefungZurVorlage: tag(2025, 8, 1) })],
|
|
{ horizontTage: 180, heute: HEUTE },
|
|
);
|
|
|
|
expect(geplant).toHaveLength(1);
|
|
expect(geplant[0]!.faelligAm.toISOString()).toBe("2026-08-01T00:00:00.000Z");
|
|
expect(geplant[0]!.ohneBasisdatum).toBe(false);
|
|
});
|
|
|
|
it("nimmt ersatzweise das Prüfdatum am Gerät, dann die Indienststellung", () => {
|
|
const ausGeraet = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ letztePruefung: tag(2025, 7, 1), indienststellung: tag(2020, 1, 1) })],
|
|
{ horizontTage: 180, heute: HEUTE },
|
|
);
|
|
expect(ausGeraet[0]!.faelligAm.toISOString()).toBe("2026-07-01T00:00:00.000Z");
|
|
|
|
const ausIndienststellung = zuPlanendeTermine(
|
|
{ id: "v1", intervallMonate: 6 },
|
|
[geraet({ indienststellung: tag(2026, 2, 1) })],
|
|
{ horizontTage: 180, heute: HEUTE },
|
|
);
|
|
expect(ausIndienststellung[0]!.faelligAm.toISOString()).toBe("2026-08-01T00:00:00.000Z");
|
|
});
|
|
|
|
it("setzt Geräte ohne jedes Datum sofort fällig", () => {
|
|
const geplant = zuPlanendeTermine(VORLAGE, [geraet()], {
|
|
horizontTage: 180,
|
|
heute: HEUTE,
|
|
});
|
|
|
|
expect(geplant).toHaveLength(1);
|
|
expect(geplant[0]!.ohneBasisdatum).toBe(true);
|
|
expect(geplant[0]!.faelligAm.toISOString()).toBe("2026-06-15T00:00:00.000Z");
|
|
});
|
|
|
|
it("plant nichts für ausgesonderte Geräte", () => {
|
|
const geplant = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ status: "AUSGESONDERT", letztePruefungZurVorlage: tag(2025, 8, 1) })],
|
|
{ horizontTage: 180, heute: HEUTE },
|
|
);
|
|
|
|
expect(geplant).toHaveLength(0);
|
|
});
|
|
|
|
it("plant gesperrte Geräte weiter — die sollen ja wieder in Dienst", () => {
|
|
const geplant = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ status: "GESPERRT", letztePruefungZurVorlage: tag(2025, 8, 1) })],
|
|
{ horizontTage: 180, heute: HEUTE },
|
|
);
|
|
|
|
expect(geplant).toHaveLength(1);
|
|
});
|
|
|
|
it("legt keinen zweiten Termin an, solange einer offen ist", () => {
|
|
const geplant = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ hatOffenenTermin: true, letztePruefungZurVorlage: tag(2025, 8, 1) })],
|
|
{ horizontTage: 180, heute: HEUTE },
|
|
);
|
|
|
|
expect(geplant).toHaveLength(0);
|
|
});
|
|
|
|
it("plant nur innerhalb des Horizonts", () => {
|
|
const geplant = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ letztePruefungZurVorlage: tag(2026, 5, 1) })],
|
|
// Fällig am 01.05.2027 — mit 30 Tagen Horizont noch nicht zu planen.
|
|
{ horizontTage: 30, heute: HEUTE },
|
|
);
|
|
|
|
expect(geplant).toHaveLength(0);
|
|
});
|
|
|
|
it("plant Überfälliges immer, egal wie lange es her ist", () => {
|
|
const geplant = zuPlanendeTermine(
|
|
VORLAGE,
|
|
[geraet({ letztePruefungZurVorlage: tag(2019, 1, 1) })],
|
|
{ horizontTage: 30, heute: HEUTE },
|
|
);
|
|
|
|
expect(geplant).toHaveLength(1);
|
|
expect(geplant[0]!.faelligAm.toISOString()).toBe("2020-01-01T00:00:00.000Z");
|
|
});
|
|
});
|
|
|
|
describe("fruehesteFaelligkeit", () => {
|
|
it("nimmt den dringendsten von mehreren Terminen", () => {
|
|
const frueheste = fruehesteFaelligkeit([
|
|
{ faelligAm: tag(2026, 9, 1) },
|
|
{ faelligAm: tag(2026, 7, 1) },
|
|
{ faelligAm: tag(2027, 1, 1) },
|
|
]);
|
|
|
|
expect(frueheste?.toISOString()).toBe("2026-07-01T00:00:00.000Z");
|
|
});
|
|
|
|
it("gibt ohne Termine nichts zurück", () => {
|
|
expect(fruehesteFaelligkeit([])).toBeNull();
|
|
});
|
|
});
|