diff --git a/admin-compliance/app/sdk/isms/_components/AuditsTab.tsx b/admin-compliance/app/sdk/isms/_components/AuditsTab.tsx new file mode 100644 index 0000000..0438256 --- /dev/null +++ b/admin-compliance/app/sdk/isms/_components/AuditsTab.tsx @@ -0,0 +1,152 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { API, AuditFinding, CAPA, InternalAudit } from '../_types' +import { EmptyState, LoadingSpinner, StatCard, StatusBadge } from './shared' + +// ============================================================================= +// TAB: AUDITS (Internal Audits + Findings + CAPA) +// ============================================================================= + +export function AuditsTab() { + const [audits, setAudits] = useState([]) + const [findings, setFindings] = useState([]) + const [capas, setCAPAs] = useState([]) + const [loading, setLoading] = useState(true) + const [subTab, setSubTab] = useState<'audits' | 'findings' | 'capa'>('audits') + + const load = useCallback(async () => { + setLoading(true) + try { + const [aRes, fRes, cRes] = await Promise.all([ + fetch(`${API}/internal-audits`), + fetch(`${API}/findings`), + fetch(`${API}/capa`), + ]) + if (aRes.ok) { const d = await aRes.json(); setAudits(d.audits || []) } + if (fRes.ok) { const d = await fRes.json(); setFindings(d.findings || []) } + if (cRes.ok) { const d = await cRes.json(); setCAPAs(d.actions || []) } + } catch { /* ignore */ } + setLoading(false) + }, []) + + useEffect(() => { load() }, [load]) + + if (loading) return + + const openFindings = findings.filter(f => f.status !== 'closed') + const majors = findings.filter(f => f.finding_type === 'major') + + return ( +
+ {/* Stats */} +
+ + 0 ? 'red' : 'green'} /> + 0 ? 'red' : 'green'} /> + +
+ + {/* Sub-tabs */} +
+ {(['audits', 'findings', 'capa'] as const).map(t => ( + + ))} +
+ + {subTab === 'audits' && ( +
+ {audits.length === 0 ? : audits.map(a => ( +
+
+
+
+ {a.audit_id} + {a.title} + +
+
+ Typ: {a.audit_type} + Datum: {new Date(a.planned_date).toLocaleDateString('de-DE')} + Auditor: {a.lead_auditor} + Findings: {a.total_findings || 0} (Major: {a.major_findings || 0}, Minor: {a.minor_findings || 0}) +
+
+
+ {a.audit_conclusion && ( +

{a.audit_conclusion}

+ )} +
+ ))} +
+ )} + + {subTab === 'findings' && ( +
+ {findings.length === 0 ? : findings.map(f => ( +
+
+
+
+ {f.finding_id} + {f.finding_type.toUpperCase()} + {f.title} + + {f.is_blocking && Blockiert} +
+

{f.description}

+
+ ISO: {f.iso_chapter} + Verantwortlich: {f.owner} + Auditor: {f.auditor} + {f.due_date && Frist: {new Date(f.due_date).toLocaleDateString('de-DE')}} +
+
+
+
+ ))} +
+ )} + + {subTab === 'capa' && ( +
+ {capas.length === 0 ? : capas.map(c => ( +
+
+
+
+ {c.capa_id} + + {c.capa_type === 'corrective' ? 'Korrektur' : 'Vorbeugung'} + + {c.title} + +
+
+ Zustaendig: {c.assigned_to} + Ziel: {new Date(c.planned_completion).toLocaleDateString('de-DE')} + {c.actual_completion && Abgeschlossen: {new Date(c.actual_completion).toLocaleDateString('de-DE')}} + {c.effectiveness_verified !== null && ( + + Wirksamkeit: {c.effectiveness_verified ? 'Bestaetigt' : 'Nicht bestaetigt'} + + )} +
+
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/isms/_components/ObjectivesTab.tsx b/admin-compliance/app/sdk/isms/_components/ObjectivesTab.tsx new file mode 100644 index 0000000..0f748d7 --- /dev/null +++ b/admin-compliance/app/sdk/isms/_components/ObjectivesTab.tsx @@ -0,0 +1,163 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { API, SecurityObjective } from '../_types' +import { EmptyState, LoadingSpinner, StatusBadge } from './shared' + +// ============================================================================= +// TAB: OBJECTIVES +// ============================================================================= + +export function ObjectivesTab() { + const [objectives, setObjectives] = useState([]) + const [loading, setLoading] = useState(true) + const [showCreate, setShowCreate] = useState(false) + + const load = useCallback(async () => { + setLoading(true) + try { + const res = await fetch(`${API}/objectives`) + if (res.ok) { + const data = await res.json() + setObjectives(data.objectives || []) + } + } catch { /* ignore */ } + setLoading(false) + }, []) + + useEffect(() => { load() }, [load]) + + const createObjective = async (form: Record) => { + try { + const res = await fetch(`${API}/objectives?created_by=admin`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }) + if (res.ok) { setShowCreate(false); load() } + } catch { /* ignore */ } + } + + if (loading) return + + const active = objectives.filter(o => o.status === 'active') + const achieved = objectives.filter(o => o.status === 'achieved') + + return ( +
+
+
+ Aktiv: {active.length} + Erreicht: {achieved.length} +
+ +
+ + {objectives.length === 0 ? ( + setShowCreate(true)} /> + ) : ( +
+ {objectives.map(o => ( +
+
+
+ {o.objective_id} + {o.title} + +
+ {o.progress_percentage}% +
+
+
= 100 ? 'bg-green-500' : 'bg-purple-500'}`} + style={{ width: `${Math.min(100, o.progress_percentage)}%` }} + /> +
+
+ KPI: {o.kpi_name} — Ziel: {o.kpi_target} {o.kpi_unit} + Verantwortlich: {o.owner} + Zieldatum: {new Date(o.target_date).toLocaleDateString('de-DE')} + Messung: {o.measurement_frequency} +
+
+ ))} +
+ )} + + {showCreate && ( + setShowCreate(false)} onSave={createObjective} /> + )} +
+ ) +} + +function ObjectiveCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record) => void }) { + const [form, setForm] = useState({ + objective_id: '', title: '', description: '', category: 'confidentiality', + specific: '', measurable: '', achievable: '', relevant: '', time_bound: '', + kpi_name: '', kpi_target: 95, kpi_unit: '%', measurement_frequency: 'monthly', + owner: '', target_date: '', related_controls: [] as string[], related_risks: [] as string[], + }) + + return ( +
+
+

Neues Sicherheitsziel (SMART)

+
+
+
+ + setForm({ ...form, objective_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="OBJ-001" /> +
+
+ + +
+
+
+ + setForm({ ...form, title: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" /> +
+
+ +