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 { return Packer.toBuffer(briefAlsDocx(text)) }