docs: README, Datenschutztexte, Dockerfile und Sicherungsskripte
Damit ist Etappe 1 abgeschlossen und betriebsfertig dokumentiert. README mit beiden Betriebswegen. Docker Compose auf eigenem Server ist der Hauptweg; Vercel und Neon sind vollstaendig beschrieben, aber mit einer klaren Begruendung, warum ich davon abrate: beide Anbieter sind US-Unternehmen und unterliegen auch bei EU-Region potenziell dem CLOUD-Act. Bei Gesundheitsdaten einer nicht entscheidungsfaehigen Person ist das eine Abwaegung, die man bewusst treffen muss. Reverse-Proxy-Beispiele fuer Caddy, nginx und Traefik - der Proxy selbst ist nicht Teil des Compose-Setups, weil er auf dem Zielserver schon steht. Die Datenbank hat keine Portfreigabe. Sicherung: pg_dump schreibt direkt in die Verschluesselung, der Klartext landet nie auf der Platte. Danach eine Selbstpruefung - laesst sich die Datei nicht wieder entschluesseln, endet der Lauf mit Fehler statt mit einem stillen Scheinerfolg. Der jeweils erste Stand eines Monats bleibt dauerhaft stehen. Die README sagt ausdruecklich, was das Backup NICHT enthaelt: den Schluessel ENC_KEY_V1 und die Dateien im Objektspeicher. Dazu eine Anleitung, die Wiederherstellung einmal im Jahr zu ueben - mit dem letzten Schritt, in der Anwendung nachzusehen, ob die Diagnosen im Klartext erscheinen. Erst dann stimmt nicht nur die Datenbank, sondern auch der Schluessel. Verarbeitungsverzeichnis und Datenschutzhinweise als ausfuellbare Textbausteine. Die Hinweise sind in einfacher Sprache geschrieben - sie sollen der betreuten Person vorgelesen werden koennen. Die Seite /datenschutz liest denselben Text aus docs/, damit es nicht zwei Fassungen gibt, die auseinanderlaufen. Dockerfile in drei Stufen mit Tesseract, deutschen Sprachdaten und poppler-utils. Zeitzone Europe/Berlin ist gesetzt - ohne sie rechnet der Server in UTC und verschiebt Fristen um einen Tag. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
import type {
|
||||
Aufgabenkreis,
|
||||
GesundheitsArt,
|
||||
KontaktArt,
|
||||
VorgangKategorie,
|
||||
VorgangStatus,
|
||||
} from '@prisma/client'
|
||||
import { formatDatum } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* Jahresbericht nach § 1863 BGB - Textentwurf.
|
||||
*
|
||||
* Diese Datei erzeugt einen ENTWURF, keinen fertigen Bericht. Sie fasst
|
||||
* zusammen, was ueber das Jahr in Kontaktprotokollen, Gesundheitsereignissen
|
||||
* und Vorgaengen festgehalten wurde, und giesst es in ganze Saetze. Was
|
||||
* daraus wird, entscheidet der Betreuer: Der Text ist im Browser
|
||||
* bearbeitbar, und erst der ueberarbeitete Text wird zu PDF oder DOCX.
|
||||
*
|
||||
* Warum ueberhaupt ein Entwurf? Weil der Bericht sonst am Jahresende aus
|
||||
* dem Gedaechtnis geschrieben wird - und dann fehlt die Haelfte der
|
||||
* Besuche, und die Wuensche der betreuten Person, die im Maerz geaeussert
|
||||
* wurden, tauchen gar nicht auf.
|
||||
*
|
||||
* Bewusst ohne Datenbank und ohne React: reine Funktionen auf reinen
|
||||
* Daten, damit die Textbausteine testbar bleiben.
|
||||
*/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Eingangsdaten
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export type JahresberichtBetreute = {
|
||||
anrede: string | null
|
||||
vorname: string
|
||||
nachname: string
|
||||
geburtsdatum: Date | null
|
||||
heimName: string | null
|
||||
heimStrasse: string | null
|
||||
heimPlz: string | null
|
||||
heimOrt: string | null
|
||||
heimZimmer: string | null
|
||||
heimEinzugAm: Date | null
|
||||
fruehereStrasse: string | null
|
||||
frueherePlz: string | null
|
||||
fruehererOrt: string | null
|
||||
pflegegrad: number | null
|
||||
pflegegradSeit: Date | null
|
||||
schwerbehinderungGdB: number | null
|
||||
schwerbehinderungMerkzeichen: string | null
|
||||
/** Klartext aus diagnosenVerschluesselt. */
|
||||
diagnosen: string | null
|
||||
/** Klartext aus ermittelterWilleVerschluesselt. */
|
||||
ermittelterWille: string | null
|
||||
patientenverfuegungVorhanden: boolean
|
||||
vorsorgevollmachtVorhanden: boolean
|
||||
aufgabenkreise: Aufgabenkreis[]
|
||||
aufgabenkreiseFreitext: string | null
|
||||
betreuungsgericht: string | null
|
||||
aktenzeichenGericht: string | null
|
||||
bestellungAm: Date | null
|
||||
bestellungBis: Date | null
|
||||
}
|
||||
|
||||
export type JahresberichtKontakt = {
|
||||
datum: Date
|
||||
art: KontaktArt
|
||||
mitBetreuter: boolean
|
||||
dauerMinuten: number | null
|
||||
gespraechspartner: string | null
|
||||
/** Klartext aus wuenscheVerschluesselt. */
|
||||
wuensche: string | null
|
||||
}
|
||||
|
||||
export type JahresberichtGesundheit = {
|
||||
datum: Date
|
||||
bisDatum: Date | null
|
||||
art: GesundheitsArt
|
||||
titel: string
|
||||
einrichtung: string | null
|
||||
abwesenheitstage: number | null
|
||||
}
|
||||
|
||||
export type JahresberichtVorgang = {
|
||||
titel: string
|
||||
kategorie: VorgangKategorie
|
||||
status: VorgangStatus
|
||||
naechsterSchritt: string | null
|
||||
gegenstelle: string | null
|
||||
genehmigungspflichtig: boolean
|
||||
erledigtAm: Date | null
|
||||
}
|
||||
|
||||
export type JahresberichtQuellen = {
|
||||
betreute: JahresberichtBetreute
|
||||
zeitraumVon: Date
|
||||
zeitraumBis: Date
|
||||
kontakte: JahresberichtKontakt[]
|
||||
gesundheit: JahresberichtGesundheit[]
|
||||
vorgaenge: JahresberichtVorgang[]
|
||||
}
|
||||
|
||||
export type Berichtsabschnitt = {
|
||||
nummer: number
|
||||
titel: string
|
||||
rechtsgrundlage: string
|
||||
absaetze: string[]
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Beschriftungen
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Stelle im Entwurf, an der der Betreuer selbst weiterschreiben muss. */
|
||||
export const ERGAENZEN = '[bitte ergänzen]'
|
||||
|
||||
export const ENTWURFSHINWEIS =
|
||||
'Entwurf. Dieser Text ist aus den erfassten Kontaktprotokollen, ' +
|
||||
'Gesundheitsereignissen und Vorgängen zusammengestellt worden. Er ist ein ' +
|
||||
'Vorschlag, keine fertige Berichterstattung: Bitte lies ihn durch, streiche, ' +
|
||||
'was nicht stimmt, und ergänze, was nur du weißt. Erst danach abgeben.'
|
||||
|
||||
export const KONTAKTART_EINZAHL: Record<KontaktArt, string> = {
|
||||
BESUCH: 'Besuch',
|
||||
TELEFONAT: 'Telefonat',
|
||||
EMAIL: 'E-Mail',
|
||||
BRIEF: 'Brief',
|
||||
VIDEOCALL: 'Videotelefonat',
|
||||
FAX: 'Fax',
|
||||
VORSPRACHE: 'persönliche Vorsprache',
|
||||
}
|
||||
|
||||
export const KONTAKTART_MEHRZAHL: Record<KontaktArt, string> = {
|
||||
BESUCH: 'Besuche',
|
||||
TELEFONAT: 'Telefonate',
|
||||
EMAIL: 'E-Mails',
|
||||
BRIEF: 'Briefe',
|
||||
VIDEOCALL: 'Videotelefonate',
|
||||
FAX: 'Faxe',
|
||||
VORSPRACHE: 'persönliche Vorsprachen',
|
||||
}
|
||||
|
||||
export const GESUNDHEITSART_MEHRZAHL: Record<GesundheitsArt, string> = {
|
||||
ARZTBESUCH: 'Arztbesuche',
|
||||
KRANKENHAUS: 'Krankenhausaufenthalte',
|
||||
EXAZERBATION: 'akute Verschlechterungen der Atemwegserkrankung',
|
||||
STURZ: 'Stürze',
|
||||
MEDIKATIONSAENDERUNG: 'Änderungen der Medikation',
|
||||
SAUERSTOFFVERSORGUNG: 'Maßnahmen der Sauerstoffversorgung',
|
||||
INHALATION: 'Inhalationsbehandlungen',
|
||||
HILFSMITTEL: 'Hilfsmittelversorgungen',
|
||||
IMPFUNG: 'Impfungen',
|
||||
REHA: 'Rehabilitationsmaßnahmen',
|
||||
PFLEGEBEGUTACHTUNG: 'Pflegebegutachtungen',
|
||||
HEILMITTEL: 'Heilmittelbehandlungen',
|
||||
SONSTIGES: 'sonstige gesundheitliche Ereignisse',
|
||||
}
|
||||
|
||||
export const KATEGORIE_BESCHRIFTUNG: Record<VorgangKategorie, string> = {
|
||||
GERICHT: 'Betreuungsgericht',
|
||||
SOZIALHILFE: 'Sozialhilfe und Hilfe zur Pflege',
|
||||
IMMOBILIE: 'Immobilie und Wohnraum',
|
||||
VERSICHERUNG: 'Versicherungen',
|
||||
HEIM_PFLEGE: 'Heim und Pflege',
|
||||
VERTRAEGE: 'Verträge',
|
||||
STEUERN: 'Steuern',
|
||||
SONSTIGES: 'Sonstiges',
|
||||
}
|
||||
|
||||
export const AUFGABENKREIS_BESCHRIFTUNG: Record<Aufgabenkreis, string> = {
|
||||
VERMOEGENSSORGE: 'Vermögenssorge',
|
||||
GESUNDHEITSSORGE: 'Gesundheitssorge',
|
||||
AUFENTHALTSBESTIMMUNG: 'Aufenthaltsbestimmung',
|
||||
WOHNUNGSANGELEGENHEITEN: 'Wohnungsangelegenheiten',
|
||||
BEHOERDENANGELEGENHEITEN: 'Behördenangelegenheiten',
|
||||
POST_UND_FERNMELDEVERKEHR: 'Post- und Fernmeldeverkehr',
|
||||
RECHTS_ANTRAGS_UND_BEHOERDENVERTRETUNG: 'Vertretung gegenüber Behörden und Gerichten',
|
||||
ENTSCHEIDUNG_UEBER_FREIHEITSENTZIEHENDE_MASSNAHMEN:
|
||||
'Entscheidung über freiheitsentziehende Maßnahmen',
|
||||
SONSTIGES: 'weitere Aufgaben',
|
||||
}
|
||||
|
||||
export const GLIEDERUNG: { nummer: number; titel: string; rechtsgrundlage: string }[] = [
|
||||
{ nummer: 1, titel: 'Persönliche Verhältnisse', rechtsgrundlage: '§ 1863 Abs. 3 Nr. 1 BGB' },
|
||||
{
|
||||
nummer: 2,
|
||||
titel: 'Persönlicher Kontakt zur betreuten Person',
|
||||
rechtsgrundlage: '§ 1863 Abs. 3 Nr. 2, § 1863 Abs. 2 BGB',
|
||||
},
|
||||
{
|
||||
nummer: 3,
|
||||
titel: 'Wünsche der betreuten Person und ihre Umsetzung',
|
||||
rechtsgrundlage: '§ 1863 Abs. 3 Nr. 3, § 1821 BGB',
|
||||
},
|
||||
{
|
||||
nummer: 4,
|
||||
titel: 'Erledigte und laufende Angelegenheiten',
|
||||
rechtsgrundlage: '§ 1863 Abs. 3 BGB',
|
||||
},
|
||||
{
|
||||
nummer: 5,
|
||||
titel: 'Einschätzung zur Fortführung der Betreuung',
|
||||
rechtsgrundlage: '§ 1863 Abs. 3 Nr. 4 BGB',
|
||||
},
|
||||
]
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Sprachliche Helfer
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** "a", "a und b", "a, b und c" - deutsche Aufzaehlung ohne Komma vor "und". */
|
||||
export function aufzaehlung(teile: readonly string[]): string {
|
||||
const sauber = teile.filter((t) => t.trim() !== '')
|
||||
if (sauber.length === 0) return ''
|
||||
if (sauber.length === 1) return sauber[0]!
|
||||
return `${sauber.slice(0, -1).join(', ')} und ${sauber[sauber.length - 1]!}`
|
||||
}
|
||||
|
||||
/** "einmal", "zweimal", "17-mal" - so, wie man es schreiben wuerde. */
|
||||
export function malAngabe(anzahl: number): string {
|
||||
const woerter = ['keinmal', 'einmal', 'zweimal', 'dreimal', 'viermal', 'fünfmal', 'sechsmal']
|
||||
return woerter[anzahl] ?? `${anzahl}-mal`
|
||||
}
|
||||
|
||||
/** 95 Minuten -> "1 Stunde und 35 Minuten". */
|
||||
export function dauerAusgeschrieben(minuten: number): string {
|
||||
const stunden = Math.floor(minuten / 60)
|
||||
const rest = minuten % 60
|
||||
const teile: string[] = []
|
||||
if (stunden > 0) teile.push(stunden === 1 ? '1 Stunde' : `${stunden} Stunden`)
|
||||
if (rest > 0 || stunden === 0) teile.push(rest === 1 ? '1 Minute' : `${rest} Minuten`)
|
||||
return aufzaehlung(teile)
|
||||
}
|
||||
|
||||
/** "Frau Maria Musterfrau" - mit Anrede, wenn sie gepflegt ist. */
|
||||
export function vollerName(betreute: Pick<JahresberichtBetreute, 'anrede' | 'vorname' | 'nachname'>): string {
|
||||
return [betreute.anrede, betreute.vorname, betreute.nachname].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
/** "Frau Musterfrau" - die Form, die im Bericht laufend gebraucht wird. */
|
||||
export function kurzName(betreute: Pick<JahresberichtBetreute, 'anrede' | 'vorname' | 'nachname'>): string {
|
||||
return betreute.anrede ? `${betreute.anrede} ${betreute.nachname}` : `${betreute.vorname} ${betreute.nachname}`
|
||||
}
|
||||
|
||||
/** "sie" oder "er" - richtet sich nach der gepflegten Anrede. */
|
||||
function pronomen(betreute: Pick<JahresberichtBetreute, 'anrede'>): { nominativ: string; possessiv: string } {
|
||||
const maennlich = (betreute.anrede ?? '').trim().toLowerCase().startsWith('herr')
|
||||
return maennlich ? { nominativ: 'er', possessiv: 'seine' } : { nominativ: 'sie', possessiv: 'ihre' }
|
||||
}
|
||||
|
||||
function anschrift(strasse: string | null, plz: string | null, ort: string | null): string | null {
|
||||
const zeile = [strasse, [plz, ort].filter(Boolean).join(' ').trim()].filter(Boolean).join(', ')
|
||||
return zeile.trim() === '' ? null : zeile
|
||||
}
|
||||
|
||||
function tageZwischen(von: Date, bis: Date): number {
|
||||
return Math.max(0, Math.round((bis.getTime() - von.getTime()) / 86_400_000))
|
||||
}
|
||||
|
||||
function imZeitraum(datum: Date, von: Date, bis: Date): boolean {
|
||||
return datum.getTime() >= von.getTime() && datum.getTime() <= bis.getTime()
|
||||
}
|
||||
|
||||
function haeufigkeit<T extends string>(werte: T[]): Map<T, number> {
|
||||
const karte = new Map<T, number>()
|
||||
for (const wert of werte) karte.set(wert, (karte.get(wert) ?? 0) + 1)
|
||||
return karte
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Die fuenf Abschnitte
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function abschnittPersoenlicheVerhaeltnisse(quellen: JahresberichtQuellen): string[] {
|
||||
const { betreute, zeitraumVon, zeitraumBis } = quellen
|
||||
const name = kurzName(betreute)
|
||||
const pron = pronomen(betreute)
|
||||
const absaetze: string[] = []
|
||||
|
||||
// --- Aufenthalt ---
|
||||
const heimAnschrift = anschrift(betreute.heimStrasse, betreute.heimPlz, betreute.heimOrt)
|
||||
if (betreute.heimName) {
|
||||
const seit = betreute.heimEinzugAm ? ` seit dem ${formatDatum(betreute.heimEinzugAm)}` : ''
|
||||
const wo = heimAnschrift ? ` (${heimAnschrift})` : ''
|
||||
const zimmer = betreute.heimZimmer ? `, Zimmer ${betreute.heimZimmer}` : ''
|
||||
absaetze.push(
|
||||
`${vollerName(betreute)}, geboren am ${formatDatum(betreute.geburtsdatum)}, lebt${seit} ` +
|
||||
`in der Einrichtung ${betreute.heimName}${wo}${zimmer}. An diesem Aufenthaltsort hat sich ` +
|
||||
`im Berichtszeitraum nichts geändert. ${ERGAENZEN}, falls doch.`,
|
||||
)
|
||||
} else {
|
||||
absaetze.push(
|
||||
`${vollerName(betreute)}, geboren am ${formatDatum(betreute.geburtsdatum)}. Zum Aufenthaltsort ` +
|
||||
`liegen in dieser Anwendung keine Angaben vor: ${ERGAENZEN}, wo ${pron.nominativ} im ` +
|
||||
`Berichtszeitraum gelebt hat.`,
|
||||
)
|
||||
}
|
||||
|
||||
const frueher = anschrift(betreute.fruehereStrasse, betreute.frueherePlz, betreute.fruehererOrt)
|
||||
if (frueher) {
|
||||
absaetze.push(
|
||||
`Die letzte eigene Wohnanschrift lautet ${frueher}. ${ERGAENZEN}: ob die Wohnung im ` +
|
||||
`Berichtszeitraum weiter bestanden hat, aufgelöst wurde oder eine Auflösung ansteht.`,
|
||||
)
|
||||
}
|
||||
|
||||
// --- Pflege und Behinderung ---
|
||||
const pflegesaetze: string[] = []
|
||||
if (betreute.pflegegrad !== null) {
|
||||
const seit = betreute.pflegegradSeit ? ` seit dem ${formatDatum(betreute.pflegegradSeit)}` : ''
|
||||
pflegesaetze.push(`${name} ist${seit} in Pflegegrad ${betreute.pflegegrad} eingestuft.`)
|
||||
} else {
|
||||
pflegesaetze.push(`Ein Pflegegrad ist in dieser Anwendung nicht hinterlegt (${ERGAENZEN}).`)
|
||||
}
|
||||
if (betreute.schwerbehinderungGdB !== null) {
|
||||
const merk = betreute.schwerbehinderungMerkzeichen
|
||||
? ` mit dem Merkzeichen ${betreute.schwerbehinderungMerkzeichen}`
|
||||
: ''
|
||||
pflegesaetze.push(
|
||||
`Es ist ein Grad der Behinderung von ${betreute.schwerbehinderungGdB}${merk} festgestellt.`,
|
||||
)
|
||||
}
|
||||
absaetze.push(pflegesaetze.join(' '))
|
||||
|
||||
// --- Gesundheitszustand im Berichtszeitraum ---
|
||||
const ereignisse = quellen.gesundheit.filter((e) => imZeitraum(e.datum, zeitraumVon, zeitraumBis))
|
||||
const gesundheitssaetze: string[] = []
|
||||
|
||||
if (betreute.diagnosen && betreute.diagnosen.trim() !== '') {
|
||||
gesundheitssaetze.push(
|
||||
`Der Gesundheitszustand ist geprägt von: ${betreute.diagnosen.trim().replace(/\s+/g, ' ')}.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (ereignisse.length === 0) {
|
||||
gesundheitssaetze.push(
|
||||
`Für den Zeitraum vom ${formatDatum(zeitraumVon)} bis zum ${formatDatum(zeitraumBis)} sind ` +
|
||||
`keine gesundheitlichen Ereignisse festgehalten. ${ERGAENZEN}, sofern es welche gab.`,
|
||||
)
|
||||
} else {
|
||||
const nachArt = haeufigkeit(ereignisse.map((e) => e.art))
|
||||
const aufzaehlungTeile = [...nachArt.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([art, anzahl]) => `${anzahl} ${GESUNDHEITSART_MEHRZAHL[art]}`)
|
||||
gesundheitssaetze.push(
|
||||
`Im Berichtszeitraum sind ${ereignisse.length} gesundheitliche Ereignisse festgehalten: ` +
|
||||
`${aufzaehlung(aufzaehlungTeile)}.`,
|
||||
)
|
||||
|
||||
const klinik = ereignisse.filter((e) => e.art === 'KRANKENHAUS' || e.art === 'REHA')
|
||||
if (klinik.length > 0) {
|
||||
const beschreibungen = klinik.map((e) => {
|
||||
const bis = e.bisDatum ? ` bis ${formatDatum(e.bisDatum)}` : ''
|
||||
const wo = e.einrichtung ? ` (${e.einrichtung})` : ''
|
||||
return `${formatDatum(e.datum)}${bis}: ${e.titel}${wo}`
|
||||
})
|
||||
gesundheitssaetze.push(
|
||||
`Stationäre Aufenthalte im Einzelnen — ${aufzaehlung(beschreibungen)}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const abwesend = ereignisse.reduce((summe, e) => summe + (e.abwesenheitstage ?? 0), 0)
|
||||
if (abwesend > 0) {
|
||||
gesundheitssaetze.push(
|
||||
`Insgesamt ergaben sich daraus ${abwesend} Abwesenheitstage aus der Einrichtung.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
absaetze.push(gesundheitssaetze.join(' '))
|
||||
|
||||
// --- Vorsorge ---
|
||||
const vorsorge: string[] = []
|
||||
vorsorge.push(
|
||||
betreute.patientenverfuegungVorhanden
|
||||
? 'Eine Patientenverfügung liegt vor und ist mir bekannt.'
|
||||
: 'Eine Patientenverfügung liegt nach meinem Kenntnisstand nicht vor.',
|
||||
)
|
||||
vorsorge.push(
|
||||
betreute.vorsorgevollmachtVorhanden
|
||||
? 'Eine Vorsorgevollmacht ist vorhanden.'
|
||||
: 'Eine Vorsorgevollmacht ist nicht vorhanden.',
|
||||
)
|
||||
absaetze.push(vorsorge.join(' '))
|
||||
|
||||
return absaetze
|
||||
}
|
||||
|
||||
function abschnittKontakt(quellen: JahresberichtQuellen): string[] {
|
||||
const { betreute, zeitraumVon, zeitraumBis } = quellen
|
||||
const name = kurzName(betreute)
|
||||
const absaetze: string[] = []
|
||||
|
||||
const imFenster = quellen.kontakte.filter((k) => imZeitraum(k.datum, zeitraumVon, zeitraumBis))
|
||||
const mitIhr = imFenster
|
||||
.filter((k) => k.mitBetreuter)
|
||||
.sort((a, b) => a.datum.getTime() - b.datum.getTime())
|
||||
const mitDritten = imFenster.filter((k) => !k.mitBetreuter)
|
||||
|
||||
absaetze.push(
|
||||
`Der Berichtszeitraum umfasst die Zeit vom ${formatDatum(zeitraumVon)} bis zum ` +
|
||||
`${formatDatum(zeitraumBis)}, also ${tageZwischen(zeitraumVon, zeitraumBis)} Tage.`,
|
||||
)
|
||||
|
||||
if (mitIhr.length === 0) {
|
||||
absaetze.push(
|
||||
`Für diesen Zeitraum ist kein persönlicher Kontakt zu ${name} protokolliert. Das entspricht ` +
|
||||
`nicht der Pflicht aus § 1863 Abs. 2 BGB, die betreute Person in dem erforderlichen Umfang ` +
|
||||
`persönlich aufzusuchen. Bitte prüfe, ob Kontakte nur nicht erfasst wurden, und trage sie ` +
|
||||
`nach — ${ERGAENZEN}.`,
|
||||
)
|
||||
} else {
|
||||
const nachArt = haeufigkeit(mitIhr.map((k) => k.art))
|
||||
const teile = [...nachArt.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([art, anzahl]) =>
|
||||
anzahl === 1
|
||||
? `ein ${KONTAKTART_EINZAHL[art]}`
|
||||
: `${anzahl} ${KONTAKTART_MEHRZAHL[art]}`,
|
||||
)
|
||||
|
||||
absaetze.push(
|
||||
`Ich hatte im Berichtszeitraum ${malAngabe(mitIhr.length)} persönlichen Kontakt zu ${name}: ` +
|
||||
`${aufzaehlung(teile)}.`,
|
||||
)
|
||||
|
||||
const abstand = Math.round(tageZwischen(zeitraumVon, zeitraumBis) / mitIhr.length)
|
||||
const luecken = mitIhr
|
||||
.slice(1)
|
||||
.map((k, i) => tageZwischen(mitIhr[i]!.datum, k.datum))
|
||||
const groessteLuecke = luecken.length > 0 ? Math.max(...luecken) : 0
|
||||
const rhythmus = [`Im Mittel entspricht das einem Kontakt alle ${abstand} Tage.`]
|
||||
if (groessteLuecke > 0) {
|
||||
rhythmus.push(
|
||||
`Die längste Zeitspanne zwischen zwei persönlichen Kontakten betrug ${groessteLuecke} Tage.`,
|
||||
)
|
||||
}
|
||||
rhythmus.push(
|
||||
`Der erste Kontakt im Berichtszeitraum war am ${formatDatum(mitIhr[0]!.datum)}, ` +
|
||||
`der letzte am ${formatDatum(mitIhr[mitIhr.length - 1]!.datum)}.`,
|
||||
)
|
||||
absaetze.push(rhythmus.join(' '))
|
||||
|
||||
const minuten = mitIhr.reduce((summe, k) => summe + (k.dauerMinuten ?? 0), 0)
|
||||
if (minuten > 0) {
|
||||
absaetze.push(
|
||||
`Für diese Kontakte sind insgesamt ${dauerAusgeschrieben(minuten)} erfasst. Besprochen ` +
|
||||
`wurden dabei ${betreute.aufgabenkreise.length > 0 ? 'die Angelegenheiten aus meinen Aufgabenkreisen' : 'die laufenden Angelegenheiten'}, ` +
|
||||
`das Befinden und die persönliche Lage von ${name}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (mitDritten.length > 0) {
|
||||
const nachArt = haeufigkeit(mitDritten.map((k) => k.art))
|
||||
const teile = [...nachArt.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([art, anzahl]) =>
|
||||
anzahl === 1 ? `ein ${KONTAKTART_EINZAHL[art]}` : `${anzahl} ${KONTAKTART_MEHRZAHL[art]}`,
|
||||
)
|
||||
const stellen = [...new Set(mitDritten.map((k) => k.gespraechspartner).filter(Boolean))] as string[]
|
||||
const wer = stellen.length > 0 ? ` Beteiligt waren dabei ${aufzaehlung(stellen.slice(0, 8))}.` : ''
|
||||
absaetze.push(
|
||||
`Hinzu kommen ${mitDritten.length} Kontakte mit Dritten in ihren Angelegenheiten — ` +
|
||||
`${aufzaehlung(teile)}.${wer}`,
|
||||
)
|
||||
}
|
||||
|
||||
return absaetze
|
||||
}
|
||||
|
||||
function abschnittWuensche(quellen: JahresberichtQuellen): string[] {
|
||||
const { betreute, zeitraumVon, zeitraumBis } = quellen
|
||||
const name = kurzName(betreute)
|
||||
const absaetze: string[] = []
|
||||
|
||||
absaetze.push(
|
||||
`Nach § 1821 BGB sind die Wünsche der betreuten Person Maßstab meines Handelns. Ich habe ` +
|
||||
`${name} bei jedem Kontakt nach ihren Wünschen gefragt und sie im Kontaktprotokoll ` +
|
||||
`festgehalten.`,
|
||||
)
|
||||
|
||||
const mitWunsch = quellen.kontakte
|
||||
.filter((k) => imZeitraum(k.datum, zeitraumVon, zeitraumBis))
|
||||
.filter((k) => (k.wuensche ?? '').trim() !== '')
|
||||
.sort((a, b) => a.datum.getTime() - b.datum.getTime())
|
||||
|
||||
if (mitWunsch.length === 0) {
|
||||
absaetze.push(
|
||||
`Für den Berichtszeitraum sind keine ausdrücklich geäußerten Wünsche festgehalten. ` +
|
||||
`${ERGAENZEN}: Was hat ${name} sich gewünscht, und wie hast du darauf reagiert? Auch ein ` +
|
||||
`Wunsch, dem nicht entsprochen werden konnte, gehört in den Bericht — zusammen mit dem Grund.`,
|
||||
)
|
||||
} else {
|
||||
absaetze.push(
|
||||
`Im Berichtszeitraum sind ${mitWunsch.length} geäußerte Wünsche festgehalten:`,
|
||||
)
|
||||
for (const kontakt of mitWunsch) {
|
||||
absaetze.push(
|
||||
`• ${formatDatum(kontakt.datum)} (${KONTAKTART_EINZAHL[kontakt.art]}): ` +
|
||||
`${kontakt.wuensche!.trim().replace(/\s+/g, ' ')}`,
|
||||
)
|
||||
}
|
||||
absaetze.push(
|
||||
`Diesen Wünschen bin ich wie folgt nachgekommen: ${ERGAENZEN}. Wo ich einem Wunsch nicht ` +
|
||||
`entsprechen konnte, ist der Grund hier zu nennen (§ 1821 Abs. 3 BGB).`,
|
||||
)
|
||||
}
|
||||
|
||||
if (betreute.ermittelterWille && betreute.ermittelterWille.trim() !== '') {
|
||||
absaetze.push(
|
||||
`Zum ermittelten Willen von ${name} ist in den Stammdaten festgehalten: ` +
|
||||
`${betreute.ermittelterWille.trim().replace(/\s+/g, ' ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
return absaetze
|
||||
}
|
||||
|
||||
function abschnittAngelegenheiten(quellen: JahresberichtQuellen): string[] {
|
||||
const { zeitraumVon, zeitraumBis } = quellen
|
||||
const absaetze: string[] = []
|
||||
|
||||
if (quellen.vorgaenge.length === 0) {
|
||||
absaetze.push(
|
||||
`Für den Berichtszeitraum sind keine Vorgänge erfasst. ${ERGAENZEN}: Welche Angelegenheiten ` +
|
||||
`hast du bearbeitet?`,
|
||||
)
|
||||
return absaetze
|
||||
}
|
||||
|
||||
const erledigt = quellen.vorgaenge.filter(
|
||||
(v) => v.status === 'ERLEDIGT' && (v.erledigtAm === null || imZeitraum(v.erledigtAm, zeitraumVon, zeitraumBis)),
|
||||
)
|
||||
const laufend = quellen.vorgaenge.filter((v) => v.status !== 'ERLEDIGT')
|
||||
|
||||
absaetze.push(
|
||||
`Im Berichtszeitraum habe ich ${quellen.vorgaenge.length} Angelegenheiten geführt; davon sind ` +
|
||||
`${erledigt.length} abgeschlossen und ${laufend.length} noch offen.`,
|
||||
)
|
||||
|
||||
const kategorien = [...new Set(quellen.vorgaenge.map((v) => v.kategorie))].sort((a, b) =>
|
||||
KATEGORIE_BESCHRIFTUNG[a].localeCompare(KATEGORIE_BESCHRIFTUNG[b], 'de'),
|
||||
)
|
||||
|
||||
for (const kategorie of kategorien) {
|
||||
const fertig = erledigt.filter((v) => v.kategorie === kategorie)
|
||||
const offen = laufend.filter((v) => v.kategorie === kategorie)
|
||||
const saetze: string[] = []
|
||||
|
||||
if (fertig.length > 0) {
|
||||
const titel = fertig.map((v) =>
|
||||
v.erledigtAm ? `${v.titel} (abgeschlossen am ${formatDatum(v.erledigtAm)})` : v.titel,
|
||||
)
|
||||
saetze.push(`Abgeschlossen wurde ${aufzaehlung(titel)}.`)
|
||||
}
|
||||
if (offen.length > 0) {
|
||||
const titel = offen.map((v) => {
|
||||
const schritt = v.naechsterSchritt ? `; nächster Schritt: ${v.naechsterSchritt}` : ''
|
||||
const wer = v.gegenstelle ? `, Gegenstelle ${v.gegenstelle}` : ''
|
||||
return `${v.titel}${wer}${schritt}`
|
||||
})
|
||||
saetze.push(`Noch offen ist ${aufzaehlung(titel)}.`)
|
||||
}
|
||||
|
||||
absaetze.push(`${KATEGORIE_BESCHRIFTUNG[kategorie]}: ${saetze.join(' ')}`)
|
||||
}
|
||||
|
||||
const genehmigungspflichtig = quellen.vorgaenge.filter((v) => v.genehmigungspflichtig)
|
||||
if (genehmigungspflichtig.length > 0) {
|
||||
absaetze.push(
|
||||
`Genehmigungspflichtig waren dabei ${aufzaehlung(genehmigungspflichtig.map((v) => v.titel))}. ` +
|
||||
`Zum Stand der jeweiligen betreuungsgerichtlichen Genehmigung: ${ERGAENZEN}.`,
|
||||
)
|
||||
}
|
||||
|
||||
return absaetze
|
||||
}
|
||||
|
||||
function abschnittEinschaetzung(quellen: JahresberichtQuellen): string[] {
|
||||
const { betreute } = quellen
|
||||
const name = kurzName(betreute)
|
||||
const absaetze: string[] = []
|
||||
|
||||
const kreise = betreute.aufgabenkreise.map((k) => AUFGABENKREIS_BESCHRIFTUNG[k])
|
||||
if (betreute.aufgabenkreiseFreitext && betreute.aufgabenkreiseFreitext.trim() !== '') {
|
||||
kreise.push(betreute.aufgabenkreiseFreitext.trim())
|
||||
}
|
||||
|
||||
absaetze.push(
|
||||
kreise.length > 0
|
||||
? `Die Betreuung besteht derzeit für die Aufgabenkreise ${aufzaehlung(kreise)}.`
|
||||
: `Die Aufgabenkreise sind in dieser Anwendung nicht hinterlegt (${ERGAENZEN}).`,
|
||||
)
|
||||
|
||||
const laufend = quellen.vorgaenge.filter((v) => v.status !== 'ERLEDIGT')
|
||||
const begruendung =
|
||||
laufend.length > 0
|
||||
? `Es sind weiterhin ${laufend.length} Angelegenheiten in Bearbeitung, insbesondere ` +
|
||||
`${aufzaehlung(laufend.slice(0, 3).map((v) => v.titel))}. Ein Wegfall des Betreuungsbedarfs ` +
|
||||
`ist daher nicht erkennbar.`
|
||||
: `Zurzeit sind keine offenen Angelegenheiten verzeichnet.`
|
||||
|
||||
absaetze.push(
|
||||
`Nach meiner Einschätzung ist die Betreuung in den bisherigen Aufgabenkreisen unverändert ` +
|
||||
`fortzuführen. ${begruendung}`,
|
||||
)
|
||||
|
||||
absaetze.push(
|
||||
`Ein Grund, die Betreuung zu erweitern, ist mir nicht bekannt; ebenso wenig ein Grund, sie ` +
|
||||
`einzuschränken oder aufzuheben. Sollte das für dich anders sein, ersetze diesen Absatz: ` +
|
||||
`${ERGAENZEN} — fortzuführen, zu erweitern (um welchen Aufgabenkreis und warum), ` +
|
||||
`einzuschränken oder aufzuheben.`,
|
||||
)
|
||||
|
||||
absaetze.push(
|
||||
`${name} ist mit der Fortführung der Betreuung durch mich einverstanden. ${ERGAENZEN}, ` +
|
||||
`falls sich das geändert hat oder der Wille nicht sicher zu ermitteln ist.`,
|
||||
)
|
||||
|
||||
return absaetze
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Zusammenbau
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Baut den Entwurf aus den Quelldaten. Reine Funktion: gleiche Eingabe,
|
||||
* gleicher Text - damit sich der Entwurf testen laesst.
|
||||
*/
|
||||
export function jahresberichtEntwurfBauen(quellen: JahresberichtQuellen): Berichtsabschnitt[] {
|
||||
const bauer = [
|
||||
abschnittPersoenlicheVerhaeltnisse,
|
||||
abschnittKontakt,
|
||||
abschnittWuensche,
|
||||
abschnittAngelegenheiten,
|
||||
abschnittEinschaetzung,
|
||||
]
|
||||
|
||||
return GLIEDERUNG.map((eintrag, i) => ({
|
||||
nummer: eintrag.nummer,
|
||||
titel: eintrag.titel,
|
||||
rechtsgrundlage: eintrag.rechtsgrundlage,
|
||||
absaetze: bauer[i]!(quellen).filter((absatz) => absatz.trim() !== ''),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Entwurf als reiner Text - so wird er in Bericht.entwurfVerschluesselt
|
||||
* abgelegt und im Browser bearbeitet. Ueberschriften tragen ihre Nummer,
|
||||
* damit sich der Text spaeter wieder in Abschnitte zerlegen laesst.
|
||||
*/
|
||||
export function entwurfAlsText(abschnitte: Berichtsabschnitt[]): string {
|
||||
return abschnitte
|
||||
.map((abschnitt) =>
|
||||
[`${abschnitt.nummer}. ${abschnitt.titel}`, '', ...abschnitt.absaetze].join('\n'),
|
||||
)
|
||||
.join('\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/** Direktweg von den Quellen zum Text. */
|
||||
export function jahresberichtEntwurfText(quellen: JahresberichtQuellen): string {
|
||||
return entwurfAlsText(jahresberichtEntwurfBauen(quellen))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gegenstueck zu entwurfAlsText: zerlegt den (moeglicherweise vom Betreuer
|
||||
* ueberarbeiteten) Text wieder in Abschnitte. Alles vor der ersten
|
||||
* nummerierten Ueberschrift landet in einem Vorspann ohne Titel - so geht
|
||||
* auch dann nichts verloren, wenn jemand oben etwas eingefuegt hat.
|
||||
*/
|
||||
export function textZuAbschnitten(text: string): { titel: string | null; absaetze: string[] }[] {
|
||||
const zeilen = text.replace(/\r\n/g, '\n').split('\n')
|
||||
const abschnitte: { titel: string | null; absaetze: string[] }[] = []
|
||||
let aktuell: { titel: string | null; absaetze: string[] } = { titel: null, absaetze: [] }
|
||||
|
||||
for (const zeile of zeilen) {
|
||||
if (/^\s*\d+\.\s+\S/.test(zeile)) {
|
||||
if (aktuell.titel !== null || aktuell.absaetze.length > 0) abschnitte.push(aktuell)
|
||||
aktuell = { titel: zeile.trim(), absaetze: [] }
|
||||
continue
|
||||
}
|
||||
if (zeile.trim() === '') continue
|
||||
aktuell.absaetze.push(zeile.trim())
|
||||
}
|
||||
if (aktuell.titel !== null || aktuell.absaetze.length > 0) abschnitte.push(aktuell)
|
||||
|
||||
return abschnitte
|
||||
}
|
||||
|
||||
/** Enthaelt der Entwurf noch Stellen, die ausgefuellt werden muessen? */
|
||||
export function offeneErgaenzungen(text: string): number {
|
||||
return text.split(ERGAENZEN).length - 1
|
||||
}
|
||||
Reference in New Issue
Block a user