Jeder erzeugte Brief ging bisher ohne Ruecksendeadresse hinaus: Im Absenderblock stand nur der Name, in der Datumszeile fehlte der Ort. Bei einer Versicherung oder Behoerde wirkt das unserioes und macht es der Gegenstelle unnoetig schwer zu antworten. Neu unter "Mein Konto" ein Feld fuer Strasse, PLZ und Ort. Ab da stehen sie im Absenderblock jedes Schreibens, und die Datumszeile lautet "Bayreuth, den 23.08.2026". Die Anschrift haengt am Benutzer, nicht an der betreuten Person: Sie gehoert zu mir und gilt fuer alle Akten. Nicht die Logik fehlte, sondern die Datenquelle - absenderBlock() und datumsZeile() konnten die Felder schon, briefkopfBauen() hat sie mit einem Kommentar auf null gesetzt, weil es sie im Datenmodell nicht gab. Aufbau: - Drei Felder am Benutzer, Migration ueber den Wrapper aus #24. Das war sein erster echter Einsatz: erzeugte Datei ohne Handarbeit brauchbar, DROP-INDEX-Zeilen bereits heraus. - Ein Absender-Typ statt eines blossen Namens, durchgereicht bis in den Serienlauf - der hat bisher {name} gebaut und haette die Anschrift sonst still verloren. - Zwei Platzhalter fuer eigene Vorlagen: betreuer_anschrift, betreuer_ort. Was fehlt, bleibt weg: Jede der drei Angaben ist einzeln freiwillig. Nur ein Ort ergibt eine Datumszeile mit Ort und einen Absenderblock, der weiterhin nur aus dem Namen besteht. Eine halb ausgefuellte Adresse sieht nach Fehler aus, ein blosser Name nach Absicht. Geprueft: sieben Einheitstests fuer den Absenderblock inklusive jeder Teilbefuellung und der Gegenprobe, dass nie die Heimadresse der betreuten Person im Absender landet; vier End-to-End-Tests vom leeren Zustand ueber das Speichern bis zum fertigen Brief. Refs #21
150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import type { Metadata } from 'next'
|
|
import { redirect } from 'next/navigation'
|
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
import { AlertCircle, Scale } from 'lucide-react'
|
|
import { SeitenKopf } from '@/components/seiten-kopf'
|
|
import { SchreibenEditor } from '@/components/schreiben/schreiben-editor'
|
|
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
|
import { db, nichtGeloescht } from '@/lib/db'
|
|
import { entwurfBauen } from '@/lib/schreiben/entwurf'
|
|
import { SCHREIBEN_EIGENSCHAFTEN, istSchreibenArt } from '@/lib/schreiben/arten'
|
|
import { VorlagenWahl } from '@/components/schreiben/vorlagen-wahl'
|
|
|
|
export const metadata: Metadata = { title: 'Schreiben' }
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
/**
|
|
* Ein Schreiben erzeugen, bearbeiten, ausgeben und ablegen.
|
|
*
|
|
* Eine Seite für alle Arten und alle drei Empfängerwege — statt die
|
|
* Bearbeitung dreimal an Vertrag, Vorgang und Freitext nachzubauen. Die
|
|
* bestehenden Seiten verlinken hierher.
|
|
*
|
|
* Der Fall ohne Bezug ist ausdrücklich vorgesehen: Am Anfang einer
|
|
* Betreuung schreibt man an Stellen, zu denen es noch keinen Datensatz
|
|
* gibt — man fragt ja gerade an, um zu erfahren, ob dort etwas besteht.
|
|
*/
|
|
export default async function SchreibenSeite({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
}) {
|
|
const parameter = await searchParams
|
|
const benutzer = await erfordereBenutzer('/schreiben/neu')
|
|
const aktive = await aktiveBetreute(benutzer)
|
|
if (!aktive) redirect('/ersteinrichtung')
|
|
|
|
const { darfSchreiben } = await erfordereZugang(aktive.id, 'DOKUMENTE', 'lesen')
|
|
if (!darfSchreiben('DOKUMENTE')) redirect('/dokumente')
|
|
|
|
const text = (name: string): string => {
|
|
const wert = parameter[name]
|
|
return typeof wert === 'string' ? wert.trim() : ''
|
|
}
|
|
|
|
const rohArt = text('art')
|
|
const art = istSchreibenArt(rohArt) ? rohArt : 'FREI'
|
|
const eigenschaften = SCHREIBEN_EIGENSCHAFTEN[art]
|
|
|
|
let entwurf
|
|
try {
|
|
entwurf = await entwurfBauen({
|
|
betreuteId: aktive.id,
|
|
benutzer,
|
|
art,
|
|
vertragId: text('vertragId') || null,
|
|
vorgangId: text('vorgangId') || null,
|
|
vorlageId: text('vorlageId') || null,
|
|
})
|
|
} catch (ausnahme) {
|
|
// Fehlender oder fremder Bezug: Das ist kein Absturz, sondern eine
|
|
// Angabe, die nicht stimmt. Die Seite sagt es und bietet den freien
|
|
// Weg an, statt den Benutzer vor eine Fehlerseite zu setzen.
|
|
const meldung =
|
|
ausnahme instanceof Error ? ausnahme.message : 'Der Entwurf ließ sich nicht bauen.'
|
|
return (
|
|
<div className="max-w-3xl space-y-6">
|
|
<SeitenKopf titel="Schreiben" beschreibung={eigenschaften.beschriftung} />
|
|
<Alert variant="fehler">
|
|
<AlertCircle aria-hidden="true" />
|
|
<AlertDescription>
|
|
{meldung} Du kannst stattdessen ein{' '}
|
|
<a href="/schreiben/neu?art=FREI" className="underline">
|
|
freies Schreiben
|
|
</a>{' '}
|
|
erzeugen.
|
|
</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const vorlagen =
|
|
art === 'FREI'
|
|
? await db.schreibvorlage.findMany({
|
|
where: { betreuteId: aktive.id, ...nichtGeloescht },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: 'asc' },
|
|
})
|
|
: []
|
|
|
|
const kontakte = await db.kontakt.findMany({
|
|
where: { betreuteId: aktive.id, ...nichtGeloescht },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
organisation: true,
|
|
strasse: true,
|
|
plz: true,
|
|
ort: true,
|
|
email: true,
|
|
},
|
|
orderBy: [{ organisation: 'asc' }, { name: 'asc' }],
|
|
take: 200,
|
|
})
|
|
|
|
return (
|
|
<div className="max-w-3xl space-y-6">
|
|
<SeitenKopf
|
|
titel={eigenschaften.beschriftung}
|
|
beschreibung="Bearbeiten, als Word-Datei herunterladen, in die Akte legen — und eine Wiedervorlage setzen, damit das Warten nicht vergessen geht."
|
|
pfad={[{ titel: 'Dokumente', href: '/dokumente' }, { titel: 'Schreiben' }]}
|
|
/>
|
|
|
|
{art === 'FREI' && vorlagen.length > 0 && (
|
|
<VorlagenWahl vorlagen={vorlagen} aktiveVorlageId={text('vorlageId') || null} />
|
|
)}
|
|
|
|
<SchreibenEditor
|
|
// Der Editor haelt den Text als eigenen State. Wechselt die Vorlage,
|
|
// muss er neu aufsetzen - sonst ueberlebt der alte Entwurf die
|
|
// Navigation und die Vorlage kommt nie im Textfeld an.
|
|
key={text('vorlageId') || 'ohne-vorlage'}
|
|
betreuteId={aktive.id}
|
|
art={art}
|
|
entwurf={{
|
|
betreff: entwurf.betreff,
|
|
text: entwurf.text,
|
|
empfaengerName: entwurf.empfaengerName,
|
|
empfaengerAnschrift: entwurf.empfaengerAnschrift,
|
|
wiedervorlageAm: entwurf.wiedervorlageAm,
|
|
}}
|
|
kontakte={kontakte}
|
|
vertragId={entwurf.vertragId}
|
|
vorgangId={entwurf.vorgangId}
|
|
fristBegruendung={eigenschaften.fristBegruendung}
|
|
warnungen={entwurf.warnungen}
|
|
/>
|
|
|
|
<Alert variant="hinweis">
|
|
<Scale aria-hidden="true" />
|
|
<AlertDescription>
|
|
Der vorgeschlagene Wortlaut ist ein Entwurf, keine Rechtsberatung. Bei einem
|
|
genehmigungspflichtigen Geschäft gehört die Genehmigung des Betreuungsgerichts vor den
|
|
Versand — nicht danach.
|
|
</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
)
|
|
}
|