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:
Florian Wilde
2026-09-03 12:07:11 +02:00
co-authored by Claude Opus 5
parent 7f866ac35c
commit e503aafc18
97 changed files with 13367 additions and 133 deletions
+74 -3
View File
@@ -80,6 +80,44 @@ const AUFNAHMEN: Aufnahme[] = [
beschreibung: "Formular für ein neues Gerät",
angemeldet: true,
},
// Phase 2 — Wartung und Prüfung
{
name: "11-fristen",
pfad: "/pruefungen",
beschreibung: "Fristen-Dashboard mit Zeitfenstern und Bündelung",
angemeldet: true,
},
{
name: "12-vorlagen",
pfad: "/pruefungen/vorlagen",
beschreibung: "Wartungsvorlagen mit Intervall und Prüfvorschrift",
angemeldet: true,
},
{
name: "13-maengel",
pfad: "/maengel",
beschreibung: "Mängel von der Meldung bis zur Behebung",
angemeldet: true,
},
{
name: "14-rueckrufe",
pfad: "/rueckrufe",
beschreibung: "Rückrufe des Herstellers",
angemeldet: true,
},
{
name: "15-lebensdauer",
pfad: "/pruefungen/lebensdauer",
beschreibung: "Geräte am Ende ihrer Nutzungsdauer",
angemeldet: true,
},
{
name: "16-protokolle",
pfad: "/pruefungen/verlauf",
beschreibung: "Abgeschlossene Prüfprotokolle",
angemeldet: true,
},
];
/**
@@ -92,6 +130,12 @@ const DYNAMISCHE_AUFNAHMEN: Array<{
/** Findet den Link auf der Übersichtsseite. */
von: string;
linkMuster: RegExp;
/**
* Zusätzliche Bedingung an die Beschriftung des Links. Nötig bei den
* Protokollen: dort stehen auch Entwürfe in der Liste, und die zeigen ein
* halb leeres Bild.
*/
textMuster?: RegExp;
}> = [
{
name: "04-geraet",
@@ -105,6 +149,20 @@ const DYNAMISCHE_AUFNAHMEN: Array<{
von: "/fahrzeuge",
linkMuster: /^\/fahrzeuge\/[0-9a-f-]{36}$/,
},
{
name: "17-checkliste",
beschreibung: "Checkliste einer Wartungsvorlage",
von: "/pruefungen/vorlagen",
linkMuster: /^\/pruefungen\/vorlagen\/[0-9a-f-]{36}$/,
},
{
name: "18-pruefprotokoll",
beschreibung: "Abgeschlossenes Prüfprotokoll mit Checkliste und Unterschrift",
von: "/pruefungen/verlauf",
linkMuster: /^\/pruefungen\/[0-9a-f-]{36}$/,
// Nur fertige Protokolle: die tragen eine Nummer, Entwürfe nicht.
textMuster: /^\d{4}-\d{4}$/,
},
];
/**
@@ -116,6 +174,12 @@ const DYNAMISCHE_AUFNAHMEN: Array<{
*/
async function fertigGeladen(seite: Page): Promise<void> {
await seite.waitForLoadState("networkidle");
// Die Entwicklungsanzeige von Next.js gehört nicht in eine Anleitung.
await seite
.addStyleTag({ content: "nextjs-portal { display: none !important; }" })
.catch(() => undefined);
try {
await seite.waitForFunction(() => !document.querySelector(".animate-pulse"), undefined, {
timeout: 15_000,
@@ -177,11 +241,18 @@ async function main(): Promise<void> {
.locator("a[href]")
.evaluateAll((elemente) =>
elemente
.map((element) => element.getAttribute("href") ?? "")
.filter((href) => href.startsWith("/")),
.map((element) => ({
href: element.getAttribute("href") ?? "",
text: (element.textContent ?? "").trim(),
}))
.filter((eintrag) => eintrag.href.startsWith("/")),
);
const treffer = ziele.find((href) => aufnahme.linkMuster.test(href));
const treffer = ziele.find(
(eintrag) =>
aufnahme.linkMuster.test(eintrag.href) &&
(!aufnahme.textMuster || aufnahme.textMuster.test(eintrag.text)),
)?.href;
if (!treffer) {
console.warn(`[screenshots] Kein Ziel für ${aufnahme.name} gefunden — übersprungen.`);
continue;