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,210 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
beanstandetePunkte,
|
||||
beantwortet,
|
||||
dauerInMinuten,
|
||||
ergebnisVorschlag,
|
||||
geraetStatusNachPruefung,
|
||||
messwertInOrdnung,
|
||||
offenePflichtpunkte,
|
||||
punktInOrdnung,
|
||||
type Antwort,
|
||||
type Punkt,
|
||||
} from "@/lib/wartung/bewertung";
|
||||
|
||||
/**
|
||||
* Bewertung einer Prüfung.
|
||||
*
|
||||
* Der Kern ist eine einzige fachliche Regel: ein durchgefallener Pflichtpunkt
|
||||
* bedeutet "nicht bestanden", ein durchgefallener Kann-Punkt "bestanden mit
|
||||
* Mängeln". Daran hängt, ob ein Gerät im Einsatz sein darf — deshalb steht das
|
||||
* hier so ausführlich unter Test.
|
||||
*/
|
||||
|
||||
const SICHTPRUEFUNG: Punkt = {
|
||||
id: "p1",
|
||||
text: "Trageeinrichtung ohne Beschädigung",
|
||||
typ: "JA_NEIN",
|
||||
pflicht: true,
|
||||
};
|
||||
|
||||
const WARNDRUCK: Punkt = {
|
||||
id: "p2",
|
||||
text: "Ansprechdruck der Warneinrichtung",
|
||||
typ: "MESSWERT",
|
||||
pflicht: true,
|
||||
minWert: 50,
|
||||
maxWert: 60,
|
||||
};
|
||||
|
||||
const BEUTEL: Punkt = {
|
||||
id: "p3",
|
||||
text: "Leinenbeutel unbeschädigt",
|
||||
typ: "JA_NEIN",
|
||||
pflicht: false,
|
||||
};
|
||||
|
||||
const BEFUND: Punkt = {
|
||||
id: "p4",
|
||||
text: "Auffälligkeiten",
|
||||
typ: "TEXT",
|
||||
pflicht: false,
|
||||
};
|
||||
|
||||
const FOTO: Punkt = { id: "p5", text: "Foto nach der Reinigung", typ: "FOTO", pflicht: false };
|
||||
|
||||
function antwort(punktId: string, teil: Partial<Antwort> = {}): Antwort {
|
||||
return { punktId, ...teil };
|
||||
}
|
||||
|
||||
describe("messwertInOrdnung", () => {
|
||||
it("prüft gegen beide Grenzen", () => {
|
||||
expect(messwertInOrdnung(55, 50, 60)).toBe(true);
|
||||
expect(messwertInOrdnung(50, 50, 60)).toBe(true);
|
||||
expect(messwertInOrdnung(60, 50, 60)).toBe(true);
|
||||
expect(messwertInOrdnung(49.9, 50, 60)).toBe(false);
|
||||
expect(messwertInOrdnung(61, 50, 60)).toBe(false);
|
||||
});
|
||||
|
||||
it("behandelt eine fehlende Grenze als offen", () => {
|
||||
expect(messwertInOrdnung(300, 270, null)).toBe(true);
|
||||
expect(messwertInOrdnung(0.2, null, 0.5)).toBe(true);
|
||||
expect(messwertInOrdnung(0.8, null, 0.5)).toBe(false);
|
||||
});
|
||||
|
||||
it("wertet einen fehlenden Wert nicht als in Ordnung", () => {
|
||||
expect(messwertInOrdnung(null, 50, 60)).toBe(false);
|
||||
expect(messwertInOrdnung(undefined)).toBe(false);
|
||||
expect(messwertInOrdnung(Number.NaN, 0, 10)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("beantwortet", () => {
|
||||
it("erkennt jede Antwortart", () => {
|
||||
expect(beantwortet(SICHTPRUEFUNG, antwort("p1", { jaNein: false }))).toBe(true);
|
||||
expect(beantwortet(WARNDRUCK, antwort("p2", { zahlwert: 0 }))).toBe(true);
|
||||
expect(beantwortet(BEFUND, antwort("p4", { text: "ohne Befund" }))).toBe(true);
|
||||
expect(beantwortet(FOTO, antwort("p5", { fotoDateiId: "abc" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("erkennt fehlende Antworten", () => {
|
||||
expect(beantwortet(SICHTPRUEFUNG, undefined)).toBe(false);
|
||||
expect(beantwortet(SICHTPRUEFUNG, antwort("p1"))).toBe(false);
|
||||
expect(beantwortet(WARNDRUCK, antwort("p2", { zahlwert: null }))).toBe(false);
|
||||
expect(beantwortet(BEFUND, antwort("p4", { text: " " }))).toBe(false);
|
||||
});
|
||||
|
||||
it("wertet die Null als Antwort — sie ist ein Messwert wie jeder andere", () => {
|
||||
expect(beantwortet(WARNDRUCK, antwort("p2", { zahlwert: 0 }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("punktInOrdnung", () => {
|
||||
it("folgt bei Ja/Nein der Antwort", () => {
|
||||
expect(punktInOrdnung(SICHTPRUEFUNG, antwort("p1", { jaNein: true }))).toBe(true);
|
||||
expect(punktInOrdnung(SICHTPRUEFUNG, antwort("p1", { jaNein: false }))).toBe(false);
|
||||
});
|
||||
|
||||
it("rechnet bei Messwerten den Bereich aus", () => {
|
||||
expect(punktInOrdnung(WARNDRUCK, antwort("p2", { zahlwert: 55 }))).toBe(true);
|
||||
expect(punktInOrdnung(WARNDRUCK, antwort("p2", { zahlwert: 45 }))).toBe(false);
|
||||
});
|
||||
|
||||
it("lässt die ausdrückliche Angabe des Prüfers gewinnen", () => {
|
||||
// Wert in der Toleranz, der Prüfer beanstandet trotzdem.
|
||||
expect(punktInOrdnung(WARNDRUCK, antwort("p2", { zahlwert: 55, inOrdnung: false }))).toBe(
|
||||
false,
|
||||
);
|
||||
// Und umgekehrt.
|
||||
expect(punktInOrdnung(SICHTPRUEFUNG, antwort("p1", { jaNein: false, inOrdnung: true }))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("offenePflichtpunkte", () => {
|
||||
it("nennt nur unbeantwortete Pflichtpunkte", () => {
|
||||
const offen = offenePflichtpunkte(
|
||||
[SICHTPRUEFUNG, WARNDRUCK, BEUTEL],
|
||||
[antwort("p1", { jaNein: true })],
|
||||
);
|
||||
|
||||
expect(offen.map((punkt) => punkt.id)).toEqual(["p2"]);
|
||||
});
|
||||
|
||||
it("ist leer, wenn alle Pflichtpunkte beantwortet sind", () => {
|
||||
const offen = offenePflichtpunkte(
|
||||
[SICHTPRUEFUNG, WARNDRUCK, BEUTEL],
|
||||
[antwort("p1", { jaNein: true }), antwort("p2", { zahlwert: 55 })],
|
||||
);
|
||||
|
||||
expect(offen).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ergebnisVorschlag", () => {
|
||||
const punkte = [SICHTPRUEFUNG, WARNDRUCK, BEUTEL, BEFUND];
|
||||
|
||||
it("bestanden, wenn nichts beanstandet ist", () => {
|
||||
const antworten = [
|
||||
antwort("p1", { jaNein: true }),
|
||||
antwort("p2", { zahlwert: 55 }),
|
||||
antwort("p3", { jaNein: true }),
|
||||
];
|
||||
|
||||
expect(ergebnisVorschlag(punkte, antworten)).toBe("BESTANDEN");
|
||||
});
|
||||
|
||||
it("mit Mängeln, wenn nur ein Kann-Punkt durchfällt", () => {
|
||||
const antworten = [
|
||||
antwort("p1", { jaNein: true }),
|
||||
antwort("p2", { zahlwert: 55 }),
|
||||
antwort("p3", { jaNein: false }),
|
||||
];
|
||||
|
||||
expect(ergebnisVorschlag(punkte, antworten)).toBe("BESTANDEN_MIT_MAENGELN");
|
||||
expect(beanstandetePunkte(punkte, antworten).map((punkt) => punkt.id)).toEqual(["p3"]);
|
||||
});
|
||||
|
||||
it("nicht bestanden, sobald ein Pflichtpunkt durchfällt", () => {
|
||||
const antworten = [
|
||||
antwort("p1", { jaNein: true }),
|
||||
// Messwert außerhalb der Toleranz — Pflichtpunkt.
|
||||
antwort("p2", { zahlwert: 42 }),
|
||||
antwort("p3", { jaNein: false }),
|
||||
];
|
||||
|
||||
expect(ergebnisVorschlag(punkte, antworten)).toBe("NICHT_BESTANDEN");
|
||||
});
|
||||
|
||||
it("wertet unbeantwortete Punkte nicht als Beanstandung", () => {
|
||||
// Sonst wäre jede halb ausgefüllte Prüfung sofort "nicht bestanden".
|
||||
expect(ergebnisVorschlag(punkte, [antwort("p1", { jaNein: true })])).toBe("BESTANDEN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("geraetStatusNachPruefung", () => {
|
||||
it("sperrt nur bei nicht bestanden", () => {
|
||||
expect(geraetStatusNachPruefung("NICHT_BESTANDEN")).toBe("GESPERRT");
|
||||
expect(geraetStatusNachPruefung("BESTANDEN_MIT_MAENGELN")).toBe("EINSATZBEREIT");
|
||||
expect(geraetStatusNachPruefung("BESTANDEN")).toBe("EINSATZBEREIT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dauerInMinuten", () => {
|
||||
it("rundet auf volle Minuten", () => {
|
||||
const beginn = new Date("2026-06-15T09:00:00.000Z");
|
||||
const ende = new Date("2026-06-15T09:24:40.000Z");
|
||||
expect(dauerInMinuten(beginn, ende)).toBe(25);
|
||||
});
|
||||
|
||||
it("gibt ohne Beginn nichts zurück", () => {
|
||||
expect(dauerInMinuten(null, new Date())).toBeNull();
|
||||
});
|
||||
|
||||
it("liefert keine negative Dauer, wenn die Uhr gesprungen ist", () => {
|
||||
const beginn = new Date("2026-06-15T10:00:00.000Z");
|
||||
const ende = new Date("2026-06-15T09:00:00.000Z");
|
||||
expect(dauerInMinuten(beginn, ende)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user