feat: Jahresbericht, Vollexport, Wissensbasis und Mitleser-Freigaben
Etappe 7. Die Ausgabeschicht - alles, was diese Anwendung nach aussen abgibt. Der Jahresbericht nach § 1863 BGB ist ein ENTWURF zum Ueberarbeiten, kein fertiger Text. Er zieht sich die fuenf Gliederungspunkte aus Kontaktprotokollen, Gesundheitsereignissen und Vorgangsstatus zusammen; was davor abgegeben wird, entscheidet der Betreuer. Das sagt auch die Oberflaeche. Ausgabe als PDF und als DOCX, damit sich weiterschreiben laesst. Vollexport als ZIP-Strom mit Gegendruck, damit auch grosse Bestaende ohne Speicherausbruch durchgehen: Originaldateien nach Jahren sortiert, alle Tabellen als entschluesseltes JSON, die erzeugten Berichte, ein LIESMICH und SHA-256 je Datei. Damit laesst sich die Anwendung verlassen - kein Datensilo. Der Export ist eine sensible Aktion: nur BETREUER und ADMIN, nur nach frischer Anmeldung, immer mit Audit-Eintrag. Ein Mitleser kommt nicht heran. 37 Hinweiskarten in daten/hinweise.json, Zod-geprueft und ohne Neustart pflegbar. Jede mit Rechtsgrundlage, Link und dem stehenden Vermerk, dass sie keine Rechtsberatung ersetzt. Modulweise Freigaben fuer Mitleser, mit ausdruecklicher Warnung bei Gesundheit, Finanzen und Sozialhilfe. Rollen- und Freigabeaenderungen erfordern eine frische Anmeldung und stehen im Audit-Log. Nebenbei geloest: Webpack loest .tsx vor .ts auf, TypeScript und Vitest umgekehrt. Ein Modul, das beide Endungen traegt, importiert sich unter einem der beiden Werkzeuge selbst. Deshalb heissen die Bausteine jetzt eindeutig (jahresbericht.entwurf.ts, .inhalt.tsx, .ausgabe.ts) und werden ueber eine gemeinsame Fassade weitergereicht. 41 Tests. Etappe: 7 Phase: Ausgabe
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import 'server-only'
|
||||
import type { BerichtArt } from '@prisma/client'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { signierteUrl } from '@/lib/speicher'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { formatDatum } from '@/lib/utils'
|
||||
import type { BerichtsZeile } from '@/components/berichte/berichts-liste'
|
||||
|
||||
/**
|
||||
* Der gemeinsame Vorspann aller Berichtsseiten.
|
||||
*
|
||||
* Wie ueberall gilt: Die aktive betreute Person kommt aus dem Cookie, ob
|
||||
* sie angezeigt werden darf, entscheidet erfordereZugang() gegen die
|
||||
* Datenbank. Ein manipuliertes Cookie kaeme hoechstens bis hierher.
|
||||
*/
|
||||
export async function berichtskontext(art: 'lesen' | 'schreiben' = 'lesen') {
|
||||
const benutzer = await erfordereBenutzer('/berichte')
|
||||
const gewaehlt = await aktiveBetreute(benutzer)
|
||||
if (!gewaehlt) throw new Error('Es ist keine betreute Person ausgewählt.')
|
||||
|
||||
const kontext = await erfordereZugang(gewaehlt.id, 'BERICHTE', art)
|
||||
return {
|
||||
...kontext,
|
||||
betreuteId: gewaehlt.id,
|
||||
darfBerichteSchreiben: kontext.darfSchreiben('BERICHTE'),
|
||||
}
|
||||
}
|
||||
|
||||
export const BERICHTSART_BESCHRIFTUNG: Record<BerichtArt, string> = {
|
||||
VERMOEGENSVERZEICHNIS: 'Vermögensverzeichnis (§ 1835 BGB)',
|
||||
JAHRESBERICHT: 'Jahresbericht (§ 1863 BGB)',
|
||||
VERMOEGENSUEBERSICHT: 'Vermögensübersicht (§ 1859 BGB)',
|
||||
RECHNUNGSLEGUNG: 'Rechnungslegung (§ 1865 BGB)',
|
||||
BELEGMAPPE: 'Belegmappe',
|
||||
UNTERLAGENPAKET_BEZIRK: 'Unterlagenpaket für den Träger',
|
||||
VOLLEXPORT: 'Vollexport',
|
||||
}
|
||||
|
||||
/**
|
||||
* Laedt die protokollierten Berichte und erzeugt zu jeder abgelegten Datei
|
||||
* eine kurzlebige, signierte Adresse. Die Adresse wird bei jedem
|
||||
* Seitenaufbau neu erzeugt und laeuft nach fuenf Minuten ab - ein
|
||||
* versehentlich weitergegebener Link ist dann wertlos.
|
||||
*/
|
||||
export async function berichtsZeilenLaden(
|
||||
betreuteId: string,
|
||||
darfSchreiben: boolean,
|
||||
): Promise<BerichtsZeile[]> {
|
||||
const berichte = await db.bericht.findMany({
|
||||
where: { betreuteId, ...nichtGeloescht },
|
||||
include: { erstelltVon: { select: { name: true } } },
|
||||
orderBy: { erstelltAm: 'desc' },
|
||||
take: 200,
|
||||
})
|
||||
|
||||
return Promise.all(
|
||||
berichte.map(async (bericht) => ({
|
||||
id: bericht.id,
|
||||
art: bericht.art,
|
||||
artBeschriftung: BERICHTSART_BESCHRIFTUNG[bericht.art],
|
||||
titel: bericht.titel,
|
||||
zeitraum:
|
||||
bericht.zeitraumVon || bericht.zeitraumBis
|
||||
? `${formatDatum(bericht.zeitraumVon)} – ${formatDatum(bericht.zeitraumBis)}`
|
||||
: bericht.stichtag
|
||||
? `Stand ${formatDatum(bericht.stichtag)}`
|
||||
: null,
|
||||
erstelltAm: bericht.erstelltAm.toISOString(),
|
||||
erstelltVon: bericht.erstelltVon?.name ?? null,
|
||||
dateiname: bericht.dateiname,
|
||||
downloadUrl: bericht.objektSchluessel
|
||||
? await signierteUrl(bericht.objektSchluessel).catch(() => null)
|
||||
: null,
|
||||
abgegebenAm: bericht.abgegebenAm ? bericht.abgegebenAm.toISOString() : null,
|
||||
abgegebenAn: bericht.abgegebenAn,
|
||||
bearbeitbar: darfSchreiben && bericht.art === 'JAHRESBERICHT',
|
||||
})),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user