all email views the same
This commit is contained in:
-678
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+678
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user