395 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|