Ausschliesslich Zeilenenden. Nachgewiesen mit "git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich kein Zeichen Inhalt geaendert. Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit echten Aenderungen zu verstecken, macht die Historie an dieser Stelle unlesbar. So laesst er sich beim Durchsehen ueberspringen. Refs #26
439 lines
16 KiB
TypeScript
439 lines
16 KiB
TypeScript
import { notFound } from 'next/navigation'
|
||
import Link from 'next/link'
|
||
import { ArrowLeft, FileDown, Clock } from 'lucide-react'
|
||
import type { VorgangStatus } from '@prisma/client'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||
import { AufgabenListe, type AufgabenZeile } from '@/components/vorgaenge/aufgaben-liste'
|
||
import { Faelligkeit } from '@/components/vorgaenge/faelligkeit'
|
||
import { GenehmigungAnlegen } from '@/components/vorgaenge/genehmigung-anlegen'
|
||
import {
|
||
GenehmigungsKette,
|
||
type GenehmigungsAnzeige,
|
||
} from '@/components/vorgaenge/genehmigungs-kette'
|
||
import { SachstandsanfrageKnopf } from '@/components/vorgaenge/sachstandsanfrage-knopf'
|
||
import { StatusWechsler } from '@/components/vorgaenge/status-wechsler'
|
||
import { VorgangLoeschen } from '@/components/vorgaenge/vorgang-loeschen'
|
||
import { VorgangsFormular } from '@/components/vorgaenge/vorgangs-formular'
|
||
import { ZeitstrahlAnzeige } from '@/components/vorgaenge/zeitstrahl-anzeige'
|
||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
|
||
import { entschluesselnWennMoeglich, oeffnen } from '@/lib/crypto/feld'
|
||
import { KATEGORIE_BESCHRIFTUNG, PRIORITAET_BESCHRIFTUNG } from '@/lib/validierung/vorgaenge'
|
||
import { sachstandsanfrageErzeugen } from '@/lib/vorgaenge/sachstandsanfrage'
|
||
import {
|
||
VORGANG_STATUS_BESCHRIFTUNG,
|
||
zeitstrahlBauen,
|
||
type ZeitstrahlStatuswechsel,
|
||
} from '@/lib/vorgaenge/zeitstrahl'
|
||
import { formatDatum } from '@/lib/utils'
|
||
|
||
/**
|
||
* Die Fallakte.
|
||
*
|
||
* Kopf mit Status, Gegenstelle, Aktenzeichen und dem nächsten Handgriff;
|
||
* darunter die Aufgabenliste, das Genehmigungsverfahren und der
|
||
* Zeitstrahl, der alles zusammenführt, was in dieser Sache passiert ist.
|
||
*/
|
||
export default async function VorgangSeite({ params }: { params: Promise<{ id: string }> }) {
|
||
const { id } = await params
|
||
const benutzer = await erfordereBenutzer(`/vorgaenge/${id}`)
|
||
const aktive = await aktiveBetreute(benutzer)
|
||
if (!aktive) notFound()
|
||
|
||
const vorgang = await db.vorgang.findFirst({
|
||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||
include: {
|
||
aufgaben: { orderBy: [{ reihenfolge: 'asc' }, { erstelltAm: 'asc' }] },
|
||
genehmigungen: {
|
||
where: nichtGeloescht,
|
||
include: { beschlussDokument: { select: { id: true, titel: true } } },
|
||
orderBy: { erstelltAm: 'asc' },
|
||
},
|
||
fristen: { where: nichtGeloescht, orderBy: { datum: 'asc' } },
|
||
buchungen: { where: nichtGeloescht, orderBy: { datum: 'desc' } },
|
||
kontaktprotokolle: { where: nichtGeloescht, orderBy: { datum: 'desc' } },
|
||
verknuepfungen: { include: { dokument: true } },
|
||
ansprechpartner: true,
|
||
},
|
||
})
|
||
if (!vorgang) notFound()
|
||
|
||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VORGAENGE', 'lesen')
|
||
const schreiben = darfSchreiben('VORGAENGE')
|
||
|
||
const offen = oeffnen('Vorgang', vorgang)
|
||
|
||
// Kontakte dieser Akte fuer die Wahl des Ansprechpartners.
|
||
const kontakte = await db.kontakt.findMany({
|
||
where: { betreuteId: aktive.id, ...nichtGeloescht },
|
||
select: { id: true, name: true, organisation: true },
|
||
orderBy: [{ organisation: 'asc' }, { name: 'asc' }],
|
||
take: 300,
|
||
})
|
||
|
||
// --- Statuswechsel aus dem Audit-Log --------------------------------------
|
||
// Das Log ist ohnehin der Nachweis gegenueber dem Gericht; daraus die
|
||
// Statuschronik zu lesen, spart eine zusaetzliche Tabelle.
|
||
const protokolle = await db.auditLog.findMany({
|
||
where: { entitaet: 'Vorgang', entitaetId: vorgang.id, aktion: 'GEAENDERT' },
|
||
orderBy: { zeitpunkt: 'asc' },
|
||
take: 200,
|
||
})
|
||
|
||
const statuswechsel: ZeitstrahlStatuswechsel[] = []
|
||
for (const eintrag of protokolle) {
|
||
const roh = entschluesselnWennMoeglich(
|
||
eintrag.aenderungVerschluesselt,
|
||
'aenderungVerschluesselt',
|
||
)
|
||
if (!roh) continue
|
||
try {
|
||
const gelesen = JSON.parse(roh) as Record<string, { alt: unknown; neu: unknown }>
|
||
const status = gelesen.status
|
||
if (!status || typeof status.neu !== 'string') continue
|
||
statuswechsel.push({
|
||
id: eintrag.id,
|
||
zeitpunkt: eintrag.zeitpunkt,
|
||
von: typeof status.alt === 'string' ? (status.alt as VorgangStatus) : null,
|
||
nach: status.neu as VorgangStatus,
|
||
wer: eintrag.userName,
|
||
})
|
||
} catch {
|
||
// Ein unlesbarer Protokolleintrag darf die Akte nicht blockieren.
|
||
continue
|
||
}
|
||
}
|
||
|
||
// --- Zeitstrahl -----------------------------------------------------------
|
||
const zeitstrahl = zeitstrahlBauen({
|
||
vorgang: {
|
||
titel: vorgang.titel,
|
||
erstelltAm: vorgang.erstelltAm,
|
||
erledigtAm: vorgang.erledigtAm,
|
||
},
|
||
dokumente: vorgang.verknuepfungen
|
||
.map((v) => v.dokument)
|
||
.filter((d) => d.geloeschtAm === null)
|
||
.map((d) => ({
|
||
id: d.id,
|
||
titel: d.titel,
|
||
eingangAm: d.eingangAm,
|
||
versandAm: d.versandAm,
|
||
erstelltAm: d.erstelltAm,
|
||
absender: d.absender,
|
||
empfaenger: d.empfaenger,
|
||
})),
|
||
kontaktprotokolle: vorgang.kontaktprotokolle.map((p) => {
|
||
const klar = oeffnen('Kontaktprotokoll', p)
|
||
return {
|
||
id: p.id,
|
||
datum: p.datum,
|
||
art: p.art,
|
||
gespraechspartner: p.gespraechspartner,
|
||
mitBetreuter: p.mitBetreuter,
|
||
inhalt: klar.inhalt ?? null,
|
||
}
|
||
}),
|
||
fristen: vorgang.fristen.map((f) => ({
|
||
id: f.id,
|
||
titel: f.titel,
|
||
datum: f.datum,
|
||
erledigtAm: f.erledigtAm,
|
||
ausschlussfrist: f.ausschlussfrist,
|
||
rechtsgrundlage: f.rechtsgrundlage,
|
||
})),
|
||
buchungen: vorgang.buchungen.map((b) => {
|
||
const klar = oeffnen('Buchung', b)
|
||
return {
|
||
id: b.id,
|
||
datum: b.datum,
|
||
betragCent: centZuZahl(b.betragCent) ?? 0,
|
||
richtung: b.richtung,
|
||
gegenstelle: b.gegenstelle,
|
||
verwendungszweck: klar.verwendungszweck ?? null,
|
||
}
|
||
}),
|
||
genehmigungen: vorgang.genehmigungen.map((g) => ({
|
||
id: g.id,
|
||
titel: g.titel,
|
||
rechtsgrundlage: g.rechtsgrundlage,
|
||
status: g.status,
|
||
mitteilungAm: g.mitteilungAm,
|
||
antragAm: g.antragAm,
|
||
anhoerungAm: g.anhoerungAm,
|
||
beschlussAm: g.beschlussAm,
|
||
rechtskraftAm: g.rechtskraftAm,
|
||
vollzugAm: g.vollzugAm,
|
||
})),
|
||
statuswechsel,
|
||
})
|
||
|
||
// --- Sachstandsanfrage ----------------------------------------------------
|
||
// Nur, wenn tatsaechlich auf jemanden gewartet wird - sonst waere der
|
||
// Knopf nur Zierde.
|
||
const wartetAufDritte = vorgang.status === 'WARTET_AUF_DRITTE'
|
||
const anfrage = wartetAufDritte
|
||
? sachstandsanfrageErzeugen({
|
||
vorgangTitel: vorgang.titel,
|
||
gegenstelle: vorgang.gegenstelle,
|
||
aktenzeichen: vorgang.aktenzeichen,
|
||
letzterKontaktAm: vorgang.letzterKontaktAm,
|
||
betreute: {
|
||
vorname: aktive.vorname,
|
||
nachname: aktive.nachname,
|
||
geburtsdatum: aktive.geburtsdatum,
|
||
},
|
||
betreuer: {
|
||
name: benutzer.name,
|
||
aktenzeichenGericht: aktive.aktenzeichenGericht,
|
||
betreuungsgericht: aktive.betreuungsgericht,
|
||
},
|
||
ansprechpartner: vorgang.ansprechpartner
|
||
? {
|
||
name: vorgang.ansprechpartner.name,
|
||
anrede: vorgang.ansprechpartner.anrede,
|
||
}
|
||
: null,
|
||
})
|
||
: null
|
||
|
||
// Dokumente der betreuten Person - Auswahl fuer das Beschlussdokument.
|
||
const dokumente = vorgang.genehmigungen.length
|
||
? await db.dokument.findMany({
|
||
where: { betreuteId: aktive.id, ...nichtGeloescht },
|
||
select: { id: true, titel: true },
|
||
orderBy: { erstelltAm: 'desc' },
|
||
take: 100,
|
||
})
|
||
: []
|
||
|
||
const aufgaben: AufgabenZeile[] = vorgang.aufgaben.map((a) => ({
|
||
id: a.id,
|
||
titel: a.titel,
|
||
beschreibung: a.beschreibung,
|
||
erledigt: a.erledigt,
|
||
faelligAm: a.faelligAm,
|
||
}))
|
||
|
||
const genehmigungen: GenehmigungsAnzeige[] = vorgang.genehmigungen.map((g) => ({
|
||
id: g.id,
|
||
titel: g.titel,
|
||
rechtsgrundlage: g.rechtsgrundlage,
|
||
status: g.status,
|
||
mitteilungAm: g.mitteilungAm,
|
||
antragAm: g.antragAm,
|
||
anhoerungAm: g.anhoerungAm,
|
||
beschlussAm: g.beschlussAm,
|
||
rechtskraftAm: g.rechtskraftAm,
|
||
vollzugAm: g.vollzugAm,
|
||
beschlussDokumentId: g.beschlussDokumentId,
|
||
beschlussDokumentTitel: g.beschlussDokument?.titel ?? null,
|
||
dokumente,
|
||
}))
|
||
|
||
return (
|
||
<div className="space-y-8">
|
||
<SeitenKopf
|
||
titel={vorgang.titel}
|
||
beschreibung={offen.beschreibung ?? undefined}
|
||
pfad={[
|
||
{ titel: 'Start', href: '/heute' },
|
||
{ titel: 'Vorgänge', href: '/vorgaenge' },
|
||
{ titel: vorgang.titel },
|
||
]}
|
||
aktionen={
|
||
<>
|
||
<Button variant="ghost" asChild>
|
||
<Link href="/vorgaenge">
|
||
<ArrowLeft aria-hidden="true" />
|
||
Zurück
|
||
</Link>
|
||
</Button>
|
||
{anfrage && (
|
||
<SachstandsanfrageKnopf
|
||
vorgangId={vorgang.id}
|
||
betreff={anfrage.betreff}
|
||
text={anfrage.text}
|
||
darfSchreiben={schreiben}
|
||
/>
|
||
)}
|
||
{schreiben && (
|
||
<Button asChild variant="outline">
|
||
<Link href={`/schreiben/neu?art=SACHSTAND&vorgangId=${vorgang.id}`}>
|
||
<FileDown aria-hidden="true" />
|
||
Als Word-Datei und in die Akte
|
||
</Link>
|
||
</Button>
|
||
)}
|
||
{schreiben && <VorgangLoeschen vorgangId={vorgang.id} titel={vorgang.titel} />}
|
||
</>
|
||
}
|
||
/>
|
||
|
||
{/* --- Kopf der Akte ---------------------------------------------- */}
|
||
<Card>
|
||
<CardContent className="grid gap-5 p-5 sm:grid-cols-2 lg:grid-cols-3">
|
||
<div className="space-y-1">
|
||
<p className="text-sm text-muted-foreground">Status</p>
|
||
<p className="font-medium">{VORGANG_STATUS_BESCHRIFTUNG[vorgang.status]}</p>
|
||
{schreiben && (
|
||
<StatusWechsler vorgangId={vorgang.id} status={vorgang.status} klein />
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<p className="text-sm text-muted-foreground">Gegenstelle</p>
|
||
<p className="font-medium">{vorgang.gegenstelle ?? '–'}</p>
|
||
{vorgang.letzterKontaktAm && (
|
||
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||
<Clock className="size-3.5" aria-hidden="true" />
|
||
Letzter Kontakt: {formatDatum(vorgang.letzterKontaktAm)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<p className="text-sm text-muted-foreground">Aktenzeichen</p>
|
||
<p className="font-medium">{vorgang.aktenzeichen ?? '–'}</p>
|
||
</div>
|
||
|
||
{/* Wen frage ich, bevor ich anrufe? Durchwahl und Adresse
|
||
stehen dabei - sonst schlaegt man sie doch wieder nach. */}
|
||
<div className="space-y-1">
|
||
<p className="text-sm text-muted-foreground">Ansprechpartner</p>
|
||
{vorgang.ansprechpartner ? (
|
||
<>
|
||
<p className="font-medium">
|
||
<Link
|
||
href={`/kontakte/${vorgang.ansprechpartner.id}`}
|
||
className="hover:underline"
|
||
>
|
||
{vorgang.ansprechpartner.name}
|
||
</Link>
|
||
</p>
|
||
{vorgang.ansprechpartner.organisation && (
|
||
<p className="text-sm text-muted-foreground">
|
||
{vorgang.ansprechpartner.organisation}
|
||
</p>
|
||
)}
|
||
{(vorgang.ansprechpartner.telefon || vorgang.ansprechpartner.email) && (
|
||
<p className="text-sm text-muted-foreground">
|
||
{[vorgang.ansprechpartner.telefon, vorgang.ansprechpartner.email]
|
||
.filter(Boolean)
|
||
.join(' · ')}
|
||
</p>
|
||
)}
|
||
</>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground">
|
||
Keiner benannt — Schreiben gehen an „Sehr geehrte Damen und Herren“.
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1 sm:col-span-2">
|
||
<p className="text-sm text-muted-foreground">Nächster Schritt</p>
|
||
<p className="font-medium">
|
||
{vorgang.naechsterSchritt ?? 'Kein offener Handgriff notiert.'}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<p className="text-sm text-muted-foreground">Einordnung</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge variant="outline">{KATEGORIE_BESCHRIFTUNG[vorgang.kategorie]}</Badge>
|
||
<Badge variant="secondary">
|
||
{PRIORITAET_BESCHRIFTUNG[vorgang.prioritaet] ?? 'Normal'}
|
||
</Badge>
|
||
<Faelligkeit datum={vorgang.faelligAm} />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* --- Aufgaben ----------------------------------------------------- */}
|
||
<section aria-labelledby="aufgaben" className="space-y-4">
|
||
<h2 id="aufgaben" className="text-xl font-semibold">
|
||
Aufgaben
|
||
</h2>
|
||
<AufgabenListe
|
||
vorgangId={vorgang.id}
|
||
aufgaben={aufgaben}
|
||
darfSchreiben={schreiben}
|
||
/>
|
||
</section>
|
||
|
||
{/* --- Genehmigungen ------------------------------------------------ */}
|
||
{(vorgang.genehmigungspflichtig || genehmigungen.length > 0) && (
|
||
<section aria-labelledby="genehmigungen" className="space-y-6">
|
||
<h2 id="genehmigungen" className="text-xl font-semibold">
|
||
Genehmigung des Betreuungsgerichts
|
||
</h2>
|
||
|
||
{genehmigungen.map((genehmigung) => (
|
||
<GenehmigungsKette
|
||
key={genehmigung.id}
|
||
genehmigung={genehmigung}
|
||
darfSchreiben={schreiben}
|
||
/>
|
||
))}
|
||
|
||
{schreiben && genehmigungen.length === 0 && (
|
||
<GenehmigungAnlegen vorgangId={vorgang.id} />
|
||
)}
|
||
</section>
|
||
)}
|
||
|
||
{/* --- Zeitstrahl --------------------------------------------------- */}
|
||
<section aria-labelledby="zeitstrahl" className="space-y-4">
|
||
<h2 id="zeitstrahl" className="text-xl font-semibold">
|
||
Zeitstrahl
|
||
</h2>
|
||
<p className="text-sm text-muted-foreground">
|
||
Dokumente, Gespräche, Fristen, Zahlungen und Statuswechsel in einer Spalte - neueste
|
||
zuerst.
|
||
</p>
|
||
<ZeitstrahlAnzeige eintraege={zeitstrahl} />
|
||
</section>
|
||
|
||
{/* --- Bearbeiten --------------------------------------------------- */}
|
||
{schreiben && (
|
||
<section aria-labelledby="bearbeiten" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle id="bearbeiten">Angaben bearbeiten</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<VorgangsFormular
|
||
modus="aendern"
|
||
betreuteId={aktive.id}
|
||
vorgangId={vorgang.id}
|
||
abbrechenHref={`/vorgaenge/${vorgang.id}`}
|
||
kontakte={kontakte}
|
||
werte={{
|
||
titel: vorgang.titel,
|
||
kategorie: vorgang.kategorie,
|
||
status: vorgang.status,
|
||
prioritaet: vorgang.prioritaet,
|
||
beschreibung: offen.beschreibung ?? null,
|
||
gegenstelle: vorgang.gegenstelle,
|
||
aktenzeichen: vorgang.aktenzeichen,
|
||
ansprechpartnerId: vorgang.ansprechpartnerId,
|
||
verantwortlich: vorgang.verantwortlich,
|
||
naechsterSchritt: vorgang.naechsterSchritt,
|
||
faelligAm: vorgang.faelligAm,
|
||
letzterKontaktAm: vorgang.letzterKontaktAm,
|
||
genehmigungspflichtig: vorgang.genehmigungspflichtig,
|
||
}}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
</section>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|