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:
2026-03-21 11:59:53 +01:00
parent 09e87c951b
commit c3edb8ad2e
1491 changed files with 265550 additions and 1292 deletions
+305
View File
@@ -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>
);
}