gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { emailLogApi, EmailLog } from '../../services/api';
|
||||
import Card from '../../components/ui/Card';
|
||||
import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import Badge from '../../components/ui/Badge';
|
||||
import Modal from '../../components/ui/Modal';
|
||||
import { ArrowLeft, CheckCircle2, XCircle, Mail, Server, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
const CONTEXT_LABELS: Record<string, string> = {
|
||||
'consent-link': 'Datenschutz-Link',
|
||||
'authorization-request': 'Vollmacht-Anfrage',
|
||||
'customer-email': 'Kunden-E-Mail',
|
||||
};
|
||||
|
||||
export default function EmailLogs() {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [contextFilter, setContextFilter] = useState<string>('');
|
||||
const [selectedLog, setSelectedLog] = useState<EmailLog | null>(null);
|
||||
|
||||
const { data: statsData } = useQuery({
|
||||
queryKey: ['email-log-stats'],
|
||||
queryFn: () => emailLogApi.getStats(),
|
||||
});
|
||||
|
||||
const { data: logsData, isLoading } = useQuery({
|
||||
queryKey: ['email-logs', page, search, statusFilter, contextFilter],
|
||||
queryFn: () => emailLogApi.getLogs({
|
||||
page,
|
||||
limit: 30,
|
||||
success: statusFilter || undefined,
|
||||
search: search || undefined,
|
||||
context: contextFilter || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
const stats = statsData?.data;
|
||||
const logs = logsData?.data || [];
|
||||
const pagination = logsData?.pagination;
|
||||
|
||||
const formatDate = (date: string) =>
|
||||
new Date(date).toLocaleDateString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" onClick={() => navigate('/settings')}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Zurück
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">E-Mail-Versandlog</h1>
|
||||
</div>
|
||||
|
||||
{/* Statistiken */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
|
||||
<p className="text-sm text-gray-500">Gesamt</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-green-600">{stats.success}</p>
|
||||
<p className="text-sm text-gray-500">Erfolgreich</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-red-600">{stats.failed}</p>
|
||||
<p className="text-sm text-gray-500">Fehlgeschlagen</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-blue-600">{stats.last24h}</p>
|
||||
<p className="text-sm text-gray-500">Letzte 24h</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filter */}
|
||||
<Card className="mb-6">
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<Input
|
||||
placeholder="Suche (Absender, Empfänger, Betreff...)"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Status</option>
|
||||
<option value="true">Erfolgreich</option>
|
||||
<option value="false">Fehlgeschlagen</option>
|
||||
</select>
|
||||
<select
|
||||
value={contextFilter}
|
||||
onChange={(e) => { setContextFilter(e.target.value); setPage(1); }}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="consent-link">Datenschutz-Link</option>
|
||||
<option value="authorization-request">Vollmacht-Anfrage</option>
|
||||
<option value="customer-email">Kunden-E-Mail</option>
|
||||
</select>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Log-Tabelle */}
|
||||
<Card>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Laden...</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">Keine E-Mail-Logs vorhanden.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Status</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Zeitpunkt</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Typ</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Von</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">An</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Betreff</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">SMTP</th>
|
||||
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map((log) => (
|
||||
<tr key={log.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 px-3">
|
||||
{log.success ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-red-500" />
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-xs text-gray-500 whitespace-nowrap">
|
||||
{formatDate(log.sentAt)}
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-700">
|
||||
{CONTEXT_LABELS[log.context] || log.context}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-sm truncate max-w-[150px]">{log.fromAddress}</td>
|
||||
<td className="py-2 px-3 text-sm truncate max-w-[150px]">{log.toAddress}</td>
|
||||
<td className="py-2 px-3 text-sm truncate max-w-[200px]">{log.subject}</td>
|
||||
<td className="py-2 px-3 text-xs text-gray-500 whitespace-nowrap">
|
||||
{log.smtpServer}:{log.smtpPort}
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<button
|
||||
onClick={() => setSelectedLog(log)}
|
||||
className="text-blue-600 hover:underline text-xs"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage(page - 1)}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={page >= pagination.totalPages}
|
||||
onClick={() => setPage(page + 1)}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Detail-Modal */}
|
||||
{selectedLog && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => setSelectedLog(null)}
|
||||
title="E-Mail-Log Details"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedLog.success ? (
|
||||
<Badge variant="success">Erfolgreich</Badge>
|
||||
) : (
|
||||
<Badge variant="danger">Fehlgeschlagen</Badge>
|
||||
)}
|
||||
<span className="text-sm text-gray-500">{formatDate(selectedLog.sentAt)}</span>
|
||||
</div>
|
||||
|
||||
{/* E-Mail-Details */}
|
||||
<div className="border rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Mail className="w-4 h-4 text-gray-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="grid grid-cols-[80px_1fr] gap-1 text-sm">
|
||||
<span className="text-gray-500">Von:</span>
|
||||
<span>{selectedLog.fromAddress}</span>
|
||||
<span className="text-gray-500">An:</span>
|
||||
<span>{selectedLog.toAddress}</span>
|
||||
<span className="text-gray-500">Betreff:</span>
|
||||
<span>{selectedLog.subject}</span>
|
||||
<span className="text-gray-500">Typ:</span>
|
||||
<span>{CONTEXT_LABELS[selectedLog.context] || selectedLog.context}</span>
|
||||
{selectedLog.triggeredBy && (
|
||||
<>
|
||||
<span className="text-gray-500">Ausgelöst von:</span>
|
||||
<span>{selectedLog.triggeredBy}</span>
|
||||
</>
|
||||
)}
|
||||
{selectedLog.messageId && (
|
||||
<>
|
||||
<span className="text-gray-500">Message-ID:</span>
|
||||
<span className="font-mono text-xs break-all">{selectedLog.messageId}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SMTP-Details */}
|
||||
<div className="border rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Server className="w-4 h-4 text-gray-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium mb-2">SMTP-Verbindung</h4>
|
||||
<div className="grid grid-cols-[100px_1fr] gap-1 text-sm">
|
||||
<span className="text-gray-500">Server:</span>
|
||||
<span className="font-mono text-xs">{selectedLog.smtpServer}:{selectedLog.smtpPort}</span>
|
||||
<span className="text-gray-500">Verschlüsselung:</span>
|
||||
<span>{selectedLog.smtpEncryption}</span>
|
||||
<span className="text-gray-500">Benutzer:</span>
|
||||
<span className="font-mono text-xs">{selectedLog.smtpUser}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SMTP-Antwort */}
|
||||
{selectedLog.smtpResponse && (
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium mb-2">SMTP-Antwort</h4>
|
||||
<pre className="text-xs bg-gray-50 p-3 rounded overflow-x-auto whitespace-pre-wrap font-mono text-gray-700">
|
||||
{selectedLog.smtpResponse}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fehlermeldung */}
|
||||
{selectedLog.errorMessage && (
|
||||
<div className="border border-red-200 rounded-lg p-4 bg-red-50">
|
||||
<h4 className="text-sm font-medium text-red-800 mb-2">Fehlermeldung</h4>
|
||||
<pre className="text-xs whitespace-pre-wrap font-mono text-red-700">
|
||||
{selectedLog.errorMessage}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user