added backup and email client
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user