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
@@ -0,0 +1,391 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { gdprApi } from '../../services/api';
import type { DataDeletionRequest, DeletionRequestStatus } from '../../types';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Select from '../../components/ui/Select';
import { ArrowLeft, FileText, Users, CheckCircle, Clock, XCircle, AlertTriangle, Download, X, ChevronRight } from 'lucide-react';
const STATUS_OPTIONS = [
{ value: '', label: 'Alle Status' },
{ value: 'PENDING', label: 'Ausstehend' },
{ value: 'IN_PROGRESS', label: 'In Bearbeitung' },
{ value: 'COMPLETED', label: 'Abgeschlossen' },
{ value: 'PARTIALLY_COMPLETED', label: 'Teilweise abgeschlossen' },
{ value: 'REJECTED', label: 'Abgelehnt' },
];
function getStatusBadge(status: DeletionRequestStatus) {
switch (status) {
case 'PENDING':
return <span className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-yellow-100 text-yellow-800"><Clock className="w-3 h-3" /> Ausstehend</span>;
case 'IN_PROGRESS':
return <span className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-800"><Clock className="w-3 h-3" /> In Bearbeitung</span>;
case 'COMPLETED':
return <span className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800"><CheckCircle className="w-3 h-3" /> Abgeschlossen</span>;
case 'PARTIALLY_COMPLETED':
return <span className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-orange-100 text-orange-800"><AlertTriangle className="w-3 h-3" /> Teilweise</span>;
case 'REJECTED':
return <span className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-800"><XCircle className="w-3 h-3" /> Abgelehnt</span>;
default:
return <span className="px-2 py-1 rounded text-xs bg-gray-100">{status}</span>;
}
}
function formatDate(date: string): string {
return new Date(date).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
interface ProcessModalProps {
request: DataDeletionRequest;
onClose: () => void;
onProcess: (action: 'complete' | 'partial' | 'reject', reason?: string) => void;
isPending: boolean;
}
function ProcessModal({ request, onClose, onProcess, isPending }: ProcessModalProps) {
const [action, setAction] = useState<'complete' | 'partial' | 'reject'>('complete');
const [reason, setReason] = useState('');
return (
<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 w-full max-w-lg mx-4">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">Löschanfrage bearbeiten</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="mb-4 p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">Kunde:</p>
<p className="font-medium">
{request.customer?.firstName} {request.customer?.lastName} ({request.customer?.customerNumber})
</p>
<p className="text-sm text-gray-600 mt-2">Quelle: {request.requestSource}</p>
<p className="text-sm text-gray-600">Angefordert: {formatDate(request.requestedAt)}</p>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Aktion</label>
<div className="space-y-2">
<label className="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="action"
checked={action === 'complete'}
onChange={() => setAction('complete')}
className="text-blue-600"
/>
<div>
<div className="font-medium">Vollständig löschen</div>
<div className="text-sm text-gray-500">Alle Kundendaten werden anonymisiert</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="action"
checked={action === 'partial'}
onChange={() => setAction('partial')}
className="text-blue-600"
/>
<div>
<div className="font-medium">Teilweise löschen</div>
<div className="text-sm text-gray-500">Nur optionale Daten werden gelöscht (aktive Verträge bleiben)</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="action"
checked={action === 'reject'}
onChange={() => setAction('reject')}
className="text-blue-600"
/>
<div>
<div className="font-medium">Ablehnen</div>
<div className="text-sm text-gray-500">Löschanfrage kann nicht durchgeführt werden</div>
</div>
</label>
</div>
</div>
{(action === 'partial' || action === 'reject') && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Begründung {action === 'reject' && '*'}
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Grund für die teilweise Löschung/Ablehnung..."
required={action === 'reject'}
/>
</div>
)}
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t">
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
<Button
onClick={() => onProcess(action, reason || undefined)}
disabled={isPending || (action === 'reject' && !reason)}
>
{isPending ? 'Verarbeite...' : 'Durchführen'}
</Button>
</div>
</div>
</div>
</div>
);
}
export default function GDPRDashboard() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [statusFilter, setStatusFilter] = useState<DeletionRequestStatus | ''>('');
const [selectedRequest, setSelectedRequest] = useState<DataDeletionRequest | null>(null);
const { data: statsData } = useQuery({
queryKey: ['gdpr-stats'],
queryFn: () => gdprApi.getDashboardStats(),
});
const { data: requestsData, isLoading } = useQuery({
queryKey: ['deletion-requests', statusFilter],
queryFn: () => gdprApi.getDeletionRequests({ status: statusFilter || undefined }),
});
const { data: consentData } = useQuery({
queryKey: ['consent-overview'],
queryFn: () => gdprApi.getConsentOverview(),
});
const processMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: { processedBy: string; action: 'complete' | 'partial' | 'reject'; retentionReason?: string } }) =>
gdprApi.processDeletionRequest(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['deletion-requests'] });
queryClient.invalidateQueries({ queryKey: ['gdpr-stats'] });
setSelectedRequest(null);
},
});
const stats = statsData?.data;
const requests = requestsData?.data || [];
// Backend gibt ein Array mit { type, label, description, granted, withdrawn, pending } zurück
const consentsList: Array<{ type: string; label?: string; description?: string; granted: number; withdrawn: number; pending: number }> =
Array.isArray(consentData?.data) ? consentData.data : [];
const handleProcess = (action: 'complete' | 'partial' | 'reject', reason?: string) => {
if (!selectedRequest) return;
const user = JSON.parse(localStorage.getItem('user') || '{}');
processMutation.mutate({
id: selectedRequest.id,
data: {
processedBy: user.email || 'System',
action,
retentionReason: reason,
},
});
};
const consentLabels: Record<string, string> = {
DATA_PROCESSING: 'Datenverarbeitung',
MARKETING_EMAIL: 'E-Mail-Marketing',
MARKETING_PHONE: 'Telefonmarketing',
DATA_SHARING_PARTNER: 'Datenweitergabe',
};
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">DSGVO-Dashboard</h1>
</div>
{/* Statistik-Kacheln */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card>
<div className="flex items-center gap-4">
<div className="p-3 bg-yellow-100 rounded-lg">
<Clock className="w-6 h-6 text-yellow-600" />
</div>
<div>
<div className="text-2xl font-bold">{stats?.deletionRequests.pending ?? '-'}</div>
<div className="text-sm text-gray-500">Offene Löschanfragen</div>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-4">
<div className="p-3 bg-green-100 rounded-lg">
<CheckCircle className="w-6 h-6 text-green-600" />
</div>
<div>
<div className="text-2xl font-bold">{stats?.deletionRequests.completedLast30Days ?? '-'}</div>
<div className="text-sm text-gray-500">Gelöscht (30 Tage)</div>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-4">
<div className="p-3 bg-purple-100 rounded-lg">
<Download className="w-6 h-6 text-purple-600" />
</div>
<div>
<div className="text-2xl font-bold">{stats?.dataExports.last30Days ?? '-'}</div>
<div className="text-sm text-gray-500">Datenexporte (30 Tage)</div>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-100 rounded-lg">
<Users className="w-6 h-6 text-blue-600" />
</div>
<div>
<div className="text-2xl font-bold">{stats?.consents.granted ?? '-'}</div>
<div className="text-sm text-gray-500">Aktive Einwilligungen</div>
</div>
</div>
</Card>
</div>
{/* Consent-Übersicht */}
{consentsList.length > 0 && (
<Card className="mb-6">
<h2 className="text-lg font-semibold mb-4">Einwilligungen nach Typ</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{consentsList.map((item) => (
<div key={item.type} className="p-4 bg-gray-50 rounded-lg">
<div className="font-medium mb-2">{item.label || consentLabels[item.type] || item.type}</div>
<div className="flex gap-4 text-sm">
<div>
<span className="text-green-600 font-medium">{item.granted}</span>
<span className="text-gray-500 ml-1">erteilt</span>
</div>
<div>
<span className="text-red-600 font-medium">{item.withdrawn}</span>
<span className="text-gray-500 ml-1">widerrufen</span>
</div>
<div>
<span className="text-gray-600 font-medium">{item.pending}</span>
<span className="text-gray-500 ml-1">ausstehend</span>
</div>
</div>
</div>
))}
</div>
</Card>
)}
{/* Löschanfragen */}
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Löschanfragen</h2>
<Select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as DeletionRequestStatus | '')}
options={STATUS_OPTIONS}
className="w-48"
/>
</div>
{isLoading ? (
<div className="text-center py-8">Laden...</div>
) : requests.length === 0 ? (
<div className="text-center py-8 text-gray-500">Keine Löschanfragen gefunden.</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-4">Kunde</th>
<th className="text-left py-3 px-4">Status</th>
<th className="text-left py-3 px-4">Quelle</th>
<th className="text-left py-3 px-4">Angefordert</th>
<th className="text-left py-3 px-4">Bearbeitet</th>
<th className="text-center py-3 px-4"></th>
</tr>
</thead>
<tbody>
{requests.map((request) => (
<tr key={request.id} className="border-b hover:bg-gray-50">
<td className="py-3 px-4">
{request.customer ? (
<div>
<div className="font-medium">{request.customer.firstName} {request.customer.lastName}</div>
<div className="text-xs text-gray-500">{request.customer.customerNumber}</div>
</div>
) : (
<span className="text-gray-400">Kunde #{request.customerId}</span>
)}
</td>
<td className="py-3 px-4">{getStatusBadge(request.status)}</td>
<td className="py-3 px-4">{request.requestSource}</td>
<td className="py-3 px-4">
<div>{formatDate(request.requestedAt)}</div>
<div className="text-xs text-gray-500">von {request.requestedBy}</div>
</td>
<td className="py-3 px-4">
{request.processedAt ? (
<div>
<div>{formatDate(request.processedAt)}</div>
<div className="text-xs text-gray-500">von {request.processedBy}</div>
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="py-3 px-4 text-center">
{(request.status === 'PENDING' || request.status === 'IN_PROGRESS') && (
<Button variant="ghost" size="sm" onClick={() => setSelectedRequest(request)}>
<ChevronRight className="w-4 h-4" />
</Button>
)}
{request.proofDocument && (
<Button
variant="ghost"
size="sm"
onClick={() => window.open(`/api/uploads/${request.proofDocument}`, '_blank')}
title="Löschnachweis anzeigen"
>
<FileText className="w-4 h-4 text-blue-500" />
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Card>
{/* Bearbeitungs-Modal */}
{selectedRequest && (
<ProcessModal
request={selectedRequest}
onClose={() => setSelectedRequest(null)}
onProcess={handleProcess}
isPending={processMutation.isPending}
/>
)}
</div>
);
}