Schreiben erzeugen, ausgeben und ablegen
Die Anschreiben gab es bisher nur als Text zum Kopieren. Danach war
Handarbeit: in Word nachbauen, ausdrucken, abschicken - und noch einmal
von Hand als Dokument in die Akte legen. Der letzte Schritt wurde am
haeufigsten vergessen, und dann fehlt der Nachweis im Bericht.
Neu unter /schreiben/neu: bearbeiten, als Word-Datei herunterladen, in
die Akte legen, Wiedervorlage setzen - in einem Zug. Eine Seite fuer alle
Arten und alle drei Empfaengerwege, statt die Bearbeitung dreimal an
Vertrag, Vorgang und Freitext nachzubauen.
Der dritte Weg ist der eigentliche Zugewinn: Schreiben an eine Stelle,
zu der es noch keinen Datensatz gibt. Am Anfang einer Betreuung ist das
der Normalfall - man fragt ja an, um zu erfahren, ob dort ueberhaupt
etwas besteht. Aus der freien Anschrift laesst sich mit einem Klick ein
Kontakt anlegen.
Zwei Entscheidungen, die erklaert gehoeren:
Die Akte bekommt ein PDF, heruntergeladen wird .docx. Der
Objektspeicher nimmt nur PDF und Bilder an, geprueft an den Magic Bytes.
Eine .docx ist ein ZIP-Archiv; sie durchzulassen hiesse jedes ZIP
durchzulassen, und diese Pruefung ist eine bewusste Zusage der
Anwendung. Sachlich ist PDF fuer den Nachweis ohnehin richtiger: Es
aendert sich nicht mehr. Die Oberflaeche sagt dazu, dass eine spaeter in
Word geaenderte Fassung von der Akte abweicht.
Der Berichtssatz in berichte/docx.ts bleibt unangetastet. Er setzt einen
Bericht mit Kopfzeilen und Unterschriftenblock; ein Brief braucht
Absender, Anschriftenfeld, Datumszeile und Grussformel. Beides in eine
Funktion zu zwingen haette beide Ausgaben schlechter gemacht. Der
Umsetzungshinweis im Issue ("muss nur angebunden werden") war insoweit
falsch - von mir selbst geschrieben.
Die vorhandenen Erzeuger (Rueckkaufswert, Kuendigung, Sachstandsanfrage)
bleiben unveraendert; ihr Wortlaut ist getestet und erprobt.
Geprueft: zehn Einheitstests, darunter der Gleichschritt der
Wiedervorlagefristen ueber zwei Module hinweg. Fuenf End-to-End-Tests
auf beiden Geraeteprofilen: Entwurf, Vertragsbezug, Word-Download,
Ablage samt Wiedervorlage, Kontakt aus freier Anschrift.
Refs #4
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import 'server-only'
|
||||
import {
|
||||
AlignmentType,
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
convertMillimetersToTwip,
|
||||
} from 'docx'
|
||||
|
||||
/**
|
||||
* Ein Brief als Word-Datei.
|
||||
*
|
||||
* Getrennt von src/lib/berichte/docx.ts, und das ist Absicht. Der dortige
|
||||
* Satz baut einen BERICHT: Titel, Kopfzeilen mit betreuter Person,
|
||||
* Aktenzeichen und Berichtszeitraum, am Ende ein Unterschriftenblock. Ein
|
||||
* Brief braucht etwas anderes - Absender, Anschriftenfeld, Datumszeile
|
||||
* rechts, Betreff, Anrede, Grussformel. Beides in eine Funktion zu zwingen
|
||||
* haette beide Ausgaben schlechter gemacht.
|
||||
*
|
||||
* Warum ueberhaupt .docx und nicht nur PDF? Weil jeder Fall eine
|
||||
* Besonderheit hat, die in den Brief gehoert. Wer das Schreiben vor dem
|
||||
* Absenden noch anfassen will, braucht eine Datei, die sich anfassen
|
||||
* laesst. Die Fassung fuer die Akte ist ein PDF - siehe
|
||||
* src/lib/schreiben/pdf.tsx.
|
||||
*
|
||||
* Das Layout folgt DIN 5008 lose: Anschriftenfeld oben links, Datum
|
||||
* rechts, Betreff fett. Nicht auf den Millimeter - der Brief geht in einen
|
||||
* gewoehnlichen Umschlag, nicht in ein Fensterkuvert der Behoerde.
|
||||
*/
|
||||
|
||||
const SCHRIFT = 'Arial'
|
||||
const GROESSE = 11
|
||||
|
||||
/**
|
||||
* Der Entwurfstext kommt als eine Zeichenkette mit Zeilenumbruechen.
|
||||
*
|
||||
* Er ist bereits vollstaendig gesetzt: Absender, Anschrift, Datum,
|
||||
* Betreff, Anrede, Text, Gruss. Diese Funktion formatiert ihn nur - sie
|
||||
* denkt sich nichts dazu und laesst nichts weg. Was der Benutzer im
|
||||
* Browser gesehen hat, steht auch in der Datei.
|
||||
*/
|
||||
function absatz(
|
||||
text: string,
|
||||
optionen: { fett?: boolean; rechts?: boolean; abstandNach?: number } = {},
|
||||
): Paragraph {
|
||||
return new Paragraph({
|
||||
alignment: optionen.rechts ? AlignmentType.RIGHT : AlignmentType.LEFT,
|
||||
spacing: { after: optionen.abstandNach ?? 0, line: 276 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text,
|
||||
bold: optionen.fett ?? false,
|
||||
size: GROESSE * 2, // docx rechnet in halben Punkten
|
||||
font: SCHRIFT,
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Erkennt die Betreffzeile, damit sie fett gesetzt werden kann.
|
||||
*
|
||||
* Die erzeugten Schreiben beginnen sie mit "Betreff:" - siehe
|
||||
* betreffZeile() in src/lib/vertraege/anschreiben.ts. Steht dort einmal
|
||||
* etwas anderes, wird die Zeile eben normal gesetzt; ein Brief mit
|
||||
* unfettem Betreff ist immer noch ein brauchbarer Brief.
|
||||
*/
|
||||
function istBetreff(zeile: string): boolean {
|
||||
return /^Betreff:/i.test(zeile.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Datumszeile steht rechts, wie im deutschen Geschaeftsbrief.
|
||||
* Erkennbar daran, dass sie mit einem Ort und Komma oder direkt mit einem
|
||||
* Datum beginnt und sonst nichts enthaelt.
|
||||
*/
|
||||
function istDatumszeile(zeile: string): boolean {
|
||||
return /^([A-Za-zÄÖÜäöüß .-]+,\s*)?\d{1,2}\.\d{1,2}\.\d{4}$/.test(zeile.trim())
|
||||
}
|
||||
|
||||
export function briefAlsDocx(text: string): Document {
|
||||
const zeilen = text.split('\n')
|
||||
|
||||
const inhalt: Paragraph[] = zeilen.map((zeile) => {
|
||||
if (zeile.trim() === '') return absatz('', { abstandNach: 120 })
|
||||
if (istBetreff(zeile)) return absatz(zeile, { fett: true, abstandNach: 120 })
|
||||
if (istDatumszeile(zeile)) return absatz(zeile, { rechts: true })
|
||||
return absatz(zeile)
|
||||
})
|
||||
|
||||
return new Document({
|
||||
creator: 'Betreuungs-Cockpit',
|
||||
description: 'Schreiben aus der Betreuungsakte',
|
||||
title: 'Schreiben',
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
page: {
|
||||
margin: {
|
||||
top: convertMillimetersToTwip(25),
|
||||
right: convertMillimetersToTwip(20),
|
||||
// Links etwas mehr: Da wird gelocht und abgeheftet.
|
||||
left: convertMillimetersToTwip(25),
|
||||
bottom: convertMillimetersToTwip(20),
|
||||
},
|
||||
},
|
||||
},
|
||||
children: inhalt,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/** Fertige .docx-Datei als Buffer. */
|
||||
export async function briefDocxErzeugen(text: string): Promise<Buffer> {
|
||||
return Packer.toBuffer(briefAlsDocx(text))
|
||||
}
|
||||
Reference in New Issue
Block a user