import "server-only"; import { htmlZuPdf, html } from "@/lib/pdf"; import { alsDatum, alsDatumZeit, alsEuro } from "@/lib/utils"; import { ERGEBNIS_BEZEICHNUNG, type Ergebnis, type PunktTyp } from "@/lib/wartung/bewertung"; /** * Das Prüfprotokoll als PDF. * * Es ist der Nachweis, den die Wehr bei einer Prüfung durch die Unfallkasse * vorlegt. Danach richtet sich der Aufbau: * * Kopf wer geprüft hat, welche Wehr, welches Gerät, welche Vorschrift * Mitte jeder Checklistenpunkt mit Soll, Ist und Befund * Fotos eingebettet, nicht verlinkt — ein Link ins Nichts ist kein Beleg * Mängel was aufgefallen ist * Fuß Unterschriften, Prüfvorschrift, Prüfdatum, Seitenzahl * * Alles ist eingebettet: keine externen Schriften, keine nachgeladenen Bilder. * Ein Protokoll muss in zehn Jahren noch genauso aussehen wie heute, auch wenn * es dann keinen Server mehr gibt, der irgendetwas nachliefert. */ /** Bild als data-URI, damit es fest im PDF steckt. */ export function alsDataUri(inhalt: Buffer, mimeTyp: string): string { return `data:${mimeTyp};base64,${inhalt.toString("base64")}`; } export interface ProtokollPunkt { text: string; typ: PunktTyp; pflicht: boolean; sollwert?: string | null; einheit?: string | null; /** Die Antwort, schon für die Anzeige aufbereitet. */ istwert: string; inOrdnung: boolean; bemerkung?: string | null; /** Foto zum Punkt, bereits als data-URI. */ bild?: string | null; } export interface ProtokollSignatur { name: string; rolle: string; unterzeichnetAm: Date; /** PNG des Zeichenfelds als data-URI. */ bild?: string | null; } export interface ProtokollDaten { organisation: { name: string; strasse?: string | null; plz?: string | null; ort?: string | null; }; geraet: { inventarnummer: string; bezeichnung: string; hersteller?: string | null; modell?: string | null; seriennummer?: string | null; typName?: string | null; lagerung?: string | null; baujahr?: Date | null; indienststellung?: Date | null; }; pruefung: { protokollNummer?: string | null; durchgefuehrtAm: Date; art: "INTERN" | "EXTERN"; ergebnis: Ergebnis; dauerMinuten?: number | null; prueferName?: string | null; /** Mitgliedsnummer oder Kennung des Prüfers — die "Prüfer-ID" im Kopf. */ prueferKennung?: string | null; externeFirma?: string | null; kosten?: number | null; naechstePruefungAm?: Date | null; bemerkung?: string | null; /** Bei einer Korrektur: Nummer des ersetzten Protokolls. */ ersetztProtokollNummer?: string | null; korrekturGrund?: string | null; }; vorlage?: { name: string; pruefvorschrift?: string | null; intervallMonate: number; } | null; punkte: ProtokollPunkt[]; maengel: Array<{ beschreibung: string; prioritaet: string }>; signaturen: ProtokollSignatur[]; erzeugtAm: Date; } const ERGEBNIS_FARBE: Record = { BESTANDEN: "#15803d", BESTANDEN_MIT_MAENGELN: "#b45309", NICHT_BESTANDEN: "#b91c1c", }; function zeile(bezeichnung: string, wert: string | null | undefined): string { return `
${html(bezeichnung)}
${html(wert && wert !== "" ? wert : "—")}
`; } function punktZeile(punkt: ProtokollPunkt, nummer: number): string { const soll = [punkt.sollwert, punkt.einheit].filter(Boolean).join(" "); return ` ${nummer} ${html(punkt.text)} ${punkt.pflicht ? '*' : ""} ${punkt.bemerkung ? `
${html(punkt.bemerkung)}
` : ""} ${html(soll || "—")} ${html(punkt.istwert || "—")} ${punkt.inOrdnung ? "i. O." : "nicht i. O."} `; } function fotoBlock(punkte: ProtokollPunkt[]): string { const mitBild = punkte.filter((punkt) => punkt.bild); if (mitBild.length === 0) return ""; return `

Fotos

${mitBild .map( (punkt) => `
${html(punkt.text)}
`, ) .join("")}
`; } /** Baut das eigenständige HTML des Protokolls. */ export function protokollHtml(daten: ProtokollDaten): string { const { organisation, geraet, pruefung, vorlage, punkte, maengel, signaturen } = daten; const anschrift = [ organisation.strasse, [organisation.plz, organisation.ort].filter(Boolean).join(" "), ] .filter(Boolean) .join(", "); const geraetetext = [geraet.hersteller, geraet.modell].filter(Boolean).join(" "); return ` Prüfprotokoll ${html(pruefung.protokollNummer ?? geraet.inventarnummer)}

Prüfprotokoll

${html(organisation.name)}${anschrift ? ` · ${html(anschrift)}` : ""}
${html(ERGEBNIS_BEZEICHNUNG[pruefung.ergebnis])}
${pruefung.protokollNummer ? `Protokoll ${html(pruefung.protokollNummer)}` : "ohne Nummer"}
${ pruefung.ersetztProtokollNummer ? `
Korrekturprotokoll. Dieses Protokoll ersetzt ${html(pruefung.ersetztProtokollNummer)}. ${pruefung.korrekturGrund ? ` Grund: ${html(pruefung.korrekturGrund)}` : ""}
` : "" }

Gerät

${zeile("Inventarnummer", geraet.inventarnummer)} ${zeile("Bezeichnung", geraet.bezeichnung)} ${zeile("Gerätetyp", geraet.typName)} ${zeile("Hersteller / Modell", geraetetext)} ${zeile("Seriennummer", geraet.seriennummer)} ${zeile("Lagerung", geraet.lagerung)} ${zeile("Baujahr", geraet.baujahr ? alsDatum(geraet.baujahr) : null)} ${zeile("Indienststellung", geraet.indienststellung ? alsDatum(geraet.indienststellung) : null)}

Prüfung

${zeile("Prüfdatum", alsDatum(pruefung.durchgefuehrtAm))} ${zeile("Prüfer", pruefung.prueferName)} ${zeile("Prüfer-Kennung", pruefung.prueferKennung)} ${zeile("Art", pruefung.art === "EXTERN" ? "extern durch Fachfirma" : "intern")} ${zeile("Fachfirma", pruefung.externeFirma)} ${zeile("Kosten", pruefung.kosten !== null && pruefung.kosten !== undefined ? alsEuro(pruefung.kosten) : null)} ${zeile("Vorlage", vorlage?.name)} ${zeile("Prüfvorschrift", vorlage?.pruefvorschrift)} ${zeile("Intervall", vorlage ? `${vorlage.intervallMonate} Monate` : null)} ${zeile("Dauer", pruefung.dauerMinuten ? `${pruefung.dauerMinuten} Minuten` : null)} ${zeile("Nächste Prüfung", pruefung.naechstePruefungAm ? alsDatum(pruefung.naechstePruefungAm) : null)}
${pruefung.bemerkung ? `

${html(pruefung.bemerkung)}

` : ""}

Checkliste

${punkte.map((punkt, index) => punktZeile(punkt, index + 1)).join("")}
Nr. Prüfpunkt Soll Ist Befund

* Pflichtpunkt — davon hängt die Einsatzbereitschaft ab.

${fotoBlock(punkte)} ${ maengel.length > 0 ? `

Festgestellte Mängel

` : "" }

Unterschriften

${signaturen .map( (signatur) => `
${signatur.bild ? `` : ""}
${html(signatur.name)}
${html(signatur.rolle)} · ${html(alsDatumZeit(signatur.unterzeichnetAm))}
`, ) .join("")}
Erstellt mit FLORIAN am ${html(alsDatumZeit(daten.erzeugtAm))}. Dieses Protokoll ist nach dem Abschluss unveränderbar gespeichert. Korrekturen erscheinen als eigenes Protokoll mit Verweis auf dieses.
`; } /** Kopfzeile des PDF — sie wiederholt sich auf jeder Seite. */ export function kopfzeileHtml(daten: ProtokollDaten): string { return `
${html(daten.organisation.name)} Prüfprotokoll ${html(daten.pruefung.protokollNummer ?? "")} · ${html(daten.geraet.inventarnummer)}
`; } /** Fußzeile mit Prüfvorschrift, Prüfdatum und Seitenzahl. */ export function fusszeileHtml(daten: ProtokollDaten): string { const vorschrift = daten.vorlage?.pruefvorschrift ?? "ohne Angabe der Vorschrift"; return `
${html(vorschrift)} · geprüft am ${html(alsDatum(daten.pruefung.durchgefuehrtAm))} Seite von
`; } /** Erzeugt das Protokoll als PDF über Gotenberg. */ export async function protokollPdf(daten: ProtokollDaten): Promise { return htmlZuPdf(protokollHtml(daten), { randOben: 0.55, randUnten: 0.55, randLinks: 0.5, randRechts: 0.5, hintergrundDrucken: true, kopfzeileHtml: kopfzeileHtml(daten), fusszeileHtml: fusszeileHtml(daten), }); }