Files
opencrm/frontend/src/components/email/EmailClientTab.tsx
T
duffyduck 67d6fd4941 E-Mail-Liste: eigener Scrollbalken statt seitenweit wachsen
User-Bug: bei vielen E-Mails wuchs die Liste links unbegrenzt nach
unten, sodass die ganze Seite gescrollt werden musste.

- ContractEmailsSection: flex-Container von minHeight:400 auf
  feste 600px Höhe gestellt. Die linke Liste hatte schon
  overflow-y-auto – jetzt greift's auch.
- EmailClientTab: h-full auf calc(100vh - 240px) (mit
  minHeight:500) bounded. h-full hat im Tab-Container vorher
  nichts gebracht, weil der Parent selbst keine feste Höhe
  hatte.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 07:51:16 +02:00

708 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<number | null>(null);
const [selectedFolder, setSelectedFolder] = useState<EmailFolder>('INBOX');
const [selectedEmail, setSelectedEmail] = useState<CachedEmail | null>(null);
const [showCompose, setShowCompose] = useState(false);
const [showAssign, setShowAssign] = useState(false);
const [replyToEmail, setReplyToEmail] = useState<CachedEmail | null>(null);
const [forwardEmail, setForwardEmail] = useState<CachedEmail | null>(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 (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<Mail className="w-16 h-16 mb-4 opacity-30" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Keine E-Mail-Konten vorhanden
</h3>
<p className="text-sm text-center max-w-md">
Erstellen Sie eine {customerEmailLabel} E-Mail-Adresse mit aktivierter Mailbox,
um E-Mails hier empfangen und versenden zu können.
</p>
</div>
);
}
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.
<div
className="flex flex-col"
style={{ height: 'calc(100vh - 240px)', minHeight: '500px' }}
>
{/* Header */}
<div className="flex items-center justify-between gap-4 p-4 border-b border-gray-200 bg-gray-50">
{/* Account Selector */}
{accounts.length > 1 ? (
<div className="flex items-center gap-3">
<Inbox className="w-5 h-5 text-gray-500" />
<select
value={selectedAccountId || ''}
onChange={(e) => {
setSelectedAccountId(Number(e.target.value));
setSelectedEmail(null);
}}
className="px-3 py-2 border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
{accounts.map((account) => (
<option key={account.id} value={account.id}>
{account.email}
</option>
))}
</select>
{selectedAccount?.email && (
<CopyButton
value={selectedAccount.email}
size="md"
title={`Postfach-Adresse "${selectedAccount.email}" in Zwischenablage kopieren`}
/>
)}
</div>
) : (
<div className="flex items-center gap-3 text-sm text-gray-600">
<Inbox className="w-5 h-5 text-gray-500" />
<span>{selectedAccount?.email}</span>
{selectedAccount?.email && (
<CopyButton
value={selectedAccount.email}
size="md"
title={`Postfach-Adresse "${selectedAccount.email}" in Zwischenablage kopieren`}
/>
)}
</div>
)}
{/* Folder Tabs */}
<div className="flex items-center gap-1 bg-gray-200 rounded-lg p-1">
<button
onClick={() => handleFolderChange('INBOX')}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedFolder === 'INBOX'
? 'bg-white text-blue-600 shadow-sm font-medium'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Inbox className="w-4 h-4" />
Posteingang
{folderCounts.inbox > 0 && (
<span
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
folderCounts.inboxUnread > 0
? 'bg-blue-100 text-blue-600 font-medium'
: 'bg-gray-100 text-gray-500'
}`}
title={`${folderCounts.inboxUnread} ungelesen / ${folderCounts.inbox} gesamt`}
>
{folderCounts.inboxUnread > 0
? `${folderCounts.inboxUnread}/${folderCounts.inbox}`
: folderCounts.inbox}
</span>
)}
</button>
<button
onClick={() => handleFolderChange('SENT')}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedFolder === 'SENT'
? 'bg-white text-blue-600 shadow-sm font-medium'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Send className="w-4 h-4" />
Gesendet
{folderCounts.sent > 0 && (
<span
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
folderCounts.sentUnread > 0
? 'bg-blue-100 text-blue-600 font-medium'
: 'bg-gray-100 text-gray-500'
}`}
title={`${folderCounts.sentUnread} ungelesen / ${folderCounts.sent} gesamt`}
>
{folderCounts.sentUnread > 0
? `${folderCounts.sentUnread}/${folderCounts.sent}`
: folderCounts.sent}
</span>
)}
</button>
{canAccessTrash && (
<button
onClick={() => handleFolderChange('TRASH')}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedFolder === 'TRASH'
? 'bg-white text-red-600 shadow-sm font-medium'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Trash2 className="w-4 h-4" />
Papierkorb
{folderCounts.trash > 0 && (
<span
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
folderCounts.trashUnread > 0
? 'bg-red-100 text-red-600 font-medium'
: 'bg-gray-100 text-gray-500'
}`}
title={`${folderCounts.trashUnread} ungelesen / ${folderCounts.trash} gesamt`}
>
{folderCounts.trashUnread > 0
? `${folderCounts.trashUnread}/${folderCounts.trash}`
: folderCounts.trash}
</span>
)}
</button>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-2">
{selectedFolder !== 'TRASH' && (
<Button
variant="secondary"
size="sm"
onClick={handleSync}
disabled={syncMutation.isPending || !selectedAccountId}
>
<RefreshCw className={`w-4 h-4 mr-1 ${syncMutation.isPending ? 'animate-spin' : ''}`} />
{syncMutation.isPending ? 'Sync...' : 'Synchronisieren'}
</Button>
)}
<Button
size="sm"
onClick={handleNewEmail}
disabled={!selectedAccount}
>
<Plus className="w-4 h-4 mr-1" />
Neue E-Mail
</Button>
</div>
</div>
{/* Content */}
<div className="flex flex-1 overflow-hidden">
{/* Email List */}
<div className="w-1/3 border-r border-gray-200 flex flex-col overflow-hidden">
{selectedFolder !== 'TRASH' && selectedAccountId && (
<div className="border-b border-gray-200 bg-gray-50 p-2 space-y-2">
{/* Suchleiste */}
<div className="flex items-center gap-1">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
title="Suche zurücksetzen"
>
<X className="w-4 h-4" />
</button>
)}
</div>
<button
onClick={() => setShowFilters((v) => !v)}
className={`flex items-center gap-1 px-2 py-1.5 text-sm border rounded ${
showFilters || activeFilterCount > 0
? 'bg-blue-50 border-blue-300 text-blue-700'
: 'bg-white border-gray-300 text-gray-600 hover:bg-gray-50'
}`}
title="Erweiterte Filter ein-/ausblenden"
>
<SlidersHorizontal className="w-4 h-4" />
{activeFilterCount > 0 && (
<span className="text-xs font-semibold">{activeFilterCount}</span>
)}
</button>
</div>
{/* Ausklappbare erweiterte Filter (alle AND-verknüpft) */}
{showFilters && (
<div className="space-y-2 pt-1 border-t border-gray-200">
<div className="grid grid-cols-2 gap-2">
<input
type="text"
value={filterFrom}
onChange={(e) => 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"
/>
<input
type="text"
value={filterTo}
onChange={(e) => 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"
/>
<input
type="text"
value={filterSubject}
onChange={(e) => 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"
/>
<input
type="text"
value={filterBody}
onChange={(e) => 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"
/>
<input
type="date"
value={filterDateFrom}
onChange={(e) => 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"
/>
<input
type="date"
value={filterDateTo}
onChange={(e) => 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"
/>
<input
type="text"
value={filterAttachmentName}
onChange={(e) => 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"
/>
</div>
<div className="grid grid-cols-3 gap-2">
<select
value={filterHasAttachments}
onChange={(e) => setFilterHasAttachments(e.target.value as 'any' | 'yes' | 'no')}
className="px-2 py-1 text-xs border border-gray-300 rounded bg-white focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="any">Anhang: egal</option>
<option value="yes">Mit Anhang</option>
<option value="no">Ohne Anhang</option>
</select>
<select
value={filterReadStatus}
onChange={(e) => setFilterReadStatus(e.target.value as 'any' | 'unread' | 'read')}
className="px-2 py-1 text-xs border border-gray-300 rounded bg-white focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="any">Status: egal</option>
<option value="unread">Ungelesen</option>
<option value="read">Gelesen</option>
</select>
<select
value={filterStarred}
onChange={(e) => setFilterStarred(e.target.value as 'any' | 'starred')}
className="px-2 py-1 text-xs border border-gray-300 rounded bg-white focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="any">Stern: egal</option>
<option value="starred">Nur markiert</option>
</select>
</div>
<div className="flex items-center justify-between text-xs text-gray-500">
<span>Filter werden mit UND verknüpft.</span>
{activeFilterCount > 0 && (
<button
onClick={clearAllFilters}
className="text-blue-600 hover:underline"
>
Alle zurücksetzen
</button>
)}
</div>
</div>
)}
</div>
)}
<div className="flex-1 overflow-auto">
{selectedFolder === 'TRASH' ? (
<TrashEmailList
emails={trashEmails}
selectedEmailId={selectedEmail?.id}
onSelectEmail={handleSelectEmail}
onEmailRestored={(emailId) => {
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}
/>
) : (
<EmailList
emails={emails}
selectedEmailId={selectedEmail?.id}
onSelectEmail={handleSelectEmail}
onEmailDeleted={(emailId) => {
// 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}
/>
)}
</div>
</div>
{/* Email Detail */}
<div className="flex-1 overflow-auto">
{emailDetail ? (
<EmailDetail
email={emailDetail}
onReply={handleReply}
onForward={handleForward}
onResend={selectedFolder !== 'TRASH' ? handleResend : undefined}
onAssignContract={handleAssignContract}
onDeleted={() => {
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}
/>
) : (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<Mail className="w-12 h-12 mb-2 opacity-30" />
<p>Wählen Sie eine E-Mail aus</p>
</div>
)}
</div>
</div>
{/* Compose Modal */}
{selectedAccount && (
<ComposeEmailModal
isOpen={showCompose}
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'] });
// 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 && (
<AssignToContractModal
isOpen={showAssign}
onClose={() => setShowAssign(false)}
email={emailDetail}
customerId={customerId}
onSuccess={() => {
refetchEmails();
}}
/>
)}
</div>
);
}