refactor(admin): split academy/[id], iace/hazards, ai-act pages
Extracted components and constants into _components/ subdirectories to bring all three pages under the 300 LOC soft target (was 651/628/612, now 255/232/278 LOC respectively). Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
116
admin-compliance/app/sdk/ai-act/_components/AISystemCard.tsx
Normal file
116
admin-compliance/app/sdk/ai-act/_components/AISystemCard.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import { AISystem } from './types'
|
||||
|
||||
interface AISystemCardProps {
|
||||
system: AISystem
|
||||
onAssess: () => void
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
assessing: boolean
|
||||
}
|
||||
|
||||
const classificationColors: Record<AISystem['classification'], string> = {
|
||||
prohibited: 'bg-red-100 text-red-700 border-red-200',
|
||||
'high-risk': 'bg-orange-100 text-orange-700 border-orange-200',
|
||||
'limited-risk': 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
'minimal-risk': 'bg-green-100 text-green-700 border-green-200',
|
||||
unclassified: 'bg-gray-100 text-gray-500 border-gray-200',
|
||||
}
|
||||
|
||||
const classificationLabels: Record<AISystem['classification'], string> = {
|
||||
prohibited: 'Verboten',
|
||||
'high-risk': 'Hochrisiko',
|
||||
'limited-risk': 'Begrenztes Risiko',
|
||||
'minimal-risk': 'Minimales Risiko',
|
||||
unclassified: 'Nicht klassifiziert',
|
||||
}
|
||||
|
||||
const statusColors: Record<AISystem['status'], string> = {
|
||||
draft: 'bg-gray-100 text-gray-500',
|
||||
classified: 'bg-blue-100 text-blue-700',
|
||||
compliant: 'bg-green-100 text-green-700',
|
||||
'non-compliant': 'bg-red-100 text-red-700',
|
||||
}
|
||||
|
||||
const statusLabels: Record<AISystem['status'], string> = {
|
||||
draft: 'Entwurf',
|
||||
classified: 'Klassifiziert',
|
||||
compliant: 'Konform',
|
||||
'non-compliant': 'Nicht konform',
|
||||
}
|
||||
|
||||
export function AISystemCard({ system, onAssess, onEdit, onDelete, assessing }: AISystemCardProps) {
|
||||
return (
|
||||
<div className={`bg-white rounded-xl border-2 p-6 ${
|
||||
system.classification === 'high-risk' ? 'border-orange-200' :
|
||||
system.classification === 'prohibited' ? 'border-red-200' : 'border-gray-200'
|
||||
}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${classificationColors[system.classification]}`}>
|
||||
{classificationLabels[system.classification]}
|
||||
</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${statusColors[system.status]}`}>
|
||||
{statusLabels[system.status]}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{system.name}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{system.description}</p>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
<span>Sektor: {system.sector}</span>
|
||||
{system.assessmentDate && (
|
||||
<span className="ml-4">Klassifiziert: {system.assessmentDate.toLocaleDateString('de-DE')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{system.obligations.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Pflichten nach AI Act:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{system.obligations.map(obl => (
|
||||
<span key={obl} className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded">
|
||||
{obl}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{system.assessmentResult && (
|
||||
<div className="mt-3 p-3 bg-blue-50 rounded-lg">
|
||||
<p className="text-xs font-medium text-blue-700">KI-Risikobewertung abgeschlossen</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<button
|
||||
onClick={onAssess}
|
||||
disabled={assessing}
|
||||
className="flex-1 px-4 py-2 text-sm bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{assessing ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Bewertung laeuft...
|
||||
</span>
|
||||
) : (
|
||||
system.classification === 'unclassified' ? 'Klassifizierung starten' : 'Risikobewertung starten'
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onEdit} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button onClick={onDelete} className="px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
admin-compliance/app/sdk/ai-act/_components/AddSystemForm.tsx
Normal file
102
admin-compliance/app/sdk/ai-act/_components/AddSystemForm.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { AISystem } from './types'
|
||||
|
||||
interface AddSystemFormProps {
|
||||
onSubmit: (system: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => void
|
||||
onCancel: () => void
|
||||
initialData?: AISystem | null
|
||||
}
|
||||
|
||||
export function AddSystemForm({ onSubmit, onCancel, initialData }: AddSystemFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: initialData?.name || '',
|
||||
description: initialData?.description || '',
|
||||
purpose: initialData?.purpose || '',
|
||||
sector: initialData?.sector || '',
|
||||
classification: (initialData?.classification || 'unclassified') as AISystem['classification'],
|
||||
status: (initialData?.status || 'draft') as AISystem['status'],
|
||||
obligations: initialData?.obligations || [] as string[],
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{initialData ? 'KI-System bearbeiten' : 'Neues KI-System registrieren'}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="z.B. Dokumenten-Scanner"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={e => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Beschreiben Sie das KI-System..."
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einsatzzweck</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.purpose}
|
||||
onChange={e => setFormData({ ...formData, purpose: e.target.value })}
|
||||
placeholder="z.B. Texterkennung"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Sektor</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.sector}
|
||||
onChange={e => setFormData({ ...formData, sector: e.target.value })}
|
||||
placeholder="z.B. Verwaltung"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Vorklassifizierung</label>
|
||||
<select
|
||||
value={formData.classification}
|
||||
onChange={e => setFormData({ ...formData, classification: e.target.value as AISystem['classification'] })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
<option value="unclassified">Noch nicht klassifiziert</option>
|
||||
<option value="minimal-risk">Minimales Risiko</option>
|
||||
<option value="limited-risk">Begrenztes Risiko</option>
|
||||
<option value="high-risk">Hochrisiko</option>
|
||||
<option value="prohibited">Verboten</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSubmit(formData)}
|
||||
disabled={!formData.name}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
|
||||
formData.name ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{initialData ? 'Speichern' : 'Registrieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
export function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 animate-pulse">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="h-5 w-24 bg-gray-200 rounded-full" />
|
||||
<div className="h-5 w-20 bg-gray-200 rounded-full" />
|
||||
</div>
|
||||
<div className="h-6 w-3/4 bg-gray-200 rounded mb-2" />
|
||||
<div className="h-4 w-full bg-gray-100 rounded mb-4" />
|
||||
<div className="h-4 w-1/2 bg-gray-100 rounded" />
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-8 flex-1 bg-gray-200 rounded-lg" />
|
||||
<div className="h-8 w-24 bg-gray-200 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
admin-compliance/app/sdk/ai-act/_components/RiskPyramid.tsx
Normal file
39
admin-compliance/app/sdk/ai-act/_components/RiskPyramid.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { AISystem } from './types'
|
||||
|
||||
interface RiskPyramidProps {
|
||||
systems: AISystem[]
|
||||
}
|
||||
|
||||
export function RiskPyramid({ systems }: RiskPyramidProps) {
|
||||
const counts = {
|
||||
prohibited: systems.filter(s => s.classification === 'prohibited').length,
|
||||
'high-risk': systems.filter(s => s.classification === 'high-risk').length,
|
||||
'limited-risk': systems.filter(s => s.classification === 'limited-risk').length,
|
||||
'minimal-risk': systems.filter(s => s.classification === 'minimal-risk').length,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">AI Act Risikopyramide</h3>
|
||||
<div className="flex flex-col items-center space-y-1">
|
||||
<div className="w-24 h-12 bg-red-500 text-white flex items-center justify-center rounded-t-lg text-sm font-medium">
|
||||
Verboten ({counts.prohibited})
|
||||
</div>
|
||||
<div className="w-40 h-12 bg-orange-500 text-white flex items-center justify-center text-sm font-medium">
|
||||
Hochrisiko ({counts['high-risk']})
|
||||
</div>
|
||||
<div className="w-56 h-12 bg-yellow-500 text-white flex items-center justify-center text-sm font-medium">
|
||||
Begrenztes Risiko ({counts['limited-risk']})
|
||||
</div>
|
||||
<div className="w-72 h-12 bg-green-500 text-white flex items-center justify-center rounded-b-lg text-sm font-medium">
|
||||
Minimales Risiko ({counts['minimal-risk']})
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm text-gray-500">
|
||||
{systems.filter(s => s.classification === 'unclassified').length} System(e) noch nicht klassifiziert
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
admin-compliance/app/sdk/ai-act/_components/types.ts
Normal file
12
admin-compliance/app/sdk/ai-act/_components/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface AISystem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
classification: 'prohibited' | 'high-risk' | 'limited-risk' | 'minimal-risk' | 'unclassified'
|
||||
purpose: string
|
||||
sector: string
|
||||
status: 'draft' | 'classified' | 'compliant' | 'non-compliant'
|
||||
obligations: string[]
|
||||
assessmentDate: Date | null
|
||||
assessmentResult: Record<string, unknown> | null
|
||||
}
|
||||
Reference in New Issue
Block a user