4fb700cf57
ContractEmailsSection (Vertragsansicht): Zusätzlich zu "Postfach öffnen" gibt es jetzt "Stressfrei wechseln Adressen" → Tab in der Kundenakte. ContractForm (Bearbeiten): Kleine ExternalLink-Icons neben den Select-Labels: - Lieferadresse + Rechnungsadresse → Kundenakte/Adressen - Bankkarte → Kundenakte/Bankkarten - Ausweis → Kundenakte/Ausweise - Anbieter + Tarif → Settings/Anbieter & Tarife - Vertriebsplattform → Settings/Vertriebsplattformen Select-Komponente nimmt jetzt ReactNode als label (statt nur string), um JSX-Labels mit eingebettetem Link zu erlauben. Rückwärts- kompatibel zu allen bestehenden String-Aufrufen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
693 lines
27 KiB
TypeScript
693 lines
27 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 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 gefiltert, nicht für TRASH)
|
|
const { data: emailsData, isLoading, refetch: refetchEmails } = useQuery({
|
|
queryKey: ['emails', 'contract', contractId, selectedFolder],
|
|
queryFn: () => cachedEmailApi.getForContract(contractId, { folder: selectedFolder as 'INBOX' | 'SENT' }),
|
|
enabled: selectedFolder !== 'TRASH',
|
|
});
|
|
|
|
const emails = emailsData?.data || [];
|
|
|
|
// Papierkorb-E-Mails laden (für den ganzen Kunden, da Trash nicht vertragsgebunden)
|
|
const { data: trashData, isLoading: trashLoading } = useQuery({
|
|
queryKey: ['emails', 'trash', customerId],
|
|
queryFn: () => cachedEmailApi.getTrash(customerId),
|
|
enabled: selectedFolder === 'TRASH' && canAccessTrash,
|
|
});
|
|
|
|
const trashEmails = trashData?.data || [];
|
|
|
|
// Ordner-Anzahlen für Badges (Vertrag)
|
|
const { data: folderCountsData } = useQuery({
|
|
queryKey: ['contract-folder-counts', contractId],
|
|
queryFn: () => cachedEmailApi.getContractFolderCounts(contractId),
|
|
});
|
|
|
|
const folderCounts = folderCountsData?.data || {
|
|
inbox: 0,
|
|
inboxUnread: 0,
|
|
sent: 0,
|
|
sentUnread: 0,
|
|
};
|
|
|
|
// Ordner-Anzahlen für das Konto (für Trash-Badge)
|
|
const { data: accountFolderCountsData } = useQuery({
|
|
queryKey: ['folder-counts', selectedAccountId],
|
|
queryFn: () => stressfreiEmailApi.getFolderCounts(selectedAccountId!),
|
|
enabled: !!selectedAccountId && canAccessTrash,
|
|
});
|
|
|
|
const accountFolderCounts = accountFolderCountsData?.data || {
|
|
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;
|
|
};
|
|
|
|
// Keine Mailbox-Konten vorhanden
|
|
if (!accountsLoading && accounts.length === 0) {
|
|
return (
|
|
<Card title="E-Mails">
|
|
<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={
|
|
<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>
|
|
}
|
|
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>
|
|
</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>
|
|
</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
|
|
{accountFolderCounts.trash > 0 && (
|
|
<span
|
|
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
|
|
accountFolderCounts.trashUnread > 0
|
|
? 'bg-red-100 text-red-600 font-medium'
|
|
: 'bg-gray-100 text-gray-500'
|
|
}`}
|
|
title={`${accountFolderCounts.trashUnread} ungelesen / ${accountFolderCounts.trash} gesamt`}
|
|
>
|
|
{accountFolderCounts.trashUnread > 0
|
|
? `${accountFolderCounts.trashUnread}/${accountFolderCounts.trash}`
|
|
: accountFolderCounts.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={{ minHeight: '400px' }}>
|
|
{/* Email List */}
|
|
<div className="w-1/3 border-r border-gray-200 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] });
|
|
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>
|
|
);
|
|
}
|