From fc83ebfd82109bd702235caf8c92d35350ee6022 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 2 Mar 2026 13:13:26 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Analyse-Module=20auf=20100%=20Runde=202?= =?UTF-8?q?=20=E2=80=94=20CREATE-Forms,=20Button-Handler,=20Persistenz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requirements: ADD-Form + Details-Panel mit Controls/Status-Anzeige Controls: ADD-Form + Effectiveness-Persistenz via PUT Evidence: Anzeigen/Herunterladen-Buttons mit fileUrl + disabled-State Risks: RiskMatrix Cell-Click filtert Risiko-Liste mit Badge + Reset AI Act: Mock-Daten entfernt, Loading-Skeleton, Edit/Delete-Handler Audit Checklist: JSON-Export, debounced Notes-Persistenz, Neue Checkliste Audit Report: Animiertes Skeleton statt Loading-Text Co-Authored-By: Claude Opus 4.6 --- .../app/(sdk)/sdk/ai-act/page.tsx | 178 +++++++++------- .../app/(sdk)/sdk/audit-checklist/page.tsx | 90 +++++++- .../app/(sdk)/sdk/audit-report/page.tsx | 37 +++- .../app/(sdk)/sdk/controls/page.tsx | 136 +++++++++++- .../app/(sdk)/sdk/evidence/page.tsx | 34 ++- .../app/(sdk)/sdk/requirements/page.tsx | 194 +++++++++++++++++- admin-compliance/app/(sdk)/sdk/risks/page.tsx | 34 ++- 7 files changed, 607 insertions(+), 96 deletions(-) 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 && (
diff --git a/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx b/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx index 1881af1..57d645b 100644 --- a/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx @@ -1,6 +1,7 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef, useCallback } from 'react' +import { useRouter } from 'next/navigation' import { useSDK, ChecklistItem as SDKChecklistItem } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' @@ -141,10 +142,12 @@ function ChecklistItemCard({ item, onStatusChange, onNotesChange, + onAddEvidence, }: { item: DisplayChecklistItem onStatusChange: (status: DisplayStatus) => void onNotesChange: (notes: string) => void + onAddEvidence: () => void }) { const [showNotes, setShowNotes] = useState(false) @@ -225,7 +228,10 @@ function ChecklistItemCard({ > {showNotes ? 'Notizen ausblenden' : 'Notizen bearbeiten'} -
@@ -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" + /> +
+
+ +