Zeilenenden der 24 verbliebenen Dateien normalisieren
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
This commit is contained in:
+163
-163
@@ -1,163 +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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { AktionMitRueckfrage } from '@/components/sozialhilfe/bausteine'
|
||||
import { BescheidFormular } from '@/components/sozialhilfe/bescheid-formular'
|
||||
import { BescheidPruefung } from '@/components/sozialhilfe/bescheid-pruefung'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import { BESCHEID_ART_BESCHRIFTUNG } from '@/lib/sozialhilfe/bescheide'
|
||||
import { betragFormatieren } from '@/lib/finanzen/betrag'
|
||||
import {
|
||||
fristAmpel,
|
||||
rechtsbehelfsfristBerechnen,
|
||||
zugangsvermutung,
|
||||
} from '@/lib/sozialhilfe/rechtsbehelf'
|
||||
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
|
||||
import { bescheidLoeschen } from '@/server/actions/sozialhilfe'
|
||||
import { formatDatum } from '@/lib/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Bescheid-Prüfung.
|
||||
*
|
||||
* Der Rechenweg der Widerspruchsfrist steht ausgeschrieben da - die
|
||||
* Anwendung kann sich irren, und wer eine Frist verpasst, soll sehen
|
||||
* können, wo die Rechnung abgebogen ist.
|
||||
*/
|
||||
export default async function BescheidSeite({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ antragId: string; bescheidId: string }>
|
||||
}) {
|
||||
const { antragId, bescheidId } = await params
|
||||
const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
|
||||
|
||||
const bescheid = await db.bescheid.findFirst({
|
||||
where: { id: bescheidId, antragId, antrag: { betreuteId }, ...nichtGeloescht },
|
||||
include: { antrag: { select: { id: true, traeger: true, aktenzeichen: true } } },
|
||||
})
|
||||
if (!bescheid) notFound()
|
||||
|
||||
const offen = oeffnen('Bescheid', bescheid)
|
||||
const heute = heuteAlsTag()
|
||||
|
||||
const ergebnis = rechtsbehelfsfristBerechnen({
|
||||
zugegangenAm: bescheid.zugegangenAm,
|
||||
bescheidVom: bescheid.bescheidVom,
|
||||
fristMonate: bescheid.rechtsbehelfsfristMonate,
|
||||
heute,
|
||||
})
|
||||
|
||||
const vorschlag = zugangsvermutung(bescheid.bescheidVom)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SeitenKopf
|
||||
titel={`${BESCHEID_ART_BESCHRIFTUNG[bescheid.art]} vom ${formatDatum(bescheid.bescheidVom)}`}
|
||||
beschreibung={
|
||||
<>
|
||||
{bescheid.antrag.traeger}
|
||||
{bescheid.antrag.aktenzeichen ? ` · Az. ${bescheid.antrag.aktenzeichen}` : ''} · Zugang{' '}
|
||||
{formatDatum(bescheid.zugegangenAm)}
|
||||
</>
|
||||
}
|
||||
pfad={[
|
||||
{ titel: 'Start', href: '/heute' },
|
||||
{ titel: 'Sozialhilfe', href: '/sozialhilfe' },
|
||||
{ titel: 'Antragsakte', href: `/sozialhilfe/${antragId}` },
|
||||
{ titel: 'Bescheid' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<BescheidPruefung
|
||||
bescheidId={bescheid.id}
|
||||
pruefStatus={bescheid.pruefStatus}
|
||||
widerspruchEingelegtAm={
|
||||
bescheid.widerspruchEingelegtAm ? alsIsoTag(bescheid.widerspruchEingelegtAm) : ''
|
||||
}
|
||||
darfSchreiben={darfSchreiben}
|
||||
rechenweg={{
|
||||
berechenbar: ergebnis.berechenbar,
|
||||
widerspruchMoeglichBis: ergebnis.widerspruchMoeglichBis
|
||||
? alsIsoTag(ergebnis.widerspruchMoeglichBis)
|
||||
: null,
|
||||
rechnerischesEnde: ergebnis.rechnerischesEnde
|
||||
? alsIsoTag(ergebnis.rechnerischesEnde)
|
||||
: null,
|
||||
verschoben: ergebnis.verschoben,
|
||||
tageVerbleibend: ergebnis.tageVerbleibend,
|
||||
abgelaufen: ergebnis.abgelaufen,
|
||||
ampel: fristAmpel(ergebnis),
|
||||
schritte: ergebnis.schritte,
|
||||
hinweise: ergebnis.hinweise,
|
||||
}}
|
||||
/>
|
||||
|
||||
{offen.notizen && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notizen</CardTitle>
|
||||
<CardDescription>Verschlüsselt gespeichert.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-line text-sm">{offen.notizen}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{darfSchreiben && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Bescheid bearbeiten</h2>
|
||||
<BescheidFormular
|
||||
antragId={antragId}
|
||||
zugangsvorschlag={{ datum: alsIsoTag(vorschlag.datum), hinweis: vorschlag.hinweis }}
|
||||
werte={{
|
||||
id: bescheid.id,
|
||||
art: bescheid.art,
|
||||
bescheidVom: alsIsoTag(bescheid.bescheidVom),
|
||||
zugegangenAm: bescheid.zugegangenAm ? alsIsoTag(bescheid.zugegangenAm) : '',
|
||||
zeitraumVon: bescheid.zeitraumVon ? alsIsoTag(bescheid.zeitraumVon) : '',
|
||||
zeitraumBis: bescheid.zeitraumBis ? alsIsoTag(bescheid.zeitraumBis) : '',
|
||||
betrag: bescheid.betragCent === null ? '' : betragFormatieren(bescheid.betragCent),
|
||||
rechtsbehelfsfristMonate: bescheid.rechtsbehelfsfristMonate,
|
||||
widerspruchEingelegtAm: bescheid.widerspruchEingelegtAm
|
||||
? alsIsoTag(bescheid.widerspruchEingelegtAm)
|
||||
: '',
|
||||
notizen: offen.notizen ?? '',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="border-t pt-6">
|
||||
<AktionMitRueckfrage
|
||||
aktion={bescheidLoeschen.bind(null, bescheid.id)}
|
||||
frage="Diesen Bescheid in den Papierkorb legen? Er bleibt dort 30 Tage."
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
<Trash2 aria-hidden="true" />
|
||||
Bescheid löschen
|
||||
</AktionMitRueckfrage>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { AktionMitRueckfrage } from '@/components/sozialhilfe/bausteine'
|
||||
import { BescheidFormular } from '@/components/sozialhilfe/bescheid-formular'
|
||||
import { BescheidPruefung } from '@/components/sozialhilfe/bescheid-pruefung'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import { BESCHEID_ART_BESCHRIFTUNG } from '@/lib/sozialhilfe/bescheide'
|
||||
import { betragFormatieren } from '@/lib/finanzen/betrag'
|
||||
import {
|
||||
fristAmpel,
|
||||
rechtsbehelfsfristBerechnen,
|
||||
zugangsvermutung,
|
||||
} from '@/lib/sozialhilfe/rechtsbehelf'
|
||||
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
|
||||
import { bescheidLoeschen } from '@/server/actions/sozialhilfe'
|
||||
import { formatDatum } from '@/lib/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Bescheid-Prüfung.
|
||||
*
|
||||
* Der Rechenweg der Widerspruchsfrist steht ausgeschrieben da - die
|
||||
* Anwendung kann sich irren, und wer eine Frist verpasst, soll sehen
|
||||
* können, wo die Rechnung abgebogen ist.
|
||||
*/
|
||||
export default async function BescheidSeite({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ antragId: string; bescheidId: string }>
|
||||
}) {
|
||||
const { antragId, bescheidId } = await params
|
||||
const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
|
||||
|
||||
const bescheid = await db.bescheid.findFirst({
|
||||
where: { id: bescheidId, antragId, antrag: { betreuteId }, ...nichtGeloescht },
|
||||
include: { antrag: { select: { id: true, traeger: true, aktenzeichen: true } } },
|
||||
})
|
||||
if (!bescheid) notFound()
|
||||
|
||||
const offen = oeffnen('Bescheid', bescheid)
|
||||
const heute = heuteAlsTag()
|
||||
|
||||
const ergebnis = rechtsbehelfsfristBerechnen({
|
||||
zugegangenAm: bescheid.zugegangenAm,
|
||||
bescheidVom: bescheid.bescheidVom,
|
||||
fristMonate: bescheid.rechtsbehelfsfristMonate,
|
||||
heute,
|
||||
})
|
||||
|
||||
const vorschlag = zugangsvermutung(bescheid.bescheidVom)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SeitenKopf
|
||||
titel={`${BESCHEID_ART_BESCHRIFTUNG[bescheid.art]} vom ${formatDatum(bescheid.bescheidVom)}`}
|
||||
beschreibung={
|
||||
<>
|
||||
{bescheid.antrag.traeger}
|
||||
{bescheid.antrag.aktenzeichen ? ` · Az. ${bescheid.antrag.aktenzeichen}` : ''} · Zugang{' '}
|
||||
{formatDatum(bescheid.zugegangenAm)}
|
||||
</>
|
||||
}
|
||||
pfad={[
|
||||
{ titel: 'Start', href: '/heute' },
|
||||
{ titel: 'Sozialhilfe', href: '/sozialhilfe' },
|
||||
{ titel: 'Antragsakte', href: `/sozialhilfe/${antragId}` },
|
||||
{ titel: 'Bescheid' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<BescheidPruefung
|
||||
bescheidId={bescheid.id}
|
||||
pruefStatus={bescheid.pruefStatus}
|
||||
widerspruchEingelegtAm={
|
||||
bescheid.widerspruchEingelegtAm ? alsIsoTag(bescheid.widerspruchEingelegtAm) : ''
|
||||
}
|
||||
darfSchreiben={darfSchreiben}
|
||||
rechenweg={{
|
||||
berechenbar: ergebnis.berechenbar,
|
||||
widerspruchMoeglichBis: ergebnis.widerspruchMoeglichBis
|
||||
? alsIsoTag(ergebnis.widerspruchMoeglichBis)
|
||||
: null,
|
||||
rechnerischesEnde: ergebnis.rechnerischesEnde
|
||||
? alsIsoTag(ergebnis.rechnerischesEnde)
|
||||
: null,
|
||||
verschoben: ergebnis.verschoben,
|
||||
tageVerbleibend: ergebnis.tageVerbleibend,
|
||||
abgelaufen: ergebnis.abgelaufen,
|
||||
ampel: fristAmpel(ergebnis),
|
||||
schritte: ergebnis.schritte,
|
||||
hinweise: ergebnis.hinweise,
|
||||
}}
|
||||
/>
|
||||
|
||||
{offen.notizen && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notizen</CardTitle>
|
||||
<CardDescription>Verschlüsselt gespeichert.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-line text-sm">{offen.notizen}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{darfSchreiben && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Bescheid bearbeiten</h2>
|
||||
<BescheidFormular
|
||||
antragId={antragId}
|
||||
zugangsvorschlag={{ datum: alsIsoTag(vorschlag.datum), hinweis: vorschlag.hinweis }}
|
||||
werte={{
|
||||
id: bescheid.id,
|
||||
art: bescheid.art,
|
||||
bescheidVom: alsIsoTag(bescheid.bescheidVom),
|
||||
zugegangenAm: bescheid.zugegangenAm ? alsIsoTag(bescheid.zugegangenAm) : '',
|
||||
zeitraumVon: bescheid.zeitraumVon ? alsIsoTag(bescheid.zeitraumVon) : '',
|
||||
zeitraumBis: bescheid.zeitraumBis ? alsIsoTag(bescheid.zeitraumBis) : '',
|
||||
betrag: bescheid.betragCent === null ? '' : betragFormatieren(bescheid.betragCent),
|
||||
rechtsbehelfsfristMonate: bescheid.rechtsbehelfsfristMonate,
|
||||
widerspruchEingelegtAm: bescheid.widerspruchEingelegtAm
|
||||
? alsIsoTag(bescheid.widerspruchEingelegtAm)
|
||||
: '',
|
||||
notizen: offen.notizen ?? '',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="border-t pt-6">
|
||||
<AktionMitRueckfrage
|
||||
aktion={bescheidLoeschen.bind(null, bescheid.id)}
|
||||
frage="Diesen Bescheid in den Papierkorb legen? Er bleibt dort 30 Tage."
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
<Trash2 aria-hidden="true" />
|
||||
Bescheid löschen
|
||||
</AktionMitRueckfrage>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,232 +1,232 @@
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Vermoegensliste, type ListenPosten } from '@/components/sozialhilfe/vermoegensliste'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
|
||||
import { alsIsoTag } from '@/lib/fristen/datum'
|
||||
import { VERMOEGENS_ART_BESCHRIFTUNG } from '@/lib/finanzen/kategorien'
|
||||
import { MIT_RUECKKAUFSWERT, VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
|
||||
import { BEWERTUNG_BESCHRIFTUNG } from '@/lib/sozialhilfe/schonvermoegen'
|
||||
import { freibetragVergleichen, uebersichtBilden } from '@/lib/sozialhilfe/vermoegensuebersicht'
|
||||
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
|
||||
import { formatCent } from '@/lib/utils'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Vermögens-Bewertungsansicht.
|
||||
*
|
||||
* Sie zählt zusammen und stellt gegenüber - sie entscheidet nichts. Was
|
||||
* geschützt ist, sagt der Träger; bleibt man uneins, das Sozialgericht.
|
||||
* Deshalb heißt die Spalte "Einschätzung" und nicht "Bewertung".
|
||||
*/
|
||||
export default async function VermoegensbewertungSeite() {
|
||||
const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
|
||||
|
||||
const [posten, vertraege] = await Promise.all([
|
||||
db.vermoegensposten.findMany({
|
||||
where: { betreuteId, ...nichtGeloescht },
|
||||
include: { wertstaende: { orderBy: { stichtag: 'desc' }, take: 1 } },
|
||||
orderBy: { bezeichnung: 'asc' },
|
||||
}),
|
||||
db.vertrag.findMany({
|
||||
where: {
|
||||
betreuteId,
|
||||
// Nur Vertraege, die ueberhaupt einen Wert darstellen koennen. Ein
|
||||
// Stromvertrag hat keinen Rueckkaufswert und wuerde die Liste zumuellen.
|
||||
art: { in: [...MIT_RUECKKAUFSWERT] },
|
||||
...nichtGeloescht,
|
||||
},
|
||||
orderBy: { bezeichnung: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
// Verträge, die schon als Vermögensposten geführt werden, nicht doppelt
|
||||
// zeigen - sonst wäre die Summe zweimal drin.
|
||||
const bereitsAlsPosten = new Set(
|
||||
posten.map((p) => p.vertragId).filter((id): id is string => id !== null),
|
||||
)
|
||||
|
||||
const zeilen: ListenPosten[] = [
|
||||
...posten.map((p) => {
|
||||
const geoeffnet = oeffnen('Vermoegensposten', p)
|
||||
const stand = p.wertstaende[0]
|
||||
return {
|
||||
id: p.id,
|
||||
bezeichnung: p.bezeichnung,
|
||||
herkunft: 'VERMOEGEN' as const,
|
||||
art: p.art,
|
||||
artBeschriftung: VERMOEGENS_ART_BESCHRIFTUNG[p.art],
|
||||
bewertung: p.bewertungSozialhilfe,
|
||||
wertCent: stand ? centZuZahl(stand.wertCent) : null,
|
||||
wertStichtag: stand ? alsIsoTag(stand.stichtag) : null,
|
||||
begruendung: geoeffnet.bewertungBegruendung ?? '',
|
||||
zusatz: stand ? stand.wertquelle : null,
|
||||
}
|
||||
}),
|
||||
...vertraege
|
||||
.filter((v) => !bereitsAlsPosten.has(v.id))
|
||||
.map((v) => {
|
||||
const geoeffnet = oeffnen('Vertrag', v)
|
||||
return {
|
||||
id: v.id,
|
||||
bezeichnung: v.bezeichnung,
|
||||
herkunft: 'VERTRAG' as const,
|
||||
art: v.art,
|
||||
artBeschriftung: VERTRAG_ART_BESCHRIFTUNG[v.art],
|
||||
bewertung: v.bewertungSozialhilfe,
|
||||
// Der Rückkaufswert steht am Vertrag nicht als Betrag; er wird
|
||||
// als Vermögensposten mit Wertstand geführt. Bis dahin: offen.
|
||||
wertCent: null,
|
||||
wertStichtag: null,
|
||||
begruendung: geoeffnet.bewertungBegruendung ?? '',
|
||||
zusatz: v.gesellschaft,
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
const uebersicht = uebersichtBilden(
|
||||
zeilen.map((z) => ({
|
||||
id: z.id,
|
||||
bezeichnung: z.bezeichnung,
|
||||
herkunft: z.herkunft,
|
||||
artBeschriftung: z.artBeschriftung,
|
||||
bewertung: z.bewertung,
|
||||
wertCent: z.wertCent === null ? null : BigInt(z.wertCent),
|
||||
wertStichtag: null,
|
||||
begruendung: z.begruendung,
|
||||
href: '#',
|
||||
})),
|
||||
)
|
||||
|
||||
const vergleich = freibetragVergleichen(uebersicht.jeBewertung.VERWERTBAR.summeCent)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SeitenKopf
|
||||
titel="Vermögen bewerten"
|
||||
beschreibung="Jeder Posten bekommt eine Einschätzung und — das ist der wichtige Teil — eine Begründung. Ohne Begründung hilft die Einordnung im Widerspruchsverfahren nicht weiter."
|
||||
pfad={[
|
||||
{ titel: 'Start', href: '/heute' },
|
||||
{ titel: 'Sozialhilfe', href: '/sozialhilfe' },
|
||||
{ titel: 'Vermögen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.VERWERTBAR}
|
||||
betrag={uebersicht.jeBewertung.VERWERTBAR.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.VERWERTBAR.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.VERWERTBAR.ohneWert}
|
||||
farbe="text-ampel-rot"
|
||||
/>
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.GESCHUETZT}
|
||||
betrag={uebersicht.jeBewertung.GESCHUETZT.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.GESCHUETZT.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.GESCHUETZT.ohneWert}
|
||||
farbe="text-ampel-gruen"
|
||||
/>
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.STRITTIG}
|
||||
betrag={uebersicht.jeBewertung.STRITTIG.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.STRITTIG.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.STRITTIG.ohneWert}
|
||||
farbe="text-ampel-orange"
|
||||
/>
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.UNGEPRUEFT}
|
||||
betrag={uebersicht.jeBewertung.UNGEPRUEFT.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.UNGEPRUEFT.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.UNGEPRUEFT.ohneWert}
|
||||
farbe="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="space-y-2 pt-5">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Vermögensfreibetrag (§ 90 Abs. 2 Nr. 9 SGB XII)
|
||||
</span>
|
||||
<span className="text-lg font-semibold tabular-nums">
|
||||
{formatCent(vergleich.freibetragCent)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<span className="text-sm text-muted-foreground">Über dem Freibetrag</span>
|
||||
<span
|
||||
className={`text-lg font-semibold tabular-nums ${
|
||||
vergleich.ausgeschoepft ? 'text-ampel-rot' : 'text-ampel-gruen'
|
||||
}`}
|
||||
>
|
||||
{formatCent(vergleich.ueberFreibetragCent)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{vergleich.hinweis}</p>
|
||||
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII" kurz />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{uebersicht.ungeprueft.length > 0 && (
|
||||
<Alert variant="warnung">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertTitle>
|
||||
{uebersicht.ungeprueft.length}{' '}
|
||||
{uebersicht.ungeprueft.length === 1 ? 'Posten ist' : 'Posten sind'} noch ungeprüft
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Vor dem Antrag sollte hier nichts mehr stehen — der Träger fragt sonst nach, und das
|
||||
kostet Wochen.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{uebersicht.ohneBegruendung.length > 0 && (
|
||||
<Alert variant="warnung">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertTitle>Bewertung ohne Begründung</AlertTitle>
|
||||
<AlertDescription>
|
||||
{uebersicht.ohneBegruendung.map((p) => p.bezeichnung).join(', ')} — trag nach, worauf
|
||||
sich die Einschätzung stützt. Im Widerspruchsverfahren zählt nur, was begründet ist.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Vermoegensliste posten={zeilen} darfSchreiben={darfSchreiben} />
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII, § 91 SGB XII" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Summenkachel({
|
||||
titel,
|
||||
betrag,
|
||||
anzahl,
|
||||
ohneWert,
|
||||
farbe,
|
||||
}: {
|
||||
titel: string
|
||||
betrag: bigint
|
||||
anzahl: number
|
||||
ohneWert: number
|
||||
farbe: string
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-5">
|
||||
<p className="text-sm text-muted-foreground">{titel}</p>
|
||||
<p className={`text-xl font-semibold tabular-nums ${farbe}`}>{formatCent(betrag)}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{anzahl} {anzahl === 1 ? 'Posten' : 'Posten'}
|
||||
{ohneWert > 0 && `, davon ${ohneWert} ohne Wert`}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Vermoegensliste, type ListenPosten } from '@/components/sozialhilfe/vermoegensliste'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
|
||||
import { alsIsoTag } from '@/lib/fristen/datum'
|
||||
import { VERMOEGENS_ART_BESCHRIFTUNG } from '@/lib/finanzen/kategorien'
|
||||
import { MIT_RUECKKAUFSWERT, VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
|
||||
import { BEWERTUNG_BESCHRIFTUNG } from '@/lib/sozialhilfe/schonvermoegen'
|
||||
import { freibetragVergleichen, uebersichtBilden } from '@/lib/sozialhilfe/vermoegensuebersicht'
|
||||
import { sozialhilfeKontext } from '@/lib/sozialhilfe/zugang'
|
||||
import { formatCent } from '@/lib/utils'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Vermögens-Bewertungsansicht.
|
||||
*
|
||||
* Sie zählt zusammen und stellt gegenüber - sie entscheidet nichts. Was
|
||||
* geschützt ist, sagt der Träger; bleibt man uneins, das Sozialgericht.
|
||||
* Deshalb heißt die Spalte "Einschätzung" und nicht "Bewertung".
|
||||
*/
|
||||
export default async function VermoegensbewertungSeite() {
|
||||
const { betreuteId, darfSchreiben } = await sozialhilfeKontext()
|
||||
|
||||
const [posten, vertraege] = await Promise.all([
|
||||
db.vermoegensposten.findMany({
|
||||
where: { betreuteId, ...nichtGeloescht },
|
||||
include: { wertstaende: { orderBy: { stichtag: 'desc' }, take: 1 } },
|
||||
orderBy: { bezeichnung: 'asc' },
|
||||
}),
|
||||
db.vertrag.findMany({
|
||||
where: {
|
||||
betreuteId,
|
||||
// Nur Vertraege, die ueberhaupt einen Wert darstellen koennen. Ein
|
||||
// Stromvertrag hat keinen Rueckkaufswert und wuerde die Liste zumuellen.
|
||||
art: { in: [...MIT_RUECKKAUFSWERT] },
|
||||
...nichtGeloescht,
|
||||
},
|
||||
orderBy: { bezeichnung: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
// Verträge, die schon als Vermögensposten geführt werden, nicht doppelt
|
||||
// zeigen - sonst wäre die Summe zweimal drin.
|
||||
const bereitsAlsPosten = new Set(
|
||||
posten.map((p) => p.vertragId).filter((id): id is string => id !== null),
|
||||
)
|
||||
|
||||
const zeilen: ListenPosten[] = [
|
||||
...posten.map((p) => {
|
||||
const geoeffnet = oeffnen('Vermoegensposten', p)
|
||||
const stand = p.wertstaende[0]
|
||||
return {
|
||||
id: p.id,
|
||||
bezeichnung: p.bezeichnung,
|
||||
herkunft: 'VERMOEGEN' as const,
|
||||
art: p.art,
|
||||
artBeschriftung: VERMOEGENS_ART_BESCHRIFTUNG[p.art],
|
||||
bewertung: p.bewertungSozialhilfe,
|
||||
wertCent: stand ? centZuZahl(stand.wertCent) : null,
|
||||
wertStichtag: stand ? alsIsoTag(stand.stichtag) : null,
|
||||
begruendung: geoeffnet.bewertungBegruendung ?? '',
|
||||
zusatz: stand ? stand.wertquelle : null,
|
||||
}
|
||||
}),
|
||||
...vertraege
|
||||
.filter((v) => !bereitsAlsPosten.has(v.id))
|
||||
.map((v) => {
|
||||
const geoeffnet = oeffnen('Vertrag', v)
|
||||
return {
|
||||
id: v.id,
|
||||
bezeichnung: v.bezeichnung,
|
||||
herkunft: 'VERTRAG' as const,
|
||||
art: v.art,
|
||||
artBeschriftung: VERTRAG_ART_BESCHRIFTUNG[v.art],
|
||||
bewertung: v.bewertungSozialhilfe,
|
||||
// Der Rückkaufswert steht am Vertrag nicht als Betrag; er wird
|
||||
// als Vermögensposten mit Wertstand geführt. Bis dahin: offen.
|
||||
wertCent: null,
|
||||
wertStichtag: null,
|
||||
begruendung: geoeffnet.bewertungBegruendung ?? '',
|
||||
zusatz: v.gesellschaft,
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
const uebersicht = uebersichtBilden(
|
||||
zeilen.map((z) => ({
|
||||
id: z.id,
|
||||
bezeichnung: z.bezeichnung,
|
||||
herkunft: z.herkunft,
|
||||
artBeschriftung: z.artBeschriftung,
|
||||
bewertung: z.bewertung,
|
||||
wertCent: z.wertCent === null ? null : BigInt(z.wertCent),
|
||||
wertStichtag: null,
|
||||
begruendung: z.begruendung,
|
||||
href: '#',
|
||||
})),
|
||||
)
|
||||
|
||||
const vergleich = freibetragVergleichen(uebersicht.jeBewertung.VERWERTBAR.summeCent)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SeitenKopf
|
||||
titel="Vermögen bewerten"
|
||||
beschreibung="Jeder Posten bekommt eine Einschätzung und — das ist der wichtige Teil — eine Begründung. Ohne Begründung hilft die Einordnung im Widerspruchsverfahren nicht weiter."
|
||||
pfad={[
|
||||
{ titel: 'Start', href: '/heute' },
|
||||
{ titel: 'Sozialhilfe', href: '/sozialhilfe' },
|
||||
{ titel: 'Vermögen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.VERWERTBAR}
|
||||
betrag={uebersicht.jeBewertung.VERWERTBAR.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.VERWERTBAR.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.VERWERTBAR.ohneWert}
|
||||
farbe="text-ampel-rot"
|
||||
/>
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.GESCHUETZT}
|
||||
betrag={uebersicht.jeBewertung.GESCHUETZT.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.GESCHUETZT.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.GESCHUETZT.ohneWert}
|
||||
farbe="text-ampel-gruen"
|
||||
/>
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.STRITTIG}
|
||||
betrag={uebersicht.jeBewertung.STRITTIG.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.STRITTIG.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.STRITTIG.ohneWert}
|
||||
farbe="text-ampel-orange"
|
||||
/>
|
||||
<Summenkachel
|
||||
titel={BEWERTUNG_BESCHRIFTUNG.UNGEPRUEFT}
|
||||
betrag={uebersicht.jeBewertung.UNGEPRUEFT.summeCent}
|
||||
anzahl={uebersicht.jeBewertung.UNGEPRUEFT.anzahl}
|
||||
ohneWert={uebersicht.jeBewertung.UNGEPRUEFT.ohneWert}
|
||||
farbe="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="space-y-2 pt-5">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Vermögensfreibetrag (§ 90 Abs. 2 Nr. 9 SGB XII)
|
||||
</span>
|
||||
<span className="text-lg font-semibold tabular-nums">
|
||||
{formatCent(vergleich.freibetragCent)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<span className="text-sm text-muted-foreground">Über dem Freibetrag</span>
|
||||
<span
|
||||
className={`text-lg font-semibold tabular-nums ${
|
||||
vergleich.ausgeschoepft ? 'text-ampel-rot' : 'text-ampel-gruen'
|
||||
}`}
|
||||
>
|
||||
{formatCent(vergleich.ueberFreibetragCent)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{vergleich.hinweis}</p>
|
||||
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII" kurz />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{uebersicht.ungeprueft.length > 0 && (
|
||||
<Alert variant="warnung">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertTitle>
|
||||
{uebersicht.ungeprueft.length}{' '}
|
||||
{uebersicht.ungeprueft.length === 1 ? 'Posten ist' : 'Posten sind'} noch ungeprüft
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Vor dem Antrag sollte hier nichts mehr stehen — der Träger fragt sonst nach, und das
|
||||
kostet Wochen.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{uebersicht.ohneBegruendung.length > 0 && (
|
||||
<Alert variant="warnung">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertTitle>Bewertung ohne Begründung</AlertTitle>
|
||||
<AlertDescription>
|
||||
{uebersicht.ohneBegruendung.map((p) => p.bezeichnung).join(', ')} — trag nach, worauf
|
||||
sich die Einschätzung stützt. Im Widerspruchsverfahren zählt nur, was begründet ist.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Vermoegensliste posten={zeilen} darfSchreiben={darfSchreiben} />
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§ 90 SGB XII, § 91 SGB XII" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Summenkachel({
|
||||
titel,
|
||||
betrag,
|
||||
anzahl,
|
||||
ohneWert,
|
||||
farbe,
|
||||
}: {
|
||||
titel: string
|
||||
betrag: bigint
|
||||
anzahl: number
|
||||
ohneWert: number
|
||||
farbe: string
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-5">
|
||||
<p className="text-sm text-muted-foreground">{titel}</p>
|
||||
<p className={`text-xl font-semibold tabular-nums ${farbe}`}>{formatCent(betrag)}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{anzahl} {anzahl === 1 ? 'Posten' : 'Posten'}
|
||||
{ohneWert > 0 && `, davon ${ohneWert} ohne Wert`}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { VertragFormular } from '@/components/vertraege/vertrag-formular'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { alsIsoTag } from '@/lib/fristen/datum'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Cent als Eingabetext: 481255 wird zu "4812,55". */
|
||||
function centAlsText(cent: bigint | null): string {
|
||||
if (cent === null) return ''
|
||||
const negativ = cent < 0n
|
||||
const betrag = negativ ? -cent : cent
|
||||
const euro = betrag / 100n
|
||||
const rest = betrag % 100n
|
||||
return `${negativ ? '-' : ''}${euro},${rest.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export default async function VertragBearbeiten({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const benutzer = await erfordereBenutzer(`/vertraege/${id}/bearbeiten`)
|
||||
const aktive = await aktiveBetreute(benutzer)
|
||||
if (!aktive) return null
|
||||
|
||||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
|
||||
if (!darfSchreiben('VERTRAEGE')) redirect(`/vertraege/${id}`)
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||||
})
|
||||
if (!vertrag) notFound()
|
||||
|
||||
const offen = oeffnen('Vertrag', vertrag)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SeitenKopf
|
||||
titel={`${vertrag.bezeichnung} bearbeiten`}
|
||||
beschreibung="Änderungen werden protokolliert. Die Vertragsnummer wird verschlüsselt gespeichert."
|
||||
pfad={[
|
||||
{ titel: 'Übersicht', href: '/heute' },
|
||||
{ titel: 'Verträge', href: '/vertraege' },
|
||||
{ titel: vertrag.bezeichnung, href: `/vertraege/${id}` },
|
||||
{ titel: 'Bearbeiten' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<VertragFormular
|
||||
betreuteId={aktive.id}
|
||||
werte={{
|
||||
id: vertrag.id,
|
||||
bezeichnung: vertrag.bezeichnung,
|
||||
gesellschaft: vertrag.gesellschaft ?? '',
|
||||
art: vertrag.art,
|
||||
status: vertrag.status,
|
||||
vertragsnummer: offen.vertragsnummer ?? '',
|
||||
beginnAm: vertrag.beginnAm ? alsIsoTag(vertrag.beginnAm) : '',
|
||||
endeAm: vertrag.endeAm ? alsIsoTag(vertrag.endeAm) : '',
|
||||
kuendigungsfristMonate: vertrag.kuendigungsfristMonate?.toString() ?? '',
|
||||
kuendigungZum: vertrag.kuendigungZum ? alsIsoTag(vertrag.kuendigungZum) : '',
|
||||
gekuendigtAm: vertrag.gekuendigtAm ? alsIsoTag(vertrag.gekuendigtAm) : '',
|
||||
beitrag: centAlsText(vertrag.beitragCent),
|
||||
rhythmus: vertrag.rhythmus ?? '',
|
||||
bewertungSozialhilfe: vertrag.bewertungSozialhilfe,
|
||||
bewertungBegruendung: offen.bewertungBegruendung ?? '',
|
||||
notizen: offen.notizen ?? '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { VertragFormular } from '@/components/vertraege/vertrag-formular'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { alsIsoTag } from '@/lib/fristen/datum'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Cent als Eingabetext: 481255 wird zu "4812,55". */
|
||||
function centAlsText(cent: bigint | null): string {
|
||||
if (cent === null) return ''
|
||||
const negativ = cent < 0n
|
||||
const betrag = negativ ? -cent : cent
|
||||
const euro = betrag / 100n
|
||||
const rest = betrag % 100n
|
||||
return `${negativ ? '-' : ''}${euro},${rest.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export default async function VertragBearbeiten({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const benutzer = await erfordereBenutzer(`/vertraege/${id}/bearbeiten`)
|
||||
const aktive = await aktiveBetreute(benutzer)
|
||||
if (!aktive) return null
|
||||
|
||||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
|
||||
if (!darfSchreiben('VERTRAEGE')) redirect(`/vertraege/${id}`)
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||||
})
|
||||
if (!vertrag) notFound()
|
||||
|
||||
const offen = oeffnen('Vertrag', vertrag)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SeitenKopf
|
||||
titel={`${vertrag.bezeichnung} bearbeiten`}
|
||||
beschreibung="Änderungen werden protokolliert. Die Vertragsnummer wird verschlüsselt gespeichert."
|
||||
pfad={[
|
||||
{ titel: 'Übersicht', href: '/heute' },
|
||||
{ titel: 'Verträge', href: '/vertraege' },
|
||||
{ titel: vertrag.bezeichnung, href: `/vertraege/${id}` },
|
||||
{ titel: 'Bearbeiten' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<VertragFormular
|
||||
betreuteId={aktive.id}
|
||||
werte={{
|
||||
id: vertrag.id,
|
||||
bezeichnung: vertrag.bezeichnung,
|
||||
gesellschaft: vertrag.gesellschaft ?? '',
|
||||
art: vertrag.art,
|
||||
status: vertrag.status,
|
||||
vertragsnummer: offen.vertragsnummer ?? '',
|
||||
beginnAm: vertrag.beginnAm ? alsIsoTag(vertrag.beginnAm) : '',
|
||||
endeAm: vertrag.endeAm ? alsIsoTag(vertrag.endeAm) : '',
|
||||
kuendigungsfristMonate: vertrag.kuendigungsfristMonate?.toString() ?? '',
|
||||
kuendigungZum: vertrag.kuendigungZum ? alsIsoTag(vertrag.kuendigungZum) : '',
|
||||
gekuendigtAm: vertrag.gekuendigtAm ? alsIsoTag(vertrag.gekuendigtAm) : '',
|
||||
beitrag: centAlsText(vertrag.beitragCent),
|
||||
rhythmus: vertrag.rhythmus ?? '',
|
||||
bewertungSozialhilfe: vertrag.bewertungSozialhilfe,
|
||||
bewertungBegruendung: offen.bewertungBegruendung ?? '',
|
||||
notizen: offen.notizen ?? '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,96 +1,96 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { KuendigungsAssistent } from '@/components/vertraege/kuendigungs-assistent'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
||||
import { kuendigungsassistent } from '@/lib/vertraege/kuendigungsassistent'
|
||||
import { laufzeitAusVertrag } from '@/lib/vertraege/kuendigung'
|
||||
import { VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function VertragKuendigen({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const benutzer = await erfordereBenutzer(`/vertraege/${id}/kuendigen`)
|
||||
const aktive = await aktiveBetreute(benutzer)
|
||||
if (!aktive) return null
|
||||
|
||||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
|
||||
const schreiben = darfSchreiben('VERTRAEGE')
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||||
})
|
||||
if (!vertrag) notFound()
|
||||
|
||||
const offen = oeffnen('Vertrag', vertrag)
|
||||
const heute = heuteAlsTag()
|
||||
|
||||
const paket = kuendigungsassistent({
|
||||
kopf: briefkopfBauen({ betreute: aktive, benutzer }),
|
||||
empfaenger: { gesellschaft: vertrag.gesellschaft, anschrift: null },
|
||||
vertragsbezeichnung: `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}`,
|
||||
vertragsnummer: offen.vertragsnummer,
|
||||
art: vertrag.art,
|
||||
laufzeit: {
|
||||
beginnAm: vertrag.beginnAm,
|
||||
endeAm: vertrag.endeAm,
|
||||
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
|
||||
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
|
||||
},
|
||||
heute,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SeitenKopf
|
||||
titel="Kündigungs-Assistent"
|
||||
beschreibung={`${vertrag.bezeichnung} — Frist prüfen, Genehmigungsfrage stellen, Schreiben erzeugen, Versandnachweis sichern.`}
|
||||
pfad={[
|
||||
{ titel: 'Übersicht', href: '/heute' },
|
||||
{ titel: 'Verträge', href: '/vertraege' },
|
||||
{ titel: vertrag.bezeichnung, href: `/vertraege/${id}` },
|
||||
{ titel: 'Kündigen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<KuendigungsAssistent
|
||||
betreuteId={aktive.id}
|
||||
vertragId={id}
|
||||
darfSchreiben={schreiben}
|
||||
heuteIso={alsIsoTag(heute)}
|
||||
daten={{
|
||||
schreiben: paket.schreiben,
|
||||
warnungen: paket.warnungen,
|
||||
genehmigung: paket.genehmigung,
|
||||
versandnachweis: {
|
||||
moeglichkeiten: [...paket.versandnachweis.moeglichkeiten],
|
||||
hinweis: paket.versandnachweis.hinweis,
|
||||
},
|
||||
plan: {
|
||||
berechenbar: paket.plan.berechenbar,
|
||||
naechsterTermin: paket.plan.naechsterTermin
|
||||
? alsIsoTag(paket.plan.naechsterTermin)
|
||||
: null,
|
||||
zugangSpaetestensAm: paket.plan.zugangSpaetestensAm
|
||||
? alsIsoTag(paket.plan.zugangSpaetestensAm)
|
||||
: null,
|
||||
spaetesterAbsendetag: paket.plan.spaetesterAbsendetag
|
||||
? alsIsoTag(paket.plan.spaetesterAbsendetag)
|
||||
: null,
|
||||
tageBisAbsendetag: paket.plan.tageBisAbsendetag,
|
||||
verpasst: paket.plan.verpasst,
|
||||
hinweise: paket.plan.hinweise,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { notFound } from 'next/navigation'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { KuendigungsAssistent } from '@/components/vertraege/kuendigungs-assistent'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
||||
import { kuendigungsassistent } from '@/lib/vertraege/kuendigungsassistent'
|
||||
import { laufzeitAusVertrag } from '@/lib/vertraege/kuendigung'
|
||||
import { VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function VertragKuendigen({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const benutzer = await erfordereBenutzer(`/vertraege/${id}/kuendigen`)
|
||||
const aktive = await aktiveBetreute(benutzer)
|
||||
if (!aktive) return null
|
||||
|
||||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
|
||||
const schreiben = darfSchreiben('VERTRAEGE')
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||||
})
|
||||
if (!vertrag) notFound()
|
||||
|
||||
const offen = oeffnen('Vertrag', vertrag)
|
||||
const heute = heuteAlsTag()
|
||||
|
||||
const paket = kuendigungsassistent({
|
||||
kopf: briefkopfBauen({ betreute: aktive, benutzer }),
|
||||
empfaenger: { gesellschaft: vertrag.gesellschaft, anschrift: null },
|
||||
vertragsbezeichnung: `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}`,
|
||||
vertragsnummer: offen.vertragsnummer,
|
||||
art: vertrag.art,
|
||||
laufzeit: {
|
||||
beginnAm: vertrag.beginnAm,
|
||||
endeAm: vertrag.endeAm,
|
||||
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
|
||||
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
|
||||
},
|
||||
heute,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SeitenKopf
|
||||
titel="Kündigungs-Assistent"
|
||||
beschreibung={`${vertrag.bezeichnung} — Frist prüfen, Genehmigungsfrage stellen, Schreiben erzeugen, Versandnachweis sichern.`}
|
||||
pfad={[
|
||||
{ titel: 'Übersicht', href: '/heute' },
|
||||
{ titel: 'Verträge', href: '/vertraege' },
|
||||
{ titel: vertrag.bezeichnung, href: `/vertraege/${id}` },
|
||||
{ titel: 'Kündigen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<KuendigungsAssistent
|
||||
betreuteId={aktive.id}
|
||||
vertragId={id}
|
||||
darfSchreiben={schreiben}
|
||||
heuteIso={alsIsoTag(heute)}
|
||||
daten={{
|
||||
schreiben: paket.schreiben,
|
||||
warnungen: paket.warnungen,
|
||||
genehmigung: paket.genehmigung,
|
||||
versandnachweis: {
|
||||
moeglichkeiten: [...paket.versandnachweis.moeglichkeiten],
|
||||
hinweis: paket.versandnachweis.hinweis,
|
||||
},
|
||||
plan: {
|
||||
berechenbar: paket.plan.berechenbar,
|
||||
naechsterTermin: paket.plan.naechsterTermin
|
||||
? alsIsoTag(paket.plan.naechsterTermin)
|
||||
: null,
|
||||
zugangSpaetestensAm: paket.plan.zugangSpaetestensAm
|
||||
? alsIsoTag(paket.plan.zugangSpaetestensAm)
|
||||
: null,
|
||||
spaetesterAbsendetag: paket.plan.spaetesterAbsendetag
|
||||
? alsIsoTag(paket.plan.spaetesterAbsendetag)
|
||||
: null,
|
||||
tageBisAbsendetag: paket.plan.tageBisAbsendetag,
|
||||
verpasst: paket.plan.verpasst,
|
||||
hinweise: paket.plan.hinweise,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,275 +1,275 @@
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { FileX2, Pencil, Shield } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Datenzeile } from '@/components/substanz-formular'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Rueckkaufswert } from '@/components/vertraege/rueckkaufswert'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
||||
import {
|
||||
kuendigungsampel,
|
||||
kuendigungsplan,
|
||||
laufzeitAusVertrag,
|
||||
} from '@/lib/vertraege/kuendigung'
|
||||
import {
|
||||
anfrageStatus,
|
||||
rueckkaufswertAnschreiben,
|
||||
wertHistorie,
|
||||
} from '@/lib/vertraege/rueckkaufswert'
|
||||
import { wertalterBestimmen } from '@/lib/finanzen/wertalter'
|
||||
import {
|
||||
BEWERTUNG_BESCHRIFTUNG,
|
||||
BEWERTUNG_ERKLAERUNG,
|
||||
jahresbeitragCent,
|
||||
kannRueckkaufswertHaben,
|
||||
RHYTHMUS_BESCHRIFTUNG,
|
||||
VERTRAG_ART_BESCHRIFTUNG,
|
||||
VERTRAG_STATUS_BESCHRIFTUNG,
|
||||
} from '@/lib/vertraege/konstanten'
|
||||
import { formatCent, formatDatum } from '@/lib/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function VertragDetail({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const benutzer = await erfordereBenutzer(`/vertraege/${id}`)
|
||||
const aktive = await aktiveBetreute(benutzer)
|
||||
if (!aktive) return null
|
||||
|
||||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
|
||||
const schreiben = darfSchreiben('VERTRAEGE')
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||||
include: {
|
||||
// Ueber die Verknuepfung, nicht ueber die Art - siehe wertHistorie().
|
||||
vermoegensposten: {
|
||||
where: nichtGeloescht,
|
||||
include: { wertstaende: { orderBy: { stichtag: 'desc' } } },
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!vertrag) notFound()
|
||||
|
||||
const offen = oeffnen('Vertrag', vertrag)
|
||||
const heute = heuteAlsTag()
|
||||
|
||||
const plan = kuendigungsplan({
|
||||
beginnAm: vertrag.beginnAm,
|
||||
endeAm: vertrag.endeAm,
|
||||
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
|
||||
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
|
||||
heute,
|
||||
})
|
||||
const ampel = kuendigungsampel(plan)
|
||||
const anfrage = anfrageStatus(vertrag.rueckkaufswertAngefragtAm, heute)
|
||||
const jahresbeitrag = jahresbeitragCent(vertrag.beitragCent, vertrag.rhythmus)
|
||||
|
||||
const kopf = briefkopfBauen({ betreute: aktive, benutzer })
|
||||
const anschreiben = rueckkaufswertAnschreiben({
|
||||
kopf,
|
||||
empfaenger: { gesellschaft: vertrag.gesellschaft, anschrift: null },
|
||||
vertragsbezeichnung: `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}`,
|
||||
vertragsnummer: offen.vertragsnummer,
|
||||
stichtag: heute,
|
||||
heute,
|
||||
})
|
||||
|
||||
const wertstaende = wertHistorie(vertrag.vermoegensposten)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SeitenKopf
|
||||
titel={vertrag.bezeichnung}
|
||||
beschreibung={
|
||||
[vertrag.gesellschaft, VERTRAG_ART_BESCHRIFTUNG[vertrag.art]]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || VERTRAG_ART_BESCHRIFTUNG[vertrag.art]
|
||||
}
|
||||
pfad={[
|
||||
{ titel: 'Übersicht', href: '/heute' },
|
||||
{ titel: 'Verträge', href: '/vertraege' },
|
||||
{ titel: vertrag.bezeichnung },
|
||||
]}
|
||||
aktionen={
|
||||
schreiben && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/vertraege/${id}/kuendigen`}>
|
||||
<FileX2 aria-hidden="true" />
|
||||
Kündigen
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href={`/vertraege/${id}/bearbeiten`}>
|
||||
<Pencil aria-hidden="true" />
|
||||
Bearbeiten
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="size-5" aria-hidden="true" />
|
||||
Vertragsdaten
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Die Vertragsnummer wird verschlüsselt gespeichert und hier nur zum Lesen angezeigt.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-x-8 sm:grid-cols-2">
|
||||
<dl className="divide-y">
|
||||
<Datenzeile beschriftung="Vertragsnummer">
|
||||
{offen.vertragsnummer ?? '–'}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Status">
|
||||
{VERTRAG_STATUS_BESCHRIFTUNG[vertrag.status]}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Laufzeitbeginn">
|
||||
{formatDatum(vertrag.beginnAm)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Vereinbartes Ende">
|
||||
{formatDatum(vertrag.endeAm)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Kündigungsfrist">
|
||||
{vertrag.kuendigungsfristMonate !== null
|
||||
? `${vertrag.kuendigungsfristMonate} Monate`
|
||||
: 'nicht erfasst'}
|
||||
</Datenzeile>
|
||||
</dl>
|
||||
<dl className="divide-y">
|
||||
<Datenzeile beschriftung="Beitrag">
|
||||
{formatCent(vertrag.beitragCent)}
|
||||
{vertrag.rhythmus ? ` (${RHYTHMUS_BESCHRIFTUNG[vertrag.rhythmus]})` : ''}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Jahresbeitrag">{formatCent(jahresbeitrag)}</Datenzeile>
|
||||
<Datenzeile beschriftung="Gekündigt am">
|
||||
{formatDatum(vertrag.gekuendigtAm)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Gekündigt zum">
|
||||
{formatDatum(vertrag.kuendigungZum)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Rückkaufswert angefragt">
|
||||
{formatDatum(vertrag.rueckkaufswertAngefragtAm)}
|
||||
</Datenzeile>
|
||||
</dl>
|
||||
|
||||
{offen.notizen && (
|
||||
<div className="mt-4 rounded-md border bg-muted/40 p-3 sm:col-span-2">
|
||||
<p className="whitespace-pre-line text-sm">{offen.notizen}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kündigungsfrist</CardTitle>
|
||||
<CardDescription>
|
||||
Eine Kündigung wird mit dem Zugang wirksam, nicht mit dem Absenden. Deshalb ist der
|
||||
Absendetag früher als der Fristablauf.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{plan.berechenbar ? (
|
||||
<>
|
||||
<dl className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-md border p-3">
|
||||
<dt className="text-sm text-muted-foreground">Nächster Kündigungstermin</dt>
|
||||
<dd className="text-lg font-semibold">{formatDatum(plan.naechsterTermin)}</dd>
|
||||
</div>
|
||||
<div className="rounded-md border p-3">
|
||||
<dt className="text-sm text-muted-foreground">Zugang spätestens</dt>
|
||||
<dd className="text-lg font-semibold">
|
||||
{formatDatum(plan.zugangSpaetestensAm)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="rounded-md border p-3">
|
||||
<dt className="text-sm text-muted-foreground">Brief spätestens absenden</dt>
|
||||
<dd className="text-lg font-semibold">
|
||||
{formatDatum(plan.spaetesterAbsendetag)}
|
||||
</dd>
|
||||
{ampel && (
|
||||
<dd className="mt-1">
|
||||
<Badge variant={ampel.variante}>{ampel.text}</Badge>
|
||||
</dd>
|
||||
)}
|
||||
</div>
|
||||
</dl>
|
||||
<ul className="space-y-1.5 text-sm text-muted-foreground">
|
||||
{plan.hinweise.map((hinweis) => (
|
||||
<li key={hinweis}>{hinweis}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{plan.hinweise[0] ??
|
||||
'Ohne Laufzeitbeginn lässt sich kein Kündigungstermin berechnen.'}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{kannRueckkaufswertHaben(vertrag.art) && (
|
||||
<Rueckkaufswert
|
||||
betreuteId={aktive.id}
|
||||
vertragId={id}
|
||||
darfSchreiben={schreiben}
|
||||
angefragtAm={vertrag.rueckkaufswertAngefragtAm?.toISOString() ?? null}
|
||||
erinnerungFaellig={anfrage.erinnerungFaellig}
|
||||
tageSeitAnfrage={anfrage.tageSeitAnfrage}
|
||||
anschreiben={anschreiben}
|
||||
stichtagVorschlag={alsIsoTag(heute)}
|
||||
alterHinweis={
|
||||
wertalterBestimmen(wertstaende[0]?.stichtag ?? null, heute)?.hinweis ?? null
|
||||
}
|
||||
werte={wertstaende.map((w) => ({
|
||||
id: w.id,
|
||||
stichtag: w.stichtag.toISOString(),
|
||||
wertCent: centZuZahl(w.wertCent) ?? 0,
|
||||
wertquelle: w.wertquelle,
|
||||
bemerkung: w.bemerkung,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bewertung fürs Sozialamt</CardTitle>
|
||||
<CardDescription>
|
||||
Meine Einordnung und meine Begründung — keine Rechtsauskunft.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Badge variant="outline">{BEWERTUNG_BESCHRIFTUNG[vertrag.bewertungSozialhilfe]}</Badge>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{BEWERTUNG_ERKLAERUNG[vertrag.bewertungSozialhilfe]}
|
||||
</p>
|
||||
{offen.bewertungBegruendung ? (
|
||||
<p className="whitespace-pre-line rounded-md border bg-muted/40 p-3 text-sm">
|
||||
{offen.bewertungBegruendung}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Noch keine Begründung hinterlegt. Sie gehört dazu, bevor der Vertrag in einer
|
||||
Aufstellung an den Bezirk auftaucht.
|
||||
</p>
|
||||
)}
|
||||
<Rechtshinweis rechtsgrundlage="§ 90 Abs. 3 SGB XII" kurz />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { FileX2, Pencil, Shield } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Datenzeile } from '@/components/substanz-formular'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Rueckkaufswert } from '@/components/vertraege/rueckkaufswert'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { centZuZahl, db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { alsIsoTag, heuteAlsTag } from '@/lib/fristen/datum'
|
||||
import { briefkopfBauen } from '@/lib/vertraege/briefkopf'
|
||||
import {
|
||||
kuendigungsampel,
|
||||
kuendigungsplan,
|
||||
laufzeitAusVertrag,
|
||||
} from '@/lib/vertraege/kuendigung'
|
||||
import {
|
||||
anfrageStatus,
|
||||
rueckkaufswertAnschreiben,
|
||||
wertHistorie,
|
||||
} from '@/lib/vertraege/rueckkaufswert'
|
||||
import { wertalterBestimmen } from '@/lib/finanzen/wertalter'
|
||||
import {
|
||||
BEWERTUNG_BESCHRIFTUNG,
|
||||
BEWERTUNG_ERKLAERUNG,
|
||||
jahresbeitragCent,
|
||||
kannRueckkaufswertHaben,
|
||||
RHYTHMUS_BESCHRIFTUNG,
|
||||
VERTRAG_ART_BESCHRIFTUNG,
|
||||
VERTRAG_STATUS_BESCHRIFTUNG,
|
||||
} from '@/lib/vertraege/konstanten'
|
||||
import { formatCent, formatDatum } from '@/lib/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function VertragDetail({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const benutzer = await erfordereBenutzer(`/vertraege/${id}`)
|
||||
const aktive = await aktiveBetreute(benutzer)
|
||||
if (!aktive) return null
|
||||
|
||||
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
|
||||
const schreiben = darfSchreiben('VERTRAEGE')
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id, betreuteId: aktive.id, ...nichtGeloescht },
|
||||
include: {
|
||||
// Ueber die Verknuepfung, nicht ueber die Art - siehe wertHistorie().
|
||||
vermoegensposten: {
|
||||
where: nichtGeloescht,
|
||||
include: { wertstaende: { orderBy: { stichtag: 'desc' } } },
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!vertrag) notFound()
|
||||
|
||||
const offen = oeffnen('Vertrag', vertrag)
|
||||
const heute = heuteAlsTag()
|
||||
|
||||
const plan = kuendigungsplan({
|
||||
beginnAm: vertrag.beginnAm,
|
||||
endeAm: vertrag.endeAm,
|
||||
laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm),
|
||||
kuendigungsfristMonate: vertrag.kuendigungsfristMonate,
|
||||
heute,
|
||||
})
|
||||
const ampel = kuendigungsampel(plan)
|
||||
const anfrage = anfrageStatus(vertrag.rueckkaufswertAngefragtAm, heute)
|
||||
const jahresbeitrag = jahresbeitragCent(vertrag.beitragCent, vertrag.rhythmus)
|
||||
|
||||
const kopf = briefkopfBauen({ betreute: aktive, benutzer })
|
||||
const anschreiben = rueckkaufswertAnschreiben({
|
||||
kopf,
|
||||
empfaenger: { gesellschaft: vertrag.gesellschaft, anschrift: null },
|
||||
vertragsbezeichnung: `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}`,
|
||||
vertragsnummer: offen.vertragsnummer,
|
||||
stichtag: heute,
|
||||
heute,
|
||||
})
|
||||
|
||||
const wertstaende = wertHistorie(vertrag.vermoegensposten)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SeitenKopf
|
||||
titel={vertrag.bezeichnung}
|
||||
beschreibung={
|
||||
[vertrag.gesellschaft, VERTRAG_ART_BESCHRIFTUNG[vertrag.art]]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || VERTRAG_ART_BESCHRIFTUNG[vertrag.art]
|
||||
}
|
||||
pfad={[
|
||||
{ titel: 'Übersicht', href: '/heute' },
|
||||
{ titel: 'Verträge', href: '/vertraege' },
|
||||
{ titel: vertrag.bezeichnung },
|
||||
]}
|
||||
aktionen={
|
||||
schreiben && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/vertraege/${id}/kuendigen`}>
|
||||
<FileX2 aria-hidden="true" />
|
||||
Kündigen
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href={`/vertraege/${id}/bearbeiten`}>
|
||||
<Pencil aria-hidden="true" />
|
||||
Bearbeiten
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="size-5" aria-hidden="true" />
|
||||
Vertragsdaten
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Die Vertragsnummer wird verschlüsselt gespeichert und hier nur zum Lesen angezeigt.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-x-8 sm:grid-cols-2">
|
||||
<dl className="divide-y">
|
||||
<Datenzeile beschriftung="Vertragsnummer">
|
||||
{offen.vertragsnummer ?? '–'}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Status">
|
||||
{VERTRAG_STATUS_BESCHRIFTUNG[vertrag.status]}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Laufzeitbeginn">
|
||||
{formatDatum(vertrag.beginnAm)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Vereinbartes Ende">
|
||||
{formatDatum(vertrag.endeAm)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Kündigungsfrist">
|
||||
{vertrag.kuendigungsfristMonate !== null
|
||||
? `${vertrag.kuendigungsfristMonate} Monate`
|
||||
: 'nicht erfasst'}
|
||||
</Datenzeile>
|
||||
</dl>
|
||||
<dl className="divide-y">
|
||||
<Datenzeile beschriftung="Beitrag">
|
||||
{formatCent(vertrag.beitragCent)}
|
||||
{vertrag.rhythmus ? ` (${RHYTHMUS_BESCHRIFTUNG[vertrag.rhythmus]})` : ''}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Jahresbeitrag">{formatCent(jahresbeitrag)}</Datenzeile>
|
||||
<Datenzeile beschriftung="Gekündigt am">
|
||||
{formatDatum(vertrag.gekuendigtAm)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Gekündigt zum">
|
||||
{formatDatum(vertrag.kuendigungZum)}
|
||||
</Datenzeile>
|
||||
<Datenzeile beschriftung="Rückkaufswert angefragt">
|
||||
{formatDatum(vertrag.rueckkaufswertAngefragtAm)}
|
||||
</Datenzeile>
|
||||
</dl>
|
||||
|
||||
{offen.notizen && (
|
||||
<div className="mt-4 rounded-md border bg-muted/40 p-3 sm:col-span-2">
|
||||
<p className="whitespace-pre-line text-sm">{offen.notizen}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kündigungsfrist</CardTitle>
|
||||
<CardDescription>
|
||||
Eine Kündigung wird mit dem Zugang wirksam, nicht mit dem Absenden. Deshalb ist der
|
||||
Absendetag früher als der Fristablauf.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{plan.berechenbar ? (
|
||||
<>
|
||||
<dl className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-md border p-3">
|
||||
<dt className="text-sm text-muted-foreground">Nächster Kündigungstermin</dt>
|
||||
<dd className="text-lg font-semibold">{formatDatum(plan.naechsterTermin)}</dd>
|
||||
</div>
|
||||
<div className="rounded-md border p-3">
|
||||
<dt className="text-sm text-muted-foreground">Zugang spätestens</dt>
|
||||
<dd className="text-lg font-semibold">
|
||||
{formatDatum(plan.zugangSpaetestensAm)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="rounded-md border p-3">
|
||||
<dt className="text-sm text-muted-foreground">Brief spätestens absenden</dt>
|
||||
<dd className="text-lg font-semibold">
|
||||
{formatDatum(plan.spaetesterAbsendetag)}
|
||||
</dd>
|
||||
{ampel && (
|
||||
<dd className="mt-1">
|
||||
<Badge variant={ampel.variante}>{ampel.text}</Badge>
|
||||
</dd>
|
||||
)}
|
||||
</div>
|
||||
</dl>
|
||||
<ul className="space-y-1.5 text-sm text-muted-foreground">
|
||||
{plan.hinweise.map((hinweis) => (
|
||||
<li key={hinweis}>{hinweis}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{plan.hinweise[0] ??
|
||||
'Ohne Laufzeitbeginn lässt sich kein Kündigungstermin berechnen.'}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{kannRueckkaufswertHaben(vertrag.art) && (
|
||||
<Rueckkaufswert
|
||||
betreuteId={aktive.id}
|
||||
vertragId={id}
|
||||
darfSchreiben={schreiben}
|
||||
angefragtAm={vertrag.rueckkaufswertAngefragtAm?.toISOString() ?? null}
|
||||
erinnerungFaellig={anfrage.erinnerungFaellig}
|
||||
tageSeitAnfrage={anfrage.tageSeitAnfrage}
|
||||
anschreiben={anschreiben}
|
||||
stichtagVorschlag={alsIsoTag(heute)}
|
||||
alterHinweis={
|
||||
wertalterBestimmen(wertstaende[0]?.stichtag ?? null, heute)?.hinweis ?? null
|
||||
}
|
||||
werte={wertstaende.map((w) => ({
|
||||
id: w.id,
|
||||
stichtag: w.stichtag.toISOString(),
|
||||
wertCent: centZuZahl(w.wertCent) ?? 0,
|
||||
wertquelle: w.wertquelle,
|
||||
bemerkung: w.bemerkung,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bewertung fürs Sozialamt</CardTitle>
|
||||
<CardDescription>
|
||||
Meine Einordnung und meine Begründung — keine Rechtsauskunft.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Badge variant="outline">{BEWERTUNG_BESCHRIFTUNG[vertrag.bewertungSozialhilfe]}</Badge>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{BEWERTUNG_ERKLAERUNG[vertrag.bewertungSozialhilfe]}
|
||||
</p>
|
||||
{offen.bewertungBegruendung ? (
|
||||
<p className="whitespace-pre-line rounded-md border bg-muted/40 p-3 text-sm">
|
||||
{offen.bewertungBegruendung}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Noch keine Begründung hinterlegt. Sie gehört dazu, bevor der Vertrag in einer
|
||||
Aufstellung an den Bezirk auftaucht.
|
||||
</p>
|
||||
)}
|
||||
<Rechtshinweis rechtsgrundlage="§ 90 Abs. 3 SGB XII" kurz />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,438 +1,438 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user