diff --git a/docs/todo.md b/docs/todo.md index c01d79be..a2c90dbc 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,21 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **↗ E-Mail-Postfach: Weiterleiten + Erneut senden** + - **Weiterleiten** (Compose-Modal-Erweiterung): neuer Button im + EmailDetail öffnet das ComposeEmailModal im Forward-Modus – + To-Feld leer (User trägt den neuen Empfänger ein), Betreff mit + „Fwd:"-Prefix, Body mit zitierten Original-Headern (Von, An, + Datum, Betreff) + Original-Text. + - **Erneut senden** (One-Click): schickt die Mail noch einmal an + die ursprüngliche Empfänger-Adresse (= die Stressfrei-Adresse + selbst). Damit läuft sie durch die heute hinterlegten Forwards + und landet beim aktuell konfigurierten Kunden-Postfach – Use-Case: + Stressfrei-Adresse wurde nach Empfang umgestellt, Original ist nur + in der alten Inbox. Confirm-Dialog mit Hinweis, dass Anhänge nicht + erneut mit gesendet werden (Weiterleiten dafür nutzen). Toast für + Erfolg/Fehler. + - [x] **🔍 E-Mail-Postfach: Suche + erweiterte Filter (Variante B)** - Suchleiste über der Email-Liste – durchsucht parallel Subject, From-Address/Name und Body. diff --git a/frontend/src/components/email/ComposeEmailModal.tsx b/frontend/src/components/email/ComposeEmailModal.tsx index 30f14b23..c62811e2 100644 --- a/frontend/src/components/email/ComposeEmailModal.tsx +++ b/frontend/src/components/email/ComposeEmailModal.tsx @@ -11,6 +11,7 @@ interface ComposeEmailModalProps { onClose: () => void; account: MailboxAccount; replyTo?: CachedEmail; + forwardOf?: CachedEmail; // Weiterleiten: Body vorausgefüllt, To leer onSuccess?: () => void; contractId?: number; // Optional: Vertrag dem die gesendete E-Mail zugeordnet wird } @@ -20,6 +21,7 @@ export default function ComposeEmailModal({ onClose, account, replyTo, + forwardOf, onSuccess, contractId, }: ComposeEmailModalProps) { @@ -47,6 +49,30 @@ export default function ComposeEmailModal({ ? `\n\n--- Ursprüngliche Nachricht ---\nVon: ${replyTo.fromName || replyTo.fromAddress}\nAm: ${originalDate}\n\n${replyTo.textBody}` : ''; setBody(quotedText); + } else if (forwardOf) { + // Weiterleiten: To leer (User trägt selbst ein), Betreff mit „Fwd:" + setTo(''); + const existingSubject = forwardOf.subject || ''; + const hasFwdPrefix = /^(Fwd|Wg):\s*/i.test(existingSubject); + setSubject(hasFwdPrefix ? existingSubject : `Fwd: ${existingSubject}`); + // Original-Header + Body zitieren (mehr Felder als bei Reply, damit + // der weitergeleitete Kontext erhalten bleibt) + const originalDate = new Date(forwardOf.receivedAt).toLocaleString('de-DE'); + let toLine = ''; + try { + const parsed = JSON.parse(forwardOf.toAddresses || '[]'); + if (Array.isArray(parsed) && parsed.length > 0) { + toLine = `\nAn: ${parsed.join(', ')}`; + } + } catch { + // toAddresses war kein JSON – ignorieren + } + const quotedText = forwardOf.textBody + ? `\n\n--- Weitergeleitete Nachricht ---\nVon: ${ + forwardOf.fromName || forwardOf.fromAddress + }${toLine}\nDatum: ${originalDate}\nBetreff: ${existingSubject}\n\n${forwardOf.textBody}` + : ''; + setBody(quotedText); } else { // Neue E-Mail: Felder leer setTo(''); @@ -57,7 +83,7 @@ export default function ComposeEmailModal({ setAttachments([]); setError(null); } - }, [isOpen, replyTo]); + }, [isOpen, replyTo, forwardOf]); // Maximale Dateigröße: 10 MB const MAX_FILE_SIZE = 10 * 1024 * 1024; @@ -194,7 +220,7 @@ export default function ComposeEmailModal({
diff --git a/frontend/src/components/email/EmailClientTab.tsx b/frontend/src/components/email/EmailClientTab.tsx index 51346c92..92f89d1b 100644 --- a/frontend/src/components/email/EmailClientTab.tsx +++ b/frontend/src/components/email/EmailClientTab.tsx @@ -26,6 +26,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) { const [showCompose, setShowCompose] = useState(false); const [showAssign, setShowAssign] = useState(false); const [replyToEmail, setReplyToEmail] = useState(null); + const [forwardEmail, setForwardEmail] = useState(null); // Such- und Filterzustand. Alle Filter sind AND-verknüpft im Backend. const [searchQuery, setSearchQuery] = useState(''); @@ -193,14 +194,74 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) { const handleReply = () => { setReplyToEmail(emailDetail || null); + setForwardEmail(null); + setShowCompose(true); + }; + + const handleForward = () => { + setForwardEmail(emailDetail || null); + setReplyToEmail(null); setShowCompose(true); }; const handleNewEmail = () => { setReplyToEmail(null); + setForwardEmail(null); setShowCompose(true); }; + // "Erneut senden": die E-Mail an die ursprüngliche Empfänger-Adresse + // (= die Stressfrei-Adresse selbst) noch einmal schicken. Use-Case: + // wenn die Forwards der Stressfrei-Adresse zwischenzeitlich auf eine + // andere Kunden-E-Mail umgestellt wurden, kommt die alte Mail dort nicht + // an – durch erneutes Senden ans Postfach läuft sie durch die jetzt + // aktuellen Forwards und landet beim neuen Empfänger. + const handleResend = async () => { + if (!emailDetail || !selectedAccount) return; + + let toAddresses: string[] = []; + try { + const parsed = JSON.parse(emailDetail.toAddresses || '[]'); + if (Array.isArray(parsed)) toAddresses = parsed; + } catch { + // Fallback: bekannte Mailbox-Adresse + if (selectedAccount.email) toAddresses = [selectedAccount.email]; + } + if (toAddresses.length === 0 && selectedAccount.email) { + toAddresses = [selectedAccount.email]; + } + + const hasAttachments = emailDetail.hasAttachments; + const lines = [ + `Diese E-Mail erneut an ${toAddresses.join(', ')} senden?`, + '', + 'Die Mail wird via SMTP wieder ans Postfach zugestellt und nimmt den', + 'Weg durch die AKTUELL hinterlegten Forwards – damit landet sie bei', + 'dem heute hinterlegten Empfänger (auch wenn er sich seit dem', + 'Original-Empfang geändert hat).', + ]; + if (hasAttachments) { + lines.push('', '⚠ Hinweis: Anhänge werden NICHT erneut versendet. Wenn du Anhänge brauchst, nutze stattdessen "Weiterleiten".'); + } + if (!confirm(lines.join('\n'))) return; + + try { + const subj = emailDetail.subject || '(Kein Betreff)'; + await stressfreiEmailApi.sendEmail(selectedAccount.id, { + to: toAddresses, + subject: subj, + text: emailDetail.textBody || undefined, + html: emailDetail.htmlBody || undefined, + }); + toast.success('E-Mail wurde erneut an ' + toAddresses.join(', ') + ' gesendet.'); + // Gesendet-Ordner-Counts aktualisieren + queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); + queryClient.invalidateQueries({ queryKey: ['emails'] }); + } catch (err: any) { + toast.error(err?.response?.data?.error || err?.message || 'Fehler beim erneuten Senden'); + } + }; + const handleAssignContract = () => { setShowAssign(true); }; @@ -551,6 +612,8 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) { { setSelectedEmail(null); @@ -582,9 +645,11 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) { onClose={() => { setShowCompose(false); setReplyToEmail(null); + setForwardEmail(null); }} account={selectedAccount} replyTo={replyToEmail || undefined} + forwardOf={forwardEmail || undefined} onSuccess={() => { // Gesendete E-Mails aktualisieren queryClient.invalidateQueries({ queryKey: ['emails', 'customer', customerId, selectedAccountId, 'SENT'] }); diff --git a/frontend/src/components/email/EmailDetail.tsx b/frontend/src/components/email/EmailDetail.tsx index 999b1079..07c3589e 100644 --- a/frontend/src/components/email/EmailDetail.tsx +++ b/frontend/src/components/email/EmailDetail.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Reply, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save, FileDown } from 'lucide-react'; +import { Reply, Forward, RotateCcw, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save, FileDown } from 'lucide-react'; import DOMPurify from 'dompurify'; import { CachedEmail, cachedEmailApi } from '../../services/api'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -13,6 +13,8 @@ import SaveEmailAsPdfModal from './SaveEmailAsPdfModal'; interface EmailDetailProps { email: CachedEmail; onReply: () => void; + onForward?: () => void; // Weiterleiten (öffnet ComposeModal im Forward-Modus) + onResend?: () => void; // Erneut an Empfänger senden (One-Click-Resend) onAssignContract: () => void; onDeleted?: () => void; // Callback nach Löschen isSentFolder?: boolean; @@ -25,6 +27,8 @@ interface EmailDetailProps { export default function EmailDetail({ email, onReply, + onForward, + onResend, onAssignContract, onDeleted, isSentFolder: _isSentFolder = false, @@ -222,6 +226,28 @@ export default function EmailDetail({ Antworten + {onForward && ( + + )} + {onResend && ( + + )} {/* E-Mail als PDF speichern */}