import { useState, useEffect, useMemo } from 'react'; import { RefreshCw, Plus, Mail, Inbox, Send, Trash2, Search, SlidersHorizontal, X } from 'lucide-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { cachedEmailApi, stressfreiEmailApi, CachedEmail, EmailFilterParams } from '../../services/api'; import { useAuth } from '../../context/AuthContext'; import { useProviderSettings } from '../../hooks/useProviderSettings'; import Button from '../ui/Button'; import CopyButton from '../ui/CopyButton'; import EmailList from './EmailList'; import EmailDetail from './EmailDetail'; import ComposeEmailModal from './ComposeEmailModal'; import AssignToContractModal from './AssignToContractModal'; import TrashEmailList from './TrashEmailList'; type EmailFolder = 'INBOX' | 'SENT' | 'TRASH'; interface EmailClientTabProps { customerId: number; } export default function EmailClientTab({ customerId }: EmailClientTabProps) { const { customerEmailLabel } = useProviderSettings(); const [selectedAccountId, setSelectedAccountId] = useState(null); const [selectedFolder, setSelectedFolder] = useState('INBOX'); const [selectedEmail, setSelectedEmail] = useState(null); 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(''); const [showFilters, setShowFilters] = useState(false); const [filterFrom, setFilterFrom] = useState(''); const [filterTo, setFilterTo] = useState(''); const [filterSubject, setFilterSubject] = useState(''); const [filterBody, setFilterBody] = useState(''); const [filterAttachmentName, setFilterAttachmentName] = useState(''); const [filterHasAttachments, setFilterHasAttachments] = useState<'any' | 'yes' | 'no'>('any'); const [filterReadStatus, setFilterReadStatus] = useState<'any' | 'unread' | 'read'>('any'); const [filterStarred, setFilterStarred] = useState<'any' | 'starred'>('any'); const [filterDateFrom, setFilterDateFrom] = useState(''); const [filterDateTo, setFilterDateTo] = useState(''); // Filter-Parameter (memoized) – fließen in queryKey + queryFn. const filterParams: EmailFilterParams = useMemo(() => { const p: EmailFilterParams = {}; if (searchQuery.trim()) p.search = searchQuery.trim(); if (filterFrom.trim()) p.fromFilter = filterFrom.trim(); if (filterTo.trim()) p.toFilter = filterTo.trim(); if (filterSubject.trim()) p.subjectFilter = filterSubject.trim(); if (filterBody.trim()) p.bodyFilter = filterBody.trim(); if (filterAttachmentName.trim()) p.attachmentNameFilter = filterAttachmentName.trim(); if (filterHasAttachments === 'yes') p.hasAttachments = true; if (filterHasAttachments === 'no') p.hasAttachments = false; if (filterReadStatus === 'read') p.isRead = true; if (filterReadStatus === 'unread') p.isRead = false; if (filterStarred === 'starred') p.isStarred = true; if (filterDateFrom) p.receivedFrom = new Date(filterDateFrom + 'T00:00:00').toISOString(); if (filterDateTo) p.receivedTo = new Date(filterDateTo + 'T23:59:59').toISOString(); return p; }, [ searchQuery, filterFrom, filterTo, filterSubject, filterBody, filterAttachmentName, filterHasAttachments, filterReadStatus, filterStarred, filterDateFrom, filterDateTo, ]); const activeFilterCount = Object.keys(filterParams).length; const clearAllFilters = () => { setSearchQuery(''); setFilterFrom(''); setFilterTo(''); setFilterSubject(''); setFilterBody(''); setFilterAttachmentName(''); setFilterHasAttachments('any'); setFilterReadStatus('any'); setFilterStarred('any'); setFilterDateFrom(''); setFilterDateTo(''); }; const queryClient = useQueryClient(); const { hasPermission } = useAuth(); const canAccessTrash = hasPermission('emails:delete'); // Mailbox-Konten laden const { data: accountsData, isLoading: accountsLoading } = useQuery({ queryKey: ['mailbox-accounts', customerId], queryFn: () => cachedEmailApi.getMailboxAccounts(customerId), }); const accounts = accountsData?.data || []; // Erstes Konto automatisch auswählen useEffect(() => { if (accounts.length > 0 && !selectedAccountId) { setSelectedAccountId(accounts[0].id); } }, [accounts, selectedAccountId]); const selectedAccount = accounts.find((a) => a.id === selectedAccountId); // E-Mails laden (nur für INBOX und SENT) const { data: emailsData, isLoading: emailsLoading, refetch: refetchEmails } = useQuery({ queryKey: ['emails', 'customer', customerId, selectedAccountId, selectedFolder, filterParams], queryFn: () => cachedEmailApi.getForCustomer(customerId, { accountId: selectedAccountId || undefined, folder: selectedFolder as 'INBOX' | 'SENT', ...filterParams, }), enabled: !!selectedAccountId && selectedFolder !== 'TRASH', }); const emails = emailsData?.data || []; // Papierkorb-E-Mails laden – jetzt strikt pro Postfach. // Bug 2026-06-21: vorher kamen alle gelöschten E-Mails des Kunden // raus, egal welches Postfach selektiert war. selectedAccountId muss // in queryKey + queryFn, sonst greift React-Query-Cache bei Wechsel // nicht und der Folder-Count aus folderCountsData liefe auseinander. const { data: trashData, isLoading: trashLoading } = useQuery({ queryKey: ['emails', 'trash', customerId, selectedAccountId], queryFn: () => cachedEmailApi.getTrash(customerId, { accountId: selectedAccountId ?? undefined, }), enabled: selectedFolder === 'TRASH' && canAccessTrash && !!selectedAccountId, }); const trashEmails = trashData?.data || []; // Ordner-Anzahlen für Badges (total und ungelesen pro Ordner) const { data: folderCountsData } = useQuery({ queryKey: ['folder-counts', selectedAccountId], queryFn: () => stressfreiEmailApi.getFolderCounts(selectedAccountId!), enabled: !!selectedAccountId, }); const folderCounts = folderCountsData?.data || { inbox: 0, inboxUnread: 0, sent: 0, sentUnread: 0, trash: 0, trashUnread: 0, }; // Einzelne E-Mail laden (mit Body) const { data: emailDetailData } = useQuery({ queryKey: ['email', selectedEmail?.id], queryFn: () => cachedEmailApi.getById(selectedEmail!.id), enabled: !!selectedEmail?.id, }); const emailDetail = emailDetailData?.data || selectedEmail; // Synchronisation const syncMutation = useMutation({ mutationFn: (accountId: number) => stressfreiEmailApi.syncEmails(accountId), onSuccess: (result) => { // Backend liefert success=false bei IMAP-Fehler, aber ohne HTTP-Error if (result && (result as any).success === false) { const err = (result as any).error || 'IMAP-Synchronisation fehlgeschlagen'; toast.error(`Sync fehlgeschlagen: ${err}`, { duration: 8000 }); return; } toast.success('E-Mails synchronisiert'); // E-Mail-Listen neu laden queryClient.invalidateQueries({ queryKey: ['emails'] }); // Ordner-Anzahlen aktualisieren queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); // Mailbox-Accounts aktualisieren queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] }); }, onError: (error: any) => { const msg = error?.response?.data?.error || error?.message || 'Unbekannter Fehler'; toast.error(`Sync fehlgeschlagen: ${msg}`, { duration: 8000 }); }, }); const handleSync = () => { if (selectedAccountId) { syncMutation.mutate(selectedAccountId); } }; const handleSelectEmail = (email: CachedEmail) => { setSelectedEmail(email); }; 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); }; // Keine Mailbox-Konten vorhanden if (!accountsLoading && accounts.length === 0) { return (

Keine E-Mail-Konten vorhanden

Erstellen Sie eine {customerEmailLabel} E-Mail-Adresse mit aktivierter Mailbox, um E-Mails hier empfangen und versenden zu können.

); } const handleFolderChange = (folder: EmailFolder) => { setSelectedFolder(folder); setSelectedEmail(null); }; return ( // Bounded auf Viewport-Höhe – sonst ignoriert h-full ohnehin den // Tab-Container und der Postfach-Inhalt wächst beliebig, sodass die // ganze Seite scrollt statt nur die E-Mail-Liste.
{/* Header */}
{/* Account Selector */} {accounts.length > 1 ? (
{selectedAccount?.email && ( )}
) : (
{selectedAccount?.email} {selectedAccount?.email && ( )}
)} {/* Folder Tabs */}
{canAccessTrash && ( )}
{/* Actions */}
{selectedFolder !== 'TRASH' && ( )}
{/* Content */}
{/* Email List */}
{selectedFolder !== 'TRASH' && selectedAccountId && (
{/* Suchleiste */}
setSearchQuery(e.target.value)} placeholder="Suche in Betreff, Absender, Inhalt…" className="w-full pl-8 pr-7 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" /> {searchQuery && ( )}
{/* Ausklappbare erweiterte Filter (alle AND-verknüpft) */} {showFilters && (
setFilterFrom(e.target.value)} placeholder="Von (Adresse/Name)" className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> setFilterTo(e.target.value)} placeholder="An" className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> setFilterSubject(e.target.value)} placeholder="Betreff enthält" className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> setFilterBody(e.target.value)} placeholder="Inhalt enthält" className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> setFilterDateFrom(e.target.value)} title="Empfangen ab" className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> setFilterDateTo(e.target.value)} title="Empfangen bis" className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> setFilterAttachmentName(e.target.value)} placeholder="Anhang-Dateiname" className="col-span-2 px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" />
Filter werden mit UND verknüpft. {activeFilterCount > 0 && ( )}
)}
)}
{selectedFolder === 'TRASH' ? ( { if (selectedEmail?.id === emailId) { setSelectedEmail(null); } // Trash und normale E-Mails neu laden + Folder-Counts aktualisieren queryClient.invalidateQueries({ queryKey: ['emails'] }); queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); }} onEmailDeleted={(emailId) => { if (selectedEmail?.id === emailId) { setSelectedEmail(null); } queryClient.invalidateQueries({ queryKey: ['emails', 'trash'] }); queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); }} isLoading={trashLoading} /> ) : ( { // Falls die gelöschte E-Mail ausgewählt war, Auswahl aufheben if (selectedEmail?.id === emailId) { setSelectedEmail(null); } // Folder-Counts aktualisieren (Trash-Counter ist jetzt dort) queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); }} isLoading={emailsLoading} folder={selectedFolder as 'INBOX' | 'SENT'} accountId={selectedAccountId} /> )}
{/* Email Detail */}
{emailDetail ? ( { setSelectedEmail(null); queryClient.invalidateQueries({ queryKey: ['emails'] }); queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); }} isSentFolder={selectedFolder === 'SENT'} isTrashView={selectedFolder === 'TRASH'} onRestored={() => { setSelectedEmail(null); queryClient.invalidateQueries({ queryKey: ['emails'] }); queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); }} accountId={selectedAccountId || undefined} /> ) : (

Wählen Sie eine E-Mail aus

)}
{/* Compose Modal */} {selectedAccount && ( { 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'] }); // Folder-Counts aktualisieren queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] }); // Falls wir im Gesendet-Ordner sind, Liste neu laden if (selectedFolder === 'SENT') { refetchEmails(); } }} /> )} {/* Assign Contract Modal */} {emailDetail && ( setShowAssign(false)} email={emailDetail} customerId={customerId} onSuccess={() => { refetchEmails(); }} /> )}
); }