all email views the same

This commit is contained in:
dufyfduck 2026-02-03 23:04:42 +01:00
parent ee8bd7a8f7
commit 6f3ab288ed
10 changed files with 1155 additions and 797 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

678
frontend/dist/assets/index-DxzmsVZ0.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenCRM</title> <title>OpenCRM</title>
<script type="module" crossorigin src="/assets/index-BdT2l8pM.js"></script> <script type="module" crossorigin src="/assets/index-DxzmsVZ0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DCjAycJ5.css"> <link rel="stylesheet" crossorigin href="/assets/index-CUVVQncv.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,13 +1,16 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Mail, MailOpen, Star, Paperclip, Plus, X, ChevronRight, Inbox, Send } from 'lucide-react'; import { Mail, MailOpen, Star, Paperclip, Plus, X, ChevronRight, Inbox, Send, RefreshCw, Trash2 } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { cachedEmailApi, CachedEmail } from '../../services/api'; import { cachedEmailApi, stressfreiEmailApi, CachedEmail } from '../../services/api';
import { useAuth } from '../../context/AuthContext';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Card from '../ui/Card'; import Card from '../ui/Card';
import EmailDetail from './EmailDetail'; import EmailDetail from './EmailDetail';
import ComposeEmailModal from './ComposeEmailModal'; import ComposeEmailModal from './ComposeEmailModal';
import TrashEmailList from './TrashEmailList';
import toast from 'react-hot-toast';
type EmailFolder = 'INBOX' | 'SENT'; type EmailFolder = 'INBOX' | 'SENT' | 'TRASH';
interface ContractEmailsSectionProps { interface ContractEmailsSectionProps {
contractId: number; contractId: number;
@ -18,21 +21,52 @@ export default function ContractEmailsSection({
contractId, contractId,
customerId, customerId,
}: ContractEmailsSectionProps) { }: ContractEmailsSectionProps) {
const [selectedAccountId, setSelectedAccountId] = useState<number | null>(null);
const [selectedFolder, setSelectedFolder] = useState<EmailFolder>('INBOX'); const [selectedFolder, setSelectedFolder] = useState<EmailFolder>('INBOX');
const [selectedEmail, setSelectedEmail] = useState<CachedEmail | null>(null); const [selectedEmail, setSelectedEmail] = useState<CachedEmail | null>(null);
const [showCompose, setShowCompose] = useState(false); const [showCompose, setShowCompose] = useState(false);
const [replyToEmail, setReplyToEmail] = useState<CachedEmail | null>(null); const [replyToEmail, setReplyToEmail] = useState<CachedEmail | null>(null);
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { hasPermission } = useAuth();
const canAccessTrash = hasPermission('emails:delete');
// E-Mails für den Vertrag laden (nach Ordner gefiltert) // 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 für den Vertrag laden (nach Ordner gefiltert, nicht für TRASH)
const { data: emailsData, isLoading, refetch: refetchEmails } = useQuery({ const { data: emailsData, isLoading, refetch: refetchEmails } = useQuery({
queryKey: ['emails', 'contract', contractId, selectedFolder], queryKey: ['emails', 'contract', contractId, selectedFolder],
queryFn: () => cachedEmailApi.getForContract(contractId, { folder: selectedFolder }), queryFn: () => cachedEmailApi.getForContract(contractId, { folder: selectedFolder as 'INBOX' | 'SENT' }),
enabled: selectedFolder !== 'TRASH',
}); });
const emails = emailsData?.data || []; const emails = emailsData?.data || [];
// Ordner-Anzahlen für Badges // Papierkorb-E-Mails laden (für den ganzen Kunden, da Trash nicht vertragsgebunden)
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 (Vertrag)
const { data: folderCountsData } = useQuery({ const { data: folderCountsData } = useQuery({
queryKey: ['contract-folder-counts', contractId], queryKey: ['contract-folder-counts', contractId],
queryFn: () => cachedEmailApi.getContractFolderCounts(contractId), queryFn: () => cachedEmailApi.getContractFolderCounts(contractId),
@ -45,14 +79,17 @@ export default function ContractEmailsSection({
sentUnread: 0, sentUnread: 0,
}; };
// Mailbox-Konten für Versand laden // Ordner-Anzahlen für das Konto (für Trash-Badge)
const { data: accountsData } = useQuery({ const { data: accountFolderCountsData } = useQuery({
queryKey: ['mailbox-accounts', customerId], queryKey: ['folder-counts', selectedAccountId],
queryFn: () => cachedEmailApi.getMailboxAccounts(customerId), queryFn: () => stressfreiEmailApi.getFolderCounts(selectedAccountId!),
enabled: !!selectedAccountId && canAccessTrash,
}); });
const accounts = accountsData?.data || []; const accountFolderCounts = accountFolderCountsData?.data || {
const firstAccount = accounts[0]; trash: 0,
trashUnread: 0,
};
// Einzelne E-Mail laden (mit Body) // Einzelne E-Mail laden (mit Body)
const { data: emailDetailData } = useQuery({ const { data: emailDetailData } = useQuery({
@ -63,6 +100,24 @@ export default function ContractEmailsSection({
const emailDetail = emailDetailData?.data || selectedEmail; 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: ['contract-folder-counts', contractId] });
if (selectedAccountId) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}
toast.success('Synchronisation abgeschlossen');
},
onError: (error: Error) => {
toast.error(error.message || 'Synchronisation fehlgeschlagen');
},
});
// Stern umschalten // Stern umschalten
const toggleStarMutation = useMutation({ const toggleStarMutation = useMutation({
mutationFn: (emailId: number) => cachedEmailApi.toggleStar(emailId), mutationFn: (emailId: number) => cachedEmailApi.toggleStar(emailId),
@ -81,6 +136,9 @@ export default function ContractEmailsSection({
queryClient.invalidateQueries({ queryKey: ['email', variables.emailId] }); queryClient.invalidateQueries({ queryKey: ['email', variables.emailId] });
// Folder-Counts aktualisieren für Badge-Update // Folder-Counts aktualisieren für Badge-Update
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] }); queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
if (selectedAccountId) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}
}, },
}); });
@ -91,9 +149,40 @@ export default function ContractEmailsSection({
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] }); queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] }); queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
setSelectedEmail(null); setSelectedEmail(null);
toast.success('Zuordnung aufgehoben');
},
onError: (error: Error) => {
toast.error(error.message || 'Fehler beim Aufheben der Zuordnung');
}, },
}); });
// E-Mail löschen (in Papierkorb)
const deleteMutation = useMutation({
mutationFn: (emailId: number) => cachedEmailApi.delete(emailId),
onSuccess: (_data, emailId) => {
queryClient.invalidateQueries({ queryKey: ['emails'] });
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
if (selectedAccountId) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}
toast.success('E-Mail in Papierkorb verschoben');
setDeleteConfirmId(null);
if (selectedEmail?.id === emailId) {
setSelectedEmail(null);
}
},
onError: (error: Error) => {
toast.error(error.message || 'Fehler beim Löschen der E-Mail');
setDeleteConfirmId(null);
},
});
const handleSync = () => {
if (selectedAccountId) {
syncMutation.mutate(selectedAccountId);
}
};
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
const now = new Date(); const now = new Date();
@ -141,6 +230,23 @@ export default function ContractEmailsSection({
unassignMutation.mutate(emailId); 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 handleFolderChange = (folder: EmailFolder) => { const handleFolderChange = (folder: EmailFolder) => {
setSelectedFolder(folder); setSelectedFolder(folder);
setSelectedEmail(null); setSelectedEmail(null);
@ -161,23 +267,90 @@ export default function ContractEmailsSection({
return email.fromName || email.fromAddress; return email.fromName || email.fromAddress;
}; };
// Keine Mailbox-Konten vorhanden
if (!accountsLoading && accounts.length === 0) {
return (
<Card title="E-Mails">
<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">Keine E-Mail-Konten vorhanden</p>
<p className="text-xs mt-1">
Erstellen Sie eine E-Mail-Adresse beim Kunden mit aktivierter Mailbox
</p>
</div>
</Card>
);
}
return ( return (
<Card <Card
title={ title={
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span>E-Mails</span> <span>E-Mails</span>
</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...' : 'Sync'}
</Button>
)}
{selectedAccount && (
<Button size="sm" onClick={handleNewEmail}>
<Plus className="w-4 h-4 mr-1" />
Neue E-Mail
</Button>
)}
</div>
}
>
{/* Header mit Account-Auswahl und Ordner-Tabs */}
<div className="flex items-center justify-between gap-4 pb-4 border-b border-gray-200 -mt-2">
{/* Account Selector */}
{accounts.length > 1 ? (
<div className="flex items-center gap-2">
<Inbox className="w-4 h-4 text-gray-500" />
<select
value={selectedAccountId || ''}
onChange={(e) => {
setSelectedAccountId(Number(e.target.value));
setSelectedEmail(null);
}}
className="px-2 py-1.5 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>
) : (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Inbox className="w-4 h-4 text-gray-500" />
<span>{selectedAccount?.email}</span>
</div>
)}
{/* Folder Tabs */} {/* Folder Tabs */}
<div className="flex items-center gap-1 bg-gray-200 rounded-lg p-1"> <div className="flex items-center gap-1 bg-gray-200 rounded-lg p-1">
<button <button
onClick={() => handleFolderChange('INBOX')} onClick={() => handleFolderChange('INBOX')}
className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md transition-colors ${ className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedFolder === 'INBOX' selectedFolder === 'INBOX'
? 'bg-white text-blue-600 shadow-sm font-medium' ? 'bg-white text-blue-600 shadow-sm font-medium'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`} }`}
> >
<Inbox className="w-3.5 h-3.5" /> <Inbox className="w-4 h-4" />
Empfangen Posteingang
{folderCounts.inbox > 0 && ( {folderCounts.inbox > 0 && (
<span <span
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${ className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
@ -195,13 +368,13 @@ export default function ContractEmailsSection({
</button> </button>
<button <button
onClick={() => handleFolderChange('SENT')} onClick={() => handleFolderChange('SENT')}
className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md transition-colors ${ className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedFolder === 'SENT' selectedFolder === 'SENT'
? 'bg-white text-blue-600 shadow-sm font-medium' ? 'bg-white text-blue-600 shadow-sm font-medium'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`} }`}
> >
<Send className="w-3.5 h-3.5" /> <Send className="w-4 h-4" />
Gesendet Gesendet
{folderCounts.sent > 0 && ( {folderCounts.sent > 0 && (
<span <span
@ -218,29 +391,50 @@ export default function ContractEmailsSection({
</span> </span>
)} )}
</button> </button>
</div> {canAccessTrash && (
</div> <button
} onClick={() => handleFolderChange('TRASH')}
actions={ className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
firstAccount && ( selectedFolder === 'TRASH'
<Button variant="secondary" size="sm" onClick={handleNewEmail}> ? 'bg-white text-red-600 shadow-sm font-medium'
<Plus className="w-4 h-4 mr-1" /> : 'text-gray-600 hover:text-gray-900'
Neue E-Mail }`}
</Button>
)
}
> >
{isLoading ? ( <Trash2 className="w-4 h-4" />
Papierkorb
{accountFolderCounts.trash > 0 && (
<span
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
accountFolderCounts.trashUnread > 0
? 'bg-red-100 text-red-600 font-medium'
: 'bg-gray-100 text-gray-500'
}`}
title={`${accountFolderCounts.trashUnread} ungelesen / ${accountFolderCounts.trash} gesamt`}
>
{accountFolderCounts.trashUnread > 0
? `${accountFolderCounts.trashUnread}/${accountFolderCounts.trash}`
: accountFolderCounts.trash}
</span>
)}
</button>
)}
</div>
</div>
{/* Content */}
{(selectedFolder === 'TRASH' ? trashLoading : isLoading) ? (
<div className="flex items-center justify-center py-8"> <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 className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
</div> </div>
) : emails.length === 0 ? ( ) : (selectedFolder === 'TRASH' ? trashEmails.length === 0 : emails.length === 0) ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-500"> <div className="flex flex-col items-center justify-center py-8 text-gray-500">
<Mail className="w-10 h-10 mb-2 opacity-30" /> <Mail className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm"> <p className="text-sm">
{selectedFolder === 'INBOX' {selectedFolder === 'INBOX'
? 'Keine E-Mails zugeordnet' ? 'Keine E-Mails zugeordnet'
: 'Keine E-Mails über diesen Vertrag gesendet'} : selectedFolder === 'SENT'
? 'Keine E-Mails über diesen Vertrag gesendet'
: 'Papierkorb ist leer'}
</p> </p>
{selectedFolder === 'INBOX' && ( {selectedFolder === 'INBOX' && (
<p className="text-xs mt-1"> <p className="text-xs mt-1">
@ -251,7 +445,31 @@ export default function ContractEmailsSection({
) : ( ) : (
<div className="flex -mx-6 -mb-6" style={{ minHeight: '400px' }}> <div className="flex -mx-6 -mb-6" style={{ minHeight: '400px' }}>
{/* Email List */} {/* Email List */}
<div className="w-2/5 border-r border-gray-200 overflow-auto"> <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] });
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
}}
onEmailDeleted={(emailId) => {
if (selectedEmail?.id === emailId) {
setSelectedEmail(null);
}
queryClient.invalidateQueries({ queryKey: ['emails', 'trash'] });
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}}
isLoading={trashLoading}
/>
) : (
<div className="divide-y divide-gray-200"> <div className="divide-y divide-gray-200">
{emails.map((email) => ( {emails.map((email) => (
<div <div
@ -295,6 +513,17 @@ export default function ContractEmailsSection({
<Star className={`w-4 h-4 ${email.isStarred ? 'fill-current' : ''}`} /> <Star className={`w-4 h-4 ${email.isStarred ? 'fill-current' : ''}`} />
</button> </button>
{/* Delete Button (nur mit 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 */} {/* Email Content */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* From/To & Date */} {/* From/To & Date */}
@ -316,24 +545,34 @@ export default function ContractEmailsSection({
<Paperclip className="w-3 h-3 text-gray-400 flex-shrink-0" /> <Paperclip className="w-3 h-3 text-gray-400 flex-shrink-0" />
)} )}
</div> </div>
</div>
{/* Unassign Button - für empfangene E-Mails und manuell zugeordnete gesendete E-Mails */} {/* 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 */}
{(selectedFolder === 'INBOX' || (selectedFolder === 'SENT' && !email.isAutoAssigned)) && ( {(selectedFolder === 'INBOX' || (selectedFolder === 'SENT' && !email.isAutoAssigned)) && (
<button <button
onClick={(e) => handleUnassign(e, email.id)} 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" className="p-0.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
title="Zuordnung aufheben" title="Zuordnung aufheben"
disabled={unassignMutation.isPending}
> >
<X className="w-4 h-4" /> <X className="w-3.5 h-3.5" />
</button> </button>
)} )}
</div>
)}
</div>
{/* Chevron */} {/* Chevron */}
<ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0 mt-2" /> <ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0 mt-2" />
</div> </div>
))} ))}
</div> </div>
)}
</div> </div>
{/* Email Detail */} {/* Email Detail */}
@ -345,12 +584,23 @@ export default function ContractEmailsSection({
onAssignContract={() => {}} onAssignContract={() => {}}
onDeleted={() => { onDeleted={() => {
setSelectedEmail(null); setSelectedEmail(null);
// Folder-Counts aktualisieren queryClient.invalidateQueries({ queryKey: ['emails'] });
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] }); queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
if (selectedAccountId) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}
}} }}
isSentFolder={selectedFolder === 'SENT'} isSentFolder={selectedFolder === 'SENT'}
isContractView={true} isContractView={selectedFolder !== 'TRASH'}
isTrashView={selectedFolder === 'TRASH'}
onRestored={() => {
setSelectedEmail(null);
queryClient.invalidateQueries({ queryKey: ['emails'] });
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
if (selectedAccountId) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}
}}
accountId={emailDetail?.stressfreiEmailId} accountId={emailDetail?.stressfreiEmailId}
/> />
) : ( ) : (
@ -363,15 +613,45 @@ export default function ContractEmailsSection({
</div> </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>
)}
{/* Compose Modal */} {/* Compose Modal */}
{firstAccount && ( {selectedAccount && (
<ComposeEmailModal <ComposeEmailModal
isOpen={showCompose} isOpen={showCompose}
onClose={() => { onClose={() => {
setShowCompose(false); setShowCompose(false);
setReplyToEmail(null); setReplyToEmail(null);
}} }}
account={firstAccount} account={selectedAccount}
replyTo={replyToEmail || undefined} replyTo={replyToEmail || undefined}
contractId={contractId} contractId={contractId}
onSuccess={() => { onSuccess={() => {

View File

@ -155,6 +155,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between gap-4 p-4 border-b border-gray-200 bg-gray-50"> <div className="flex items-center justify-between gap-4 p-4 border-b border-gray-200 bg-gray-50">
{/* Account Selector */} {/* Account Selector */}
{accounts.length > 1 ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Inbox className="w-5 h-5 text-gray-500" /> <Inbox className="w-5 h-5 text-gray-500" />
<select <select
@ -172,6 +173,12 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
))} ))}
</select> </select>
</div> </div>
) : (
<div className="flex items-center gap-3 text-sm text-gray-600">
<Inbox className="w-5 h-5 text-gray-500" />
<span>{selectedAccount?.email}</span>
</div>
)}
{/* Folder Tabs */} {/* Folder Tabs */}
<div className="flex items-center gap-1 bg-gray-200 rounded-lg p-1"> <div className="flex items-center gap-1 bg-gray-200 rounded-lg p-1">

View File

@ -64,6 +64,14 @@ export default function EmailDetail({
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['emails'] }); queryClient.invalidateQueries({ queryKey: ['emails'] });
queryClient.invalidateQueries({ queryKey: ['email', email.id] }); queryClient.invalidateQueries({ queryKey: ['email', email.id] });
if (email.contractId) {
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', email.contractId] });
}
toast.success('Vertragszuordnung aufgehoben');
},
onError: (error: Error) => {
console.error('Unassign error:', error);
toast.error(error.message || 'Fehler beim Aufheben der Zuordnung');
}, },
}); });
@ -264,10 +272,8 @@ export default function EmailDetail({
{email.contract.contractNumber} {email.contract.contractNumber}
</Link> </Link>
</span> </span>
{/* Löschen-Button: {/* X-Button nur für manuell zugeordnete E-Mails (nicht für automatisch zugeordnete aus Vertrag gesendete) */}
- In Vertragsansicht bei gesendeten E-Mails ausblenden {!email.isAutoAssigned && (
- Bei automatisch zugeordneten E-Mails (aus Vertrag gesendet) ausblenden */}
{!(isContractView && isSentFolder) && !email.isAutoAssigned && (
<button <button
onClick={() => unassignMutation.mutate()} onClick={() => unassignMutation.mutate()}
className="ml-2 p-1 hover:bg-green-100 rounded" className="ml-2 p-1 hover:bg-green-100 rounded"

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Mail, MailOpen, Star, Paperclip, ChevronRight, Trash2 } from 'lucide-react'; import { Mail, MailOpen, Star, Paperclip, ChevronRight, Trash2, X } from 'lucide-react';
import { CachedEmail, cachedEmailApi } from '../../services/api'; import { CachedEmail, cachedEmailApi } from '../../services/api';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
@ -87,6 +87,23 @@ export default function EmailList({
}, },
}); });
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) => { const handleDeleteClick = (e: React.MouseEvent, emailId: number) => {
e.stopPropagation(); e.stopPropagation();
setDeleteConfirmId(emailId); setDeleteConfirmId(emailId);
@ -229,10 +246,21 @@ export default function EmailList({
{/* Contract Badge */} {/* Contract Badge */}
{email.contract && ( {email.contract && (
<div className="mt-1"> <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"> <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} {email.contract.contractNumber}
</span> </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>
)} )}
</div> </div>

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Undo2, Trash2, ChevronRight, Inbox, Send } from 'lucide-react'; import { Undo2, Trash2, ChevronRight, Inbox, Send, X } from 'lucide-react';
import { CachedEmail, cachedEmailApi } from '../../services/api'; import { CachedEmail, cachedEmailApi } from '../../services/api';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -75,6 +75,23 @@ export default function TrashEmailList({
}, },
}); });
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 handleRestoreClick = (e: React.MouseEvent, emailId: number) => { const handleRestoreClick = (e: React.MouseEvent, emailId: number) => {
e.stopPropagation(); e.stopPropagation();
setActionConfirmId(emailId); setActionConfirmId(emailId);
@ -201,6 +218,26 @@ export default function TrashEmailList({
<div className="text-xs text-red-500 mt-1"> <div className="text-xs text-red-500 mt-1">
{formatDeletedAt(email.deletedAt)} {formatDeletedAt(email.deletedAt)}
</div> </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 ursprüngliche INBOX oder manuell zugeordnete gesendete E-Mails */}
{(email.folder === 'INBOX' || (email.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> </div>
{/* Chevron */} {/* Chevron */}