all email views the same
This commit is contained in:
parent
2444310a28
commit
96560358bc
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
File diff suppressed because one or more lines are too long
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue