import { NextResponse, type NextRequest } from 'next/server' import { finanzKontext } from '@/lib/finanzen/seite' import { tagAusText } from '@/lib/finanzen/datum' import { betreuungsjahr } from '@/lib/finanzen/betreuungsjahr' import { standardVariante, type Variante } from '@/lib/finanzen/rechnungslegung' import { berichtVermerken, rechnungslegungDaten, vermoegensverzeichnisDaten, } from '@/lib/berichte/gemeinsam' import { vermoegensverzeichnisDateiname, vermoegensverzeichnisPdf, } from '@/lib/berichte/vermoegensverzeichnis' import { rechnungslegungDateiname, rechnungslegungPdf } from '@/lib/berichte/rechnungslegung' /** * Auslieferung der beiden Finanzberichte als PDF. * * Die Datei wird bei jedem Abruf frisch aus den Daten erzeugt - es gibt * keinen zwischengespeicherten Stand, der von der Datenbank abweichen * koennte. Dass ein Bericht erzeugt wurde, wird in der Tabelle Bericht * und im Audit-Log festgehalten: Gegenueber dem Gericht muss ich sagen * koennen, was ich wann in welcher Fassung abgegeben habe. * * Ausgeliefert wird immer als Anhang und mit nosniff - ein Dokument aus * dieser Anwendung wird nie im Browser als HTML ausgefuehrt. */ export async function GET(anfrage: NextRequest): Promise { const { betreute, benutzer } = await finanzKontext() const parameter = anfrage.nextUrl.searchParams const art = parameter.get('art') try { if (art === 'vermoegensverzeichnis') { const stichtag = tagAusText(parameter.get('stichtag')) if (!stichtag) { return fehlerantwort('Es fehlt ein gültiger Stichtag im Format JJJJ-MM-TT.') } const monateRoh = Number.parseInt(parameter.get('monate') ?? '12', 10) const monateRueckblick = Number.isInteger(monateRoh) && monateRoh >= 1 && monateRoh <= 60 ? monateRoh : 12 const daten = await vermoegensverzeichnisDaten({ betreute, stichtag, erstelltVon: benutzer.name, monateRueckblick, }) const pdf = await vermoegensverzeichnisPdf(daten) const dateiname = vermoegensverzeichnisDateiname(daten) await berichtVermerken({ betreuteId: betreute.id, benutzer, art: 'VERMOEGENSVERZEICHNIS', titel: `Vermögensverzeichnis zum ${dateiname.slice(0, 10)}`, stichtag, dateiname, }) return pdfAntwort(pdf, dateiname) } if (art === 'rechnungslegung') { if (!betreute.bestellungAm) { return fehlerantwort( 'Ohne Bestellungsdatum lässt sich kein Betreuungsjahr bestimmen. Bitte zuerst in den ' + 'Stammdaten das Datum der Bestellung eintragen.', ) } const jahrNummer = Number.parseInt(parameter.get('jahr') ?? '', 10) if (!Number.isInteger(jahrNummer) || jahrNummer < 1) { return fehlerantwort('Es fehlt die Nummer des Betreuungsjahres.') } const gewaehlt = parameter.get('variante') const variante: Variante = gewaehlt === 'UEBERSICHT_1859' || gewaehlt === 'VOLL_1865' ? gewaehlt : standardVariante(betreute.befreiterBetreuer) const daten = await rechnungslegungDaten({ betreute, jahrNummer, variante, erstelltVon: benutzer.name, }) const pdf = await rechnungslegungPdf(daten) const dateiname = rechnungslegungDateiname(daten) const jahr = betreuungsjahr(betreute.bestellungAm, jahrNummer) await berichtVermerken({ betreuteId: betreute.id, benutzer, art: variante === 'UEBERSICHT_1859' ? 'VERMOEGENSUEBERSICHT' : 'RECHNUNGSLEGUNG', titel: daten.kopf.titel, zeitraumVon: jahr.von, zeitraumBis: jahr.bis, dateiname, }) return pdfAntwort(pdf, dateiname) } return fehlerantwort( 'Unbekannte Berichtsart. Erwartet wird „vermoegensverzeichnis“ oder „rechnungslegung“.', ) } catch (ausnahme) { console.error('[finanzen] Bericht konnte nicht erzeugt werden:', ausnahme) return fehlerantwort( ausnahme instanceof Error ? ausnahme.message : 'Der Bericht konnte nicht erzeugt werden.', 500, ) } } function pdfAntwort(pdf: Buffer, dateiname: string): Response { // Der Dateiname wird zweimal angegeben: einmal auf ASCII reduziert fuer // aeltere Browser, einmal UTF-8-kodiert nach RFC 5987 fuer die uebrigen. const ascii = dateiname.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_') return new NextResponse(new Uint8Array(pdf), { status: 200, headers: { 'Content-Type': 'application/pdf', 'Content-Length': String(pdf.byteLength), 'Content-Disposition': `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(dateiname)}`, 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'no-store, max-age=0', }, }) } function fehlerantwort(meldung: string, status = 400): Response { return new NextResponse(meldung, { status, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'no-store, max-age=0', }, }) }