import { NextResponse } from "next/server"; import { z } from "zod"; import { aktuelleSitzung } from "@/lib/auth"; import { fuerOrganisation } from "@/lib/db"; import { hatRecht } from "@/lib/authz"; import { etikettenPdf, ETIKETTEN_JE_SEITE } from "@/lib/stammdaten/etiketten"; import { AuditAktion, protokolliere } from "@/lib/audit"; /** * Etikettenbogen als PDF. * * Eigener Endpunkt statt einer tRPC-Prozedur: Ein PDF ist eine Datei, keine * JSON-Antwort. Über tRPC müsste es Base64-kodiert durch die Leitung, und der * Browser könnte es nicht einfach in einem neuen Tab öffnen. * * Nimmt zwei Formen entgegen: * - JSON { ids: [...], startPosition?: n } aus der Geräteliste * - Formular mit einem Feld "id" von der Detailseite */ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const eingabe = z.object({ ids: z.array(z.string().uuid()).min(1).max(500), /** Ab welchem Etikett des Bogens gedruckt wird — für angebrochene Bögen. */ startPosition: z.number().int().min(1).max(ETIKETTEN_JE_SEITE).default(1), }); export async function POST(anfrage: Request) { const sitzung = await aktuelleSitzung(); if (!sitzung) { return NextResponse.json({ fehler: "Bitte zuerst anmelden." }, { status: 401 }); } if (!hatRecht(sitzung, "geraet.lesen")) { return NextResponse.json({ fehler: "Fehlende Berechtigung." }, { status: 403 }); } let daten: z.infer; const inhaltstyp = anfrage.headers.get("content-type") ?? ""; if (inhaltstyp.includes("application/json")) { const geprueft = eingabe.safeParse(await anfrage.json()); if (!geprueft.success) { return NextResponse.json( { fehler: "Es wurde kein gültiges Gerät übergeben." }, { status: 400 }, ); } daten = geprueft.data; } else { const formular = await anfrage.formData(); const id = formular.get("id"); const start = Number(formular.get("startPosition") ?? 1); const geprueft = eingabe.safeParse({ ids: typeof id === "string" ? [id] : [], startPosition: Number.isFinite(start) ? start : 1, }); if (!geprueft.success) { return NextResponse.json( { fehler: "Es wurde kein gültiges Gerät übergeben." }, { status: 400 }, ); } daten = geprueft.data; } const { geraete, wehrname } = await fuerOrganisation(sitzung.organisationId, async (db) => { const [liste, organisation] = await Promise.all([ db.geraet.findMany({ where: { id: { in: daten.ids }, geloeschtAm: null }, select: { inventarnummer: true, bezeichnung: true, fahrzeug: { select: { funkrufname: true } }, standort: { select: { kuerzel: true, name: true } }, }, orderBy: { inventarnummer: "asc" }, }), db.organisation.findUnique({ where: { id: sitzung.organisationId }, select: { name: true, kurzname: true }, }), ]); return { geraete: liste, wehrname: organisation?.kurzname ?? organisation?.name ?? "Feuerwehr", }; }); if (geraete.length === 0) { return NextResponse.json({ fehler: "Keines dieser Geräte gibt es." }, { status: 404 }); } try { const pdf = await etikettenPdf( geraete.map((geraet) => ({ inventarnummer: geraet.inventarnummer, bezeichnung: geraet.bezeichnung, zusatz: geraet.fahrzeug?.funkrufname ?? geraet.standort?.kuerzel ?? geraet.standort?.name, })), wehrname, daten.startPosition, ); // Der Abruf von Etiketten ist protokollpflichtig: daran lässt sich später // nachvollziehen, wann welche Aufkleber gedruckt wurden. await protokolliere({ organisationId: sitzung.organisationId, aktion: AuditAktion.EXPORTIERT, entitaet: "Geraet", beschreibung: `Etikettenbogen für ${geraete.length} Geräte erzeugt`, sitzung, kontext: { anzahl: geraete.length, startPosition: daten.startPosition }, }); return new NextResponse(new Uint8Array(pdf), { status: 200, headers: { "Content-Type": "application/pdf", "Content-Disposition": `inline; filename="etiketten-${geraete.length}.pdf"`, "Cache-Control": "no-store", }, }); } catch (fehler) { console.error("[etiketten] PDF fehlgeschlagen:", fehler); return NextResponse.json( { fehler: "Der Etikettenbogen ließ sich nicht erzeugen. Läuft der Gotenberg-Container? (docker compose ps)", }, { status: 502 }, ); } }