diff --git a/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx b/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx index 0d1f255..0572ac7 100644 --- a/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/ai-act/page.tsx @@ -320,42 +320,126 @@ export default function AIActPage() { const [error, setError] = useState(null) const [loading, setLoading] = useState(true) - // Load systems from SDK state on mount + // Fetch systems from backend 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[]) + const fetchSystems = async () => { + setLoading(true) + try { + const res = await fetch('/api/sdk/v1/compliance/ai/systems') + if (res.ok) { + const data = await res.json() + const backendSystems = data.systems || [] + setSystems(backendSystems.map((s: Record) => ({ + id: s.id as string, + name: s.name as string, + description: (s.description || '') as string, + purpose: (s.purpose || '') as string, + sector: (s.sector || '') as string, + classification: (s.classification || 'unclassified') as AISystem['classification'], + status: (s.status || 'draft') as AISystem['status'], + obligations: (s.obligations || []) as string[], + assessmentDate: s.assessment_date ? new Date(s.assessment_date as string) : null, + assessmentResult: (s.assessment_result || null) as Record | null, + }))) + } + } catch { + // Backend unavailable — start with empty list + } finally { + setLoading(false) + } } - setLoading(false) - }, []) // eslint-disable-line react-hooks/exhaustive-deps + fetchSystems() + }, []) - const handleAddSystem = (data: Omit) => { + const handleAddSystem = async (data: Omit) => { + setError(null) if (editingSystem) { - // Edit existing system - setSystems(prev => prev.map(s => - s.id === editingSystem.id - ? { ...s, ...data } - : s - )) + // Edit existing system via PUT + try { + const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${editingSystem.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: data.name, + description: data.description, + purpose: data.purpose, + sector: data.sector, + classification: data.classification, + status: data.status, + obligations: data.obligations, + }), + }) + if (res.ok) { + const updated = await res.json() + setSystems(prev => prev.map(s => + s.id === editingSystem.id + ? { ...s, ...data, id: updated.id || editingSystem.id } + : s + )) + } else { + setError('Speichern fehlgeschlagen') + } + } catch { + // Fallback: update locally + 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, + // Create new system via POST + try { + const res = await fetch('/api/sdk/v1/compliance/ai/systems', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: data.name, + description: data.description, + purpose: data.purpose, + sector: data.sector, + classification: data.classification, + status: data.status, + obligations: data.obligations, + }), + }) + if (res.ok) { + const created = await res.json() + const newSystem: AISystem = { + ...data, + id: created.id, + assessmentDate: created.assessment_date ? new Date(created.assessment_date) : null, + assessmentResult: created.assessment_result || null, + } + setSystems(prev => [...prev, newSystem]) + } else { + setError('Registrierung fehlgeschlagen') + } + } catch { + // Fallback: add locally + 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) => { + const handleDelete = async (id: string) => { if (!confirm('Moechten Sie dieses KI-System wirklich loeschen?')) return - setSystems(prev => prev.filter(s => s.id !== id)) + try { + const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${id}`, { method: 'DELETE' }) + if (res.ok) { + setSystems(prev => prev.filter(s => s.id !== id)) + } else { + setError('Loeschen fehlgeschlagen') + } + } catch { + setError('Backend nicht erreichbar') + } } const handleEdit = (system: AISystem) => { @@ -364,37 +448,26 @@ export default function AIActPage() { } const handleAssess = async (systemId: string) => { - const system = systems.find(s => s.id === systemId) - if (!system) return - setAssessingId(systemId) setError(null) try { - const res = await fetch('/api/sdk/v1/compliance/ai/assess-risk', { + const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${systemId}/assess`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - system_name: system.name, - description: system.description, - purpose: system.purpose, - sector: system.sector, - current_classification: system.classification, - }), }) if (res.ok) { const result = await res.json() - // Update system with assessment result setSystems(prev => prev.map(s => s.id === systemId ? { ...s, - assessmentDate: new Date(), - assessmentResult: result, - classification: result.risk_level || result.classification || s.classification, - status: result.risk_level === 'high-risk' || result.classification === 'high-risk' ? 'non-compliant' : 'classified', + assessmentDate: result.assessment_date ? new Date(result.assessment_date) : new Date(), + assessmentResult: result.assessment_result || result, + classification: (result.classification || s.classification) as AISystem['classification'], + status: (result.status || 'classified') as AISystem['status'], obligations: result.obligations || s.obligations, } : s diff --git a/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx b/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx index 57d645b..033d538 100644 --- a/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/audit-checklist/page.tsx @@ -271,6 +271,18 @@ function LoadingSkeleton() { // MAIN PAGE // ============================================================================= +interface PastSession { + id: string + name: string + status: string + auditor_name: string + created_at: string + completed_at?: string + completion_percentage: number + total_items: number + completed_items: number +} + export default function AuditChecklistPage() { const { state, dispatch } = useSDK() const router = useRouter() @@ -279,6 +291,9 @@ export default function AuditChecklistPage() { const [error, setError] = useState(null) const [activeSessionId, setActiveSessionId] = useState(null) const notesTimerRef = useRef>>({}) + const [pastSessions, setPastSessions] = useState([]) + const [pdfLanguage, setPdfLanguage] = useState<'de' | 'en'>('de') + const [generatingPdf, setGeneratingPdf] = useState(false) // Fetch checklist from backend on mount useEffect(() => { @@ -354,6 +369,21 @@ export default function AuditChecklistPage() { } fetchChecklist() + + // Also fetch all sessions for history view + const fetchAllSessions = async () => { + try { + const res = await fetch('/api/sdk/v1/compliance/audit/sessions') + if (res.ok) { + const data = await res.json() + const sessions = data.sessions || [] + setPastSessions(sessions) + } + } catch { + // Silently fail + } + } + fetchAllSessions() }, []) // eslint-disable-line react-hooks/exhaustive-deps // Convert SDK checklist items to display items @@ -468,6 +498,32 @@ export default function AuditChecklistPage() { URL.revokeObjectURL(url) } + const handlePdfDownload = async () => { + if (!activeSessionId) { + setError('Kein aktives Audit vorhanden. Erstellen Sie zuerst eine Checkliste.') + return + } + setGeneratingPdf(true) + setError(null) + try { + const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${activeSessionId}/pdf?language=${pdfLanguage}`) + if (!res.ok) throw new Error('Fehler bei der PDF-Generierung') + const blob = await res.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `audit-checklist-${activeSessionId}.pdf` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + } catch (err) { + setError(err instanceof Error ? err.message : 'PDF-Download fehlgeschlagen') + } finally { + setGeneratingPdf(false) + } + } + const handleNewChecklist = async () => { try { setError(null) @@ -508,8 +564,25 @@ export default function AuditChecklistPage() { onClick={handleExport} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors" > - Exportieren + Export JSON +
+ + +
+
+
+

{session?.name || 'Audit Report'}

+ {session && ( + + {statusLabels[session.status] || session.status} + + )} +
+ {session && ( +
+ Auditor: {session.auditor_name} + {session.auditor_organization && | {session.auditor_organization}} + | Erstellt: {new Date(session.created_at).toLocaleDateString('de-DE')} +
+ )} +
+
+ + +
+ + + {/* Error */} + {error && ( +
+ {error} + +
+ )} + + {/* Progress */} + {session && ( +
+
+

Fortschritt

+ = 80 ? 'text-green-600' : + session.completion_percentage >= 50 ? 'text-yellow-600' : 'text-red-600' + }`}> + {session.completion_percentage}% + +
+
+
= 80 ? 'bg-green-500' : + session.completion_percentage >= 50 ? 'bg-yellow-500' : 'bg-red-500' + }`} + style={{ width: `${session.completion_percentage}%` }} + /> +
+
+
+
{compliantCount}
+
Konform
+
+
+
{nonCompliantCount}
+
Nicht konform
+
+
+
{pendingCount}
+
Ausstehend
+
+
+
+ )} + + {/* Checklist Items */} +
+

Pruefpunkte ({items.length})

+ {items.map((item) => ( + + ))} +
+ + {items.length === 0 && !loading && ( +
+

Keine Pruefpunkte

+

Diese Session hat noch keine Checklist-Items.

+
+ )} +
+ ) +} + +function ChecklistItemRow({ + item, + onSignOff, + readOnly, +}: { + item: ChecklistItem + onSignOff: (requirementId: string, status: string, notes: string) => void + readOnly: boolean +}) { + const [editing, setEditing] = useState(false) + const [notes, setNotes] = useState(item.auditor_notes || '') + + const statusColors: Record = { + compliant: 'border-green-200 bg-green-50', + non_compliant: 'border-red-200 bg-red-50', + 'non-compliant': 'border-red-200 bg-red-50', + partially_compliant: 'border-yellow-200 bg-yellow-50', + not_assessed: 'border-gray-200 bg-gray-50', + pending: 'border-gray-200 bg-gray-50', + } + + const statusLabels: Record = { + compliant: 'Konform', + non_compliant: 'Nicht konform', + 'non-compliant': 'Nicht konform', + partially_compliant: 'Teilweise', + not_assessed: 'Nicht geprueft', + pending: 'Ausstehend', + } + + return ( +
+
+
+
+ {item.article && ( + {item.article} + )} + + {statusLabels[item.status] || item.status} + +
+

{item.title}

+ {item.auditor_notes && !editing && ( +

{item.auditor_notes}

+ )} + {item.signed_off_by && ( +

+ Geprueft von {item.signed_off_by} + {item.signed_off_at && ` am ${new Date(item.signed_off_at).toLocaleDateString('de-DE')}`} +

+ )} +
+ {!readOnly && ( +
+ + +
+ )} +
+ {editing && ( +
+