2026-08-21 01:58:46 +02:00
import React , { useState } from 'react' ;
import { useTranslation } from 'react-i18next' ;
import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query' ;
import {
fetchAIRegistry ,
fetchDPIATemplate ,
fetchIncidents ,
createIncident ,
updateIncident ,
fetchRetentionPolicies ,
updateRetentionPolicy ,
type AIRegistryEntry ,
type ComplianceIncident ,
type RetentionPolicyEntry ,
type IncidentCreate ,
2026-08-27 01:49:55 +02:00
submitDsarRequest ,
downloadDsgvoExport ,
type DsarRequestResponse ,
type DsarType ,
2026-08-21 01:58:46 +02:00
} from '../api/compliance' ;
2026-08-27 01:49:55 +02:00
import { useUsers } from '../api/users' ;
2026-08-21 01:58:46 +02:00
2026-08-27 01:49:55 +02:00
type SubTab = 'registry' | 'incidents' | 'retention' | 'dsar' ;
2026-08-21 01:58:46 +02:00
export function ComplianceTab() {
const { t } = useTranslation ( ) ;
const [ subTab , setSubTab ] = useState < SubTab > ( 'registry' ) ;
const subTabs : { key : SubTab ; label : string } [ ] = [
{ key : 'registry' , label : t ( 'compliance.aiRegistry' , 'AI-Register' ) } ,
{ key : 'incidents' , label : t ( 'compliance.incidents' , 'Vorfälle' ) } ,
{ key : 'retention' , label : t ( 'compliance.retention' , 'Aufbewahrung' ) } ,
2026-08-27 01:49:55 +02:00
{ key : 'dsar' , label : t ( 'compliance.dsar.tab' , 'DSGVO-Anfragen' ) } ,
2026-08-21 01:58:46 +02:00
] ;
return (
< div data-testid = "compliance-tab" className = "space-y-4" >
< div className = "border-b border-secondary-200 mb-4" >
< nav className = "flex gap-1" role = "tablist" >
{ subTabs . map ( ( tab ) = > (
< button
key = { tab . key }
onClick = { ( ) = > setSubTab ( tab . key ) }
className = { ` px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
subTab === tab . key
? 'border-primary-600 text-primary-600'
: 'border-transparent text-secondary-500 hover:text-secondary-700 hover:border-secondary-300'
} ` }
role = "tab"
aria-selected = { subTab === tab . key }
aria-label = { tab . label }
>
{ tab . label }
< / button >
) ) }
< / nav >
< / div >
{ subTab === 'registry' && < AIRegistryPanel / > }
{ subTab === 'incidents' && < IncidentsPanel / > }
{ subTab === 'retention' && < RetentionPanel / > }
2026-08-27 01:49:55 +02:00
{ subTab === 'dsar' && < DsarPanel / > }
2026-08-21 01:58:46 +02:00
< / div >
) ;
}
// ─── AI Registry Panel ───
function AIRegistryPanel() {
const { t } = useTranslation ( ) ;
const { data , isLoading , error } = useQuery ( {
queryKey : [ 'compliance' , 'ai-registry' ] ,
queryFn : fetchAIRegistry ,
} ) ;
const handleDPIAExport = async ( agentId : string , agentName : string ) = > {
try {
const template = await fetchDPIATemplate ( agentId ) ;
const blob = new Blob ( [ JSON . stringify ( template , null , 2 ) ] , {
type : 'application/json' ,
} ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = ` dpia_ ${ agentName . replace ( /\s+/g , '_' ) . toLowerCase ( ) } .json ` ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
} catch {
// Error handled by query client
}
} ;
if ( isLoading ) {
return < p className = "text-secondary-500" aria-live = "polite" > { t ( 'common.loading' , 'Laden...' ) } < / p > ;
}
if ( error ) {
return < p className = "text-red-600" aria-live = "polite" > { t ( 'common.error' , 'Fehler beim Laden' ) } < / p > ;
}
if ( ! data || data . items . length === 0 ) {
return < p className = "text-secondary-500" > { t ( 'compliance.noAgents' , 'Keine AI-Agenten gefunden' ) } < / p > ;
}
return (
< div className = "overflow-x-auto" >
< table className = "min-w-full divide-y divide-secondary-200" role = "table" >
< thead className = "bg-secondary-50" >
< tr >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.name' , 'Name' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.intendedPurpose' , 'Zweck' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.owner' , 'Verantwortlich' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.riskClass' , 'Risiko' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.oversight' , 'Oversight' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.dataCategories' , 'Daten' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.status' , 'Status' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.actions' , 'Aktionen' ) } < / th >
< / tr >
< / thead >
< tbody className = "divide-y divide-secondary-100" >
{ data . items . map ( ( entry : AIRegistryEntry ) = > {
const meta = entry . ai_use_case_metadata as Record < string , unknown > ;
return (
< tr key = { entry . agent_id } className = "hover:bg-secondary-50" >
< td className = "px-4 py-2 text-sm text-secondary-900" > { entry . name } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { String ( meta . intended_purpose || '' ) } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { String ( meta . owner || '' ) } < / td >
< td className = "px-4 py-2 text-sm" >
< span className = { ` inline-flex px-2 py-0.5 rounded text-xs font-medium ${
meta . risk_class === 'high' ? 'bg-red-100 text-red-700' :
meta . risk_class === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
} ` } >
{ String ( meta . risk_class || 'low' ) }
< / span >
< / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { String ( meta . oversight_policy || '' ) } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" >
{ Array . isArray ( meta . data_categories ) ? ( meta . data_categories as string [ ] ) . join ( ', ' ) : '' }
< / td >
< td className = "px-4 py-2 text-sm" >
< span className = { ` inline-flex px-2 py-0.5 rounded text-xs font-medium ${
entry . is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
} ` } >
{ entry . is_active ? t ( 'compliance.active' , 'Aktiv' ) : t ( 'compliance.inactive' , 'Inaktiv' ) }
< / span >
< / td >
< td className = "px-4 py-2 text-sm" >
< button
onClick = { ( ) = > handleDPIAExport ( entry . agent_id , entry . name ) }
className = "text-primary-600 hover:text-primary-700 text-xs font-medium"
aria-label = { ` ${ t ( 'compliance.dpiaExport' , 'DPIA Export' ) } ${ entry . name } ` }
>
{ t ( 'compliance.dpiaExport' , 'DPIA Export' ) }
< / button >
< / td >
< / tr >
) ;
} ) }
< / tbody >
< / table >
{ data . items . some ( ( e ) = > e . validation_warnings . length > 0 ) && (
< div className = "mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800" >
{ t ( 'compliance.warningsNote' , 'Einige Agenten haben Validierungswarnungen. Siehe Details im AI-Register.' ) }
< / div >
) }
< / div >
) ;
}
// ─── Incidents Panel ───
function IncidentsPanel() {
const { t } = useTranslation ( ) ;
const queryClient = useQueryClient ( ) ;
const [ showForm , setShowForm ] = useState ( false ) ;
const [ formData , setFormData ] = useState < IncidentCreate > ( {
incident_type : 'ai' ,
title : '' ,
description : '' ,
provider : '' ,
measures_taken : '' ,
status : 'open' ,
} ) ;
const { data , isLoading , error } = useQuery ( {
queryKey : [ 'compliance' , 'incidents' ] ,
queryFn : ( ) = > fetchIncidents ( ) ,
} ) ;
const createMutation = useMutation ( {
mutationFn : ( data : IncidentCreate ) = > createIncident ( data ) ,
onSuccess : ( ) = > {
queryClient . invalidateQueries ( { queryKey : [ 'compliance' , 'incidents' ] } ) ;
setShowForm ( false ) ;
setFormData ( { incident_type : 'ai' , title : '' , description : '' , provider : '' , measures_taken : '' , status : 'open' } ) ;
} ,
} ) ;
const updateMutation = useMutation ( {
mutationFn : ( { id , data } : { id : string ; data : { status? : string } } ) = >
updateIncident ( id , data ) ,
onSuccess : ( ) = > {
queryClient . invalidateQueries ( { queryKey : [ 'compliance' , 'incidents' ] } ) ;
} ,
} ) ;
const handleSubmit = ( e : React.FormEvent ) = > {
e . preventDefault ( ) ;
createMutation . mutate ( formData ) ;
} ;
if ( isLoading ) {
return < p className = "text-secondary-500" aria-live = "polite" > { t ( 'common.loading' , 'Laden...' ) } < / p > ;
}
if ( error ) {
return < p className = "text-red-600" aria-live = "polite" > { t ( 'common.error' , 'Fehler beim Laden' ) } < / p > ;
}
return (
< div className = "space-y-4" >
< button
onClick = { ( ) = > setShowForm ( ! showForm ) }
className = "px-4 py-2 bg-primary-600 text-white rounded text-sm font-medium hover:bg-primary-700"
aria-label = { t ( 'compliance.createIncident' , 'Vorfall erstellen' ) }
>
{ showForm ? t ( 'common.cancel' , 'Abbrechen' ) : t ( 'compliance.createIncident' , 'Vorfall erstellen' ) }
< / button >
{ showForm && (
< form onSubmit = { handleSubmit } className = "space-y-3 p-4 border border-secondary-200 rounded" >
< div >
< label className = "block text-sm font-medium text-secondary-700 mb-1" htmlFor = "incident-type" >
{ t ( 'compliance.incidentType' , 'Typ' ) }
< / label >
< select
id = "incident-type"
value = { formData . incident_type }
onChange = { ( e ) = > setFormData ( { . . . formData , incident_type : e.target.value } ) }
className = "w-full px-3 py-2 border border-secondary-300 rounded text-sm"
aria-label = { t ( 'compliance.incidentType' , 'Typ' ) }
>
< option value = "ai" > AI < / option >
< option value = "privacy" > Privacy < / option >
< option value = "security" > Security < / option >
< / select >
< / div >
< div >
< label className = "block text-sm font-medium text-secondary-700 mb-1" htmlFor = "incident-title" >
{ t ( 'compliance.title' , 'Titel' ) } *
< / label >
< input
id = "incident-title"
type = "text"
required
value = { formData . title }
onChange = { ( e ) = > setFormData ( { . . . formData , title : e.target.value } ) }
className = "w-full px-3 py-2 border border-secondary-300 rounded text-sm"
aria-label = { t ( 'compliance.title' , 'Titel' ) }
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-secondary-700 mb-1" htmlFor = "incident-description" >
{ t ( 'compliance.description' , 'Beschreibung' ) }
< / label >
< textarea
id = "incident-description"
value = { formData . description }
onChange = { ( e ) = > setFormData ( { . . . formData , description : e.target.value } ) }
className = "w-full px-3 py-2 border border-secondary-300 rounded text-sm"
rows = { 3 }
aria-label = { t ( 'compliance.description' , 'Beschreibung' ) }
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-secondary-700 mb-1" htmlFor = "incident-provider" >
{ t ( 'compliance.provider' , 'Provider' ) }
< / label >
< input
id = "incident-provider"
type = "text"
value = { formData . provider }
onChange = { ( e ) = > setFormData ( { . . . formData , provider : e.target.value } ) }
className = "w-full px-3 py-2 border border-secondary-300 rounded text-sm"
aria-label = { t ( 'compliance.provider' , 'Provider' ) }
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-secondary-700 mb-1" htmlFor = "incident-measures" >
{ t ( 'compliance.measures' , 'Maßnahmen' ) }
< / label >
< textarea
id = "incident-measures"
value = { formData . measures_taken }
onChange = { ( e ) = > setFormData ( { . . . formData , measures_taken : e.target.value } ) }
className = "w-full px-3 py-2 border border-secondary-300 rounded text-sm"
rows = { 2 }
aria-label = { t ( 'compliance.measures' , 'Maßnahmen' ) }
/ >
< / div >
{ createMutation . isError && (
< p className = "text-red-600 text-sm" role = "alert" > { t ( 'compliance.createError' , 'Fehler beim Erstellen' ) } < / p >
) }
< button
type = "submit"
disabled = { createMutation . isPending }
className = "px-4 py-2 bg-primary-600 text-white rounded text-sm font-medium hover:bg-primary-700 disabled:opacity-50"
aria-label = { t ( 'common.save' , 'Speichern' ) }
>
{ createMutation . isPending ? t ( 'common.saving' , 'Speichern...' ) : t ( 'common.save' , 'Speichern' ) }
< / button >
< / form >
) }
{ ! data || data . items . length === 0 ? (
< p className = "text-secondary-500" > { t ( 'compliance.noIncidents' , 'Keine Vorfälle erfasst' ) } < / p >
) : (
< div className = "overflow-x-auto" >
< table className = "min-w-full divide-y divide-secondary-200" role = "table" >
< thead className = "bg-secondary-50" >
< tr >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.title' , 'Titel' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.incidentType' , 'Typ' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.status' , 'Status' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.provider' , 'Provider' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.createdAt' , 'Erstellt' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.actions' , 'Aktionen' ) } < / th >
< / tr >
< / thead >
< tbody className = "divide-y divide-secondary-100" >
{ data . items . map ( ( incident : ComplianceIncident ) = > (
< tr key = { incident . id } className = "hover:bg-secondary-50" >
< td className = "px-4 py-2 text-sm text-secondary-900" > { incident . title } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { incident . incident_type } < / td >
< td className = "px-4 py-2 text-sm" >
< span className = { ` inline-flex px-2 py-0.5 rounded text-xs font-medium ${
incident . status === 'open' ? 'bg-red-100 text-red-700' :
incident . status === 'resolved' ? 'bg-green-100 text-green-700' :
'bg-gray-100 text-gray-600'
} ` } >
{ incident . status }
< / span >
< / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { incident . provider } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { incident . created_at || '' } < / td >
< td className = "px-4 py-2 text-sm" >
{ incident . status === 'open' && (
< button
onClick = { ( ) = > updateMutation . mutate ( { id : incident.id , data : { status : 'resolved' } } ) }
className = "text-primary-600 hover:text-primary-700 text-xs font-medium"
aria-label = { ` ${ t ( 'compliance.resolve' , 'Auflösen' ) } ${ incident . title } ` }
>
{ t ( 'compliance.resolve' , 'Auflösen' ) }
< / button >
) }
< / td >
< / tr >
) ) }
< / tbody >
< / table >
< / div >
) }
< / div >
) ;
}
// ─── Retention Policies Panel ───
function RetentionPanel() {
const { t } = useTranslation ( ) ;
const queryClient = useQueryClient ( ) ;
const [ editingKey , setEditingKey ] = useState < string | null > ( null ) ;
const [ editDays , setEditDays ] = useState < number > ( 0 ) ;
const { data , isLoading , error } = useQuery ( {
queryKey : [ 'compliance' , 'retention-policies' ] ,
queryFn : fetchRetentionPolicies ,
} ) ;
const updateMutation = useMutation ( {
mutationFn : ( { key , days } : { key : string ; days : number } ) = >
updateRetentionPolicy ( key , days ) ,
onSuccess : ( ) = > {
queryClient . invalidateQueries ( { queryKey : [ 'compliance' , 'retention-policies' ] } ) ;
setEditingKey ( null ) ;
} ,
} ) ;
if ( isLoading ) {
return < p className = "text-secondary-500" aria-live = "polite" > { t ( 'common.loading' , 'Laden...' ) } < / p > ;
}
if ( error ) {
return < p className = "text-red-600" aria-live = "polite" > { t ( 'common.error' , 'Fehler beim Laden' ) } < / p > ;
}
if ( ! data || data . items . length === 0 ) {
return < p className = "text-secondary-500" > { t ( 'compliance.noPolicies' , 'Keine Aufbewahrungsrichtlinien' ) } < / p > ;
}
return (
< div className = "overflow-x-auto" >
< table className = "min-w-full divide-y divide-secondary-200" role = "table" >
< thead className = "bg-secondary-50" >
< tr >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.policy' , 'Richtlinie' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.description' , 'Beschreibung' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.defaultDays' , 'Standard (Tage)' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.currentDays' , 'Aktuell (Tage)' ) } < / th >
< th className = "px-4 py-2 text-left text-xs font-medium text-secondary-500 uppercase" > { t ( 'compliance.actions' , 'Aktionen' ) } < / th >
< / tr >
< / thead >
< tbody className = "divide-y divide-secondary-100" >
{ data . items . map ( ( policy : RetentionPolicyEntry ) = > (
< tr key = { policy . key } className = "hover:bg-secondary-50" >
< td className = "px-4 py-2 text-sm font-medium text-secondary-900" > { policy . label } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { policy . description } < / td >
< td className = "px-4 py-2 text-sm text-secondary-600" > { policy . default_days } < / td >
< td className = "px-4 py-2 text-sm text-secondary-900" >
{ editingKey === policy . key ? (
< input
type = "number"
min = { 1 }
max = { 3650 }
value = { editDays }
onChange = { ( e ) = > setEditDays ( parseInt ( e . target . value , 10 ) || 1 ) }
className = "w-20 px-2 py-1 border border-secondary-300 rounded text-sm"
aria-label = { t ( 'compliance.days' , 'Tage' ) }
/ >
) : (
policy . current_days
) }
< / td >
< td className = "px-4 py-2 text-sm" >
{ editingKey === policy . key ? (
< div className = "flex gap-2" >
< button
onClick = { ( ) = > updateMutation . mutate ( { key : policy.key , days : editDays } ) }
disabled = { updateMutation . isPending }
className = "text-green-600 hover:text-green-700 text-xs font-medium disabled:opacity-50"
aria-label = { t ( 'common.save' , 'Speichern' ) }
>
{ updateMutation . isPending ? t ( 'common.saving' , '...' ) : t ( 'common.save' , 'Speichern' ) }
< / button >
< button
onClick = { ( ) = > setEditingKey ( null ) }
className = "text-secondary-500 hover:text-secondary-700 text-xs font-medium"
aria-label = { t ( 'common.cancel' , 'Abbrechen' ) }
>
{ t ( 'common.cancel' , 'Abbrechen' ) }
< / button >
< / div >
) : (
policy . editable && (
< button
onClick = { ( ) = > {
setEditingKey ( policy . key ) ;
setEditDays ( policy . current_days ) ;
} }
className = "text-primary-600 hover:text-primary-700 text-xs font-medium"
aria-label = { ` ${ t ( 'common.edit' , 'Bearbeiten' ) } ${ policy . label } ` }
>
{ t ( 'common.edit' , 'Bearbeiten' ) }
< / button >
)
) }
< / td >
< / tr >
) ) }
< / tbody >
< / table >
< / div >
) ;
}
2026-08-27 01:49:55 +02:00
// ─── DSAR Panel (GDPR Art. 15/17/20) ───
const DSAR_TYPES : { value : DsarType ; labelKey : string ; fallback : string } [ ] = [
{ value : 'access' , labelKey : 'compliance.dsar.typeAccess' , fallback : 'Auskunft (Art. 15)' } ,
{ value : 'deletion' , labelKey : 'compliance.dsar.typeDeletion' , fallback : 'Löschung (Art. 17)' } ,
{ value : 'rectification' , labelKey : 'compliance.dsar.typeRectification' , fallback : 'Berichtigung (Art. 16)' } ,
] ;
function DsarPanel() {
const { t } = useTranslation ( ) ;
const [ userId , setUserId ] = useState ( '' ) ;
const [ dsarType , setDsarType ] = useState < DsarType > ( 'access' ) ;
const [ confirmDelete , setConfirmDelete ] = useState ( false ) ;
const [ lastJob , setLastJob ] = useState < DsarRequestResponse | null > ( null ) ;
const [ exportError , setExportError ] = useState ( false ) ;
const { data : usersData , isLoading : usersLoading } = useUsers ( 1 , 200 ) ;
const dsarMutation = useMutation ( {
mutationFn : ( ) = > submitDsarRequest ( userId , dsarType ) ,
onSuccess : ( resp ) = > {
setLastJob ( resp ) ;
setConfirmDelete ( false ) ;
} ,
} ) ;
const selectedUser = usersData ? . items . find ( ( u ) = > u . id === userId ) ;
const handleExportClick = async ( ) = > {
setExportError ( false ) ;
try {
await downloadDsgvoExport ( userId , selectedUser ? . name ? ? undefined ) ;
} catch {
setExportError ( true ) ;
}
} ;
const handleSubmit = ( ) = > {
if ( ! userId ) return ;
if ( dsarType === 'deletion' && ! confirmDelete ) {
setConfirmDelete ( true ) ;
return ;
}
dsarMutation . mutate ( ) ;
} ;
return (
< div className = "space-y-4 max-w-2xl" >
< p className = "text-sm text-secondary-600" > { t ( 'compliance.dsar.description' , 'DSGVO-Anfrage für eine Person auslösen: Auskunft (Art. 15), Löschung (Art. 17) oder Berichtigung (Art. 16). Der Vorgang wird als Hintergrund-Job durch den Worker ausgeführt.' ) } < / p >
< div >
< label htmlFor = "dsar-user-select" className = "block text-xs font-medium text-secondary-500 uppercase mb-1" > { t ( 'compliance.dsar.person' , 'Person' ) } < / label >
< select
id = "dsar-user-select"
value = { userId }
onChange = { ( e ) = > { setUserId ( e . target . value ) ; setConfirmDelete ( false ) ; } }
className = "w-full max-w-md px-3 py-3 border border-secondary-300 rounded bg-white text-sm"
aria-label = { t ( 'compliance.dsar.person' , 'Person' ) }
>
< option value = "" > { usersLoading ? t ( 'common.loading' , 'Laden...' ) : t ( 'compliance.dsar.selectPerson' , '-- Bitte wählen --' ) } < / option >
{ ( usersData ? . items ? ? [ ] ) . map ( ( u ) = > (
< option key = { u . id } value = { u . id } > { u . name || u . email } < / option >
) ) }
< / select >
< / div >
< fieldset >
< legend className = "block text-xs font-medium text-secondary-500 uppercase mb-1" > { t ( 'compliance.dsar.requestType' , 'Antragsart' ) } < / legend >
< div className = "space-y-1" role = "radiogroup" aria-label = { t ( 'compliance.dsar.requestType' , 'Antragsart' ) } >
{ DSAR_TYPES . map ( ( { value , labelKey , fallback } ) = > (
< label
key = { value }
className = { ` flex items-center gap-2 px-3 py-3 min-h-[44px] rounded cursor-pointer border ${
dsarType === value ? 'border-primary-500 bg-primary-50' : 'border-secondary-200 hover:bg-secondary-50'
} ` }
>
< input
type = "radio"
name = "dsar-type"
checked = { dsarType === value }
onChange = { ( ) = > { setDsarType ( value ) ; setConfirmDelete ( false ) ; } }
aria-label = { t ( labelKey , fallback ) }
/ >
< span className = "text-sm" > { t ( labelKey , fallback ) } < / span >
< / label >
) ) }
< / div >
< / fieldset >
< div className = "flex flex-wrap gap-2 items-center" >
{ dsarType === 'access' && (
< button
onClick = { handleExportClick }
disabled = { ! userId }
className = "px-4 py-3 min-h-[44px] text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded disabled:opacity-50"
aria-label = { t ( 'compliance.dsar.downloadExport' , 'Datenexport herunterladen' ) }
>
{ t ( 'compliance.dsar.downloadExport' , 'Datenexport herunterladen' ) }
< / button >
) }
< button
onClick = { handleSubmit }
disabled = { ! userId || dsarMutation . isPending }
className = { ` px-4 py-3 min-h-[44px] text-sm font-medium text-white rounded disabled:opacity-50 ${
dsarType === 'deletion' ? 'bg-red-600 hover:bg-red-700' : 'bg-primary-600 hover:bg-primary-700'
} ` }
aria-label = { t ( 'compliance.dsar.submit' , 'Anfrage stellen' ) }
>
{ dsarType === 'deletion' && confirmDelete
? t ( 'compliance.dsar.confirmDeletion' , 'Wirklich löschen? Unwiderruflich!' )
: t ( 'compliance.dsar.submit' , 'Anfrage stellen' ) }
< / button >
{ ! userId && (
< span className = "text-xs text-secondary-400" > { t ( 'compliance.dsar.chooseFirst' , 'Bitte zuerst eine Person wählen.' ) } < / span >
) }
< / div >
{ dsarMutation . isPending && < p className = "text-sm text-secondary-500" aria-live = "polite" > { t ( 'common.saving' , 'Wird gesendet...' ) } < / p > }
{ dsarMutation . isError && < p className = "text-sm text-red-600" aria-live = "polite" > { t ( 'common.errorOccurred' , 'Fehler beim Senden' ) } < / p > }
{ exportError && < p className = "text-sm text-red-600" aria-live = "polite" > { t ( 'common.errorOccurred' , 'Fehler beim Erstellen des Exports' ) } < / p > }
{ lastJob && (
< div className = "rounded border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800" role = "status" >
{ t ( 'compliance.dsar.queued' , 'Anfrage eingereicht — Job' ) } { ' ' }
< code className = "font-mono" > { lastJob . job_id } < / code > { ' ' }
{ t ( 'compliance.dsar.statusQueued' , '(in Warteschlange). Die Bearbeitung erfolgt im Hintergrund.' ) }
< / div >
) }
< / div >
) ;
}