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,163 @@
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Archive,
|
||||
CalendarClock,
|
||||
FolderArchive,
|
||||
Paperclip,
|
||||
ScrollText,
|
||||
} from 'lucide-react'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { BerichtsListe } from '@/components/berichte/berichts-liste'
|
||||
import { berichtskontext, berichtsZeilenLaden } from '@/components/berichte/kontext'
|
||||
import { HinweisKarten } from '@/components/hinweise/HinweisKarten'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { formatDatum, relativeTage, tageBis } from '@/lib/utils'
|
||||
|
||||
export const metadata = { title: 'Berichte' }
|
||||
|
||||
/** Fristen, hinter denen ein Bericht steht - danach sucht die Übersicht. */
|
||||
const BERICHTSFRISTEN = [
|
||||
'anfangsbericht',
|
||||
'jahresbericht',
|
||||
'vermoegensverzeichnis',
|
||||
'vermoegensuebersicht',
|
||||
'rechnungslegung',
|
||||
]
|
||||
|
||||
const WEGE = [
|
||||
{
|
||||
href: '/berichte/jahresbericht',
|
||||
titel: 'Jahresbericht',
|
||||
beschreibung:
|
||||
'Textentwurf aus Kontaktprotokollen, Gesundheitsereignissen und Vorgängen – zum Überarbeiten, nicht zum blinden Abgeben.',
|
||||
icon: ScrollText,
|
||||
},
|
||||
{
|
||||
href: '/berichte/belegmappe',
|
||||
titel: 'Belegmappe',
|
||||
beschreibung:
|
||||
'Alle Belege eines Zeitraums oder eines Vorgangs als ein PDF mit Deckblatt und Verzeichnis.',
|
||||
icon: FolderArchive,
|
||||
},
|
||||
{
|
||||
href: '/berichte/unterlagenpaket',
|
||||
titel: 'Unterlagen für den Träger',
|
||||
beschreibung:
|
||||
'Die nachgeforderten Unterlagen als ein PDF mit Anschreiben, laufender Nummer und Inhaltsverzeichnis.',
|
||||
icon: Paperclip,
|
||||
},
|
||||
{
|
||||
href: '/berichte/export',
|
||||
titel: 'Vollexport',
|
||||
beschreibung:
|
||||
'Alles, was gespeichert ist, als ZIP – damit du auch ohne diese Anwendung weiterarbeiten kannst.',
|
||||
icon: Archive,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Die Berichtsuebersicht beantwortet zwei Fragen:
|
||||
* "Was ist demnaechst faellig?" und "Was habe ich wann abgegeben?".
|
||||
*
|
||||
* Die erste Frage kommt aus den Fristen - dort stehen die gesetzlichen
|
||||
* Pflichten mit Datum. Die zweite aus den protokollierten Berichten.
|
||||
*/
|
||||
export default async function BerichteSeite() {
|
||||
const { betreuteId, darfBerichteSchreiben } = await berichtskontext('lesen')
|
||||
|
||||
const [fristen, zeilen] = await Promise.all([
|
||||
db.frist.findMany({
|
||||
where: {
|
||||
betreuteId,
|
||||
status: 'OFFEN',
|
||||
...nichtGeloescht,
|
||||
OR: BERICHTSFRISTEN.map((schluessel) => ({
|
||||
systemSchluessel: { startsWith: schluessel },
|
||||
})),
|
||||
},
|
||||
orderBy: { datum: 'asc' },
|
||||
take: 10,
|
||||
}),
|
||||
berichtsZeilenLaden(betreuteId, darfBerichteSchreiben),
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SeitenKopf
|
||||
titel="Berichte"
|
||||
pfad={[{ titel: 'Berichte' }]}
|
||||
beschreibung="Was das Betreuungsgericht und der Sozialhilfeträger von dir sehen wollen – und was du ihnen wann vorgelegt hast."
|
||||
/>
|
||||
|
||||
<section className="space-y-3" aria-label="Demnächst fällig">
|
||||
<h2 className="text-base font-semibold">Demnächst fällig</h2>
|
||||
{fristen.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Zurzeit steht keine Berichtsfrist offen. Prüfe unter{' '}
|
||||
<Link href="/fristen" className="underline underline-offset-4">
|
||||
Fristen
|
||||
</Link>
|
||||
, ob die Pflichtfristen aus dem Bestellungsdatum schon abgeleitet sind.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y rounded-md border">
|
||||
{fristen.map((frist) => {
|
||||
const tage = tageBis(frist.datum)
|
||||
const variante =
|
||||
tage < 0 ? 'rot' : tage <= 7 ? 'orange' : tage <= 31 ? 'gelb' : 'gruen'
|
||||
return (
|
||||
<li key={frist.id} className="flex flex-wrap items-center gap-3 p-3">
|
||||
<CalendarClock className="size-5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-medium">{frist.titel}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatDatum(frist.datum)}
|
||||
{frist.rechtsgrundlage ? ` · ${frist.rechtsgrundlage}` : ''}
|
||||
</span>
|
||||
</span>
|
||||
{frist.ausschlussfrist && <Badge variant="rot">Ausschlussfrist</Badge>}
|
||||
<Badge variant={variante}>
|
||||
{tage < 0 ? `überfällig, ${relativeTage(tage)}` : relativeTage(tage)}
|
||||
</Badge>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2" aria-label="Was möchtest du erzeugen?">
|
||||
{WEGE.map((weg) => {
|
||||
const Icon = weg.icon
|
||||
return (
|
||||
<Card key={weg.href}>
|
||||
<CardHeader className="flex flex-row items-start gap-3 space-y-0">
|
||||
<Icon className="mt-0.5 size-5 shrink-0 text-primary" aria-hidden="true" />
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="text-lg">
|
||||
<Link href={weg.href} className="underline-offset-4 hover:underline">
|
||||
{weg.titel}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">{weg.beschreibung}</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="space-y-3" aria-label="Erzeugte Berichte">
|
||||
<h2 className="text-base font-semibold">Was ich abgegeben habe</h2>
|
||||
<BerichtsListe zeilen={zeilen} darfSchreiben={darfBerichteSchreiben} />
|
||||
</section>
|
||||
|
||||
<HinweisKarten kontext={['BERICHTE']} />
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§ 1863 BGB" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user