305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { Mail, MailOpen, Star, Paperclip, ChevronRight, Trash2, X } from 'lucide-react';
|
|
import { CachedEmail, cachedEmailApi } from '../../services/api';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import Button from '../ui/Button';
|
|
import toast from 'react-hot-toast';
|
|
|
|
interface EmailListProps {
|
|
emails: CachedEmail[];
|
|
selectedEmailId?: number;
|
|
onSelectEmail: (email: CachedEmail) => void;
|
|
onEmailDeleted?: (emailId: number) => void;
|
|
isLoading?: boolean;
|
|
folder?: 'INBOX' | 'SENT';
|
|
accountId?: number | null; // Für Folder-Count-Aktualisierung bei Lesen/Ungelesen
|
|
}
|
|
|
|
export default function EmailList({
|
|
emails,
|
|
selectedEmailId,
|
|
onSelectEmail,
|
|
onEmailDeleted,
|
|
isLoading,
|
|
folder = 'INBOX',
|
|
accountId,
|
|
}: EmailListProps) {
|
|
const isSentFolder = folder === 'SENT';
|
|
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
|
|
const { hasPermission } = useAuth();
|
|
|
|
// Für gesendete E-Mails: Empfänger extrahieren
|
|
const getDisplayName = (email: CachedEmail) => {
|
|
if (isSentFolder) {
|
|
// Bei gesendeten E-Mails: Ersten Empfänger anzeigen
|
|
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)';
|
|
}
|
|
}
|
|
// Bei empfangenen E-Mails: Absender anzeigen
|
|
return email.fromName || email.fromAddress;
|
|
};
|
|
const queryClient = useQueryClient();
|
|
|
|
const toggleStarMutation = useMutation({
|
|
mutationFn: (emailId: number) => cachedEmailApi.toggleStar(emailId),
|
|
onSuccess: (_data, emailId) => {
|
|
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
|
queryClient.invalidateQueries({ queryKey: ['email', emailId] });
|
|
},
|
|
});
|
|
|
|
const toggleReadMutation = useMutation({
|
|
mutationFn: ({ emailId, isRead }: { emailId: number; isRead: boolean }) =>
|
|
cachedEmailApi.markAsRead(emailId, isRead),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
|
queryClient.invalidateQueries({ queryKey: ['email', variables.emailId] });
|
|
// Folder-Counts aktualisieren für Badge-Update
|
|
if (accountId) {
|
|
queryClient.invalidateQueries({ queryKey: ['folder-counts', accountId] });
|
|
}
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (emailId: number) => cachedEmailApi.delete(emailId),
|
|
onSuccess: (_data, emailId) => {
|
|
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
|
// Folder-Counts aktualisieren
|
|
if (accountId) {
|
|
queryClient.invalidateQueries({ queryKey: ['folder-counts', accountId] });
|
|
}
|
|
toast.success('E-Mail in Papierkorb verschoben');
|
|
setDeleteConfirmId(null);
|
|
onEmailDeleted?.(emailId);
|
|
},
|
|
onError: (error: Error) => {
|
|
console.error('Delete error:', error);
|
|
toast.error(error.message || 'Fehler beim Löschen der E-Mail');
|
|
setDeleteConfirmId(null);
|
|
},
|
|
});
|
|
|
|
const unassignMutation = useMutation({
|
|
mutationFn: (emailId: number) => cachedEmailApi.unassignFromContract(emailId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['emails'] });
|
|
toast.success('Vertragszuordnung aufgehoben');
|
|
},
|
|
onError: (error: Error) => {
|
|
console.error('Unassign error:', error);
|
|
toast.error(error.message || 'Fehler beim Aufheben der Zuordnung');
|
|
},
|
|
});
|
|
|
|
const handleUnassign = (e: React.MouseEvent, emailId: number) => {
|
|
e.stopPropagation();
|
|
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 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 });
|
|
}
|
|
onSelectEmail(email);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (emails.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
|
<Mail className="w-12 h-12 mb-2 opacity-50" />
|
|
<p>Keine E-Mails vorhanden</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="divide-y divide-gray-200">
|
|
{emails.map((email) => (
|
|
<div
|
|
key={email.id}
|
|
onClick={() => handleSelectEmail(email)}
|
|
className={[
|
|
'flex items-start gap-3 p-3 cursor-pointer transition-colors',
|
|
selectedEmailId === email.id
|
|
? 'bg-blue-100'
|
|
: ['hover:bg-gray-100', !email.isRead ? 'bg-white' : 'bg-gray-50/50'].join(' ')
|
|
].join(' ')}
|
|
style={{
|
|
borderLeft: selectedEmailId === 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 (nur für User mit emails:delete 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 */}
|
|
{(folder === 'INBOX' || (folder === '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>
|
|
))}
|
|
|
|
{/* 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>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|