added backup and email client
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
import { useState } from 'react';
|
||||
import { Mail, MailOpen, Star, Paperclip, Plus, X, ChevronRight, Inbox, Send } from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { cachedEmailApi, CachedEmail } from '../../services/api';
|
||||
import Button from '../ui/Button';
|
||||
import Card from '../ui/Card';
|
||||
import EmailDetail from './EmailDetail';
|
||||
import ComposeEmailModal from './ComposeEmailModal';
|
||||
|
||||
type EmailFolder = 'INBOX' | 'SENT';
|
||||
|
||||
interface ContractEmailsSectionProps {
|
||||
contractId: number;
|
||||
customerId: number;
|
||||
}
|
||||
|
||||
export default function ContractEmailsSection({
|
||||
contractId,
|
||||
customerId,
|
||||
}: ContractEmailsSectionProps) {
|
||||
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 queryClient = useQueryClient();
|
||||
|
||||
// E-Mails für den Vertrag laden (nach Ordner gefiltert)
|
||||
const { data: emailsData, isLoading, refetch: refetchEmails } = useQuery({
|
||||
queryKey: ['emails', 'contract', contractId, selectedFolder],
|
||||
queryFn: () => cachedEmailApi.getForContract(contractId, { folder: selectedFolder }),
|
||||
});
|
||||
|
||||
const emails = emailsData?.data || [];
|
||||
|
||||
// Ordner-Anzahlen für Badges
|
||||
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,
|
||||
};
|
||||
|
||||
// Mailbox-Konten für Versand laden
|
||||
const { data: accountsData } = useQuery({
|
||||
queryKey: ['mailbox-accounts', customerId],
|
||||
queryFn: () => cachedEmailApi.getMailboxAccounts(customerId),
|
||||
});
|
||||
|
||||
const accounts = accountsData?.data || [];
|
||||
const firstAccount = accounts[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;
|
||||
|
||||
// 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] });
|
||||
},
|
||||
});
|
||||
|
||||
// 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);
|
||||
},
|
||||
});
|
||||
|
||||
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 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;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-4">
|
||||
<span>E-Mails</span>
|
||||
{/* 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-2.5 py-1 text-xs 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-3.5 h-3.5" />
|
||||
Empfangen
|
||||
{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-2.5 py-1 text-xs 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-3.5 h-3.5" />
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
firstAccount && (
|
||||
<Button variant="secondary" size="sm" onClick={handleNewEmail}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Neue E-Mail
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{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>
|
||||
) : 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'
|
||||
: 'Keine E-Mails über diesen Vertrag gesendet'}
|
||||
</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-2/5 border-r border-gray-200 overflow-auto">
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
{/* Unassign Button - für empfangene E-Mails und manuell zugeordnete gesendete E-Mails */}
|
||||
{(selectedFolder === 'INBOX' || (selectedFolder === 'SENT' && !email.isAutoAssigned)) && (
|
||||
<button
|
||||
onClick={(e) => handleUnassign(e, email.id)}
|
||||
className="flex-shrink-0 mt-1 p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
|
||||
title="Zuordnung aufheben"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 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);
|
||||
// Folder-Counts aktualisieren
|
||||
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
|
||||
}}
|
||||
isSentFolder={selectedFolder === 'SENT'}
|
||||
isContractView={true}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* Compose Modal */}
|
||||
{firstAccount && (
|
||||
<ComposeEmailModal
|
||||
isOpen={showCompose}
|
||||
onClose={() => {
|
||||
setShowCompose(false);
|
||||
setReplyToEmail(null);
|
||||
}}
|
||||
account={firstAccount}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user