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 89cf92eaf5
commit f2876f877e
1491 changed files with 265550 additions and 1292 deletions
+235
View File
@@ -0,0 +1,235 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { publicApi } from '../../services/api';
import { Shield, CheckCircle2, FileDown, Loader2 } from 'lucide-react';
export default function ConsentPage() {
const { hash } = useParams<{ hash: string }>();
const queryClient = useQueryClient();
const [allChecked, setAllChecked] = useState(false);
const [checks, setChecks] = useState<Record<string, boolean>>({});
const { data, isLoading, error } = useQuery({
queryKey: ['public-consent', hash],
queryFn: () => publicApi.getConsentPage(hash!),
enabled: !!hash,
});
const grantMutation = useMutation({
mutationFn: () => publicApi.grantAllConsents(hash!),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['public-consent', hash] });
},
});
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="flex items-center gap-3 text-gray-500">
<Loader2 className="w-5 h-5 animate-spin" />
Laden...
</div>
</div>
);
}
if (error || !data?.data) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg shadow-sm border max-w-md text-center">
<Shield className="w-12 h-12 text-red-400 mx-auto mb-4" />
<h1 className="text-xl font-bold text-gray-900 mb-2">Ungültiger Link</h1>
<p className="text-gray-500">
Dieser Datenschutz-Link ist ungültig oder abgelaufen. Bitte kontaktieren Sie Ihren Berater.
</p>
</div>
</div>
);
}
const { customer, consents, privacyPolicyHtml } = data.data;
const allGranted = consents.every((c) => c.status === 'GRANTED');
const consentTypes = consents.map((c) => c.consentType);
const allBoxesChecked = consentTypes.every((t) => checks[t]);
const handleToggle = (type: string) => {
setChecks((prev) => ({ ...prev, [type]: !prev[type] }));
};
const handleCheckAll = () => {
const newValue = !allChecked;
setAllChecked(newValue);
const newChecks: Record<string, boolean> = {};
consentTypes.forEach((t) => { newChecks[t] = newValue; });
setChecks(newChecks);
};
const handleSubmit = () => {
if (allBoxesChecked) {
grantMutation.mutate();
}
};
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b">
<div className="max-w-3xl mx-auto px-4 py-6">
<div className="flex items-center gap-3">
<Shield className="w-8 h-8 text-blue-600" />
<div>
<h1 className="text-xl font-bold text-gray-900">Datenschutzerklärung</h1>
<p className="text-sm text-gray-500">
{customer.firstName} {customer.lastName} (Nr. {customer.customerNumber})
</p>
</div>
</div>
</div>
</div>
<div className="max-w-3xl mx-auto px-4 py-8">
{/* Bereits zugestimmt */}
{allGranted ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
<div className="flex items-start gap-4">
<CheckCircle2 className="w-8 h-8 text-green-600 flex-shrink-0" />
<div>
<h2 className="text-lg font-semibold text-green-800 mb-1">
Einwilligungen bereits erteilt
</h2>
<p className="text-green-700 text-sm mb-3">
Sie haben allen Einwilligungen zugestimmt. Vielen Dank!
</p>
<div className="space-y-1">
{consents.map((c) => (
<div key={c.consentType} className="flex items-center gap-2 text-sm text-green-700">
<CheckCircle2 className="w-4 h-4" />
<span>{c.label}</span>
{c.grantedAt && (
<span className="text-green-500">
(am {new Date(c.grantedAt).toLocaleDateString('de-DE')})
</span>
)}
</div>
))}
</div>
</div>
</div>
</div>
) : grantMutation.isSuccess ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
<div className="flex items-start gap-4">
<CheckCircle2 className="w-8 h-8 text-green-600 flex-shrink-0" />
<div>
<h2 className="text-lg font-semibold text-green-800 mb-1">
Vielen Dank!
</h2>
<p className="text-green-700 text-sm">
Ihre Einwilligungen wurden erfolgreich gespeichert.
</p>
</div>
</div>
</div>
) : null}
{/* Datenschutzerklärung */}
<div className="bg-white border rounded-lg shadow-sm mb-6">
<div className="p-6 border-b flex items-center justify-between">
<h2 className="font-semibold text-gray-900">Datenschutzerklärung</h2>
<a
href={publicApi.getConsentPdfUrl(hash!)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800"
>
<FileDown className="w-4 h-4" />
Als PDF herunterladen
</a>
</div>
<div
className="p-6 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: privacyPolicyHtml }}
/>
</div>
{/* Einwilligungen (nur wenn noch nicht erteilt) */}
{!allGranted && !grantMutation.isSuccess && (
<div className="bg-white border rounded-lg shadow-sm">
<div className="p-6 border-b">
<h2 className="font-semibold text-gray-900 mb-1">Einwilligungen</h2>
<p className="text-sm text-gray-500">
Bitte stimmen Sie allen Punkten zu, damit wir Sie beraten können.
</p>
</div>
<div className="p-6 space-y-4">
{/* Alle auswählen */}
<label className="flex items-start gap-3 p-3 rounded-lg bg-blue-50 border border-blue-200 cursor-pointer hover:bg-blue-100 transition-colors">
<input
type="checkbox"
checked={allChecked && allBoxesChecked}
onChange={handleCheckAll}
className="mt-0.5 rounded border-blue-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="font-medium text-blue-900">Allen zustimmen</span>
</div>
</label>
<div className="border-t pt-4 space-y-3">
{consents.map((c) => (
<label
key={c.consentType}
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
checks[c.consentType]
? 'bg-green-50 border-green-200'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}
>
<input
type="checkbox"
checked={checks[c.consentType] || false}
onChange={() => handleToggle(c.consentType)}
className="mt-0.5 rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<div>
<span className="font-medium text-gray-900">{c.label} *</span>
<p className="text-sm text-gray-500 mt-0.5">{c.description}</p>
</div>
</label>
))}
</div>
<p className="text-xs text-gray-400">* Pflichtfeld</p>
{grantMutation.isError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
Fehler beim Speichern. Bitte versuchen Sie es erneut.
</div>
)}
<button
onClick={handleSubmit}
disabled={!allBoxesChecked || grantMutation.isPending}
className={`w-full py-3 px-4 rounded-lg font-medium text-white transition-colors ${
allBoxesChecked && !grantMutation.isPending
? 'bg-blue-600 hover:bg-blue-700'
: 'bg-gray-300 cursor-not-allowed'
}`}
>
{grantMutation.isPending ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Wird gespeichert...
</span>
) : (
'Zustimmen'
)}
</button>
</div>
</div>
)}
</div>
</div>
);
}