67d6fd4941
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>
708 lines
29 KiB
TypeScript
708 lines
29 KiB
TypeScript
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>
|
||
);
|
||
}
|