feat(sdk): Replace mock data with real API calls in consent, DSR, and consent-management
- /sdk/consent: Replace hardcoded mockDocuments with GET /api/admin/consent/documents - /sdk/dsr: Replace createMockDSRList with fetchSDKDSRList via /api/sdk/v1/dsgvo/dsr - /sdk/dsr/new: Replace console.log mock with real POST to create DSR requests - /sdk/dsr/[requestId]: Replace mock lookup with real GET/PUT for DSR details and status updates - /sdk/consent-management: Add real stats, GDPR process counts, and email template editor - lib/sdk/dsr/api.ts: Add transformBackendDSR adapter (flat backend → nested frontend types) Prepares for removal of /dsgvo and /compliance pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
|
||||
|
||||
@@ -41,6 +42,13 @@ interface Version {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// Email template editor types
|
||||
interface EmailTemplateData {
|
||||
key: string
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export default function ConsentManagementPage() {
|
||||
const { state } = useSDK()
|
||||
const [activeTab, setActiveTab] = useState<Tab>('documents')
|
||||
@@ -50,6 +58,18 @@ export default function ConsentManagementPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedDocument, setSelectedDocument] = useState<string>('')
|
||||
|
||||
// Stats state
|
||||
const [consentStats, setConsentStats] = useState<{ activeConsents: number; documentCount: number; openDSRs: number }>({ activeConsents: 0, documentCount: 0, openDSRs: 0 })
|
||||
|
||||
// GDPR tab state
|
||||
const [dsrCounts, setDsrCounts] = useState<Record<string, number>>({})
|
||||
const [dsrOverview, setDsrOverview] = useState<{ open: number; completed: number; in_progress: number; overdue: number }>({ open: 0, completed: 0, in_progress: 0, overdue: 0 })
|
||||
|
||||
// Email template editor state
|
||||
const [editingTemplate, setEditingTemplate] = useState<EmailTemplateData | null>(null)
|
||||
const [previewTemplate, setPreviewTemplate] = useState<EmailTemplateData | null>(null)
|
||||
const [savedTemplates, setSavedTemplates] = useState<Record<string, EmailTemplateData>>({})
|
||||
|
||||
// Auth token (in production, get from auth context)
|
||||
const [authToken, setAuthToken] = useState<string>('')
|
||||
|
||||
@@ -58,6 +78,13 @@ export default function ConsentManagementPage() {
|
||||
if (token) {
|
||||
setAuthToken(token)
|
||||
}
|
||||
// Load saved email templates from localStorage
|
||||
try {
|
||||
const saved = localStorage.getItem('sdk-email-templates')
|
||||
if (saved) {
|
||||
setSavedTemplates(JSON.parse(saved))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,6 +92,10 @@ export default function ConsentManagementPage() {
|
||||
loadDocuments()
|
||||
} else if (activeTab === 'versions' && selectedDocument) {
|
||||
loadVersions(selectedDocument)
|
||||
} else if (activeTab === 'stats') {
|
||||
loadStats()
|
||||
} else if (activeTab === 'gdpr') {
|
||||
loadGDPRData()
|
||||
}
|
||||
}, [activeTab, selectedDocument, authToken])
|
||||
|
||||
@@ -110,6 +141,111 @@ export default function ConsentManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const token = localStorage.getItem('bp_admin_token')
|
||||
const [statsRes, docsRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/stats`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}),
|
||||
fetch(`${API_BASE}/documents`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}),
|
||||
])
|
||||
|
||||
let activeConsents = 0
|
||||
let documentCount = 0
|
||||
let openDSRs = 0
|
||||
|
||||
if (statsRes.ok) {
|
||||
const statsData = await statsRes.json()
|
||||
activeConsents = statsData.total_consents || statsData.active_consents || 0
|
||||
}
|
||||
|
||||
if (docsRes.ok) {
|
||||
const docsData = await docsRes.json()
|
||||
documentCount = (docsData.documents || []).length
|
||||
}
|
||||
|
||||
// Try to get DSR count
|
||||
try {
|
||||
const dsrRes = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
||||
headers: {
|
||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||
}
|
||||
})
|
||||
if (dsrRes.ok) {
|
||||
const dsrData = await dsrRes.json()
|
||||
const dsrs = dsrData.dsrs || []
|
||||
const now = new Date()
|
||||
openDSRs = dsrs.filter((r: any) => r.status !== 'completed' && r.status !== 'rejected').length
|
||||
}
|
||||
} catch { /* DSR endpoint might not be available */ }
|
||||
|
||||
setConsentStats({ activeConsents, documentCount, openDSRs })
|
||||
} catch (err) {
|
||||
console.error('Failed to load stats:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGDPRData() {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
||||
headers: {
|
||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||
}
|
||||
})
|
||||
if (!res.ok) return
|
||||
|
||||
const data = await res.json()
|
||||
const dsrs = data.dsrs || []
|
||||
const now = new Date()
|
||||
|
||||
// Count per article type
|
||||
const counts: Record<string, number> = {}
|
||||
const typeMapping: Record<string, string> = {
|
||||
'access': '15',
|
||||
'rectification': '16',
|
||||
'erasure': '17',
|
||||
'restriction': '18',
|
||||
'portability': '20',
|
||||
'objection': '21',
|
||||
}
|
||||
|
||||
for (const dsr of dsrs) {
|
||||
if (dsr.status === 'completed' || dsr.status === 'rejected') continue
|
||||
const article = typeMapping[dsr.request_type]
|
||||
if (article) {
|
||||
counts[article] = (counts[article] || 0) + 1
|
||||
}
|
||||
}
|
||||
setDsrCounts(counts)
|
||||
|
||||
// Calculate overview
|
||||
const open = dsrs.filter((r: any) => r.status === 'received' || r.status === 'verified').length
|
||||
const completed = dsrs.filter((r: any) => r.status === 'completed').length
|
||||
const in_progress = dsrs.filter((r: any) => r.status === 'in_progress').length
|
||||
const overdue = dsrs.filter((r: any) => {
|
||||
if (r.status === 'completed' || r.status === 'rejected') return false
|
||||
const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at)
|
||||
return deadline < now
|
||||
}).length
|
||||
|
||||
setDsrOverview({ open, completed, in_progress, overdue })
|
||||
} catch (err) {
|
||||
console.error('Failed to load GDPR data:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function saveEmailTemplate(template: EmailTemplateData) {
|
||||
const updated = { ...savedTemplates, [template.key]: template }
|
||||
setSavedTemplates(updated)
|
||||
localStorage.setItem('sdk-email-templates', JSON.stringify(updated))
|
||||
setEditingTemplate(null)
|
||||
}
|
||||
|
||||
const tabs: { id: Tab; label: string }[] = [
|
||||
{ id: 'documents', label: 'Dokumente' },
|
||||
{ id: 'versions', label: 'Versionen' },
|
||||
@@ -459,11 +595,33 @@ export default function ConsentManagementPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">Aktiv</span>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
|
||||
{savedTemplates[template.key] ? 'Angepasst' : 'Aktiv'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const existing = savedTemplates[template.key]
|
||||
setEditingTemplate({
|
||||
key: template.key,
|
||||
subject: existing?.subject || `Betreff: ${template.name}`,
|
||||
body: existing?.body || `Sehr geehrte(r) {{name}},\n\n${template.description}\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team`,
|
||||
})
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
|
||||
<button
|
||||
onClick={() => {
|
||||
const existing = savedTemplates[template.key]
|
||||
setPreviewTemplate({
|
||||
key: template.key,
|
||||
subject: existing?.subject || `Betreff: ${template.name}`,
|
||||
body: existing?.body || `Sehr geehrte(r) {{name}},\n\n${template.description}\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team`,
|
||||
})
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
|
||||
>
|
||||
Vorschau
|
||||
</button>
|
||||
</div>
|
||||
@@ -536,16 +694,19 @@ export default function ConsentManagementPage() {
|
||||
</span>
|
||||
<span className="text-slate-300">|</span>
|
||||
<span className="text-slate-500">
|
||||
Offene Anfragen: <span className="font-medium text-slate-700">0</span>
|
||||
Offene Anfragen: <span className={`font-medium ${(dsrCounts[process.article] || 0) > 0 ? 'text-orange-600' : 'text-slate-700'}`}>{dsrCounts[process.article] || 0}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<button className="px-3 py-1.5 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg">
|
||||
<Link
|
||||
href={`/sdk/dsr?type=${process.article === '15' ? 'access' : process.article === '16' ? 'rectification' : process.article === '17' ? 'erasure' : process.article === '18' ? 'restriction' : process.article === '20' ? 'portability' : 'objection'}`}
|
||||
className="px-3 py-1.5 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg text-center"
|
||||
>
|
||||
Anfragen
|
||||
</button>
|
||||
</Link>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
|
||||
Vorlage
|
||||
</button>
|
||||
@@ -560,19 +721,19 @@ export default function ConsentManagementPage() {
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-4">DSR Uebersicht</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-slate-900">0</div>
|
||||
<div className={`text-2xl font-bold ${dsrOverview.open > 0 ? 'text-blue-600' : 'text-slate-900'}`}>{dsrOverview.open}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Offen</div>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-green-700">0</div>
|
||||
<div className="text-2xl font-bold text-green-700">{dsrOverview.completed}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Erledigt</div>
|
||||
</div>
|
||||
<div className="bg-yellow-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-700">0</div>
|
||||
<div className="text-2xl font-bold text-yellow-700">{dsrOverview.in_progress}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">In Bearbeitung</div>
|
||||
</div>
|
||||
<div className="bg-red-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-red-700">0</div>
|
||||
<div className={`text-2xl font-bold ${dsrOverview.overdue > 0 ? 'text-red-700' : 'text-slate-400'}`}>{dsrOverview.overdue}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Ueberfaellig</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -587,29 +748,131 @@ export default function ConsentManagementPage() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-slate-50 rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-slate-900">0</div>
|
||||
<div className="text-3xl font-bold text-slate-900">{consentStats.activeConsents}</div>
|
||||
<div className="text-sm text-slate-500 mt-1">Aktive Zustimmungen</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-slate-900">0</div>
|
||||
<div className="text-3xl font-bold text-slate-900">{consentStats.documentCount}</div>
|
||||
<div className="text-sm text-slate-500 mt-1">Dokumente</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-slate-900">0</div>
|
||||
<div className={`text-3xl font-bold ${consentStats.openDSRs > 0 ? 'text-orange-600' : 'text-slate-900'}`}>
|
||||
{consentStats.openDSRs}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500 mt-1">Offene DSR-Anfragen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Zustimmungsrate nach Dokument</h3>
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Noch keine Daten verfuegbar
|
||||
<div className="text-center py-8 text-slate-400 text-sm">
|
||||
Diagramm wird in einer zukuenftigen Version verfuegbar sein
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Email Template Edit Modal */}
|
||||
{editingTemplate && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-900">E-Mail Vorlage bearbeiten</h3>
|
||||
<button onClick={() => setEditingTemplate(null)} className="text-slate-400 hover:text-slate-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Betreff</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingTemplate.subject}
|
||||
onChange={(e) => setEditingTemplate({ ...editingTemplate, subject: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Inhalt</label>
|
||||
<textarea
|
||||
value={editingTemplate.body}
|
||||
onChange={(e) => setEditingTemplate({ ...editingTemplate, body: e.target.value })}
|
||||
rows={12}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-slate-500 mb-1">Verfuegbare Platzhalter:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['{{name}}', '{{email}}', '{{referenceNumber}}', '{{date}}', '{{deadline}}', '{{company}}'].map(v => (
|
||||
<span key={v} className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs font-mono">{v}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-slate-200 flex justify-end gap-3">
|
||||
<button onClick={() => setEditingTemplate(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => saveEmailTemplate(editingTemplate)}
|
||||
className="px-4 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg font-medium"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Template Preview Modal */}
|
||||
{previewTemplate && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-900">Vorschau</h3>
|
||||
<button onClick={() => setPreviewTemplate(null)} className="text-slate-400 hover:text-slate-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4">
|
||||
<div className="text-xs text-slate-500 mb-1">Betreff:</div>
|
||||
<div className="font-medium text-slate-900">
|
||||
{previewTemplate.subject
|
||||
.replace(/\{\{name\}\}/g, 'Max Mustermann')
|
||||
.replace(/\{\{email\}\}/g, 'max@example.de')
|
||||
.replace(/\{\{referenceNumber\}\}/g, 'DSR-2025-000001')
|
||||
.replace(/\{\{date\}\}/g, new Date().toLocaleDateString('de-DE'))
|
||||
.replace(/\{\{deadline\}\}/g, '30 Tage')
|
||||
.replace(/\{\{company\}\}/g, 'BreakPilot GmbH')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-slate-200 rounded-lg p-4 whitespace-pre-wrap text-sm text-slate-700">
|
||||
{previewTemplate.body
|
||||
.replace(/\{\{name\}\}/g, 'Max Mustermann')
|
||||
.replace(/\{\{email\}\}/g, 'max@example.de')
|
||||
.replace(/\{\{referenceNumber\}\}/g, 'DSR-2025-000001')
|
||||
.replace(/\{\{date\}\}/g, new Date().toLocaleDateString('de-DE'))
|
||||
.replace(/\{\{deadline\}\}/g, '30 Tage')
|
||||
.replace(/\{\{company\}\}/g, 'BreakPilot GmbH')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-slate-200 flex justify-end">
|
||||
<button onClick={() => setPreviewTemplate(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user