import 'server-only' import { db, nichtGeloescht } from '@/lib/db' import { oeffnen } from '@/lib/crypto/feld' import { briefkopfBauen, type Absender } from '@/lib/vertraege/briefkopf' import { absenderBlock, anschriftBlock, betreffZeile, bestellungsAbsatz, datumsZeile, grussformel, type Briefkopf, type Empfaenger, } from '@/lib/vertraege/anschreiben' import { rueckkaufswertAnschreiben } from '@/lib/vertraege/rueckkaufswert' import { kuendigungsassistent } from '@/lib/vertraege/kuendigungsassistent' import { sachstandsanfrageErzeugen } from '@/lib/vorgaenge/sachstandsanfrage' import { laufzeitAusVertrag } from '@/lib/vertraege/kuendigung' import { VERTRAG_ART_BESCHRIFTUNG } from '@/lib/vertraege/konstanten' import { heuteAlsTag } from '@/lib/fristen/datum' import { SCHREIBEN_EIGENSCHAFTEN, type SchreibenArt } from '@/lib/schreiben/arten' import { platzhalterErsetzen, type PlatzhalterWerte } from '@/lib/schreiben/platzhalter' import { formatDatum } from '@/lib/utils' /** * Baut den Entwurf eines Schreibens - Text, Betreff, Empfaenger. * * Hier laufen die vorhandenen Erzeuger zusammen (Rueckkaufswert, * Kuendigung, Sachstandsanfrage). Sie bleiben unveraendert: Ihr Wortlaut * ist getestet und in der Praxis erprobt, und ihn fuer eine neue * Ausgabeform umzuschreiben waere ein Risiko ohne Gewinn. * * Was hier dazukommt, ist der vierte Fall: das freie Schreiben an eine * Stelle, zu der es noch keinen Datensatz gibt. Am Anfang einer Betreuung * ist das der Normalfall - man fragt ja gerade an, um zu erfahren, ob dort * ueberhaupt etwas besteht. */ export type SchreibenEntwurf = { art: SchreibenArt betreff: string /** Vollstaendiger Brieftext, bearbeitbar. */ text: string empfaengerName: string empfaengerAnschrift: string /** Vorgeschlagenes Datum der Wiedervorlage, ISO-Tag oder leer. */ wiedervorlageAm: string /** Woran haengt das Schreiben? */ vertragId: string | null vorgangId: string | null /** Was vor dem Abschicken zu klaeren ist - aus dem Kuendigungsassistenten. */ warnungen: string[] } function tagAlsIsoText(datum: Date): string { return datum.toISOString().slice(0, 10) } function wiedervorlageVorschlag(art: SchreibenArt, heute: Date): string { const tage = SCHREIBEN_EIGENSCHAFTEN[art].wiedervorlageTage if (tage === null) return '' const ziel = new Date(heute) ziel.setUTCDate(ziel.getUTCDate() + tage) return tagAlsIsoText(ziel) } /** * Der Rumpf eines freien Schreibens. * * Bewusst kurz und mit sichtbaren Luecken in eckigen Klammern. Ein * vorformulierter Text, der fast passt, wird abgeschickt, ohne gelesen zu * werden; eine offensichtliche Luecke nicht. * * Der Bestellungsabsatz steht drin und nicht zur Wahl: Ohne den Nachweis * der Vertretungsmacht kommt die Auskunft nicht, und das faellt erst nach * vier Wochen auf. */ function freierEntwurf(kopf: Briefkopf, empfaenger: Empfaenger, heute: Date): string { return [ absenderBlock(kopf), '', anschriftBlock(empfaenger), '', datumsZeile(kopf.betreuerOrt, heute), '', betreffZeile('[Betreff eintragen]', null, kopf.betreuteName, kopf.betreuteGeburtsdatum), '', 'Sehr geehrte Damen und Herren,', '', bestellungsAbsatz(kopf), '', '[Hier steht dein Anliegen.]', '', 'Für Rückfragen stehe ich gerne zur Verfügung.', '', grussformel(kopf.betreuerName), '', 'Anlage: Kopie der Bestellungsurkunde', ].join('\n') } /** Zieht die Betreffzeile aus einem fertigen Brieftext. */ export function betreffAusText(text: string): string { const zeile = text.split('\n').find((z) => /^Betreff:/i.test(z.trim())) return zeile ? zeile.replace(/^Betreff:\s*/i, '').trim() : '' } export type EntwurfEingabe = { betreuteId: string /** Wer schreibt - mit Anschrift, wenn hinterlegt. */ benutzer: Absender art: SchreibenArt vertragId?: string | null vorgangId?: string | null /** Eigene Vorlage als Grundlage des freien Schreibens. */ vorlageId?: string | null /** Fuer den Test ueberschreibbar. */ heute?: Date } export async function entwurfBauen(eingabe: EntwurfEingabe): Promise { const heute = eingabe.heute ?? heuteAlsTag() const betreute = await db.betreute.findFirst({ where: { id: eingabe.betreuteId, ...nichtGeloescht }, }) if (!betreute) throw new Error('Die betreute Person gibt es nicht (mehr).') const kopf = briefkopfBauen({ betreute: oeffnen('Betreute', betreute), benutzer: eingabe.benutzer, }) const gemeinsam = { art: eingabe.art, wiedervorlageAm: wiedervorlageVorschlag(eingabe.art, heute), vertragId: eingabe.vertragId ?? null, vorgangId: eingabe.vorgangId ?? null, warnungen: [] as string[], } // --- Vertragsbezogene Schreiben ------------------------------------------- if (eingabe.art === 'RUECKKAUFSWERT' || eingabe.art === 'KUENDIGUNG') { if (!eingabe.vertragId) throw new Error('Für dieses Schreiben fehlt der Vertrag.') const vertrag = await db.vertrag.findFirst({ where: { id: eingabe.vertragId, betreuteId: eingabe.betreuteId, ...nichtGeloescht }, }) if (!vertrag) throw new Error('Diesen Vertrag gibt es nicht (mehr).') const offen = oeffnen('Vertrag', vertrag) const empfaenger: Empfaenger = { gesellschaft: vertrag.gesellschaft, anschrift: null } const bezeichnung = `${VERTRAG_ART_BESCHRIFTUNG[vertrag.art]} — ${vertrag.bezeichnung}` if (eingabe.art === 'RUECKKAUFSWERT') { const text = rueckkaufswertAnschreiben({ kopf, empfaenger, vertragsbezeichnung: bezeichnung, vertragsnummer: offen.vertragsnummer, stichtag: heute, heute, }) return { ...gemeinsam, text, betreff: betreffAusText(text), empfaengerName: vertrag.gesellschaft ?? '', empfaengerAnschrift: '', } } const paket = kuendigungsassistent({ kopf, empfaenger, vertragsbezeichnung: bezeichnung, vertragsnummer: offen.vertragsnummer, art: vertrag.art, laufzeit: { beginnAm: vertrag.beginnAm, endeAm: vertrag.endeAm, laufzeitJahre: laufzeitAusVertrag(vertrag.beginnAm, vertrag.endeAm), kuendigungsfristMonate: vertrag.kuendigungsfristMonate, }, kuendigungZum: vertrag.kuendigungZum, heute, }) return { ...gemeinsam, text: paket.schreiben, betreff: betreffAusText(paket.schreiben), empfaengerName: vertrag.gesellschaft ?? '', empfaengerAnschrift: '', warnungen: paket.warnungen, } } // --- Sachstandsanfrage zum Vorgang ---------------------------------------- if (eingabe.art === 'SACHSTAND') { if (!eingabe.vorgangId) throw new Error('Für dieses Schreiben fehlt der Vorgang.') const vorgang = await db.vorgang.findFirst({ where: { id: eingabe.vorgangId, betreuteId: eingabe.betreuteId, ...nichtGeloescht }, // Der benannte Ansprechpartner, nicht irgendeiner der verknuepften // Kontakte: Wer angeredet wird, ist eine Entscheidung und kein Zufall. include: { ansprechpartner: { select: { name: true, anrede: true, organisation: true, strasse: true, plz: true, ort: true, }, }, }, }) if (!vorgang) throw new Error('Diesen Vorgang gibt es nicht (mehr).') const anfrage = sachstandsanfrageErzeugen({ vorgangTitel: vorgang.titel, gegenstelle: vorgang.gegenstelle, aktenzeichen: vorgang.aktenzeichen, letzterKontaktAm: vorgang.letzterKontaktAm, betreute: { vorname: betreute.vorname, nachname: betreute.nachname, geburtsdatum: betreute.geburtsdatum, }, betreuer: { name: eingabe.benutzer.name, aktenzeichenGericht: betreute.aktenzeichenGericht, betreuungsgericht: betreute.betreuungsgericht, }, ansprechpartner: vorgang.ansprechpartner, heute, }) // Ist ein Ansprechpartner benannt, kommt seine Anschrift gleich mit - // sie steht im Kontaktverzeichnis und muesste sonst abgetippt werden. const anschrift = vorgang.ansprechpartner ? [ vorgang.ansprechpartner.strasse, [vorgang.ansprechpartner.plz, vorgang.ansprechpartner.ort] .filter(Boolean) .join(' ') .trim() || null, ] .filter(Boolean) .join('\n') : '' return { ...gemeinsam, text: anfrage.text, betreff: anfrage.betreff, empfaengerName: vorgang.ansprechpartner?.name ?? vorgang.gegenstelle ?? '', empfaengerAnschrift: anschrift, } } // --- Freies Schreiben, wahlweise aus eigener Vorlage ----------------------- if (eingabe.vorlageId) { const vorlage = await db.schreibvorlage.findFirst({ where: { id: eingabe.vorlageId, betreuteId: eingabe.betreuteId, ...nichtGeloescht }, }) if (!vorlage) throw new Error('Diese Vorlage gibt es nicht (mehr).') // Die Werte, die eine Vorlage kennen kann. Vertragsbezogene bleiben // hier leer und erscheinen als sichtbare Luecke - ein freies Schreiben // hat keinen Vertrag. const werte: PlatzhalterWerte = { betreute_name: kopf.betreuteName, betreute_geburtsdatum: kopf.betreuteGeburtsdatum ? formatDatum(kopf.betreuteGeburtsdatum) : null, betreute_anschrift: kopf.betreuteAnschrift, betreuer_name: kopf.betreuerName, betreuer_anschrift: [kopf.betreuerStrasse, [kopf.betreuerPlz, kopf.betreuerOrt].filter(Boolean).join(' ')] .filter((zeile) => zeile && zeile.trim() !== '') .join('\n') || null, betreuer_ort: kopf.betreuerOrt, betreuungsgericht: kopf.betreuungsgericht, aktenzeichen_gericht: kopf.aktenzeichenGericht, bestellung_datum: kopf.bestellungAm ? formatDatum(kopf.bestellungAm) : null, bestellungsabsatz: bestellungsAbsatz(kopf), stichtag: formatDatum(heute), datum_heute: formatDatum(heute), } const rumpf = platzhalterErsetzen(vorlage.text, werte) const betreffErsetzt = platzhalterErsetzen(vorlage.betreff, werte) // Der Briefrahmen kommt dazu - die Vorlage traegt nur den Inhalt. const text = [ absenderBlock(kopf), '', anschriftBlock({ gesellschaft: null, anschrift: null }), '', datumsZeile(kopf.betreuerOrt, heute), '', betreffErsetzt.text ? `Betreff: ${betreffErsetzt.text}` : 'Betreff: [Betreff eintragen]', '', rumpf.text, ].join('\n') return { ...gemeinsam, text, betreff: betreffErsetzt.text, empfaengerName: '', empfaengerAnschrift: '', warnungen: rumpf.unbekannte.length > 0 ? [ `Die Vorlage enthält unbekannte Platzhalter: ${rumpf.unbekannte .map((name) => `{{${name}}}`) .join(', ')}. Sie sind im Text markiert - bitte vor dem Absenden ersetzen.`, ] : [], } } const text = freierEntwurf(kopf, { gesellschaft: null, anschrift: null }, heute) return { ...gemeinsam, text, betreff: '', empfaengerName: '', empfaengerAnschrift: '', } }