all email views the same

This commit is contained in:
dufyfduck 2026-02-03 23:04:42 +01:00
parent 2444310a28
commit 96560358bc
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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenCRM</title>
<script type="module" crossorigin src="/assets/index-BdT2l8pM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DCjAycJ5.css">
<script type="module" crossorigin src="/assets/index-DxzmsVZ0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CUVVQncv.css">
</head>
<body>
<div id="root"></div>

View File

@ -1,13 +1,16 @@
import { useState } from 'react';
import { Mail, MailOpen, Star, Paperclip, Plus, X, ChevronRight, Inbox, Send } from 'lucide-react';
import { useState, useEffect } from '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 { cachedEmailApi, CachedEmail } from '../../services/api';
import { cachedEmailApi, stressfreiEmailApi, CachedEmail } from '../../services/api';
import { useAuth } from '../../context/AuthContext';
import Button from '../ui/Button';
import Card from '../ui/Card';
import EmailDetail from './EmailDetail';
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 {
contractId: number;
@ -18,21 +21,52 @@ export default function ContractEmailsSection({
contractId,
customerId,
}: ContractEmailsSectionProps) {
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 [replyToEmail, setReplyToEmail] = useState<CachedEmail | null>(null);
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
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({
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 || [];
// 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({
queryKey: ['contract-folder-counts', contractId],
queryFn: () => cachedEmailApi.getContractFolderCounts(contractId),
@ -45,14 +79,17 @@ export default function ContractEmailsSection({
sentUnread: 0,
};
// Mailbox-Konten für Versand laden
const { data: accountsData } = useQuery({
queryKey: ['mailbox-accounts', customerId],
queryFn: () => cachedEmailApi.getMailboxAccounts(customerId),
// Ordner-Anzahlen für das Konto (für Trash-Badge)
const { data: accountFolderCountsData } = useQuery({
queryKey: ['folder-counts', selectedAccountId],
queryFn: () => stressfreiEmailApi.getFolderCounts(selectedAccountId!),
enabled: !!selectedAccountId && canAccessTrash,
});
const accounts = accountsData?.data || [];
const firstAccount = accounts[0];
const accountFolderCounts = accountFolderCountsData?.data || {
trash: 0,
trashUnread: 0,
};
// Einzelne E-Mail laden (mit Body)
const { data: emailDetailData } = useQuery({
@ -63,6 +100,24 @@ export default function ContractEmailsSection({
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
const toggleStarMutation = useMutation({
mutationFn: (emailId: number) => cachedEmailApi.toggleStar(emailId),
@ -81,6 +136,9 @@ export default function ContractEmailsSection({
queryClient.invalidateQueries({ queryKey: ['email', variables.emailId] });
// Folder-Counts aktualisieren für Badge-Update
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: ['contract-folder-counts', contractId] });
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 date = new Date(dateStr);
const now = new Date();
@ -141,6 +230,23 @@ export default function ContractEmailsSection({
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) => {
setSelectedFolder(folder);
setSelectedEmail(null);
@ -161,86 +267,174 @@ export default function ContractEmailsSection({
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 (
<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>
)
<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>
}
>
{isLoading ? (
{/* 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 */}
<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
{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="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></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">
<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'}
: selectedFolder === 'SENT'
? 'Keine E-Mails über diesen Vertrag gesendet'
: 'Papierkorb ist leer'}
</p>
{selectedFolder === 'INBOX' && (
<p className="text-xs mt-1">
@ -251,7 +445,31 @@ export default function ContractEmailsSection({
) : (
<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="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">
{emails.map((email) => (
<div
@ -295,6 +513,17 @@ export default function ContractEmailsSection({
<Star className={`w-4 h-4 ${email.isStarred ? 'fill-current' : ''}`} />
</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 */}
<div className="flex-1 min-w-0">
{/* From/To & Date */}
@ -316,24 +545,34 @@ export default function ContractEmailsSection({
<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>
)}
{/* 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)) && (
<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>
{/* Chevron */}
<ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0 mt-2" />
</div>
))}
</div>
)}
</div>
{/* Email Detail */}
@ -345,12 +584,23 @@ export default function ContractEmailsSection({
onAssignContract={() => {}}
onDeleted={() => {
setSelectedEmail(null);
// Folder-Counts aktualisieren
queryClient.invalidateQueries({ queryKey: ['emails', 'contract', contractId] });
queryClient.invalidateQueries({ queryKey: ['emails'] });
queryClient.invalidateQueries({ queryKey: ['contract-folder-counts', contractId] });
if (selectedAccountId) {
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
}
}}
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}
/>
) : (
@ -363,15 +613,45 @@ export default function ContractEmailsSection({
</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 */}
{firstAccount && (
{selectedAccount && (
<ComposeEmailModal
isOpen={showCompose}
onClose={() => {
setShowCompose(false);
setReplyToEmail(null);
}}
account={firstAccount}
account={selectedAccount}
replyTo={replyToEmail || undefined}
contractId={contractId}
onSuccess={() => {

View File

@ -155,23 +155,30 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
{/* 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>
{accounts.length > 1 ? (
<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>
) : (
<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 */}
<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: () => {
queryClient.invalidateQueries({ queryKey: ['emails'] });
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}
</Link>
</span>
{/* Löschen-Button:
- In Vertragsansicht bei gesendeten E-Mails ausblenden
- Bei automatisch zugeordneten E-Mails (aus Vertrag gesendet) ausblenden */}
{!(isContractView && isSentFolder) && !email.isAutoAssigned && (
{/* X-Button nur für manuell zugeordnete E-Mails (nicht für automatisch zugeordnete aus Vertrag gesendete) */}
{!email.isAutoAssigned && (
<button
onClick={() => unassignMutation.mutate()}
className="ml-2 p-1 hover:bg-green-100 rounded"

View File

@ -1,5 +1,5 @@
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 { useMutation, useQueryClient } from '@tanstack/react-query';
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) => {
e.stopPropagation();
setDeleteConfirmId(emailId);
@ -229,10 +246,21 @@ export default function EmailList({
{/* Contract Badge */}
{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">
{email.contract.contractNumber}
</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>

View File

@ -1,5 +1,5 @@
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 { useMutation, useQueryClient } from '@tanstack/react-query';
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) => {
e.stopPropagation();
setActionConfirmId(emailId);
@ -201,6 +218,26 @@ export default function TrashEmailList({
<div className="text-xs text-red-500 mt-1">
{formatDeletedAt(email.deletedAt)}
</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>
{/* Chevron */}