Files
opencrm/frontend/src/components/email/EmailClientTab.tsx
T

395 lines
14 KiB
TypeScript

import { useState, useEffect } from 'react';
import { RefreshCw, Plus, Mail, Inbox, Send, Trash2 } 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 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 [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 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],
queryFn: () =>
cachedEmailApi.getForCustomer(customerId, {
accountId: selectedAccountId || undefined,
folder: selectedFolder as 'INBOX' | 'SENT',
}),
enabled: !!selectedAccountId && selectedFolder !== 'TRASH',
});
const emails = emailsData?.data || [];
// Papierkorb-E-Mails laden
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 (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: () => {
// 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] });
},
});
const handleSync = () => {
if (selectedAccountId) {
syncMutation.mutate(selectedAccountId);
}
};
const handleSelectEmail = (email: CachedEmail) => {
setSelectedEmail(email);
};
const handleReply = () => {
setReplyToEmail(emailDetail || null);
setShowCompose(true);
};
const handleNewEmail = () => {
setReplyToEmail(null);
setShowCompose(true);
};
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 Stressfrei-Wechseln 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 (
<div className="flex flex-col h-full" style={{ minHeight: '600px' }}>
{/* Header */}
<div className="flex items-center justify-between gap-4 p-4 border-b border-gray-200 bg-gray-50">
{/* Account Selector */}
<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>
</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 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>
{/* Email Detail */}
<div className="flex-1 overflow-auto">
{emailDetail ? (
<EmailDetail
email={emailDetail}
onReply={handleReply}
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);
}}
account={selectedAccount}
replyTo={replyToEmail || 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>
);
}