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>
722 lines
28 KiB
TypeScript
722 lines
28 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import { Mail, MailOpen, Star, Paperclip, Plus, X, ChevronRight, Inbox, Send, RefreshCw, Trash2, ExternalLink } from 'lucide-react';
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import { cachedEmailApi, stressfreiEmailApi, CachedEmail } from '../../services/api';
|
||
import { useAuth } from '../../context/AuthContext';
|
||
import Button from '../ui/Button';
|
||
import Card from '../ui/Card';
|
||
import CopyButton from '../ui/CopyButton';
|
||
import EmailDetail from './EmailDetail';
|
||
import ComposeEmailModal from './ComposeEmailModal';
|
||
import TrashEmailList from './TrashEmailList';
|
||
import toast from 'react-hot-toast';
|
||
|
||
type EmailFolder = 'INBOX' | 'SENT' | 'TRASH';
|
||
|
||
interface ContractEmailsSectionProps {
|
||
contractId: number;
|
||
customerId: number;
|
||
}
|
||
|
||
export default function ContractEmailsSection({
|
||
contractId,
|
||
customerId,
|
||
}: ContractEmailsSectionProps) {
|
||
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 [replyToEmail, setReplyToEmail] = useState<CachedEmail | null>(null);
|
||
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
|
||
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 für den Vertrag laden (nach Ordner UND Postfach gefiltert).
|
||
// Bug 2026-06-21: vorher gingen Mails aus allen Postfächern in den
|
||
// gewählten Vertrags-Ordner – obwohl der User ein bestimmtes Postfach
|
||
// ausgewählt hatte. selectedAccountId muss in queryKey + queryFn.
|
||
const { data: emailsData, isLoading, refetch: refetchEmails } = useQuery({
|
||
queryKey: ['emails', 'contract', contractId, selectedAccountId, selectedFolder],
|
||
queryFn: () => cachedEmailApi.getForContract(contractId, {
|
||
folder: selectedFolder as 'INBOX' | 'SENT',
|
||
accountId: selectedAccountId ?? undefined,
|
||
}),
|
||
enabled: selectedFolder !== 'TRASH' && !!selectedAccountId,
|
||
});
|
||
|
||
const emails = emailsData?.data || [];
|
||
|
||
// Papierkorb-E-Mails laden – jetzt strikt: nur das aktuell ausgewählte
|
||
// Postfach UND nur dem Vertrag zugeordnete Mails. Wenn man also den
|
||
// Vertrags-Papierkorb öffnet, sieht man nicht mehr alle gelöschten
|
||
// E-Mails des Kunden, sondern wirklich nur die, die diesem Vertrag
|
||
// aus diesem Postfach zugeordnet waren.
|
||
const { data: trashData, isLoading: trashLoading } = useQuery({
|
||
queryKey: ['emails', 'trash', customerId, selectedAccountId, contractId],
|
||
queryFn: () => cachedEmailApi.getTrash(customerId, {
|
||
accountId: selectedAccountId ?? undefined,
|
||
contractId,
|
||
}),
|
||
enabled: selectedFolder === 'TRASH' && canAccessTrash && !!selectedAccountId,
|
||
});
|
||
|
||
const trashEmails = trashData?.data || [];
|
||
|
||
// Ordner-Anzahlen für Badges (Vertrag + Postfach). Badge und Liste
|
||
// müssen mit derselben Filter-Kombination laufen, sonst zeigt der
|
||
// Badge eine andere Zahl als die sichtbare Liste.
|
||
const { data: folderCountsData } = useQuery({
|
||
queryKey: ['contract-folder-counts', contractId, selectedAccountId],
|
||
queryFn: () => cachedEmailApi.getContractFolderCounts(contractId, selectedAccountId ?? undefined),
|
||
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: () => {
|
||
// E-Mail-Listen neu laden
|
||
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
||
// Ordner-Anzahlen aktualisieren
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
if (selectedAccountId) {
|
||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||
}
|
||
toast.success('Synchronisation abgeschlossen');
|
||
},
|
||
onError: (error: Error) => {
|
||
toast.error(error.message || 'Synchronisation fehlgeschlagen');
|
||
},
|
||
});
|
||
|
||
// Stern umschalten
|
||
const toggleStarMutation = useMutation({
|
||
mutationFn: (emailId: number) => cachedEmailApi.toggleStar(emailId),
|
||
onSuccess: (_data, emailId) => {
|
||
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
|
||
queryClient.invalidateQueries({ queryKey: ['email', emailId] });
|
||
},
|
||
});
|
||
|
||
// Als gelesen/ungelesen markieren
|
||
const toggleReadMutation = useMutation({
|
||
mutationFn: ({ emailId, isRead }: { emailId: number; isRead: boolean }) =>
|
||
cachedEmailApi.markAsRead(emailId, isRead),
|
||
onSuccess: (_data, variables) => {
|
||
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
|
||
queryClient.invalidateQueries({ queryKey: ['email', variables.emailId] });
|
||
// Folder-Counts aktualisieren für Badge-Update
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
if (selectedAccountId) {
|
||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||
}
|
||
},
|
||
});
|
||
|
||
// Zuordnung aufheben
|
||
const unassignMutation = useMutation({
|
||
mutationFn: (emailId: number) => cachedEmailApi.unassignFromContract(emailId),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
setSelectedEmail(null);
|
||
toast.success('Zuordnung aufgehoben');
|
||
},
|
||
onError: (error: Error) => {
|
||
toast.error(error.message || 'Fehler beim Aufheben der Zuordnung');
|
||
},
|
||
});
|
||
|
||
// E-Mail löschen (in Papierkorb)
|
||
const deleteMutation = useMutation({
|
||
mutationFn: (emailId: number) => cachedEmailApi.delete(emailId),
|
||
onSuccess: (_data, emailId) => {
|
||
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
if (selectedAccountId) {
|
||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||
}
|
||
toast.success('E-Mail in Papierkorb verschoben');
|
||
setDeleteConfirmId(null);
|
||
if (selectedEmail?.id === emailId) {
|
||
setSelectedEmail(null);
|
||
}
|
||
},
|
||
onError: (error: Error) => {
|
||
toast.error(error.message || 'Fehler beim Löschen der E-Mail');
|
||
setDeleteConfirmId(null);
|
||
},
|
||
});
|
||
|
||
const handleSync = () => {
|
||
if (selectedAccountId) {
|
||
syncMutation.mutate(selectedAccountId);
|
||
}
|
||
};
|
||
|
||
const formatDate = (dateStr: string) => {
|
||
const date = new Date(dateStr);
|
||
const now = new Date();
|
||
const isToday = date.toDateString() === now.toDateString();
|
||
|
||
if (isToday) {
|
||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||
};
|
||
|
||
const handleStarClick = (e: React.MouseEvent, emailId: number) => {
|
||
e.stopPropagation();
|
||
toggleStarMutation.mutate(emailId);
|
||
};
|
||
|
||
const handleReadToggle = (e: React.MouseEvent, email: CachedEmail) => {
|
||
e.stopPropagation();
|
||
toggleReadMutation.mutate({ emailId: email.id, isRead: !email.isRead });
|
||
};
|
||
|
||
const handleSelectEmail = (email: CachedEmail) => {
|
||
// E-Mail als gelesen markieren wenn noch nicht gelesen
|
||
if (!email.isRead) {
|
||
toggleReadMutation.mutate({ emailId: email.id, isRead: true });
|
||
}
|
||
setSelectedEmail(email);
|
||
};
|
||
|
||
const handleReply = () => {
|
||
setReplyToEmail(emailDetail || null);
|
||
setShowCompose(true);
|
||
};
|
||
|
||
const handleNewEmail = () => {
|
||
setReplyToEmail(null);
|
||
setShowCompose(true);
|
||
};
|
||
|
||
const handleUnassign = (e: React.MouseEvent, emailId: number) => {
|
||
e.stopPropagation();
|
||
if (selectedEmail?.id === emailId) {
|
||
setSelectedEmail(null);
|
||
}
|
||
unassignMutation.mutate(emailId);
|
||
};
|
||
|
||
const handleDeleteClick = (e: React.MouseEvent, emailId: number) => {
|
||
e.stopPropagation();
|
||
setDeleteConfirmId(emailId);
|
||
};
|
||
|
||
const handleDeleteConfirm = (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
if (deleteConfirmId) {
|
||
deleteMutation.mutate(deleteConfirmId);
|
||
}
|
||
};
|
||
|
||
const handleDeleteCancel = (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
setDeleteConfirmId(null);
|
||
};
|
||
|
||
const handleFolderChange = (folder: EmailFolder) => {
|
||
setSelectedFolder(folder);
|
||
setSelectedEmail(null);
|
||
};
|
||
|
||
// Für gesendete E-Mails: Empfänger extrahieren
|
||
const getDisplayName = (email: CachedEmail) => {
|
||
if (selectedFolder === 'SENT') {
|
||
try {
|
||
const toAddresses = JSON.parse(email.toAddresses);
|
||
if (toAddresses.length > 0) {
|
||
return `An: ${toAddresses[0]}${toAddresses.length > 1 ? ` (+${toAddresses.length - 1})` : ''}`;
|
||
}
|
||
} catch {
|
||
return 'An: (Unbekannt)';
|
||
}
|
||
}
|
||
return email.fromName || email.fromAddress;
|
||
};
|
||
|
||
// Shared Card-Titel mit Quicklinks zur Kundenakte – wird sowohl im
|
||
// Normal-Zweig als auch im "keine Mailbox vorhanden"-Zweig gezeigt,
|
||
// damit man auch ohne eingerichtetes Postfach direkt zu den
|
||
// Stressfrei-Wechseln-Adressen springen kann.
|
||
const cardTitle = (
|
||
<div className="flex items-center gap-3">
|
||
<span>E-Mails</span>
|
||
<Link
|
||
to={`/customers/${customerId}?tab=emails`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 hover:underline font-normal"
|
||
title="Komplettes E-Mail-Postfach des Kunden in neuem Tab öffnen"
|
||
>
|
||
<ExternalLink className="w-3 h-3" />
|
||
Postfach öffnen
|
||
</Link>
|
||
<Link
|
||
to={`/customers/${customerId}?tab=stressfrei`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 hover:underline font-normal"
|
||
title="Stressfrei-Wechseln-Adressen des Kunden in neuem Tab öffnen"
|
||
>
|
||
<ExternalLink className="w-3 h-3" />
|
||
Stressfrei wechseln Adressen
|
||
</Link>
|
||
</div>
|
||
);
|
||
|
||
// Keine Mailbox-Konten vorhanden
|
||
if (!accountsLoading && accounts.length === 0) {
|
||
return (
|
||
<Card title={cardTitle}>
|
||
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
||
<Mail className="w-10 h-10 mb-2 opacity-30" />
|
||
<p className="text-sm">Keine E-Mail-Konten vorhanden</p>
|
||
<p className="text-xs mt-1">
|
||
Erstellen Sie eine E-Mail-Adresse beim Kunden mit aktivierter Mailbox
|
||
</p>
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Card
|
||
title={cardTitle}
|
||
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...' : 'Sync'}
|
||
</Button>
|
||
)}
|
||
{selectedAccount && (
|
||
<Button size="sm" onClick={handleNewEmail}>
|
||
<Plus className="w-4 h-4 mr-1" />
|
||
Neue E-Mail
|
||
</Button>
|
||
)}
|
||
</div>
|
||
}
|
||
>
|
||
{/* Header mit Account-Auswahl und Ordner-Tabs */}
|
||
<div className="flex items-center justify-between gap-4 pb-4 border-b border-gray-200 -mt-2">
|
||
{/* Account Selector */}
|
||
{accounts.length > 1 ? (
|
||
<div className="flex items-center gap-2">
|
||
<Inbox className="w-4 h-4 text-gray-500" />
|
||
<select
|
||
value={selectedAccountId || ''}
|
||
onChange={(e) => {
|
||
setSelectedAccountId(Number(e.target.value));
|
||
setSelectedEmail(null);
|
||
}}
|
||
className="px-2 py-1.5 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}
|
||
title={`Postfach-Adresse "${selectedAccount.email}" in Zwischenablage kopieren`}
|
||
/>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||
<Inbox className="w-4 h-4 text-gray-500" />
|
||
<span>{selectedAccount?.email}</span>
|
||
{selectedAccount?.email && (
|
||
<CopyButton
|
||
value={selectedAccount.email}
|
||
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>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{(selectedFolder === 'TRASH' ? trashLoading : isLoading) ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||
</div>
|
||
) : (selectedFolder === 'TRASH' ? trashEmails.length === 0 : emails.length === 0) ? (
|
||
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
||
<Mail className="w-10 h-10 mb-2 opacity-30" />
|
||
<p className="text-sm">
|
||
{selectedFolder === 'INBOX'
|
||
? 'Keine E-Mails zugeordnet'
|
||
: selectedFolder === 'SENT'
|
||
? 'Keine E-Mails über diesen Vertrag gesendet'
|
||
: 'Papierkorb ist leer'}
|
||
</p>
|
||
{selectedFolder === 'INBOX' && (
|
||
<p className="text-xs mt-1">
|
||
E-Mails können im E-Mail-Tab des Kunden zugeordnet werden
|
||
</p>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div
|
||
className="flex -mx-6 -mb-6"
|
||
style={{ height: '600px' }}
|
||
>
|
||
{/* Email List – scrollt intern, damit die Vertrags-Seite nicht
|
||
elendig lang wird. */}
|
||
<div className="w-1/3 border-r border-gray-200 overflow-y-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] });
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
}}
|
||
onEmailDeleted={(emailId) => {
|
||
if (selectedEmail?.id === emailId) {
|
||
setSelectedEmail(null);
|
||
}
|
||
queryClient.invalidateQueries({ queryKey: ['emails', 'trash'] });
|
||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||
}}
|
||
isLoading={trashLoading}
|
||
/>
|
||
) : (
|
||
<div className="divide-y divide-gray-200">
|
||
{emails.map((email) => (
|
||
<div
|
||
key={email.id}
|
||
onClick={() => handleSelectEmail(email)}
|
||
className={[
|
||
'flex items-start gap-2 p-3 cursor-pointer transition-colors',
|
||
selectedEmail?.id === email.id
|
||
? 'bg-blue-100'
|
||
: ['hover:bg-gray-100', !email.isRead ? 'bg-white' : 'bg-gray-50/50'].join(' ')
|
||
].join(' ')}
|
||
style={{
|
||
borderLeft: selectedEmail?.id === email.id ? '4px solid #2563eb' : '4px solid transparent'
|
||
}}
|
||
>
|
||
{/* Read Status */}
|
||
<button
|
||
onClick={(e) => handleReadToggle(e, email)}
|
||
className={`
|
||
flex-shrink-0 mt-1 p-1 -ml-1 rounded hover:bg-gray-200
|
||
${!email.isRead ? 'text-blue-600' : 'text-gray-400'}
|
||
`}
|
||
title={email.isRead ? 'Als ungelesen markieren' : 'Als gelesen markieren'}
|
||
>
|
||
{email.isRead ? (
|
||
<MailOpen className="w-4 h-4" />
|
||
) : (
|
||
<Mail className="w-4 h-4" />
|
||
)}
|
||
</button>
|
||
|
||
{/* Star */}
|
||
<button
|
||
onClick={(e) => handleStarClick(e, email.id)}
|
||
className={`
|
||
flex-shrink-0 mt-1 p-1 -ml-1 rounded hover:bg-gray-200
|
||
${email.isStarred ? 'text-yellow-500' : 'text-gray-400'}
|
||
`}
|
||
title={email.isStarred ? 'Stern entfernen' : 'Als wichtig markieren'}
|
||
>
|
||
<Star className={`w-4 h-4 ${email.isStarred ? 'fill-current' : ''}`} />
|
||
</button>
|
||
|
||
{/* Delete Button (nur mit Permission) */}
|
||
{hasPermission('emails:delete') && (
|
||
<button
|
||
onClick={(e) => handleDeleteClick(e, email.id)}
|
||
className="flex-shrink-0 mt-1 p-1 -ml-1 rounded hover:bg-red-100 text-gray-400 hover:text-red-600"
|
||
title="E-Mail löschen"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
|
||
{/* Email Content */}
|
||
<div className="flex-1 min-w-0">
|
||
{/* From/To & Date */}
|
||
<div className="flex items-center justify-between gap-2 mb-1">
|
||
<span className={`text-sm truncate ${!email.isRead ? 'font-semibold text-gray-900' : 'text-gray-700'}`}>
|
||
{getDisplayName(email)}
|
||
</span>
|
||
<span className="text-xs text-gray-500 flex-shrink-0">
|
||
{formatDate(email.receivedAt)}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Subject */}
|
||
<div className="flex items-center gap-2">
|
||
<span className={`text-sm truncate ${!email.isRead ? 'font-medium text-gray-900' : 'text-gray-600'}`}>
|
||
{email.subject || '(Kein Betreff)'}
|
||
</span>
|
||
{email.hasAttachments && (
|
||
<Paperclip className="w-3 h-3 text-gray-400 flex-shrink-0" />
|
||
)}
|
||
</div>
|
||
|
||
{/* Contract Badge */}
|
||
{email.contract && (
|
||
<div className="mt-1 flex items-center gap-1">
|
||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||
{email.contract.contractNumber}
|
||
</span>
|
||
{/* X-Button nur für INBOX oder manuell zugeordnete gesendete E-Mails */}
|
||
{(selectedFolder === 'INBOX' || (selectedFolder === 'SENT' && !email.isAutoAssigned)) && (
|
||
<button
|
||
onClick={(e) => handleUnassign(e, email.id)}
|
||
className="p-0.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
|
||
title="Zuordnung aufheben"
|
||
disabled={unassignMutation.isPending}
|
||
>
|
||
<X className="w-3.5 h-3.5" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Chevron */}
|
||
<ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0 mt-2" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Email Detail */}
|
||
<div className="flex-1 overflow-auto">
|
||
{emailDetail && selectedEmail ? (
|
||
<EmailDetail
|
||
email={emailDetail}
|
||
onReply={handleReply}
|
||
onAssignContract={() => {}}
|
||
onDeleted={() => {
|
||
setSelectedEmail(null);
|
||
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
if (selectedAccountId) {
|
||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||
}
|
||
}}
|
||
isSentFolder={selectedFolder === 'SENT'}
|
||
isContractView={selectedFolder !== 'TRASH'}
|
||
isTrashView={selectedFolder === 'TRASH'}
|
||
onRestored={() => {
|
||
setSelectedEmail(null);
|
||
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
if (selectedAccountId) {
|
||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||
}
|
||
}}
|
||
accountId={emailDetail?.stressfreiEmailId}
|
||
/>
|
||
) : (
|
||
<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>
|
||
)}
|
||
|
||
{/* Lösch-Bestätigung Modal */}
|
||
{deleteConfirmId && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md mx-4">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||
E-Mail löschen?
|
||
</h3>
|
||
<p className="text-gray-600 mb-4">
|
||
Die E-Mail wird in den Papierkorb verschoben.
|
||
</p>
|
||
<div className="flex justify-end gap-3">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={handleDeleteCancel}
|
||
disabled={deleteMutation.isPending}
|
||
>
|
||
Abbrechen
|
||
</Button>
|
||
<Button
|
||
variant="danger"
|
||
onClick={handleDeleteConfirm}
|
||
disabled={deleteMutation.isPending}
|
||
>
|
||
{deleteMutation.isPending ? 'Löschen...' : 'Löschen'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Compose Modal */}
|
||
{selectedAccount && (
|
||
<ComposeEmailModal
|
||
isOpen={showCompose}
|
||
onClose={() => {
|
||
setShowCompose(false);
|
||
setReplyToEmail(null);
|
||
}}
|
||
account={selectedAccount}
|
||
replyTo={replyToEmail || undefined}
|
||
contractId={contractId}
|
||
onSuccess={() => {
|
||
// Gesendete E-Mails im Vertrag aktualisieren
|
||
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId, 'SENT'] });
|
||
// Folder-Counts aktualisieren
|
||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||
// Falls wir im Gesendet-Ordner sind, Liste neu laden
|
||
if (selectedFolder === 'SENT') {
|
||
refetchEmails();
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|