Eigene Vorlagen fuer Anschreiben
Wer zum dritten Mal denselben Brief tippt, tippt ihn beim vierten Mal
falsch. Neu unter Einstellungen -> Schreibvorlagen: eigene Texte
hinterlegen, beim freien Schreiben auswaehlen, Platzhalter fuellen sich
aus den Daten, die ohnehin im System stehen.
Aufbau:
- Die Ersetzung als reine Funktionen in schreiben/platzhalter.ts. Drei
Faelle, keiner davon still: Wert einsetzen, sichtbare Luecke
[Aktenzeichen des Gerichts], unbekannter Platzhalter markiert. Ein
Tippfehler in der Vorlage darf das Schreiben nicht verhindern - aber
er muss ins Auge springen.
- Die Bestellungsabsatz-Warnung ist zweistufig: Der erste Speicherversuch
ohne ihn kommt mit einer Rueckfrage zurueck, erst die Bestaetigung
speichert. Ohne den Nachweis der Vertretungsmacht kommt keine Auskunft,
und das faellt erst nach vier Wochen auf. Ein ausformulierter Absatz
zaehlt so viel wie der Platzhalter, ein halber Nachweis ("Beschluss der
Eigentuemerversammlung") nicht.
- Vier mitgelieferte Vorlagen bleiben Code und damit unveraenderlich;
"Als Kopie uebernehmen" legt eine eigene Fassung an. Fuer Rueckkaufswert
und Kuendigung fuehrt die Oberflaeche weiter zuerst zu den
fallbezogenen Erzeugern - die rechnen Kuendigungstermine aus, was eine
Vorlage nicht kann.
- Vorlagen haengen an der betreuten Person, nicht am Benutzer: Sie
beziehen sich auf ihren Fall, liegen im Papierkorb wie alles andere und
sind im Vollexport ihrer Akte enthalten.
Der Editor haelt Betreff und Text als eigenen State. Beim Wechsel der
Vorlage ueberlebte der alte Entwurf die Navigation, die Vorlage kam nie
im Textfeld an - ein key auf dem Editor setzt ihn neu auf. Gefunden hat
das erst der End-to-End-Test.
Geprueft: dreizehn Einheitstests fuer die Ersetzung, sechs
End-to-End-Tests fuer Warnung, Kopie, Aufloesung und Papierkorb.
Refs #7
This commit is contained in:
@@ -19,6 +19,8 @@ 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.
|
||||
@@ -109,6 +111,8 @@ export type EntwurfEingabe = {
|
||||
art: SchreibenArt
|
||||
vertragId?: string | null
|
||||
vorgangId?: string | null
|
||||
/** Eigene Vorlage als Grundlage des freien Schreibens. */
|
||||
vorlageId?: string | null
|
||||
/** Fuer den Test ueberschreibbar. */
|
||||
heute?: Date
|
||||
}
|
||||
@@ -230,7 +234,64 @@ export async function entwurfBauen(eingabe: EntwurfEingabe): Promise<SchreibenEn
|
||||
}
|
||||
}
|
||||
|
||||
// --- Freies Schreiben ------------------------------------------------------
|
||||
// --- 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,
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Die mitgelieferten Schreibvorlagen - als Platzhalter-Fassung.
|
||||
*
|
||||
* Sie sind Code und damit unveraenderlich; "wiederherstellen" heisst,
|
||||
* sie erneut als Kopie in die eigenen Vorlagen zu holen.
|
||||
*
|
||||
* Bewusst NICHT dieselben Texte wie die fest verdrahteten Erzeuger in
|
||||
* rueckkaufswert.ts und kuendigungsassistent.ts: Die dortigen bauen
|
||||
* fallbezogene Saetze (Kuendigungstermin, Warnungen), die eine Vorlage
|
||||
* nicht leisten kann. Diese hier sind der gemeinsame Kern als
|
||||
* Ausgangspunkt fuer eigene Fassungen - die Spezialerzeuger bleiben der
|
||||
* bessere Weg, wo es sie gibt, und die Oberflaeche fuehrt weiter zuerst
|
||||
* dorthin.
|
||||
*/
|
||||
|
||||
export type MitgelieferteVorlage = {
|
||||
schluessel: string
|
||||
name: string
|
||||
beschreibung: string
|
||||
betreff: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export const MITGELIEFERTE_VORLAGEN: readonly MitgelieferteVorlage[] = [
|
||||
{
|
||||
schluessel: 'auskunftsersuchen',
|
||||
name: 'Auskunftsersuchen',
|
||||
beschreibung: 'Bestehen bei Ihnen Verträge? Der erste Brief an eine unbekannte Stelle.',
|
||||
betreff: 'Auskunftsersuchen — {{betreute_name}}, geb. {{betreute_geburtsdatum}}',
|
||||
text: [
|
||||
'Sehr geehrte Damen und Herren,',
|
||||
'',
|
||||
'{{bestellungsabsatz}}',
|
||||
'',
|
||||
'zur Ordnung der Vermögensangelegenheiten bitte ich um Auskunft, ob bei',
|
||||
'Ihnen Verträge, Konten oder sonstige Geschäftsbeziehungen für die von mir',
|
||||
'vertretene Person bestehen oder in den letzten Jahren bestanden haben.',
|
||||
'',
|
||||
'Falls ja, bitte ich um eine Aufstellung mit Vertragsnummern, aktuellem',
|
||||
'Stand und den jährlichen Beiträgen bzw. Leistungen.',
|
||||
'',
|
||||
'Für Rückfragen stehe ich gerne zur Verfügung.',
|
||||
'',
|
||||
'Mit freundlichen Grüßen',
|
||||
'{{betreuer_name}}',
|
||||
'',
|
||||
'Anlage: Kopie der Bestellungsurkunde',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
schluessel: 'adressaenderung',
|
||||
name: 'Adressänderung',
|
||||
beschreibung: 'Neue Anschrift nach Heimeinzug mitteilen.',
|
||||
betreff: 'Anschriftenänderung — {{betreute_name}}, geb. {{betreute_geburtsdatum}}',
|
||||
text: [
|
||||
'Sehr geehrte Damen und Herren,',
|
||||
'',
|
||||
'{{bestellungsabsatz}}',
|
||||
'',
|
||||
'die Anschrift der von mir vertretenen Person hat sich geändert. Die neue',
|
||||
'Anschrift lautet:',
|
||||
'',
|
||||
'{{betreute_anschrift}}',
|
||||
'',
|
||||
'Bitte stellen Sie künftige Post zu dieser Sache außerdem an mich als',
|
||||
'rechtlichen Betreuer zu.',
|
||||
'',
|
||||
'Mit freundlichen Grüßen',
|
||||
'{{betreuer_name}}',
|
||||
'',
|
||||
'Anlage: Kopie der Bestellungsurkunde',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
schluessel: 'vollmachtsnachweis',
|
||||
name: 'Nachweis der Vertretungsmacht',
|
||||
beschreibung: 'Wenn eine Stelle die Auskunft verweigert: der Nachweis, förmlich.',
|
||||
betreff: 'Nachweis der Vertretungsmacht — {{betreute_name}}, geb. {{betreute_geburtsdatum}}',
|
||||
text: [
|
||||
'Sehr geehrte Damen und Herren,',
|
||||
'',
|
||||
'auf Ihr Schreiben, in dem Sie um einen Nachweis meiner Vertretungsmacht',
|
||||
'bitten, teile ich mit:',
|
||||
'',
|
||||
'{{bestellungsabsatz}}',
|
||||
'',
|
||||
'Eine Kopie der Bestellungsurkunde liegt bei. Das Original stelle ich bei',
|
||||
'Bedarf zur Einsicht — bitte nennen Sie mir dafür einen Termin.',
|
||||
'',
|
||||
'Ich bitte nunmehr um Bearbeitung meines Anliegens vom {{datum_heute}}.',
|
||||
'',
|
||||
'Mit freundlichen Grüßen',
|
||||
'{{betreuer_name}}',
|
||||
'',
|
||||
'Anlage: Kopie der Bestellungsurkunde',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
schluessel: 'rueckkaufswert_kurz',
|
||||
name: 'Rückkaufswert (Kurzfassung)',
|
||||
beschreibung:
|
||||
'Die Kurzfassung der Rückkaufswert-Anfrage. Für den vollen Fragenkatalog nimm den Weg über den Vertrag.',
|
||||
betreff:
|
||||
'Auskunft über Rückkaufswert — {{vertrag_bezeichnung}}, Nr. {{vertrag_nummer}}',
|
||||
text: [
|
||||
'Sehr geehrte Damen und Herren,',
|
||||
'',
|
||||
'{{bestellungsabsatz}}',
|
||||
'',
|
||||
'zu dem oben genannten Vertrag bei {{vertrag_gesellschaft}} bitte ich um',
|
||||
'Mitteilung des Rückkaufswertes einschließlich Überschussanteilen zum',
|
||||
'Stichtag {{stichtag}}, außerdem der beitragsfreien Versicherungssumme.',
|
||||
'',
|
||||
'Da im Verfahren beim Sozialhilfeträger eine Frist läuft, bitte ich um',
|
||||
'Antwort innerhalb von vier Wochen.',
|
||||
'',
|
||||
'Mit freundlichen Grüßen',
|
||||
'{{betreuer_name}}',
|
||||
'',
|
||||
'Anlage: Kopie der Bestellungsurkunde',
|
||||
].join('\n'),
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Platzhalter in Schreibvorlagen.
|
||||
*
|
||||
* Rein und ohne Datenbank. Der Vertrag ist einfach: {{name}} wird ersetzt,
|
||||
* und zwar nach drei Regeln, die alle demselben Grundsatz folgen -
|
||||
* NICHTS geht stillschweigend schief:
|
||||
*
|
||||
* 1. Bekannter Platzhalter, Daten vorhanden -> Wert eingesetzt.
|
||||
* 2. Bekannter Platzhalter, Daten fehlen -> sichtbare Luecke
|
||||
* "[Aktenzeichen]". Eine stille Leerstelle wuerde erst der Empfaenger
|
||||
* bemerken - oder niemand.
|
||||
* 3. Unbekannter Platzhalter -> markiert als
|
||||
* "[Unbekannter Platzhalter: {{xyz}}]". Kein Abbruch: Ein Tippfehler
|
||||
* in einer Vorlage darf das Schreiben nicht verhindern, aber er muss
|
||||
* ins Auge springen. Falsch ausgefuellt ist schlimmer als
|
||||
* offensichtlich leer.
|
||||
*/
|
||||
|
||||
export type PlatzhalterName =
|
||||
| 'betreute_name'
|
||||
| 'betreute_geburtsdatum'
|
||||
| 'betreute_anschrift'
|
||||
| 'betreuer_name'
|
||||
| 'betreuungsgericht'
|
||||
| 'aktenzeichen_gericht'
|
||||
| 'bestellung_datum'
|
||||
| 'bestellungsabsatz'
|
||||
| 'vertrag_gesellschaft'
|
||||
| 'vertrag_nummer'
|
||||
| 'vertrag_bezeichnung'
|
||||
| 'stichtag'
|
||||
| 'datum_heute'
|
||||
|
||||
export type PlatzhalterBeschreibung = {
|
||||
name: PlatzhalterName
|
||||
beschriftung: string
|
||||
/** Woher der Wert kommt - steht in der Hilfe neben dem Editor. */
|
||||
quelle: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Die verfuegbaren Platzhalter, in der Reihenfolge der Anzeige.
|
||||
*
|
||||
* Diese Liste steht beim Bearbeiten NEBEN dem Editor - niemand soll
|
||||
* Platzhalternamen raten muessen.
|
||||
*/
|
||||
export const PLATZHALTER: readonly PlatzhalterBeschreibung[] = [
|
||||
{ name: 'betreute_name', beschriftung: 'Name der betreuten Person', quelle: 'Stammdaten' },
|
||||
{ name: 'betreute_geburtsdatum', beschriftung: 'Geburtsdatum', quelle: 'Stammdaten' },
|
||||
{ name: 'betreute_anschrift', beschriftung: 'Anschrift (Heim)', quelle: 'Stammdaten' },
|
||||
{ name: 'betreuer_name', beschriftung: 'Dein Name', quelle: 'Dein Zugang' },
|
||||
{ name: 'betreuungsgericht', beschriftung: 'Betreuungsgericht', quelle: 'Stammdaten' },
|
||||
{ name: 'aktenzeichen_gericht', beschriftung: 'Aktenzeichen des Gerichts', quelle: 'Stammdaten' },
|
||||
{ name: 'bestellung_datum', beschriftung: 'Datum der Bestellung', quelle: 'Stammdaten' },
|
||||
{
|
||||
name: 'bestellungsabsatz',
|
||||
beschriftung: 'Der ganze Bestellungsabsatz',
|
||||
quelle: 'Aus Gericht, Aktenzeichen und Bestellungsdatum gebaut',
|
||||
},
|
||||
{ name: 'vertrag_gesellschaft', beschriftung: 'Gesellschaft', quelle: 'Vertrag, falls gewählt' },
|
||||
{ name: 'vertrag_nummer', beschriftung: 'Vertragsnummer', quelle: 'Vertrag, falls gewählt' },
|
||||
{ name: 'vertrag_bezeichnung', beschriftung: 'Vertragsbezeichnung', quelle: 'Vertrag, falls gewählt' },
|
||||
{ name: 'stichtag', beschriftung: 'Stichtag', quelle: 'Beim Erzeugen gewählt' },
|
||||
{ name: 'datum_heute', beschriftung: 'Heutiges Datum', quelle: 'Beim Erzeugen' },
|
||||
]
|
||||
|
||||
const BEKANNT = new Set<string>(PLATZHALTER.map((p) => p.name))
|
||||
|
||||
/** Die sichtbare Luecke, wenn Daten fehlen: "[Aktenzeichen des Gerichts]". */
|
||||
function luecke(name: PlatzhalterName): string {
|
||||
const eintrag = PLATZHALTER.find((p) => p.name === name)
|
||||
return `[${eintrag?.beschriftung ?? name}]`
|
||||
}
|
||||
|
||||
export type PlatzhalterWerte = Partial<Record<PlatzhalterName, string | null>>
|
||||
|
||||
export type ErsetzungsErgebnis = {
|
||||
text: string
|
||||
/** Bekannte Platzhalter ohne Daten - als Luecke eingesetzt. */
|
||||
luecken: PlatzhalterName[]
|
||||
/** Unbekannte Platzhalter - markiert im Text. */
|
||||
unbekannte: string[]
|
||||
}
|
||||
|
||||
/** Findet alle {{...}}-Vorkommen. Grosszuegig beim Namen: auch Tippfehler
|
||||
* sollen gefunden (und dann markiert) werden. */
|
||||
const MUSTER = /\{\{\s*([a-zA-Z0-9_äöüÄÖÜß-]+)\s*\}\}/g
|
||||
|
||||
export function platzhalterErsetzen(
|
||||
vorlage: string,
|
||||
werte: PlatzhalterWerte,
|
||||
): ErsetzungsErgebnis {
|
||||
const luecken = new Set<PlatzhalterName>()
|
||||
const unbekannte = new Set<string>()
|
||||
|
||||
const text = vorlage.replace(MUSTER, (treffer, roh: string) => {
|
||||
const name = roh.trim()
|
||||
|
||||
if (!BEKANNT.has(name)) {
|
||||
unbekannte.add(name)
|
||||
return `[Unbekannter Platzhalter: {{${name}}}]`
|
||||
}
|
||||
|
||||
const wert = werte[name as PlatzhalterName]
|
||||
if (wert === null || wert === undefined || wert.trim() === '') {
|
||||
luecken.add(name as PlatzhalterName)
|
||||
return luecke(name as PlatzhalterName)
|
||||
}
|
||||
|
||||
return wert
|
||||
})
|
||||
|
||||
return { text, luecken: [...luecken], unbekannte: [...unbekannte] }
|
||||
}
|
||||
|
||||
/** Welche Platzhalter benutzt eine Vorlage? Fuer die Anzeige in der Liste. */
|
||||
export function benutztePlatzhalter(vorlage: string): string[] {
|
||||
const gefunden = new Set<string>()
|
||||
for (const treffer of vorlage.matchAll(MUSTER)) {
|
||||
gefunden.add(treffer[1]!.trim())
|
||||
}
|
||||
return [...gefunden]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fehlt der Bestellungsabsatz?
|
||||
*
|
||||
* Erfuellt ist die Pruefung durch den Platzhalter {{bestellungsabsatz}}
|
||||
* ODER durch einen von Hand geschriebenen Absatz, der erkennbar die
|
||||
* Bestellung nachweist (Beschluss + Betreuer im selben Text). Wer den
|
||||
* Absatz ausformuliert hat, soll nicht gegaengelt werden.
|
||||
*
|
||||
* Die Warnung ist eine Warnung, kein Verbot: Ein Schreiben an die eigene
|
||||
* Hausverwaltung braucht keinen Bestellungsnachweis. Aber wer ihn
|
||||
* weglaesst, soll es wissentlich tun.
|
||||
*/
|
||||
export function bestellungsabsatzFehlt(vorlage: string): boolean {
|
||||
if (/\{\{\s*bestellungsabsatz\s*\}\}/.test(vorlage)) return false
|
||||
const klein = vorlage.toLowerCase()
|
||||
const nennBestellung =
|
||||
klein.includes('beschluss') && (klein.includes('betreuer') || klein.includes('betreuung'))
|
||||
return !nennBestellung
|
||||
}
|
||||
Reference in New Issue
Block a user