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>
211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
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();
|
|
});
|
|
});
|