diff --git a/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx b/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx
index df43284..0d1f255 100644
--- a/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx
@@ -1,6 +1,6 @@
'use client'
-import React, { useState } from 'react'
+import React, { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
@@ -22,47 +22,32 @@ interface AISystem {
}
// =============================================================================
-// INITIAL DATA
+// LOADING SKELETON
// =============================================================================
-const initialSystems: AISystem[] = [
- {
- id: 'ai-1',
- name: 'Kundenservice Chatbot',
- description: 'KI-gestuetzter Chatbot fuer Kundenanfragen',
- classification: 'limited-risk',
- purpose: 'Automatisierte Beantwortung von Kundenanfragen',
- sector: 'Kundenservice',
- status: 'classified',
- obligations: ['Transparenzpflicht', 'Kennzeichnung als KI-System'],
- assessmentDate: new Date('2024-01-15'),
- assessmentResult: null,
- },
- {
- id: 'ai-2',
- name: 'Bewerber-Screening',
- description: 'KI-System zur Vorauswahl von Bewerbungen',
- classification: 'high-risk',
- purpose: 'Automatisierte Bewertung von Bewerbungsunterlagen',
- sector: 'Personal',
- status: 'non-compliant',
- obligations: ['Risikomanagementsystem', 'Datenlenkung', 'Technische Dokumentation', 'Menschliche Aufsicht', 'Transparenz'],
- assessmentDate: new Date('2024-01-10'),
- assessmentResult: null,
- },
- {
- id: 'ai-3',
- name: 'Empfehlungsalgorithmus',
- description: 'Personalisierte Produktempfehlungen',
- classification: 'minimal-risk',
- purpose: 'Verbesserung der Kundenerfahrung durch personalisierte Empfehlungen',
- sector: 'E-Commerce',
- status: 'compliant',
- obligations: [],
- assessmentDate: new Date('2024-01-05'),
- assessmentResult: null,
- },
-]
+function LoadingSkeleton() {
+ return (
+
+ {[1, 2, 3, 4].map(i => (
+
+ ))}
+
+ )
+}
// =============================================================================
// COMPONENTS
@@ -103,23 +88,25 @@ function RiskPyramid({ systems }: { systems: AISystem[] }) {
function AddSystemForm({
onSubmit,
onCancel,
+ initialData,
}: {
onSubmit: (system: Omit) => void
onCancel: () => void
+ initialData?: AISystem | null
}) {
const [formData, setFormData] = useState({
- name: '',
- description: '',
- purpose: '',
- sector: '',
- classification: 'unclassified' as AISystem['classification'],
- status: 'draft' as AISystem['status'],
- obligations: [] as string[],
+ 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 (
-
Neues KI-System registrieren
+
{initialData ? 'KI-System bearbeiten' : 'Neues KI-System registrieren'}
@@ -189,7 +176,7 @@ function AddSystemForm({
formData.name ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
- Registrieren
+ {initialData ? 'Speichern' : 'Registrieren'}
@@ -200,11 +187,13 @@ function AISystemCard({
system,
onAssess,
onEdit,
+ onDelete,
assessing,
}: {
system: AISystem
onAssess: () => void
onEdit: () => void
+ onDelete: () => void
assessing: boolean
}) {
const classificationColors = {
@@ -306,6 +295,12 @@ function AISystemCard({
>
Bearbeiten
+
)
@@ -317,23 +312,57 @@ function AISystemCard({
export default function AIActPage() {
const { state } = useSDK()
- const [systems, setSystems] = useState(initialSystems)
+ const [systems, setSystems] = useState([])
const [filter, setFilter] = useState('all')
const [showAddForm, setShowAddForm] = useState(false)
+ const [editingSystem, setEditingSystem] = useState(null)
const [assessingId, setAssessingId] = useState(null)
const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ // Load systems from SDK state on mount
+ useEffect(() => {
+ setLoading(true)
+ // Try to load from SDK state (aiSystems if available)
+ const sdkState = state as unknown as Record
+ if (Array.isArray(sdkState.aiSystems) && sdkState.aiSystems.length > 0) {
+ setSystems(sdkState.aiSystems as AISystem[])
+ }
+ setLoading(false)
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
const handleAddSystem = (data: Omit) => {
- const newSystem: AISystem = {
- ...data,
- id: `ai-${Date.now()}`,
- assessmentDate: data.classification !== 'unclassified' ? new Date() : null,
- assessmentResult: null,
+ if (editingSystem) {
+ // Edit existing system
+ setSystems(prev => prev.map(s =>
+ s.id === editingSystem.id
+ ? { ...s, ...data }
+ : s
+ ))
+ setEditingSystem(null)
+ } else {
+ // Create new system
+ const newSystem: AISystem = {
+ ...data,
+ id: `ai-${Date.now()}`,
+ assessmentDate: data.classification !== 'unclassified' ? new Date() : null,
+ assessmentResult: null,
+ }
+ setSystems(prev => [...prev, newSystem])
}
- setSystems(prev => [...prev, newSystem])
setShowAddForm(false)
}
+ const handleDelete = (id: string) => {
+ if (!confirm('Moechten Sie dieses KI-System wirklich loeschen?')) return
+ setSystems(prev => prev.filter(s => s.id !== id))
+ }
+
+ const handleEdit = (system: AISystem) => {
+ setEditingSystem(system)
+ setShowAddForm(true)
+ }
+
const handleAssess = async (systemId: string) => {
const system = systems.find(s => s.id === systemId)
if (!system) return
@@ -420,11 +449,12 @@ export default function AIActPage() {
)}
- {/* Add System Form */}
+ {/* Add/Edit System Form */}
{showAddForm && (
setShowAddForm(false)}
+ onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
+ initialData={editingSystem}
/>
)}
@@ -474,20 +504,26 @@ export default function AIActPage() {
))}
- {/* AI Systems List */}
-
- {filteredSystems.map(system => (
-
handleAssess(system.id)}
- onEdit={() => {/* Edit handler */}}
- assessing={assessingId === system.id}
- />
- ))}
-
+ {/* Loading */}
+ {loading && }
- {filteredSystems.length === 0 && (
+ {/* AI Systems List */}
+ {!loading && (
+
+ {filteredSystems.map(system => (
+
handleAssess(system.id)}
+ onEdit={() => handleEdit(system)}
+ onDelete={() => handleDelete(system.id)}
+ assessing={assessingId === system.id}
+ />
+ ))}
+
+ )}
+
+ {!loading && filteredSystems.length === 0 && (
@@ -267,10 +273,12 @@ function LoadingSkeleton() {
export default function AuditChecklistPage() {
const { state, dispatch } = useSDK()
+ const router = useRouter()
const [filter, setFilter] = useState
('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [activeSessionId, setActiveSessionId] = useState(null)
+ const notesTimerRef = useRef>>({})
// Fetch checklist from backend on mount
useEffect(() => {
@@ -411,11 +419,76 @@ export default function AuditChecklistPage() {
}
}
- const handleNotesChange = async (itemId: string, notes: string) => {
+ const handleNotesChange = useCallback((itemId: string, notes: string) => {
const updatedChecklist = state.checklist.map(item =>
item.id === itemId ? { ...item, notes } : item
)
dispatch({ type: 'SET_STATE', payload: { checklist: updatedChecklist } })
+
+ // Debounced persistence to backend
+ if (notesTimerRef.current[itemId]) {
+ clearTimeout(notesTimerRef.current[itemId])
+ }
+ notesTimerRef.current[itemId] = setTimeout(async () => {
+ if (activeSessionId) {
+ const item = state.checklist.find(i => i.id === itemId)
+ try {
+ await fetch(`/api/sdk/v1/compliance/audit/checklist/${activeSessionId}/items/${item?.requirementId || itemId}/sign-off`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ notes }),
+ })
+ } catch {
+ // Silently fail
+ }
+ }
+ }, 1000)
+ }, [state.checklist, activeSessionId, dispatch])
+
+ const handleExport = () => {
+ const exportData = displayItems.map(item => ({
+ id: item.id,
+ requirementId: item.requirementId,
+ question: item.question,
+ category: item.category,
+ status: item.status,
+ notes: item.notes,
+ priority: item.priority,
+ verifiedBy: item.verifiedBy,
+ verifiedAt: item.verifiedAt?.toISOString() || null,
+ }))
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `audit-checklist-${new Date().toISOString().split('T')[0]}.json`
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ }
+
+ const handleNewChecklist = async () => {
+ try {
+ setError(null)
+ const res = await fetch('/api/sdk/v1/compliance/audit/sessions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: `Compliance Audit ${new Date().toLocaleDateString('de-DE')}`,
+ auditor_name: 'Aktueller Benutzer',
+ regulation_codes: ['GDPR'],
+ }),
+ })
+ if (res.ok) {
+ // Reload data
+ window.location.reload()
+ } else {
+ setError('Fehler beim Erstellen der neuen Checkliste')
+ }
+ } catch {
+ setError('Verbindung zum Backend fehlgeschlagen')
+ }
}
const stepInfo = STEP_EXPLANATIONS['audit-checklist']
@@ -431,10 +504,16 @@ export default function AuditChecklistPage() {
tips={stepInfo.tips}
>
-
diff --git a/admin-compliance/app/(sdk)/sdk/audit-report/page.tsx b/admin-compliance/app/(sdk)/sdk/audit-report/page.tsx
index 9125ec1..75450a6 100644
--- a/admin-compliance/app/(sdk)/sdk/audit-report/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/audit-report/page.tsx
@@ -8,7 +8,7 @@
import { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
-import StepHeader from '@/components/sdk/StepHeader/StepHeader'
+import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
interface AuditSession {
id: string
@@ -177,7 +177,14 @@ export default function AuditReportPage() {
return (
-
+
{error && (
@@ -216,7 +223,31 @@ export default function AuditReportPage() {
{loading ? (
-
Lade Audit-Sessions...
+
+ {[1, 2, 3].map(i => (
+
+ ))}
+
) : sessions.length === 0 ? (
Keine Audit-Sessions vorhanden
diff --git a/admin-compliance/app/(sdk)/sdk/controls/page.tsx b/admin-compliance/app/(sdk)/sdk/controls/page.tsx
index 1941f6c..61e4129 100644
--- a/admin-compliance/app/(sdk)/sdk/controls/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/controls/page.tsx
@@ -283,6 +283,98 @@ function ControlCard({
)
}
+function AddControlForm({
+ onSubmit,
+ onCancel,
+}: {
+ onSubmit: (data: { name: string; description: string; type: ControlType; category: string; owner: string }) => void
+ onCancel: () => void
+}) {
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ type: 'TECHNICAL' as ControlType,
+ category: '',
+ owner: '',
+ })
+
+ return (
+
+
Neue Kontrolle
+
+
+
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder="z.B. Zugriffskontrolle"
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ setFormData({ ...formData, category: e.target.value })}
+ placeholder="z.B. Zutrittskontrolle"
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ />
+
+
+
+ setFormData({ ...formData, owner: e.target.value })}
+ placeholder="z.B. IT Security"
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ />
+
+
+
+
+
+ Abbrechen
+
+ 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'
+ }`}
+ >
+ Hinzufuegen
+
+
+
+ )
+}
+
function LoadingSkeleton() {
return (
@@ -311,6 +403,7 @@ export default function ControlsPage() {
const [filter, setFilter] = useState
('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const [showAddForm, setShowAddForm] = useState(false)
// Track effectiveness locally as it's not in the SDK state type
const [effectivenessMap, setEffectivenessMap] = useState>({})
@@ -436,8 +529,36 @@ export default function ControlsPage() {
}
}
- const handleEffectivenessChange = (controlId: string, effectiveness: number) => {
+ const handleEffectivenessChange = async (controlId: string, effectiveness: number) => {
setEffectivenessMap(prev => ({ ...prev, [controlId]: effectiveness }))
+
+ // Persist to backend
+ try {
+ await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ effectiveness_score: effectiveness }),
+ })
+ } catch {
+ // Silently fail — local state is already updated
+ }
+ }
+
+ const handleAddControl = (data: { name: string; description: string; type: ControlType; category: string; owner: string }) => {
+ const newControl: SDKControl = {
+ id: `ctrl-${Date.now()}`,
+ name: data.name,
+ description: data.description,
+ type: data.type,
+ category: data.category,
+ implementationStatus: 'NOT_IMPLEMENTED',
+ effectiveness: 'LOW',
+ evidence: [],
+ owner: data.owner || null,
+ dueDate: null,
+ }
+ dispatch({ type: 'ADD_CONTROL', payload: newControl })
+ setShowAddForm(false)
}
const stepInfo = STEP_EXPLANATIONS['controls']
@@ -452,7 +573,10 @@ export default function ControlsPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
-
+ setShowAddForm(true)}
+ className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
+ >
@@ -460,6 +584,14 @@ export default function ControlsPage() {
+ {/* Add Form */}
+ {showAddForm && (
+ setShowAddForm(false)}
+ />
+ )}
+
{/* Error Banner */}
{error && (
diff --git a/admin-compliance/app/(sdk)/sdk/evidence/page.tsx b/admin-compliance/app/(sdk)/sdk/evidence/page.tsx
index d8feb06..2be256b 100644
--- a/admin-compliance/app/(sdk)/sdk/evidence/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/evidence/page.tsx
@@ -162,7 +162,7 @@ const evidenceTemplates: EvidenceTemplate[] = [
// COMPONENTS
// =============================================================================
-function EvidenceCard({ evidence, onDelete }: { evidence: DisplayEvidence; onDelete: () => void }) {
+function EvidenceCard({ evidence, onDelete, onView, onDownload }: { evidence: DisplayEvidence; onDelete: () => void; onView: () => void; onDownload: () => void }) {
const typeIcons = {
document: (
+
+ {expanded && (
+
+
+
Vollstaendige Beschreibung
+
{requirement.description || 'Keine Beschreibung vorhanden.'}
+
+
+
Zugeordnete Kontrollen ({linkedControls.length})
+ {linkedControls.length > 0 ? (
+
+ {linkedControls.map(c => (
+ {c.name}
+ ))}
+
+ ) : (
+
Keine Kontrollen zugeordnet
+ )}
+
+
+
Status-Historie
+
+
+ {requirement.status === 'NOT_STARTED' ? 'Nicht begonnen' :
+ requirement.status === 'IN_PROGRESS' ? 'In Bearbeitung' :
+ requirement.status === 'IMPLEMENTED' ? 'Implementiert' : 'Verifiziert'}
+
+
+
+
+ )}
)
}
@@ -255,6 +392,8 @@ export default function RequirementsPage() {
const [searchQuery, setSearchQuery] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState
(null)
+ const [showAddForm, setShowAddForm] = useState(false)
+ const [expandedId, setExpandedId] = useState(null)
// Fetch requirements from backend on mount
useEffect(() => {
@@ -371,6 +510,22 @@ export default function RequirementsPage() {
}
}
+ const handleAddRequirement = (data: { regulation: string; article: string; title: string; description: string; criticality: RiskSeverity }) => {
+ const newReq: SDKRequirement = {
+ id: `req-${Date.now()}`,
+ regulation: data.regulation,
+ article: data.article,
+ title: data.title,
+ description: data.description,
+ criticality: data.criticality,
+ applicableModules: [],
+ status: 'NOT_STARTED',
+ controls: [],
+ }
+ dispatch({ type: 'ADD_REQUIREMENT', payload: newReq })
+ setShowAddForm(false)
+ }
+
const stepInfo = STEP_EXPLANATIONS['requirements']
return (
@@ -383,7 +538,10 @@ export default function RequirementsPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
-
+ setShowAddForm(true)}
+ className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
+ >
@@ -391,6 +549,14 @@ export default function RequirementsPage() {
+ {/* Add Form */}
+ {showAddForm && (
+ setShowAddForm(false)}
+ />
+ )}
+
{/* Error Banner */}
{error && (
@@ -476,13 +642,21 @@ export default function RequirementsPage() {
{/* Requirements List */}
{!loading && (
- {filteredRequirements.map(requirement => (
- handleStatusChange(requirement.id, status)}
- />
- ))}
+ {filteredRequirements.map(requirement => {
+ const linkedControls = state.controls
+ .filter(c => c.evidence.includes(requirement.id))
+ .map(c => ({ id: c.id, name: c.name }))
+ return (
+ handleStatusChange(requirement.id, status)}
+ expanded={expandedId === requirement.id}
+ onToggleDetails={() => setExpandedId(expandedId === requirement.id ? null : requirement.id)}
+ linkedControls={linkedControls}
+ />
+ )
+ })}
)}
diff --git a/admin-compliance/app/(sdk)/sdk/risks/page.tsx b/admin-compliance/app/(sdk)/sdk/risks/page.tsx
index c7990fc..e41cae1 100644
--- a/admin-compliance/app/(sdk)/sdk/risks/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/risks/page.tsx
@@ -387,6 +387,7 @@ export default function RisksPage() {
const [editingRisk, setEditingRisk] = useState
(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const [matrixFilter, setMatrixFilter] = useState<{ likelihood: number; impact: number } | null>(null)
// Fetch risks from backend on mount
useEffect(() => {
@@ -595,14 +596,43 @@ export default function RisksPage() {
{loading && }
{/* Matrix */}
- {!loading && {}} />}
+ {!loading && (
+ {
+ if (matrixFilter && matrixFilter.likelihood === l && matrixFilter.impact === i) {
+ setMatrixFilter(null)
+ } else {
+ setMatrixFilter({ likelihood: l, impact: i })
+ }
+ }}
+ />
+ )}
+
+ {/* Matrix Filter Badge */}
+ {matrixFilter && (
+
+
+ Gefiltert: L={matrixFilter.likelihood} I={matrixFilter.impact}
+ setMatrixFilter(null)}
+ className="text-purple-500 hover:text-purple-700 font-bold"
+ >
+ ×
+
+
+
+ )}
{/* Risk List */}
{!loading && state.risks.length > 0 && (
-
Alle Risiken
+
+ {matrixFilter ? `Risiken (L=${matrixFilter.likelihood}, I=${matrixFilter.impact})` : 'Alle Risiken'}
+
{state.risks
+ .filter(risk => !matrixFilter || (risk.likelihood === matrixFilter.likelihood && risk.impact === matrixFilter.impact))
.sort((a, b) => b.inherentRiskScore - a.inherentRiskScore)
.map(risk => (