feat(dsr): Go DSR deprecated, Python Export-Endpoint, Frontend an Backend-APIs anbinden
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s

- Go: DEPRECATED-Kommentare an allen DSR-Handlern und Routes
- Python: GET /dsr/export?format=csv|json (Semikolon-CSV, 12 Spalten)
- API-Client: 12 neue Funktionen (verify, assign, extend, complete, reject, communications, exception-checks, history)
- Detail-Seite: Alle Actions verdrahtet (keine Coming-soon-Alerts mehr), Communications + Art.17(3)-Checks + Audit-Log live
- Haupt-Seite: CSV-Export-Button im Header
- Tests: 54/54 bestanden (4 neue Export-Tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-06 18:21:43 +01:00
parent 3593a4ff78
commit 095eff26d9
7 changed files with 526 additions and 102 deletions

View File

@@ -13,7 +13,21 @@ import {
DSRCommunication,
DSRVerifyIdentityRequest
} from '@/lib/sdk/dsr/types'
import { fetchSDKDSR, updateSDKDSRStatus } from '@/lib/sdk/dsr/api'
import {
fetchSDKDSR,
updateSDKDSRStatus,
verifyDSRIdentity,
assignDSR,
extendDSRDeadline,
completeDSR,
rejectDSR,
fetchDSRCommunications,
sendDSRCommunication,
fetchDSRExceptionChecks,
initDSRExceptionChecks,
updateDSRExceptionCheck,
fetchDSRHistory,
} from '@/lib/sdk/dsr/api'
import {
DSRWorkflowStepper,
DSRIdentityModal,
@@ -22,12 +36,6 @@ import {
DSRDataExportComponent
} from '@/components/sdk/dsr'
// =============================================================================
// MOCK COMMUNICATIONS
// =============================================================================
// TODO: Backend fehlt — Communications API noch nicht implementiert
// =============================================================================
// COMPONENTS
// =============================================================================
@@ -155,56 +163,38 @@ function ActionButtons({
)
}
function AuditLog({ request }: { request: DSRRequest }) {
type AuditEvent = { action: string; timestamp: string; user: string }
const events: AuditEvent[] = [
{ action: 'Erstellt', timestamp: request.createdAt, user: request.createdBy }
]
if (request.assignment.assignedAt) {
events.push({
action: `Zugewiesen an ${request.assignment.assignedTo}`,
timestamp: request.assignment.assignedAt,
user: request.assignment.assignedBy || 'System'
})
}
if (request.identityVerification.verifiedAt) {
events.push({
action: 'Identitaet verifiziert',
timestamp: request.identityVerification.verifiedAt,
user: request.identityVerification.verifiedBy || 'System'
})
}
if (request.completedAt) {
events.push({
action: request.status === 'rejected' ? 'Abgelehnt' : 'Abgeschlossen',
timestamp: request.completedAt,
user: request.updatedBy || 'System'
})
}
function AuditLog({ history }: { history: any[] }) {
return (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700">Aktivitaeten</h4>
<div className="space-y-2">
{events.map((event, idx) => (
<div key={idx} className="flex items-start gap-2 text-xs">
{history.length === 0 && (
<div className="text-xs text-gray-400">Keine Eintraege</div>
)}
{history.map((entry, idx) => (
<div key={entry.id || idx} className="flex items-start gap-2 text-xs">
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 mt-1.5 flex-shrink-0" />
<div>
<div className="text-gray-900">{event.action}</div>
<div className="text-gray-900">
{entry.previous_status
? `${entry.previous_status}${entry.new_status}`
: entry.new_status
}
{entry.comment && `: ${entry.comment}`}
</div>
<div className="text-gray-500">
{new Date(event.timestamp).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
{entry.created_at
? new Date(entry.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
: ''
}
{' - '}
{event.user}
{entry.changed_by || 'System'}
</div>
</div>
</div>
@@ -225,10 +215,17 @@ export default function DSRDetailPage() {
const [request, setRequest] = useState<DSRRequest | null>(null)
const [communications, setCommunications] = useState<DSRCommunication[]>([])
const [history, setHistory] = useState<any[]>([])
const [exceptionChecks, setExceptionChecks] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true)
const [showIdentityModal, setShowIdentityModal] = useState(false)
const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details')
const reloadRequest = async () => {
const found = await fetchSDKDSR(requestId)
if (found) setRequest(found)
}
// Load data from SDK backend
useEffect(() => {
const loadData = async () => {
@@ -237,8 +234,40 @@ export default function DSRDetailPage() {
const found = await fetchSDKDSR(requestId)
if (found) {
setRequest(found)
// TODO: Backend fehlt — Communications API noch nicht implementiert
setCommunications([])
// Load communications, history, and exception checks in parallel
const [comms, hist] = await Promise.all([
fetchDSRCommunications(requestId).catch(() => []),
fetchDSRHistory(requestId).catch(() => []),
])
setCommunications(comms.map((c: any) => ({
id: c.id,
dsrId: c.dsr_id,
type: c.communication_type,
channel: c.channel,
subject: c.subject,
content: c.content,
createdAt: c.created_at,
createdBy: c.created_by,
sentAt: c.sent_at,
sentBy: c.sent_by,
})))
setHistory(hist)
// Load exception checks for erasure requests
if (found.type === 'erasure') {
try {
const checks = await fetchDSRExceptionChecks(requestId)
if (checks.length === 0) {
// Auto-initialize if none exist
const initialized = await initDSRExceptionChecks(requestId)
setExceptionChecks(initialized)
} else {
setExceptionChecks(checks)
}
} catch {
setExceptionChecks([])
}
}
}
} catch (error) {
console.error('Failed to load DSR:', error)
@@ -252,46 +281,109 @@ export default function DSRDetailPage() {
const handleVerifyIdentity = async (verification: DSRVerifyIdentityRequest) => {
if (!request) return
try {
await updateSDKDSRStatus(request.id, 'verified')
setRequest({
...request,
identityVerification: {
verified: true,
method: verification.method,
verifiedAt: new Date().toISOString(),
verifiedBy: 'Current User',
notes: verification.notes
},
status: request.status === 'identity_verification' ? 'processing' : request.status
const updated = await verifyDSRIdentity(request.id, {
method: verification.method,
notes: verification.notes,
document_ref: verification.documentRef,
})
setRequest(updated)
// Reload history
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to verify identity:', err)
// Still update locally as fallback
setRequest({
...request,
identityVerification: {
verified: true,
method: verification.method,
verifiedAt: new Date().toISOString(),
verifiedBy: 'Current User',
notes: verification.notes
},
status: request.status === 'identity_verification' ? 'processing' : request.status
})
}
}
const handleAssign = async () => {
if (!request) return
const assignee = prompt('Zuweisen an (Name/ID):')
if (!assignee) return
try {
const updated = await assignDSR(request.id, assignee)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to assign:', err)
}
}
const handleExtendDeadline = async () => {
if (!request) return
const reason = prompt('Grund fuer die Fristverlaengerung:')
if (!reason) return
const daysStr = prompt('Um wie viele Tage verlaengern? (Standard: 60)', '60')
const days = parseInt(daysStr || '60', 10) || 60
try {
const updated = await extendDSRDeadline(request.id, reason, days)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to extend deadline:', err)
}
}
const handleComplete = async () => {
if (!request) return
const summary = prompt('Zusammenfassung der Bearbeitung:')
if (summary === null) return
try {
const updated = await completeDSR(request.id, summary || undefined)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to complete:', err)
}
}
const handleReject = async () => {
if (!request) return
const reason = prompt('Ablehnungsgrund:')
if (!reason) return
const legalBasis = prompt('Rechtsgrundlage (optional):')
try {
const updated = await rejectDSR(request.id, reason, legalBasis || undefined)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to reject:', err)
}
}
const handleSendCommunication = async (message: any) => {
const newComm: DSRCommunication = {
id: `comm-${Date.now()}`,
dsrId: requestId,
...message,
createdAt: new Date().toISOString(),
createdBy: 'Current User',
sentAt: message.type === 'outgoing' ? new Date().toISOString() : undefined,
sentBy: message.type === 'outgoing' ? 'Current User' : undefined
if (!request) return
try {
const result = await sendDSRCommunication(requestId, {
communication_type: message.type || 'outgoing',
channel: message.channel || 'email',
subject: message.subject,
content: message.content,
})
// Map backend response to frontend format
const newComm: DSRCommunication = {
id: result.id,
dsrId: result.dsr_id,
type: result.communication_type,
channel: result.channel,
subject: result.subject,
content: result.content,
createdAt: result.created_at,
createdBy: result.created_by || 'Current User',
sentAt: result.sent_at,
sentBy: result.sent_by,
}
setCommunications(prev => [newComm, ...prev])
} catch (err) {
console.error('Failed to send communication:', err)
}
}
const handleExceptionCheckChange = async (checkId: string, applies: boolean, notes?: string) => {
try {
const updated = await updateDSRExceptionCheck(requestId, checkId, { applies, notes })
setExceptionChecks(prev => prev.map(c => c.id === checkId ? updated : c))
} catch (err) {
console.error('Failed to update exception check:', err)
}
setCommunications(prev => [newComm, ...prev])
}
if (isLoading) {
@@ -528,10 +620,48 @@ export default function DSRDetailPage() {
<div>
{/* Art. 17 - Erasure */}
{request.type === 'erasure' && (
<DSRErasureChecklistComponent
checklist={request.erasureChecklist}
onChange={(checklist) => setRequest({ ...request, erasureChecklist: checklist })}
/>
<div className="space-y-4">
<DSRErasureChecklistComponent
checklist={request.erasureChecklist}
onChange={(checklist) => setRequest({ ...request, erasureChecklist: checklist })}
/>
{/* Art. 17(3) Exception Checks from Backend */}
{exceptionChecks.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3">Art. 17(3) Ausnahmepruefung</h3>
<p className="text-sm text-gray-500 mb-4">
Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift.
</p>
<div className="space-y-3">
{exceptionChecks.map((check) => (
<div key={check.id} className="bg-gray-50 rounded-xl p-4 border border-gray-200">
<div className="flex items-start gap-3">
<input
type="checkbox"
checked={check.applies || false}
onChange={(e) => handleExceptionCheckChange(check.id, e.target.checked, check.notes)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div className="flex-1">
<div className="font-medium text-gray-900 text-sm">
{check.article}: {check.label}
</div>
<div className="text-xs text-gray-500 mt-0.5">{check.description}</div>
{check.checked_by && (
<div className="text-xs text-gray-400 mt-1">
Geprueft von {check.checked_by} am{' '}
{check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'}
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Art. 15/20 - Data Export */}
@@ -697,16 +827,16 @@ export default function DSRDetailPage() {
<ActionButtons
request={request}
onVerifyIdentity={() => setShowIdentityModal(true)}
onExtendDeadline={() => alert('Fristverlaengerung - Coming soon')}
onComplete={() => alert('Abschliessen - Coming soon')}
onReject={() => alert('Ablehnen - Coming soon')}
onAssign={() => alert('Zuweisen - Coming soon')}
onExtendDeadline={handleExtendDeadline}
onComplete={handleComplete}
onReject={handleReject}
onAssign={handleAssign}
/>
</div>
{/* Audit Log Card */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<AuditLog request={request} />
<AuditLog history={history} />
</div>
</div>
</div>

View File

@@ -808,15 +808,31 @@ export default function DSRPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Anfrage erfassen
</button>
<div className="flex items-center gap-2">
<button
onClick={() => {
const link = document.createElement('a')
link.href = '/api/sdk/v1/compliance/dsr/export?format=csv'
link.download = 'dsr_export.csv'
link.click()
}}
className="flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
CSV Export
</button>
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Anfrage erfassen
</button>
</div>
</StepHeader>
{/* Tab Navigation */}

View File

@@ -245,6 +245,171 @@ export async function updateSDKDSRStatus(id: string, status: string): Promise<vo
}
}
// =============================================================================
// WORKFLOW ACTIONS
// =============================================================================
/**
* Verify identity of DSR requester
*/
export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise<DSRRequest> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify(data),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return transformBackendDSR(await res.json())
}
/**
* Assign DSR to a user
*/
export async function assignDSR(id: string, assigneeId: string): Promise<DSRRequest> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify({ assignee_id: assigneeId }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return transformBackendDSR(await res.json())
}
/**
* Extend DSR deadline (Art. 12 Abs. 3 DSGVO)
*/
export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise<DSRRequest> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify({ reason, days }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return transformBackendDSR(await res.json())
}
/**
* Complete a DSR
*/
export async function completeDSR(id: string, summary?: string): Promise<DSRRequest> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify({ summary }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return transformBackendDSR(await res.json())
}
/**
* Reject a DSR with legal basis
*/
export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise<DSRRequest> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify({ reason, legal_basis: legalBasis }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return transformBackendDSR(await res.json())
}
// =============================================================================
// COMMUNICATIONS
// =============================================================================
/**
* Fetch communications for a DSR
*/
export async function fetchDSRCommunications(id: string): Promise<any[]> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, {
headers: getSdkHeaders(),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
/**
* Send a communication for a DSR
*/
export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise<any> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
// =============================================================================
// EXCEPTION CHECKS (Art. 17)
// =============================================================================
/**
* Fetch exception checks for an erasure DSR
*/
export async function fetchDSRExceptionChecks(id: string): Promise<any[]> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, {
headers: getSdkHeaders(),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
/**
* Initialize Art. 17(3) exception checks for an erasure DSR
*/
export async function initDSRExceptionChecks(id: string): Promise<any[]> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, {
method: 'POST',
headers: getSdkHeaders(),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
/**
* Update a single exception check
*/
export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise<any> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, {
method: 'PUT',
headers: getSdkHeaders(),
body: JSON.stringify(data),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
// =============================================================================
// HISTORY
// =============================================================================
/**
* Fetch status change history for a DSR
*/
export async function fetchDSRHistory(id: string): Promise<any[]> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, {
headers: getSdkHeaders(),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
/**
* Update DSR fields (priority, notes, etc.)
*/
export async function updateDSR(id: string, data: Record<string, any>): Promise<DSRRequest> {
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
method: 'PUT',
headers: getSdkHeaders(),
body: JSON.stringify(data),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return transformBackendDSR(await res.json())
}
// =============================================================================
// MOCK DATA FUNCTIONS (kept as fallback)
// =============================================================================