From 8044ddb776cbb63755ab2f71fc244b75e39f136b Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:13:38 +0200 Subject: [PATCH] refactor(admin): split modules, security-backlog, consent pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract components and hooks to _components/ and _hooks/ subdirectories to bring all three page.tsx files under the 500-LOC hard cap. modules/page.tsx: 595 → 239 LOC security-backlog/page.tsx: 586 → 174 LOC consent/page.tsx: 569 → 305 LOC Co-Authored-By: Claude Sonnet 4.6 --- .../sdk/consent/_components/DocumentCard.tsx | 117 +++++ .../sdk/consent/_hooks/useConsentDocuments.ts | 197 ++++++++ admin-compliance/app/sdk/consent/page.tsx | 334 ++----------- .../sdk/modules/_components/ModuleCard.tsx | 119 +++++ .../app/sdk/modules/_hooks/useModules.ts | 283 +++++++++++ admin-compliance/app/sdk/modules/page.tsx | 400 +-------------- .../_components/ItemModal.tsx | 106 ++++ .../_components/SecurityItemCard.tsx | 166 +++++++ .../_hooks/useSecurityBacklog.ts | 179 +++++++ .../app/sdk/security-backlog/page.tsx | 462 +----------------- 10 files changed, 1249 insertions(+), 1114 deletions(-) create mode 100644 admin-compliance/app/sdk/consent/_components/DocumentCard.tsx create mode 100644 admin-compliance/app/sdk/consent/_hooks/useConsentDocuments.ts create mode 100644 admin-compliance/app/sdk/modules/_components/ModuleCard.tsx create mode 100644 admin-compliance/app/sdk/modules/_hooks/useModules.ts create mode 100644 admin-compliance/app/sdk/security-backlog/_components/ItemModal.tsx create mode 100644 admin-compliance/app/sdk/security-backlog/_components/SecurityItemCard.tsx create mode 100644 admin-compliance/app/sdk/security-backlog/_hooks/useSecurityBacklog.ts diff --git a/admin-compliance/app/sdk/consent/_components/DocumentCard.tsx b/admin-compliance/app/sdk/consent/_components/DocumentCard.tsx new file mode 100644 index 0000000..2461241 --- /dev/null +++ b/admin-compliance/app/sdk/consent/_components/DocumentCard.tsx @@ -0,0 +1,117 @@ +'use client' + +import React from 'react' +import type { LegalDocument } from '../_hooks/useConsentDocuments' + +export function DocumentCard({ + document, + onDelete, + onEdit, + onPreview, +}: { + document: LegalDocument + onDelete: (id: string) => void + onEdit: (id: string) => void + onPreview: (doc: LegalDocument) => void +}) { + const typeColors = { + 'privacy-policy': 'bg-blue-100 text-blue-700', + terms: 'bg-green-100 text-green-700', + 'cookie-policy': 'bg-yellow-100 text-yellow-700', + imprint: 'bg-gray-100 text-gray-700', + dpa: 'bg-purple-100 text-purple-700', + } + + const typeLabels = { + 'privacy-policy': 'Datenschutz', + terms: 'AGB', + 'cookie-policy': 'Cookie-Richtlinie', + imprint: 'Impressum', + dpa: 'AVV', + } + + const statusColors = { + draft: 'bg-yellow-100 text-yellow-700', + active: 'bg-green-100 text-green-700', + archived: 'bg-gray-100 text-gray-500', + } + + const statusLabels = { + draft: 'Entwurf', + active: 'Aktiv', + archived: 'Archiviert', + } + + return ( +
+
+
+
+ + {typeLabels[document.type]} + + + {statusLabels[document.status]} + + + {document.language} + + + v{document.version} + +
+

{document.name}

+
+
+ + {document.changes.length > 0 && ( +
+ Letzte Aenderungen: +
    + {document.changes.slice(0, 2).map((change, i) => ( +
  • {change}
  • + ))} +
+
+ )} + +
+
+ Autor: {document.author} + | + Aktualisiert: {document.lastUpdated.toLocaleDateString('de-DE')} +
+
+ + + {document.status === 'draft' && ( + + )} + +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/consent/_hooks/useConsentDocuments.ts b/admin-compliance/app/sdk/consent/_hooks/useConsentDocuments.ts new file mode 100644 index 0000000..ba8d9cf --- /dev/null +++ b/admin-compliance/app/sdk/consent/_hooks/useConsentDocuments.ts @@ -0,0 +1,197 @@ +'use client' + +import { useState, useEffect } from 'react' + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface LegalDocument { + id: string + type: 'privacy-policy' | 'terms' | 'cookie-policy' | 'imprint' | 'dpa' + name: string + version: string + language: string + status: 'draft' | 'active' | 'archived' + lastUpdated: Date + publishedAt: Date | null + author: string + changes: string[] +} + +interface ApiDocument { + id: string + type: string + name: string + description: string + mandatory: boolean + created_at: string + updated_at: string +} + +// ============================================================================= +// HELPERS +// ============================================================================= + +function mapDocumentType(apiType: string): LegalDocument['type'] { + const mapping: Record = { + 'privacy_policy': 'privacy-policy', + 'privacy-policy': 'privacy-policy', + 'terms': 'terms', + 'terms_of_service': 'terms', + 'cookie_policy': 'cookie-policy', + 'cookie-policy': 'cookie-policy', + 'imprint': 'imprint', + 'dpa': 'dpa', + 'avv': 'dpa', + } + return mapping[apiType] || 'terms' +} + +function transformApiDocument(doc: ApiDocument): LegalDocument { + return { + id: doc.id, + type: mapDocumentType(doc.type), + name: doc.name, + version: '1.0', + language: 'de', + status: 'active', + lastUpdated: new Date(doc.updated_at), + publishedAt: new Date(doc.created_at), + author: 'System', + changes: doc.description ? [doc.description] : [], + } +} + +// ============================================================================= +// HOOK +// ============================================================================= + +export function useConsentDocuments() { + const [documents, setDocuments] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [previewDoc, setPreviewDoc] = useState<{ name: string; content: string } | null>(null) + + useEffect(() => { + loadDocuments() + }, []) + + async function loadDocuments() { + setLoading(true) + setError(null) + try { + const token = localStorage.getItem('bp_admin_token') + const res = await fetch('/api/admin/consent/documents', { + headers: token ? { 'Authorization': `Bearer ${token}` } : {} + }) + if (res.ok) { + const data = await res.json() + const apiDocs: ApiDocument[] = data.documents || [] + setDocuments(apiDocs.map(transformApiDocument)) + } else { + setError('Fehler beim Laden der Dokumente') + } + } catch { + setError('Verbindungsfehler zum Server') + } finally { + setLoading(false) + } + } + + async function handleRagSuggest(docType: string, docName: string): Promise { + const query = docType || docName || 'Datenschutzerklärung' + try { + const res = await fetch('/api/sdk/v1/rag/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: `${query} DSGVO Vorlage`, limit: 1 }), + }) + if (res.ok) { + const data = await res.json() + const first = data.results?.[0] + if (first?.text) return first.text + } + } catch { /* silently ignore */ } + return null + } + + async function handleCreateDocument(form: { + type: string + name: string + description: string + content: string + }): Promise { + if (!form.name.trim()) return false + try { + const token = localStorage.getItem('bp_admin_token') + const res = await fetch('/api/admin/consent/documents', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + }, + body: JSON.stringify(form), + }) + if (res.ok) { + await loadDocuments() + return true + } else { + setError('Fehler beim Erstellen des Dokuments') + } + } catch { + setError('Verbindungsfehler beim Erstellen') + } + return false + } + + async function handleDeleteDocument(id: string) { + if (!confirm('Dokument wirklich löschen?')) return + try { + const token = localStorage.getItem('bp_admin_token') + const res = await fetch(`/api/admin/consent/documents/${id}`, { + method: 'DELETE', + headers: token ? { 'Authorization': `Bearer ${token}` } : {}, + }) + if (res.ok || res.status === 204) { + setDocuments(prev => prev.filter(d => d.id !== id)) + } else { + setError('Fehler beim Löschen des Dokuments') + } + } catch { + setError('Verbindungsfehler beim Löschen') + } + } + + async function handlePreview(doc: LegalDocument) { + try { + const token = localStorage.getItem('bp_admin_token') + const res = await fetch(`/api/admin/consent/documents/${doc.id}/versions`, { + headers: token ? { 'Authorization': `Bearer ${token}` } : {} + }) + if (res.ok) { + const data = await res.json() + const versions = Array.isArray(data) ? data : (data.versions || []) + const published = versions.find((v: { status: string; content?: string }) => v.status === 'published') + const latest = published || versions[0] + setPreviewDoc({ name: doc.name, content: latest?.content || '

Kein Inhalt verfügbar.

' }) + } else { + setPreviewDoc({ name: doc.name, content: '

Vorschau nicht verfügbar.

' }) + } + } catch { + setPreviewDoc({ name: doc.name, content: '

Fehler beim Laden der Vorschau.

' }) + } + } + + return { + documents, + loading, + error, + previewDoc, + setPreviewDoc, + handleRagSuggest, + handleCreateDocument, + handleDeleteDocument, + handlePreview, + } +} diff --git a/admin-compliance/app/sdk/consent/page.tsx b/admin-compliance/app/sdk/consent/page.tsx index 0baecfe..2e44d4a 100644 --- a/admin-compliance/app/sdk/consent/page.tsx +++ b/admin-compliance/app/sdk/consent/page.tsx @@ -1,312 +1,29 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { useRouter } from 'next/navigation' -import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' - -// ============================================================================= -// TYPES -// ============================================================================= - -interface LegalDocument { - id: string - type: 'privacy-policy' | 'terms' | 'cookie-policy' | 'imprint' | 'dpa' - name: string - version: string - language: string - status: 'draft' | 'active' | 'archived' - lastUpdated: Date - publishedAt: Date | null - author: string - changes: string[] -} - -interface ApiDocument { - id: string - type: string - name: string - description: string - mandatory: boolean - created_at: string - updated_at: string -} - -// Map API document type to UI type -function mapDocumentType(apiType: string): LegalDocument['type'] { - const mapping: Record = { - 'privacy_policy': 'privacy-policy', - 'privacy-policy': 'privacy-policy', - 'terms': 'terms', - 'terms_of_service': 'terms', - 'cookie_policy': 'cookie-policy', - 'cookie-policy': 'cookie-policy', - 'imprint': 'imprint', - 'dpa': 'dpa', - 'avv': 'dpa', - } - return mapping[apiType] || 'terms' -} - -// Transform API response to UI format -function transformApiDocument(doc: ApiDocument): LegalDocument { - return { - id: doc.id, - type: mapDocumentType(doc.type), - name: doc.name, - version: '1.0', - language: 'de', - status: 'active', - lastUpdated: new Date(doc.updated_at), - publishedAt: new Date(doc.created_at), - author: 'System', - changes: doc.description ? [doc.description] : [], - } -} - -// ============================================================================= -// COMPONENTS -// ============================================================================= - -function DocumentCard({ - document, - onDelete, - onEdit, - onPreview, -}: { - document: LegalDocument - onDelete: (id: string) => void - onEdit: (id: string) => void - onPreview: (doc: LegalDocument) => void -}) { - const typeColors = { - 'privacy-policy': 'bg-blue-100 text-blue-700', - terms: 'bg-green-100 text-green-700', - 'cookie-policy': 'bg-yellow-100 text-yellow-700', - imprint: 'bg-gray-100 text-gray-700', - dpa: 'bg-purple-100 text-purple-700', - } - - const typeLabels = { - 'privacy-policy': 'Datenschutz', - terms: 'AGB', - 'cookie-policy': 'Cookie-Richtlinie', - imprint: 'Impressum', - dpa: 'AVV', - } - - const statusColors = { - draft: 'bg-yellow-100 text-yellow-700', - active: 'bg-green-100 text-green-700', - archived: 'bg-gray-100 text-gray-500', - } - - const statusLabels = { - draft: 'Entwurf', - active: 'Aktiv', - archived: 'Archiviert', - } - - return ( -
-
-
-
- - {typeLabels[document.type]} - - - {statusLabels[document.status]} - - - {document.language} - - - v{document.version} - -
-

{document.name}

-
-
- - {document.changes.length > 0 && ( -
- Letzte Aenderungen: -
    - {document.changes.slice(0, 2).map((change, i) => ( -
  • {change}
  • - ))} -
-
- )} - -
-
- Autor: {document.author} - | - Aktualisiert: {document.lastUpdated.toLocaleDateString('de-DE')} -
-
- - - {document.status === 'draft' && ( - - )} - -
-
-
- ) -} - -// ============================================================================= -// MAIN PAGE -// ============================================================================= +import { DocumentCard } from './_components/DocumentCard' +import { useConsentDocuments } from './_hooks/useConsentDocuments' export default function ConsentPage() { - const { state } = useSDK() const router = useRouter() - const [documents, setDocuments] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) const [filter, setFilter] = useState('all') const [showCreateModal, setShowCreateModal] = useState(false) const [newDocForm, setNewDocForm] = useState({ type: 'privacy_policy', name: '', description: '', content: '' }) const [creating, setCreating] = useState(false) - const [previewDoc, setPreviewDoc] = useState<{ name: string; content: string } | null>(null) - useEffect(() => { - loadDocuments() - }, []) - - async function loadDocuments() { - setLoading(true) - setError(null) - try { - const token = localStorage.getItem('bp_admin_token') - const res = await fetch('/api/admin/consent/documents', { - headers: token ? { 'Authorization': `Bearer ${token}` } : {} - }) - if (res.ok) { - const data = await res.json() - const apiDocs: ApiDocument[] = data.documents || [] - setDocuments(apiDocs.map(transformApiDocument)) - } else { - setError('Fehler beim Laden der Dokumente') - } - } catch { - setError('Verbindungsfehler zum Server') - } finally { - setLoading(false) - } - } - - async function handleRagSuggest() { - const docType = newDocForm.type || newDocForm.name || 'Datenschutzerklärung' - try { - const res = await fetch('/api/sdk/v1/rag/search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: `${docType} DSGVO Vorlage`, limit: 1 }), - }) - if (res.ok) { - const data = await res.json() - const first = data.results?.[0] - if (first?.text) setNewDocForm(d => ({ ...d, content: first.text })) - } - } catch { /* silently ignore */ } - } - - async function handleCreateDocument() { - if (!newDocForm.name.trim()) return - setCreating(true) - try { - const token = localStorage.getItem('bp_admin_token') - const res = await fetch('/api/admin/consent/documents', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(token ? { 'Authorization': `Bearer ${token}` } : {}), - }, - body: JSON.stringify(newDocForm), - }) - if (res.ok) { - setShowCreateModal(false) - setNewDocForm({ type: 'privacy_policy', name: '', description: '', content: '' }) - await loadDocuments() - } else { - setError('Fehler beim Erstellen des Dokuments') - } - } catch { - setError('Verbindungsfehler beim Erstellen') - } finally { - setCreating(false) - } - } - - async function handleDeleteDocument(id: string) { - if (!confirm('Dokument wirklich löschen?')) return - try { - const token = localStorage.getItem('bp_admin_token') - const res = await fetch(`/api/admin/consent/documents/${id}`, { - method: 'DELETE', - headers: token ? { 'Authorization': `Bearer ${token}` } : {}, - }) - if (res.ok || res.status === 204) { - setDocuments(prev => prev.filter(d => d.id !== id)) - } else { - setError('Fehler beim Löschen des Dokuments') - } - } catch { - setError('Verbindungsfehler beim Löschen') - } - } - - function handleEdit(id: string) { - router.push('/sdk/workflow') - } - - async function handlePreview(doc: LegalDocument) { - try { - const token = localStorage.getItem('bp_admin_token') - const res = await fetch(`/api/admin/consent/documents/${doc.id}/versions`, { - headers: token ? { 'Authorization': `Bearer ${token}` } : {} - }) - if (res.ok) { - const data = await res.json() - const versions = Array.isArray(data) ? data : (data.versions || []) - const published = versions.find((v: { status: string; content?: string }) => v.status === 'published') - const latest = published || versions[0] - setPreviewDoc({ name: doc.name, content: latest?.content || '

Kein Inhalt verfügbar.

' }) - } else { - setPreviewDoc({ name: doc.name, content: '

Vorschau nicht verfügbar.

' }) - } - } catch { - setPreviewDoc({ name: doc.name, content: '

Fehler beim Laden der Vorschau.

' }) - } - } + const { + documents, + loading, + error, + previewDoc, + setPreviewDoc, + handleRagSuggest, + handleCreateDocument, + handleDeleteDocument, + handlePreview, + } = useConsentDocuments() const filteredDocuments = filter === 'all' ? documents @@ -315,6 +32,25 @@ export default function ConsentPage() { const activeCount = documents.filter(d => d.status === 'active').length const draftCount = documents.filter(d => d.status === 'draft').length + async function onRagSuggest() { + const text = await handleRagSuggest(newDocForm.type, newDocForm.name) + if (text) setNewDocForm(d => ({ ...d, content: text })) + } + + async function onCreateDocument() { + setCreating(true) + const ok = await handleCreateDocument(newDocForm) + if (ok) { + setShowCreateModal(false) + setNewDocForm({ type: 'privacy_policy', name: '', description: '', content: '' }) + } + setCreating(false) + } + + function handleEdit(id: string) { + router.push('/sdk/workflow') + } + const stepInfo = STEP_EXPLANATIONS['consent'] return ( @@ -531,7 +267,7 @@ export default function ConsentPage() { + + + ) : ( + + )} + + + ) +} diff --git a/admin-compliance/app/sdk/modules/_hooks/useModules.ts b/admin-compliance/app/sdk/modules/_hooks/useModules.ts new file mode 100644 index 0000000..e10ced1 --- /dev/null +++ b/admin-compliance/app/sdk/modules/_hooks/useModules.ts @@ -0,0 +1,283 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useSDK, ServiceModule } from '@/lib/sdk' + +// ============================================================================= +// TYPES +// ============================================================================= + +export type ModuleCategory = 'gdpr' | 'ai-act' | 'iso27001' | 'nis2' | 'custom' +export type ModuleStatus = 'active' | 'inactive' | 'pending' + +export interface DisplayModule extends ServiceModule { + category: ModuleCategory + status: ModuleStatus + requirementsCount: number + controlsCount: number + completionPercent: number +} + +interface BackendModule { + id: string + name: string + display_name: string + description: string + service_type: string | null + processes_pii: boolean + ai_components: boolean + criticality: string + is_active: boolean + compliance_score: number | null + regulation_count: number + risk_count: number +} + +// ============================================================================= +// FALLBACK MODULES +// ============================================================================= + +const fallbackModules: Omit[] = [ + { + id: 'mod-gdpr', + name: 'DSGVO Compliance', + description: 'Datenschutz-Grundverordnung - Vollstaendige Umsetzung aller Anforderungen', + category: 'gdpr', + regulations: ['DSGVO', 'BDSG'], + criticality: 'HIGH', + processesPersonalData: true, + hasAIComponents: false, + requirementsCount: 45, + controlsCount: 32, + }, + { + id: 'mod-ai-act', + name: 'AI Act Compliance', + description: 'EU AI Act - Klassifizierung und Anforderungen fuer KI-Systeme', + category: 'ai-act', + regulations: ['EU AI Act'], + criticality: 'HIGH', + processesPersonalData: false, + hasAIComponents: true, + requirementsCount: 28, + controlsCount: 18, + }, + { + id: 'mod-iso27001', + name: 'ISO 27001', + description: 'Informationssicherheits-Managementsystem nach ISO/IEC 27001', + category: 'iso27001', + regulations: ['ISO 27001', 'ISO 27002'], + criticality: 'MEDIUM', + processesPersonalData: false, + hasAIComponents: false, + requirementsCount: 114, + controlsCount: 93, + }, + { + id: 'mod-nis2', + name: 'NIS2 Richtlinie', + description: 'Netz- und Informationssicherheit fuer kritische Infrastrukturen', + category: 'nis2', + regulations: ['NIS2'], + criticality: 'HIGH', + processesPersonalData: false, + hasAIComponents: false, + requirementsCount: 36, + controlsCount: 24, + }, +] + +// ============================================================================= +// HELPERS +// ============================================================================= + +function categorizeModule(name: string): ModuleCategory { + const lower = name.toLowerCase() + if (lower.includes('dsgvo') || lower.includes('gdpr') || lower.includes('datenschutz')) return 'gdpr' + if (lower.includes('ai act') || lower.includes('ki-verordnung')) return 'ai-act' + if (lower.includes('iso 27001') || lower.includes('iso27001') || lower.includes('isms')) return 'iso27001' + if (lower.includes('nis2') || lower.includes('netz- und informations')) return 'nis2' + return 'custom' +} + +function mapBackendToDisplay(m: BackendModule): Omit { + return { + id: m.id, + name: m.display_name || m.name, + description: m.description || '', + category: categorizeModule(m.display_name || m.name), + regulations: [], + criticality: (m.criticality || 'MEDIUM').toUpperCase(), + processesPersonalData: m.processes_pii, + hasAIComponents: m.ai_components, + requirementsCount: m.regulation_count || 0, + controlsCount: m.risk_count || 0, + } +} + +// ============================================================================= +// HOOK +// ============================================================================= + +export function useModules() { + const { state, dispatch } = useSDK() + const [availableModules, setAvailableModules] = useState[]>(fallbackModules) + const [isLoadingModules, setIsLoadingModules] = useState(true) + const [backendError, setBackendError] = useState(null) + const [actionError, setActionError] = useState(null) + + useEffect(() => { + async function loadModules() { + try { + const response = await fetch('/api/sdk/v1/modules') + if (response.ok) { + const data = await response.json() + if (data.modules && data.modules.length > 0) { + const mapped = data.modules.map(mapBackendToDisplay) + setAvailableModules(mapped) + setBackendError(null) + } + } else { + setBackendError('Backend nicht erreichbar — zeige Standard-Module') + } + } catch { + setBackendError('Backend nicht erreichbar — zeige Standard-Module') + } finally { + setIsLoadingModules(false) + } + } + loadModules() + }, []) + + const displayModules: DisplayModule[] = availableModules.map(template => { + const activeModule = state.modules.find(m => m.id === template.id) + const isActive = !!activeModule + + const linkedRequirements = state.requirements.filter(r => + r.applicableModules.includes(template.id) + ) + const completedRequirements = linkedRequirements.filter( + r => r.status === 'IMPLEMENTED' || r.status === 'VERIFIED' + ) + const completionPercent = linkedRequirements.length > 0 + ? Math.round((completedRequirements.length / linkedRequirements.length) * 100) + : 0 + + return { + ...template, + status: isActive ? 'active' as ModuleStatus : 'inactive' as ModuleStatus, + completionPercent, + } + }) + + const activeModulesCount = state.modules.length + const totalRequirements = displayModules + .filter(m => state.modules.some(sm => sm.id === m.id)) + .reduce((sum, m) => sum + m.requirementsCount, 0) + const totalControls = displayModules + .filter(m => state.modules.some(sm => sm.id === m.id)) + .reduce((sum, m) => sum + m.controlsCount, 0) + + async function handleActivateModule(module: DisplayModule) { + const serviceModule: ServiceModule = { + id: module.id, + name: module.name, + description: module.description, + regulations: module.regulations, + criticality: module.criticality, + processesPersonalData: module.processesPersonalData, + hasAIComponents: module.hasAIComponents, + } + dispatch({ type: 'ADD_MODULE', payload: serviceModule }) + setActionError(null) + + try { + const res = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(module.id)}/activate`, { + method: 'POST', + }) + if (!res.ok) throw new Error('Aktivierung fehlgeschlagen') + } catch { + const rollbackModules = state.modules.filter(m => m.id !== module.id) + dispatch({ type: 'SET_STATE', payload: { modules: rollbackModules } }) + setActionError(`Modul "${module.name}" konnte nicht aktiviert werden.`) + setTimeout(() => setActionError(null), 5000) + } + } + + async function handleDeactivateModule(moduleId: string) { + const previousModules = [...state.modules] + const updatedModules = state.modules.filter(m => m.id !== moduleId) + dispatch({ type: 'SET_STATE', payload: { modules: updatedModules } }) + setActionError(null) + + try { + const res = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(moduleId)}/deactivate`, { + method: 'POST', + }) + if (!res.ok) throw new Error('Deaktivierung fehlgeschlagen') + } catch { + dispatch({ type: 'SET_STATE', payload: { modules: previousModules } }) + setActionError('Modul konnte nicht deaktiviert werden.') + setTimeout(() => setActionError(null), 5000) + } + } + + async function handleCreateModule( + newModuleName: string, + newModuleCategory: ModuleCategory, + newModuleDescription: string, + ): Promise { + if (!newModuleName.trim()) return false + + try { + const res = await fetch('/api/sdk/v1/modules', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: newModuleName, + category: newModuleCategory, + description: newModuleDescription, + }), + }) + + if (res.ok) { + const data = await res.json() + const newMod: Omit = { + id: data.id || `custom-${Date.now()}`, + name: newModuleName, + description: newModuleDescription, + category: newModuleCategory, + regulations: [], + criticality: 'MEDIUM', + processesPersonalData: false, + hasAIComponents: false, + requirementsCount: 0, + controlsCount: 0, + } + setAvailableModules(prev => [...prev, newMod]) + return true + } + return false + } catch { + setActionError('Modul konnte nicht erstellt werden.') + setTimeout(() => setActionError(null), 5000) + return false + } + } + + return { + state, + availableModules, + displayModules, + isLoadingModules, + backendError, + actionError, + activeModulesCount, + totalRequirements, + totalControls, + handleActivateModule, + handleDeactivateModule, + handleCreateModule, + } +} diff --git a/admin-compliance/app/sdk/modules/page.tsx b/admin-compliance/app/sdk/modules/page.tsx index 337d7b2..aff9c22 100644 --- a/admin-compliance/app/sdk/modules/page.tsx +++ b/admin-compliance/app/sdk/modules/page.tsx @@ -1,402 +1,46 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { useRouter } from 'next/navigation' -import { useSDK, ServiceModule } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' - -// ============================================================================= -// TYPES -// ============================================================================= - -type ModuleCategory = 'gdpr' | 'ai-act' | 'iso27001' | 'nis2' | 'custom' -type ModuleStatus = 'active' | 'inactive' | 'pending' - -interface DisplayModule extends ServiceModule { - category: ModuleCategory - status: ModuleStatus - requirementsCount: number - controlsCount: number - completionPercent: number -} - -interface BackendModule { - id: string - name: string - display_name: string - description: string - service_type: string | null - processes_pii: boolean - ai_components: boolean - criticality: string - is_active: boolean - compliance_score: number | null - regulation_count: number - risk_count: number -} - -// ============================================================================= -// FALLBACK MODULES (used when backend is unavailable) -// ============================================================================= - -const fallbackModules: Omit[] = [ - { - id: 'mod-gdpr', - name: 'DSGVO Compliance', - description: 'Datenschutz-Grundverordnung - Vollstaendige Umsetzung aller Anforderungen', - category: 'gdpr', - regulations: ['DSGVO', 'BDSG'], - criticality: 'HIGH', - processesPersonalData: true, - hasAIComponents: false, - requirementsCount: 45, - controlsCount: 32, - }, - { - id: 'mod-ai-act', - name: 'AI Act Compliance', - description: 'EU AI Act - Klassifizierung und Anforderungen fuer KI-Systeme', - category: 'ai-act', - regulations: ['EU AI Act'], - criticality: 'HIGH', - processesPersonalData: false, - hasAIComponents: true, - requirementsCount: 28, - controlsCount: 18, - }, - { - id: 'mod-iso27001', - name: 'ISO 27001', - description: 'Informationssicherheits-Managementsystem nach ISO/IEC 27001', - category: 'iso27001', - regulations: ['ISO 27001', 'ISO 27002'], - criticality: 'MEDIUM', - processesPersonalData: false, - hasAIComponents: false, - requirementsCount: 114, - controlsCount: 93, - }, - { - id: 'mod-nis2', - name: 'NIS2 Richtlinie', - description: 'Netz- und Informationssicherheit fuer kritische Infrastrukturen', - category: 'nis2', - regulations: ['NIS2'], - criticality: 'HIGH', - processesPersonalData: false, - hasAIComponents: false, - requirementsCount: 36, - controlsCount: 24, - }, -] - -// ============================================================================= -// HELPERS -// ============================================================================= - -function categorizeModule(name: string): ModuleCategory { - const lower = name.toLowerCase() - if (lower.includes('dsgvo') || lower.includes('gdpr') || lower.includes('datenschutz')) return 'gdpr' - if (lower.includes('ai act') || lower.includes('ki-verordnung')) return 'ai-act' - if (lower.includes('iso 27001') || lower.includes('iso27001') || lower.includes('isms')) return 'iso27001' - if (lower.includes('nis2') || lower.includes('netz- und informations')) return 'nis2' - return 'custom' -} - -function mapBackendToDisplay(m: BackendModule): Omit { - return { - id: m.id, - name: m.display_name || m.name, - description: m.description || '', - category: categorizeModule(m.display_name || m.name), - regulations: [], - criticality: (m.criticality || 'MEDIUM').toUpperCase(), - processesPersonalData: m.processes_pii, - hasAIComponents: m.ai_components, - requirementsCount: m.regulation_count || 0, - controlsCount: m.risk_count || 0, - } -} - -// ============================================================================= -// COMPONENTS -// ============================================================================= - -function ModuleCard({ - module, - isActive, - onActivate, - onDeactivate, - onConfigure, -}: { - module: DisplayModule - isActive: boolean - onActivate: () => void - onDeactivate: () => void - onConfigure: () => void -}) { - const categoryColors = { - gdpr: 'bg-blue-100 text-blue-700', - 'ai-act': 'bg-purple-100 text-purple-700', - iso27001: 'bg-green-100 text-green-700', - nis2: 'bg-orange-100 text-orange-700', - custom: 'bg-gray-100 text-gray-700', - } - - const statusColors = { - active: 'bg-green-100 text-green-700', - inactive: 'bg-gray-100 text-gray-500', - pending: 'bg-yellow-100 text-yellow-700', - } - - return ( -
-
-
-
- - {module.category.toUpperCase()} - - - {module.status === 'active' ? 'Aktiv' : module.status === 'pending' ? 'Ausstehend' : 'Inaktiv'} - - {module.hasAIComponents && ( - - KI - - )} -
-

{module.name}

-

{module.description}

- {module.regulations.length > 0 && ( -
- {module.regulations.map(reg => ( - - {reg} - - ))} -
- )} -
-
- -
-
- Anforderungen: - {module.requirementsCount} -
-
- Kontrollen: - {module.controlsCount} -
-
- -
-
- Fortschritt - {module.completionPercent}% -
-
-
-
-
- -
- {isActive ? ( - <> - - - - ) : ( - - )} -
-
- ) -} - -// ============================================================================= -// MAIN PAGE -// ============================================================================= +import { ModuleCard } from './_components/ModuleCard' +import { useModules } from './_hooks/useModules' +import type { ModuleCategory } from './_hooks/useModules' export default function ModulesPage() { - const { state, dispatch } = useSDK() const router = useRouter() const [filter, setFilter] = useState('all') - const [availableModules, setAvailableModules] = useState[]>(fallbackModules) - const [isLoadingModules, setIsLoadingModules] = useState(true) - const [backendError, setBackendError] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [newModuleName, setNewModuleName] = useState('') const [newModuleCategory, setNewModuleCategory] = useState('custom') const [newModuleDescription, setNewModuleDescription] = useState('') - const [actionError, setActionError] = useState(null) - // Load modules from backend - useEffect(() => { - async function loadModules() { - try { - const response = await fetch('/api/sdk/v1/modules') - if (response.ok) { - const data = await response.json() - if (data.modules && data.modules.length > 0) { - const mapped = data.modules.map(mapBackendToDisplay) - setAvailableModules(mapped) - setBackendError(null) - } - } else { - setBackendError('Backend nicht erreichbar — zeige Standard-Module') - } - } catch { - setBackendError('Backend nicht erreichbar — zeige Standard-Module') - } finally { - setIsLoadingModules(false) - } - } - loadModules() - }, []) - - // Convert SDK modules to display modules with additional UI properties - const displayModules: DisplayModule[] = availableModules.map(template => { - const activeModule = state.modules.find(m => m.id === template.id) - const isActive = !!activeModule - - // Calculate completion based on linked requirements and controls - const linkedRequirements = state.requirements.filter(r => - r.applicableModules.includes(template.id) - ) - const completedRequirements = linkedRequirements.filter( - r => r.status === 'IMPLEMENTED' || r.status === 'VERIFIED' - ) - const completionPercent = linkedRequirements.length > 0 - ? Math.round((completedRequirements.length / linkedRequirements.length) * 100) - : 0 - - return { - ...template, - status: isActive ? 'active' as ModuleStatus : 'inactive' as ModuleStatus, - completionPercent, - } - }) + const { + state, + availableModules, + displayModules, + isLoadingModules, + backendError, + actionError, + activeModulesCount, + totalRequirements, + totalControls, + handleActivateModule, + handleDeactivateModule, + handleCreateModule, + } = useModules() const filteredModules = filter === 'all' ? displayModules : displayModules.filter(m => m.category === filter || m.status === filter) - const activeModulesCount = state.modules.length - const totalRequirements = displayModules - .filter(m => state.modules.some(sm => sm.id === m.id)) - .reduce((sum, m) => sum + m.requirementsCount, 0) - const totalControls = displayModules - .filter(m => state.modules.some(sm => sm.id === m.id)) - .reduce((sum, m) => sum + m.controlsCount, 0) - - const handleActivateModule = async (module: DisplayModule) => { - const serviceModule: ServiceModule = { - id: module.id, - name: module.name, - description: module.description, - regulations: module.regulations, - criticality: module.criticality, - processesPersonalData: module.processesPersonalData, - hasAIComponents: module.hasAIComponents, - } - dispatch({ type: 'ADD_MODULE', payload: serviceModule }) - setActionError(null) - - try { - const res = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(module.id)}/activate`, { - method: 'POST', - }) - if (!res.ok) throw new Error('Aktivierung fehlgeschlagen') - } catch { - // Rollback optimistic update - const rollbackModules = state.modules.filter(m => m.id !== module.id) - dispatch({ type: 'SET_STATE', payload: { modules: rollbackModules } }) - setActionError(`Modul "${module.name}" konnte nicht aktiviert werden.`) - setTimeout(() => setActionError(null), 5000) - } - } - - const handleDeactivateModule = async (moduleId: string) => { - const previousModules = [...state.modules] - const updatedModules = state.modules.filter(m => m.id !== moduleId) - dispatch({ type: 'SET_STATE', payload: { modules: updatedModules } }) - setActionError(null) - - try { - const res = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(moduleId)}/deactivate`, { - method: 'POST', - }) - if (!res.ok) throw new Error('Deaktivierung fehlgeschlagen') - } catch { - // Rollback optimistic update - dispatch({ type: 'SET_STATE', payload: { modules: previousModules } }) - setActionError('Modul konnte nicht deaktiviert werden.') - setTimeout(() => setActionError(null), 5000) - } - } - - const handleCreateModule = async () => { - if (!newModuleName.trim()) return - - try { - const res = await fetch('/api/sdk/v1/modules', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: newModuleName, - category: newModuleCategory, - description: newModuleDescription, - }), - }) - - if (res.ok) { - const data = await res.json() - const newMod: Omit = { - id: data.id || `custom-${Date.now()}`, - name: newModuleName, - description: newModuleDescription, - category: newModuleCategory, - regulations: [], - criticality: 'MEDIUM', - processesPersonalData: false, - hasAIComponents: false, - requirementsCount: 0, - controlsCount: 0, - } - setAvailableModules(prev => [...prev, newMod]) - } - + async function onCreateModule() { + const ok = await handleCreateModule(newModuleName, newModuleCategory, newModuleDescription) + if (ok) { setShowCreateModal(false) setNewModuleName('') setNewModuleCategory('custom') setNewModuleDescription('') - } catch { - setActionError('Modul konnte nicht erstellt werden.') - setTimeout(() => setActionError(null), 5000) } } @@ -484,7 +128,7 @@ export default function ModulesPage() { Abbrechen +
+
+
+ + setForm(p => ({ ...p, title: e.target.value }))} + placeholder="Kurzbeschreibung des Befunds" + className="w-full border rounded px-3 py-2 text-sm" + /> +
+
+ +