Schreiben als E-Mail uebernehmen
Im Schreiben-Editor gibt es jetzt den Umschalter Brief/E-Mail. Die E-Mail-Fassung wird aus dem - womoeglich bearbeiteten - Brieftext abgeleitet: ohne Absenderblock, Anschrift und Datumszeile, aber mit dem Bestellungsabsatz. Betreff und Text einzeln kopierbar. Die offene Entscheidung aus dem Issue (mailto oder .eml) ist nach Rueckfrage mit BEIDES beantwortet: - mailto fuer kurze Texte. Der Knopf verschwindet, sobald der kodierte Link 1800 Zeichen uebersteigt - mailto uebergibt den Text in der Adresszeile, deren Laenge nirgends zugesichert ist, und ein halbierter Brief faellt erst beim Empfaenger auf. Die Grenze wird NACH der URL-Kodierung gerechnet; 900 Umlaute sind roh harmlos und kodiert weit drueber. - .eml immer: RFC 5322 mit X-Unsent: 1, quoted-printable fuer den Rumpf, RFC-2047-Kodierung fuer Umlaute im Betreff. Bewusst ohne From-Kopf - ein erfundener Absender wuerde je nach Programm uebernommen. Beide Knoepfe erscheinen nur mit E-Mail-Adresse (aus dem Kontakt uebernommen oder von Hand). Ohne Adresse bleibt das Kopieren. Die Ablage kennt den Unterschied: Eine E-Mail heisst in der Akte "E-Mail: ...", und abgelegt wird die E-Mail-Fassung samt Empfaengeradresse im Kopf der PDF - in der Akte soll stehen, was tatsaechlich hinausging. Sicherung in emailFassung(): Findet sich keine Anrede (frei umgebauter Text), wird NICHTS entfernt. Lieber ein Briefkopf zu viel in der Mail als stillschweigend verlorener Inhalt. Geprueft: 20 Einheitstests, vier End-to-End-Tests auf beiden Geraeteprofilen. 590 Unit-Tests, 81 e2e, null Typfehler. Refs #5
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
FileDown,
|
||||
FolderInput,
|
||||
Info,
|
||||
Mail,
|
||||
UserPlus,
|
||||
} from 'lucide-react'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
type SchreibenZustand,
|
||||
} from '@/server/actions/schreiben'
|
||||
import type { SchreibenArt } from '@/lib/schreiben/arten'
|
||||
import { emailFassung, mailtoLink, siehtAusWieEmail } from '@/lib/schreiben/email'
|
||||
|
||||
export type KontaktAuswahl = {
|
||||
id: string
|
||||
@@ -34,6 +36,7 @@ export type KontaktAuswahl = {
|
||||
strasse: string | null
|
||||
plz: string | null
|
||||
ort: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +70,10 @@ export function SchreibenEditor({
|
||||
const [betreff, setBetreff] = React.useState(entwurf.betreff)
|
||||
const [empfaengerName, setEmpfaengerName] = React.useState(entwurf.empfaengerName)
|
||||
const [empfaengerAnschrift, setEmpfaengerAnschrift] = React.useState('')
|
||||
const [empfaengerEmail, setEmpfaengerEmail] = React.useState('')
|
||||
const [versandweg, setVersandweg] = React.useState<'BRIEF' | 'EMAIL'>('BRIEF')
|
||||
const [betreffKopiert, setBetreffKopiert] = React.useState(false)
|
||||
const [mailKopiert, setMailKopiert] = React.useState(false)
|
||||
const [versandAm, setVersandAm] = React.useState(new Date().toISOString().slice(0, 10))
|
||||
const [wiedervorlageAm, setWiedervorlageAm] = React.useState(entwurf.wiedervorlageAm)
|
||||
const [kopiert, setKopiert] = React.useState(false)
|
||||
@@ -94,6 +101,8 @@ export function SchreibenEditor({
|
||||
<input type="hidden" name="wiedervorlageAm" value={wiedervorlageAm} />
|
||||
<input type="hidden" name="vertragId" value={vertragId ?? ''} />
|
||||
<input type="hidden" name="vorgangId" value={vorgangId ?? ''} />
|
||||
<input type="hidden" name="versandweg" value={versandweg} />
|
||||
<input type="hidden" name="empfaengerEmail" value={empfaengerEmail} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -107,6 +116,7 @@ export function SchreibenEditor({
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
)
|
||||
setEmpfaengerEmail(kontakt.email ?? '')
|
||||
}
|
||||
|
||||
async function textKopieren() {
|
||||
@@ -121,6 +131,109 @@ export function SchreibenEditor({
|
||||
|
||||
const abgelegt = Boolean(ablageZustand.erfolg)
|
||||
|
||||
/**
|
||||
* Die E-Mail-Fassung: Betreff und Rumpf getrennt, ohne Briefkopf.
|
||||
*
|
||||
* Abgeleitet aus dem AKTUELLEN Text im Feld - wer den Brief bearbeitet
|
||||
* hat, bekommt die bearbeitete Fassung, nicht den Ursprungsentwurf.
|
||||
*/
|
||||
function EmailAusgabe() {
|
||||
const fassung = emailFassung(text, betreff)
|
||||
const adresseGueltig = siehtAusWieEmail(empfaengerEmail)
|
||||
const mailto = adresseGueltig ? mailtoLink(empfaengerEmail, fassung) : null
|
||||
|
||||
async function kopieren(inhalt: string, merker: (wert: boolean) => void) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(inhalt)
|
||||
merker(true)
|
||||
window.setTimeout(() => merker(false), 2500)
|
||||
} catch {
|
||||
/* Zwischenablage gesperrt - dann markiert man eben von Hand. */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-md border p-4">
|
||||
<Feld
|
||||
id="emailBetreff"
|
||||
beschriftung="Betreff der E-Mail"
|
||||
erklaerung="Aus der Betreffzeile des Briefes übernommen."
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Input id="emailBetreff" value={fassung.betreff} readOnly className="flex-1" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => void kopieren(fassung.betreff, setBetreffKopiert)}
|
||||
>
|
||||
<Copy aria-hidden="true" />
|
||||
{betreffKopiert ? 'Kopiert' : 'Kopieren'}
|
||||
</Button>
|
||||
</div>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="emailText"
|
||||
beschriftung="Text der E-Mail"
|
||||
erklaerung="Ohne Absenderblock, Anschrift und Datumszeile — die gehören in den Brief, nicht in die Mail. Der Nachweis der Bestellung bleibt drin."
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
id="emailText"
|
||||
value={fassung.text}
|
||||
readOnly
|
||||
rows={14}
|
||||
className="font-mono text-sm leading-relaxed"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => void kopieren(fassung.text, setMailKopiert)}
|
||||
>
|
||||
<Copy aria-hidden="true" />
|
||||
{mailKopiert ? 'Kopiert' : 'Text kopieren'}
|
||||
</Button>
|
||||
</div>
|
||||
</Feld>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{mailto && (
|
||||
<Button asChild variant="outline">
|
||||
<a href={mailto}>
|
||||
<Mail aria-hidden="true" />
|
||||
Im Mailprogramm öffnen
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{adresseGueltig && (
|
||||
<form action="/schreiben/eml" method="post" className="inline">
|
||||
<VersteckteFelder />
|
||||
<Button type="submit" variant="outline">
|
||||
<FileDown aria-hidden="true" />
|
||||
Als E-Mail-Entwurf (.eml)
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!adresseGueltig && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Trag oben eine E-Mail-Adresse ein, dann erscheinen hier die Knöpfe fürs
|
||||
Mailprogramm. Kopieren geht auch ohne.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{adresseGueltig && !mailto && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Der Text ist zu lang für einen mailto-Link — manche Mailprogramme würden ihn
|
||||
stillschweigend abschneiden. Nimm die .eml-Datei, sie trägt den vollen Text.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{warnungen.length > 0 && (
|
||||
@@ -193,6 +306,21 @@ export function SchreibenEditor({
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="empfaengerEmail"
|
||||
beschriftung="E-Mail-Adresse"
|
||||
erklaerung="Nur nötig, wenn das Schreiben als E-Mail hinausgehen soll. Wird aus dem gewählten Kontakt übernommen."
|
||||
>
|
||||
<Input
|
||||
id="empfaengerEmail"
|
||||
type="email"
|
||||
value={empfaengerEmail}
|
||||
onChange={(ereignis) => setEmpfaengerEmail(ereignis.target.value)}
|
||||
maxLength={200}
|
||||
placeholder="service@versicherung.de"
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
{empfaengerName.trim() !== '' && empfaengerAnschrift.trim() !== '' && (
|
||||
<form action={kontaktAnlegen}>
|
||||
<VersteckteFelder />
|
||||
@@ -263,8 +391,32 @@ export function SchreibenEditor({
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Und jetzt?</CardTitle>
|
||||
<CardDescription>
|
||||
Wie geht es hinaus? Die Wahl bestimmt auch, wie das Schreiben in der Akte
|
||||
gekennzeichnet wird.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<div className="flex gap-2" role="group" aria-label="Versandweg">
|
||||
<Button
|
||||
type="button"
|
||||
variant={versandweg === 'BRIEF' ? 'default' : 'outline'}
|
||||
onClick={() => setVersandweg('BRIEF')}
|
||||
>
|
||||
<FileDown aria-hidden="true" />
|
||||
Als Brief
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={versandweg === 'EMAIL' ? 'default' : 'outline'}
|
||||
onClick={() => setVersandweg('EMAIL')}
|
||||
>
|
||||
<Mail aria-hidden="true" />
|
||||
Als E-Mail
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{versandweg === 'EMAIL' && <EmailAusgabe />}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Feld
|
||||
id="versandAm"
|
||||
@@ -293,26 +445,31 @@ export function SchreibenEditor({
|
||||
</Feld>
|
||||
</div>
|
||||
|
||||
{/* Word-Datei: eigene Route, damit der Browser einen echten
|
||||
Download bekommt. */}
|
||||
<form action="/schreiben/docx" method="post" className="inline">
|
||||
<VersteckteFelder />
|
||||
<Button type="submit" variant="outline">
|
||||
<FileDown aria-hidden="true" />
|
||||
Als Word-Datei
|
||||
</Button>
|
||||
</form>
|
||||
{versandweg === 'BRIEF' && (
|
||||
<>
|
||||
{/* Word-Datei: eigene Route, damit der Browser einen echten
|
||||
Download bekommt. */}
|
||||
<form action="/schreiben/docx" method="post" className="inline">
|
||||
<VersteckteFelder />
|
||||
<Button type="submit" variant="outline">
|
||||
<FileDown aria-hidden="true" />
|
||||
Als Word-Datei
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Alert variant="hinweis">
|
||||
<Info aria-hidden="true" />
|
||||
<AlertDescription>
|
||||
In die Akte kommt eine <strong>PDF-Fassung</strong> dieses Textes — sie ist der
|
||||
Nachweis, was hinausgegangen ist, und lässt sich nicht mehr ändern. Die Word-Datei
|
||||
ist die Arbeitsfassung zum Ausdrucken und Unterschreiben.{' '}
|
||||
<strong>Änderst du sie danach noch in Word</strong>, weicht die Akte davon ab — dann
|
||||
lieber erst dort fertig schreiben, den Text hierher zurückkopieren und dann ablegen.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert variant="hinweis">
|
||||
<Info aria-hidden="true" />
|
||||
<AlertDescription>
|
||||
In die Akte kommt eine <strong>PDF-Fassung</strong> dieses Textes — sie ist der
|
||||
Nachweis, was hinausgegangen ist, und lässt sich nicht mehr ändern. Die
|
||||
Word-Datei ist die Arbeitsfassung zum Ausdrucken und Unterschreiben.{' '}
|
||||
<strong>Änderst du sie danach noch in Word</strong>, weicht die Akte davon ab —
|
||||
dann lieber erst dort fertig schreiben, den Text hierher zurückkopieren und dann
|
||||
ablegen.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ablageZustand.fehler && (
|
||||
<Alert variant="fehler">
|
||||
|
||||
Reference in New Issue
Block a user