Files
breakpilot-compliance/admin-compliance/app/sdk/isms/page.tsx
Benjamin Admin 7ec6b9f6c0
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
fix(cleanup): ISMS Bugfix, 13 tote AI-Endpoints entfernt, Compliance-Hub Proxy fix
- ISMS: markStepCompleted entfernt (existiert nicht in SDKContext, verursachte Application Error)
- AI Routes: 13 ungenutzte Endpoints entfernt (ai_routes.py 1266→379 Zeilen, -887)
- Schemas: 12 ungenutzte AI-Schema-Klassen entfernt (-108 Zeilen)
- Compliance-Hub: 5 Fetch-URLs von /api/admin/... auf /api/sdk/v1/... umgestellt
- Tests: 1361 passed, 0 Regressionen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:13:19 +01:00

1261 lines
52 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface ISMSOverview {
overall_status: string
certification_readiness: number
chapters: ChapterStatus[]
scope_approved: boolean
soa_approved: boolean
last_management_review: string | null
last_internal_audit: string | null
open_major_findings: number
open_minor_findings: number
policies_count: number
policies_approved: number
objectives_count: number
objectives_achieved: number
}
interface ChapterStatus {
chapter: string
title: string
status: string
completion_percentage: number
open_findings: number
key_documents: string[]
last_reviewed: string | null
}
interface ISMSScope {
id: string
scope_statement: string
included_locations: string[]
included_processes: string[]
included_services: string[]
excluded_items: string[]
exclusion_justification: string
organizational_boundary: string
physical_boundary: string
technical_boundary: string
status: string
version: string
created_by: string
approved_by: string | null
approved_at: string | null
effective_date: string | null
review_date: string | null
created_at: string
}
interface ISMSContext {
id: string
internal_issues: Record<string, unknown>[] | null
external_issues: Record<string, unknown>[] | null
interested_parties: Record<string, unknown>[] | null
regulatory_requirements: string[]
contractual_requirements: string[]
swot_strengths: string[]
swot_weaknesses: string[]
swot_opportunities: string[]
swot_threats: string[]
status: string
created_at: string
}
interface ISMSPolicy {
id: string
policy_id: string
title: string
policy_type: string
description: string
policy_text: string
applies_to: string[]
review_frequency_months: number
related_controls: string[]
status: string
version: string
authored_by: string
reviewed_by: string | null
approved_by: string | null
approved_at: string | null
effective_date: string | null
next_review_date: string | null
created_at: string
}
interface SecurityObjective {
id: string
objective_id: string
title: string
description: string
category: string
kpi_name: string
kpi_target: number
kpi_unit: string
measurement_frequency: string
owner: string
target_date: string
progress_percentage: number
status: string
achieved_date: string | null
}
interface SoAEntry {
id: string
annex_a_control: string
annex_a_title: string
annex_a_category: string
is_applicable: boolean
applicability_justification: string
implementation_status: string
implementation_notes: string
breakpilot_control_ids: string[]
coverage_level: string
evidence_description: string
version: string
approved_at: string | null
}
interface InternalAudit {
id: string
audit_id: string
title: string
audit_type: string
scope_description: string
iso_chapters_covered: string[]
planned_date: string
actual_end_date: string | null
lead_auditor: string
audit_team: string[]
status: string
total_findings: number
major_findings: number
minor_findings: number
ofi_count: number
audit_conclusion: string | null
overall_assessment: string | null
}
interface AuditFinding {
id: string
finding_id: string
finding_type: string
iso_chapter: string
annex_a_control: string | null
title: string
description: string
objective_evidence: string
owner: string
auditor: string
status: string
due_date: string | null
closed_date: string | null
internal_audit_id: string | null
is_blocking: boolean
}
interface CAPA {
id: string
capa_id: string
finding_id: string
capa_type: string
title: string
description: string
assigned_to: string
status: string
planned_completion: string
actual_completion: string | null
effectiveness_verified: boolean | null
}
interface ManagementReview {
id: string
review_id: string
title: string
review_date: string
review_period_start: string
review_period_end: string
chairperson: string
attendees: Record<string, unknown>[] | null
status: string
approved_by: string | null
approved_at: string | null
next_review_date: string | null
action_items: Record<string, unknown>[] | null
}
interface ReadinessCheck {
id: string
check_date: string
overall_status: string
certification_possible: boolean
readiness_score: number
chapter_4_status: string
chapter_5_status: string
chapter_6_status: string
chapter_7_status: string
chapter_8_status: string
chapter_9_status: string
chapter_10_status: string
potential_majors: PotentialFinding[]
potential_minors: PotentialFinding[]
priority_actions: string[]
}
interface PotentialFinding {
check: string
status: string
recommendation: string
iso_reference: string
}
type TabId = 'overview' | 'policies' | 'soa' | 'objectives' | 'audits' | 'reviews'
const API = '/api/sdk/v1/isms'
// =============================================================================
// HELPER COMPONENTS
// =============================================================================
function StatusBadge({ status, size = 'sm' }: { status: string; size?: 'sm' | 'md' }) {
const colors: Record<string, string> = {
ready: 'bg-green-100 text-green-700',
compliant: 'bg-green-100 text-green-700',
approved: 'bg-green-100 text-green-700',
pass: 'bg-green-100 text-green-700',
implemented: 'bg-green-100 text-green-700',
completed: 'bg-green-100 text-green-700',
verified: 'bg-green-100 text-green-700',
achieved: 'bg-green-100 text-green-700',
closed: 'bg-green-100 text-green-700',
at_risk: 'bg-yellow-100 text-yellow-700',
partial: 'bg-yellow-100 text-yellow-700',
warning: 'bg-yellow-100 text-yellow-700',
planned: 'bg-blue-100 text-blue-700',
draft: 'bg-gray-100 text-gray-700',
active: 'bg-blue-100 text-blue-700',
in_progress: 'bg-blue-100 text-blue-700',
not_ready: 'bg-red-100 text-red-700',
non_compliant: 'bg-red-100 text-red-700',
fail: 'bg-red-100 text-red-700',
open: 'bg-red-100 text-red-700',
corrective_action_pending: 'bg-orange-100 text-orange-700',
verification_pending: 'bg-yellow-100 text-yellow-700',
}
const cls = colors[status] || 'bg-gray-100 text-gray-600'
const labels: Record<string, string> = {
ready: 'Bereit', not_ready: 'Nicht bereit', at_risk: 'Risiko',
compliant: 'Konform', non_compliant: 'Nicht konform', partial: 'Teilweise',
approved: 'Genehmigt', draft: 'Entwurf', pass: 'Bestanden', fail: 'Fehlgeschlagen',
warning: 'Warnung', implemented: 'Implementiert', planned: 'Geplant',
active: 'Aktiv', achieved: 'Erreicht', completed: 'Abgeschlossen',
open: 'Offen', closed: 'Geschlossen', verified: 'Verifiziert',
corrective_action_pending: 'CAPA ausstehend', verification_pending: 'Verifizierung',
in_progress: 'In Bearbeitung',
not_applicable: 'N/A',
}
const pad = size === 'md' ? 'px-3 py-1 text-sm' : 'px-2 py-0.5 text-xs'
return <span className={`${cls} ${pad} rounded-full font-medium`}>{labels[status] || status}</span>
}
function StatCard({ label, value, sub, color = 'purple' }: { label: string; value: string | number; sub?: string; color?: string }) {
const colors: Record<string, string> = {
purple: 'border-purple-200 bg-purple-50',
green: 'border-green-200 bg-green-50',
red: 'border-red-200 bg-red-50',
yellow: 'border-yellow-200 bg-yellow-50',
blue: 'border-blue-200 bg-blue-50',
}
return (
<div className={`border rounded-xl p-4 ${colors[color] || colors.purple}`}>
<div className="text-2xl font-bold text-gray-900">{value}</div>
<div className="text-sm font-medium text-gray-700 mt-1">{label}</div>
{sub && <div className="text-xs text-gray-500 mt-0.5">{sub}</div>}
</div>
)
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-20">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
)
}
function EmptyState({ text, action, onAction }: { text: string; action?: string; onAction?: () => void }) {
return (
<div className="text-center py-16 text-gray-500">
<p>{text}</p>
{action && onAction && (
<button onClick={onAction} className="mt-3 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">
{action}
</button>
)}
</div>
)
}
// =============================================================================
// TAB: OVERVIEW
// =============================================================================
function OverviewTab() {
const [overview, setOverview] = useState<ISMSOverview | null>(null)
const [readiness, setReadiness] = useState<ReadinessCheck | null>(null)
const [scope, setScope] = useState<ISMSScope | null>(null)
const [loading, setLoading] = useState(true)
const [running, setRunning] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const [ovRes, rdRes, scRes] = await Promise.allSettled([
fetch(`${API}/overview`),
fetch(`${API}/readiness-check/latest`),
fetch(`${API}/scope`),
])
if (ovRes.status === 'fulfilled' && ovRes.value.ok) setOverview(await ovRes.value.json())
if (rdRes.status === 'fulfilled' && rdRes.value.ok) setReadiness(await rdRes.value.json())
if (scRes.status === 'fulfilled' && scRes.value.ok) setScope(await scRes.value.json())
} catch { /* ignore */ }
setLoading(false)
}, [])
useEffect(() => { load() }, [load])
const runCheck = async () => {
setRunning(true)
try {
const res = await fetch(`${API}/readiness-check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ triggered_by: 'admin-ui' }),
})
if (res.ok) {
setReadiness(await res.json())
load()
}
} catch { /* ignore */ }
setRunning(false)
}
if (loading) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* Readiness Score */}
<div className="bg-white border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">ISO 27001 Zertifizierungsbereitschaft</h3>
<button
onClick={runCheck}
disabled={running}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 text-sm"
>
{running ? 'Pruefe...' : 'Readiness-Check starten'}
</button>
</div>
{overview && (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<StatCard
label="Bereitschaft"
value={`${Math.round(overview.certification_readiness)}%`}
color={overview.certification_readiness >= 80 ? 'green' : overview.certification_readiness >= 50 ? 'yellow' : 'red'}
/>
<StatCard label="Policies" value={`${overview.policies_approved}/${overview.policies_count}`} sub="genehmigt" color="blue" />
<StatCard label="Ziele" value={`${overview.objectives_achieved}/${overview.objectives_count}`} sub="erreicht" color="blue" />
<StatCard label="Major Findings" value={overview.open_major_findings} color={overview.open_major_findings > 0 ? 'red' : 'green'} />
<StatCard label="Minor Findings" value={overview.open_minor_findings} color={overview.open_minor_findings > 0 ? 'yellow' : 'green'} />
</div>
)}
{/* Chapter Overview */}
{overview?.chapters && (
<div>
<h4 className="font-medium text-gray-700 mb-3">ISO 27001 Kapitel-Status</h4>
<div className="space-y-2">
{overview.chapters.map(ch => (
<div key={ch.chapter} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<span className="font-mono text-sm font-bold text-gray-600 w-8">Kap.{ch.chapter}</span>
<span className="flex-1 text-sm font-medium text-gray-800">{ch.title}</span>
<div className="w-32">
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${ch.completion_percentage >= 80 ? 'bg-green-500' : ch.completion_percentage >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${ch.completion_percentage}%` }}
/>
</div>
</div>
<span className="text-xs text-gray-500 w-10 text-right">{Math.round(ch.completion_percentage)}%</span>
<StatusBadge status={ch.status} />
</div>
))}
</div>
</div>
)}
</div>
{/* Readiness Findings */}
{readiness && (
<div className="bg-white border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Readiness-Check Ergebnis</h3>
<div className="flex items-center gap-2">
<StatusBadge status={readiness.overall_status} size="md" />
<span className="text-sm text-gray-500">Score: {Math.round(readiness.readiness_score)}%</span>
</div>
</div>
{readiness.potential_majors.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-medium text-red-700 mb-2">Potenzielle Major-Findings ({readiness.potential_majors.length})</h4>
<div className="space-y-2">
{readiness.potential_majors.map((f, i) => (
<div key={i} className="flex items-start gap-3 p-3 bg-red-50 border border-red-200 rounded-lg">
<span className="text-red-500 mt-0.5">&#10006;</span>
<div>
<div className="text-sm font-medium text-gray-900">{f.check}</div>
<div className="text-xs text-gray-600 mt-0.5">{f.recommendation}</div>
<div className="text-xs text-gray-400 mt-0.5">ISO Referenz: {f.iso_reference}</div>
</div>
</div>
))}
</div>
</div>
)}
{readiness.potential_minors.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-medium text-yellow-700 mb-2">Potenzielle Minor-Findings ({readiness.potential_minors.length})</h4>
<div className="space-y-2">
{readiness.potential_minors.map((f, i) => (
<div key={i} className="flex items-start gap-3 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<span className="text-yellow-500 mt-0.5">&#9888;</span>
<div>
<div className="text-sm font-medium text-gray-900">{f.check}</div>
<div className="text-xs text-gray-600 mt-0.5">{f.recommendation}</div>
</div>
</div>
))}
</div>
</div>
)}
{readiness.priority_actions.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Prioritaere Massnahmen</h4>
<ol className="list-decimal list-inside text-sm text-gray-600 space-y-1">
{readiness.priority_actions.map((a, i) => <li key={i}>{a}</li>)}
</ol>
</div>
)}
</div>
)}
{/* Scope Summary */}
{scope && (
<div className="bg-white border rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">ISMS Scope (Kap. 4.3)</h3>
<StatusBadge status={scope.status} size="md" />
</div>
<p className="text-sm text-gray-700 mb-3">{scope.scope_statement}</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-600">Standorte:</span>
<ul className="list-disc list-inside text-gray-700 mt-1">
{scope.included_locations?.map((l, i) => <li key={i}>{l}</li>)}
</ul>
</div>
<div>
<span className="font-medium text-gray-600">Prozesse:</span>
<ul className="list-disc list-inside text-gray-700 mt-1">
{scope.included_processes?.map((p, i) => <li key={i}>{p}</li>)}
</ul>
</div>
</div>
{scope.approved_by && (
<div className="mt-3 text-xs text-gray-500">
Genehmigt von {scope.approved_by} am {new Date(scope.approved_at!).toLocaleDateString('de-DE')}
</div>
)}
</div>
)}
</div>
)
}
// =============================================================================
// TAB: POLICIES
// =============================================================================
function PoliciesTab() {
const [policies, setPolicies] = useState<ISMSPolicy[]>([])
const [loading, setLoading] = useState(true)
const [showCreate, setShowCreate] = useState(false)
const [filter, setFilter] = useState<string>('')
const load = useCallback(async () => {
setLoading(true)
try {
const url = filter ? `${API}/policies?policy_type=${filter}` : `${API}/policies`
const res = await fetch(url)
if (res.ok) {
const data = await res.json()
setPolicies(data.policies || [])
}
} catch { /* ignore */ }
setLoading(false)
}, [filter])
useEffect(() => { load() }, [load])
const createPolicy = async (form: Record<string, unknown>) => {
try {
const res = await fetch(`${API}/policies`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (res.ok) { setShowCreate(false); load() }
} catch { /* ignore */ }
}
const approvePolicy = async (policyId: string) => {
try {
await fetch(`${API}/policies/${policyId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reviewed_by: 'admin',
approved_by: 'admin',
effective_date: new Date().toISOString().split('T')[0],
}),
})
load()
} catch { /* ignore */ }
}
if (loading) return <LoadingSpinner />
const policyTypes = ['master', 'topic', 'operational', 'standard']
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex gap-2">
<button onClick={() => setFilter('')} className={`px-3 py-1.5 rounded-lg text-sm ${!filter ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>Alle</button>
{policyTypes.map(t => (
<button key={t} onClick={() => setFilter(t)} className={`px-3 py-1.5 rounded-lg text-sm capitalize ${filter === t ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>{t}</button>
))}
</div>
<button onClick={() => setShowCreate(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">Neue Policy</button>
</div>
{policies.length === 0 ? (
<EmptyState text="Keine Policies vorhanden" action="Policy erstellen" onAction={() => setShowCreate(true)} />
) : (
<div className="space-y-3">
{policies.map(p => (
<div key={p.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{p.policy_id}</span>
<span className="text-sm font-medium text-gray-900">{p.title}</span>
<StatusBadge status={p.status} />
<span className="text-xs text-gray-400">v{p.version}</span>
</div>
<p className="text-xs text-gray-600 mt-1 line-clamp-2">{p.description}</p>
<div className="flex gap-3 mt-1 text-xs text-gray-500">
<span>Typ: {p.policy_type}</span>
<span>Review: alle {p.review_frequency_months} Monate</span>
{p.next_review_date && <span>Naechste Review: {new Date(p.next_review_date).toLocaleDateString('de-DE')}</span>}
</div>
</div>
{p.status === 'draft' && (
<button onClick={() => approvePolicy(p.id)} className="px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xs">Genehmigen</button>
)}
</div>
</div>
))}
</div>
)}
{/* Create Modal */}
{showCreate && (
<PolicyCreateModal onClose={() => setShowCreate(false)} onSave={createPolicy} />
)}
</div>
)
}
function PolicyCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record<string, unknown>) => void }) {
const [form, setForm] = useState({
policy_id: '', title: '', policy_type: 'topic', description: '', policy_text: '',
applies_to: ['Alle Mitarbeiter'], review_frequency_months: 12, related_controls: [] as string[],
authored_by: 'admin',
})
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6">
<h3 className="text-lg font-semibold mb-4">Neue ISMS Policy</h3>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Policy-ID</label>
<input value={form.policy_id} onChange={e => setForm({ ...form, policy_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="z.B. POL-SEC-001" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Typ</label>
<select value={form.policy_type} onChange={e => setForm({ ...form, policy_type: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm">
<option value="master">Master Policy</option>
<option value="topic">Topic Policy</option>
<option value="operational">Operational</option>
<option value="standard">Standard</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-600">Titel</label>
<input value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Beschreibung</label>
<textarea value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" rows={2} />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Policy-Text</label>
<textarea value={form.policy_text} onChange={e => setForm({ ...form, policy_text: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" rows={5} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Review-Frequenz (Monate)</label>
<input type="number" value={form.review_frequency_months} onChange={e => setForm({ ...form, review_frequency_months: Number(e.target.value) })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Autor</label>
<input value={form.authored_by} onChange={e => setForm({ ...form, authored_by: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={() => onSave(form)} className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">Erstellen</button>
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: SOA (Statement of Applicability)
// =============================================================================
function SoATab() {
const [entries, setEntries] = useState<SoAEntry[]>([])
const [stats, setStats] = useState({ applicable: 0, notApplicable: 0, implemented: 0, planned: 0 })
const [loading, setLoading] = useState(true)
const [filterStatus, setFilterStatus] = useState('')
const [filterApplicable, setFilterApplicable] = useState<string>('')
const load = useCallback(async () => {
setLoading(true)
try {
const params = new URLSearchParams()
if (filterStatus) params.set('implementation_status', filterStatus)
if (filterApplicable) params.set('is_applicable', filterApplicable)
const res = await fetch(`${API}/soa?${params}`)
if (res.ok) {
const data = await res.json()
setEntries(data.entries || [])
setStats({
applicable: data.applicable_count || 0,
notApplicable: data.not_applicable_count || 0,
implemented: data.implemented_count || 0,
planned: data.planned_count || 0,
})
}
} catch { /* ignore */ }
setLoading(false)
}, [filterStatus, filterApplicable])
useEffect(() => { load() }, [load])
if (loading) return <LoadingSpinner />
return (
<div className="space-y-4">
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<StatCard label="Anwendbar" value={stats.applicable} color="blue" />
<StatCard label="Nicht anwendbar" value={stats.notApplicable} color="purple" />
<StatCard label="Implementiert" value={stats.implemented} color="green" />
<StatCard label="Geplant" value={stats.planned} color="yellow" />
</div>
{/* Filters */}
<div className="flex gap-2">
<select value={filterApplicable} onChange={e => setFilterApplicable(e.target.value)} className="border rounded-lg px-3 py-1.5 text-sm">
<option value="">Alle Controls</option>
<option value="true">Anwendbar</option>
<option value="false">Nicht anwendbar</option>
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="border rounded-lg px-3 py-1.5 text-sm">
<option value="">Alle Status</option>
<option value="implemented">Implementiert</option>
<option value="planned">Geplant</option>
<option value="not_started">Nicht gestartet</option>
<option value="not_applicable">N/A</option>
</select>
</div>
{entries.length === 0 ? (
<EmptyState text="Noch keine SoA-Eintraege vorhanden. Erstellen Sie Eintraege fuer alle 93 Annex-A-Controls." />
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 text-left">
<th className="px-3 py-2 font-medium text-gray-600">Control</th>
<th className="px-3 py-2 font-medium text-gray-600">Titel</th>
<th className="px-3 py-2 font-medium text-gray-600">Kategorie</th>
<th className="px-3 py-2 font-medium text-gray-600">Anwendbar</th>
<th className="px-3 py-2 font-medium text-gray-600">Status</th>
<th className="px-3 py-2 font-medium text-gray-600">Coverage</th>
<th className="px-3 py-2 font-medium text-gray-600">Version</th>
</tr>
</thead>
<tbody>
{entries.map(e => (
<tr key={e.id} className="border-t hover:bg-gray-50">
<td className="px-3 py-2 font-mono text-xs">{e.annex_a_control}</td>
<td className="px-3 py-2 text-gray-800">{e.annex_a_title}</td>
<td className="px-3 py-2 text-gray-600">{e.annex_a_category}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${e.is_applicable ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}`}>
{e.is_applicable ? 'Ja' : 'Nein'}
</span>
</td>
<td className="px-3 py-2"><StatusBadge status={e.implementation_status} /></td>
<td className="px-3 py-2 text-xs text-gray-600">{e.coverage_level || '-'}</td>
<td className="px-3 py-2 text-xs text-gray-400">v{e.version}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}
// =============================================================================
// TAB: OBJECTIVES
// =============================================================================
function ObjectivesTab() {
const [objectives, setObjectives] = useState<SecurityObjective[]>([])
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<string, unknown>) => {
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 <LoadingSpinner />
const active = objectives.filter(o => o.status === 'active')
const achieved = objectives.filter(o => o.status === 'achieved')
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex gap-3 text-sm text-gray-600">
<span>Aktiv: {active.length}</span>
<span>Erreicht: {achieved.length}</span>
</div>
<button onClick={() => setShowCreate(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">Neues Ziel</button>
</div>
{objectives.length === 0 ? (
<EmptyState text="Keine Sicherheitsziele definiert" action="Ziel erstellen" onAction={() => setShowCreate(true)} />
) : (
<div className="space-y-3">
{objectives.map(o => (
<div key={o.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{o.objective_id}</span>
<span className="text-sm font-medium text-gray-900">{o.title}</span>
<StatusBadge status={o.status} />
</div>
<span className="text-sm font-bold text-purple-600">{o.progress_percentage}%</span>
</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden mb-2">
<div
className={`h-full rounded-full transition-all ${o.progress_percentage >= 100 ? 'bg-green-500' : 'bg-purple-500'}`}
style={{ width: `${Math.min(100, o.progress_percentage)}%` }}
/>
</div>
<div className="flex gap-4 text-xs text-gray-500">
<span>KPI: {o.kpi_name} Ziel: {o.kpi_target} {o.kpi_unit}</span>
<span>Verantwortlich: {o.owner}</span>
<span>Zieldatum: {new Date(o.target_date).toLocaleDateString('de-DE')}</span>
<span>Messung: {o.measurement_frequency}</span>
</div>
</div>
))}
</div>
)}
{showCreate && (
<ObjectiveCreateModal onClose={() => setShowCreate(false)} onSave={createObjective} />
)}
</div>
)
}
function ObjectiveCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record<string, unknown>) => 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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6">
<h3 className="text-lg font-semibold mb-4">Neues Sicherheitsziel (SMART)</h3>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Ziel-ID</label>
<input value={form.objective_id} onChange={e => setForm({ ...form, objective_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="OBJ-001" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Kategorie</label>
<select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm">
<option value="confidentiality">Vertraulichkeit</option>
<option value="integrity">Integritaet</option>
<option value="availability">Verfuegbarkeit</option>
<option value="compliance">Compliance</option>
<option value="awareness">Awareness</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-600">Titel</label>
<input value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Beschreibung</label>
<textarea value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" rows={2} />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">KPI Name</label>
<input value={form.kpi_name} onChange={e => setForm({ ...form, kpi_name: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="Patch-Rate" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Zielwert</label>
<input type="number" value={form.kpi_target} onChange={e => setForm({ ...form, kpi_target: Number(e.target.value) })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Einheit</label>
<input value={form.kpi_unit} onChange={e => setForm({ ...form, kpi_unit: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Verantwortlich</label>
<input value={form.owner} onChange={e => setForm({ ...form, owner: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Zieldatum</label>
<input type="date" value={form.target_date} onChange={e => setForm({ ...form, target_date: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={() => onSave(form)} className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">Erstellen</button>
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: AUDITS (Internal Audits + Findings + CAPA)
// =============================================================================
function AuditsTab() {
const [audits, setAudits] = useState<InternalAudit[]>([])
const [findings, setFindings] = useState<AuditFinding[]>([])
const [capas, setCAPAs] = useState<CAPA[]>([])
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 <LoadingSpinner />
const openFindings = findings.filter(f => f.status !== 'closed')
const majors = findings.filter(f => f.finding_type === 'major')
return (
<div className="space-y-4">
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<StatCard label="Interne Audits" value={audits.length} color="blue" />
<StatCard label="Offene Findings" value={openFindings.length} color={openFindings.length > 0 ? 'red' : 'green'} />
<StatCard label="Major Findings" value={majors.length} color={majors.length > 0 ? 'red' : 'green'} />
<StatCard label="CAPAs" value={capas.length} color="purple" />
</div>
{/* Sub-tabs */}
<div className="flex gap-2 border-b">
{(['audits', 'findings', 'capa'] as const).map(t => (
<button key={t} onClick={() => setSubTab(t)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${subTab === t ? 'border-purple-600 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
>
{t === 'audits' ? 'Interne Audits' : t === 'findings' ? 'Findings' : 'CAPA'}
</button>
))}
</div>
{subTab === 'audits' && (
<div className="space-y-3">
{audits.length === 0 ? <EmptyState text="Noch keine internen Audits geplant" /> : audits.map(a => (
<div key={a.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{a.audit_id}</span>
<span className="text-sm font-medium text-gray-900">{a.title}</span>
<StatusBadge status={a.status} />
</div>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>Typ: {a.audit_type}</span>
<span>Datum: {new Date(a.planned_date).toLocaleDateString('de-DE')}</span>
<span>Auditor: {a.lead_auditor}</span>
<span>Findings: {a.total_findings || 0} (Major: {a.major_findings || 0}, Minor: {a.minor_findings || 0})</span>
</div>
</div>
</div>
{a.audit_conclusion && (
<p className="text-xs text-gray-600 mt-2 bg-gray-50 rounded p-2">{a.audit_conclusion}</p>
)}
</div>
))}
</div>
)}
{subTab === 'findings' && (
<div className="space-y-3">
{findings.length === 0 ? <EmptyState text="Keine Audit-Findings vorhanden" /> : findings.map(f => (
<div key={f.id} className={`bg-white border rounded-xl p-4 ${f.is_blocking ? 'border-red-300' : ''}`}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{f.finding_id}</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
f.finding_type === 'major' ? 'bg-red-100 text-red-700' :
f.finding_type === 'minor' ? 'bg-yellow-100 text-yellow-700' :
f.finding_type === 'ofi' ? 'bg-blue-100 text-blue-700' :
'bg-green-100 text-green-700'
}`}>{f.finding_type.toUpperCase()}</span>
<span className="text-sm font-medium text-gray-900">{f.title}</span>
<StatusBadge status={f.status} />
{f.is_blocking && <span className="px-2 py-0.5 bg-red-600 text-white text-xs rounded-full">Blockiert</span>}
</div>
<p className="text-xs text-gray-600 mt-1">{f.description}</p>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>ISO: {f.iso_chapter}</span>
<span>Verantwortlich: {f.owner}</span>
<span>Auditor: {f.auditor}</span>
{f.due_date && <span>Frist: {new Date(f.due_date).toLocaleDateString('de-DE')}</span>}
</div>
</div>
</div>
</div>
))}
</div>
)}
{subTab === 'capa' && (
<div className="space-y-3">
{capas.length === 0 ? <EmptyState text="Keine Korrektur-/Vorbeugungsmassnahmen vorhanden" /> : capas.map(c => (
<div key={c.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{c.capa_id}</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${c.capa_type === 'corrective' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}`}>
{c.capa_type === 'corrective' ? 'Korrektur' : 'Vorbeugung'}
</span>
<span className="text-sm font-medium text-gray-900">{c.title}</span>
<StatusBadge status={c.status} />
</div>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>Zustaendig: {c.assigned_to}</span>
<span>Ziel: {new Date(c.planned_completion).toLocaleDateString('de-DE')}</span>
{c.actual_completion && <span>Abgeschlossen: {new Date(c.actual_completion).toLocaleDateString('de-DE')}</span>}
{c.effectiveness_verified !== null && (
<span className={c.effectiveness_verified ? 'text-green-600' : 'text-red-600'}>
Wirksamkeit: {c.effectiveness_verified ? 'Bestaetigt' : 'Nicht bestaetigt'}
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)
}
// =============================================================================
// TAB: MANAGEMENT REVIEWS
// =============================================================================
function ReviewsTab() {
const [reviews, setReviews] = useState<ManagementReview[]>([])
const [loading, setLoading] = useState(true)
const [showCreate, setShowCreate] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const res = await fetch(`${API}/management-reviews`)
if (res.ok) {
const data = await res.json()
setReviews(data.reviews || [])
}
} catch { /* ignore */ }
setLoading(false)
}, [])
useEffect(() => { load() }, [load])
const createReview = async (form: Record<string, unknown>) => {
try {
const res = await fetch(`${API}/management-reviews?created_by=admin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (res.ok) { setShowCreate(false); load() }
} catch { /* ignore */ }
}
const approveReview = async (reviewId: string) => {
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
try {
await fetch(`${API}/management-reviews/${reviewId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
approved_by: 'admin',
next_review_date: nextYear.toISOString().split('T')[0],
}),
})
load()
} catch { /* ignore */ }
}
if (loading) return <LoadingSpinner />
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-600">{reviews.length} Management-Reviews</h3>
<button onClick={() => setShowCreate(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">Neue Review</button>
</div>
{reviews.length === 0 ? (
<EmptyState text="Keine Management-Reviews vorhanden" action="Review planen" onAction={() => setShowCreate(true)} />
) : (
<div className="space-y-3">
{reviews.map(r => (
<div key={r.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{r.review_id}</span>
<span className="text-sm font-medium text-gray-900">{r.title}</span>
<StatusBadge status={r.status} />
</div>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>Datum: {new Date(r.review_date).toLocaleDateString('de-DE')}</span>
<span>Zeitraum: {new Date(r.review_period_start).toLocaleDateString('de-DE')} - {new Date(r.review_period_end).toLocaleDateString('de-DE')}</span>
<span>Vorsitz: {r.chairperson}</span>
{r.next_review_date && <span>Naechste Review: {new Date(r.next_review_date).toLocaleDateString('de-DE')}</span>}
</div>
</div>
{r.status === 'draft' && (
<button onClick={() => approveReview(r.id)} className="px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xs">Genehmigen</button>
)}
</div>
</div>
))}
</div>
)}
{showCreate && (
<ReviewCreateModal onClose={() => setShowCreate(false)} onSave={createReview} />
)}
</div>
)
}
function ReviewCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record<string, unknown>) => void }) {
const today = new Date().toISOString().split('T')[0]
const [form, setForm] = useState({
title: '', review_date: today,
review_period_start: '', review_period_end: today,
chairperson: '', attendees: [] as Record<string, unknown>[],
})
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg p-6">
<h3 className="text-lg font-semibold mb-4">Neue Management-Review</h3>
<div className="space-y-3">
<div>
<label className="text-xs font-medium text-gray-600">Titel</label>
<input value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="Q1 2026 Management Review" />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Review-Datum</label>
<input type="date" value={form.review_date} onChange={e => setForm({ ...form, review_date: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Zeitraum von</label>
<input type="date" value={form.review_period_start} onChange={e => setForm({ ...form, review_period_start: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Zeitraum bis</label>
<input type="date" value={form.review_period_end} onChange={e => setForm({ ...form, review_period_end: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-600">Vorsitzender</label>
<input value={form.chairperson} onChange={e => setForm({ ...form, chairperson: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={() => onSave(form)} className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">Erstellen</button>
</div>
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
const TABS: { id: TabId; label: string }[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'policies', label: 'Policies' },
{ id: 'soa', label: 'SoA (Annex A)' },
{ id: 'objectives', label: 'Ziele' },
{ id: 'audits', label: 'Audits & Findings' },
{ id: 'reviews', label: 'Management Reviews' },
]
export default function ISMSPage() {
const [tab, setTab] = useState<TabId>('overview')
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">ISMS ISO 27001</h1>
<p className="text-sm text-gray-600 mt-1">
Informationssicherheits-Managementsystem: Scope, Policies, SoA, Audits, CAPA und Management-Reviews
</p>
</div>
{/* Tabs */}
<div className="flex gap-1 bg-white border rounded-xl p-1 mb-6">
{TABS.map(t => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`flex-1 px-4 py-2.5 rounded-lg text-sm font-medium transition-colors ${
tab === t.id
? 'bg-purple-600 text-white shadow-sm'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{t.label}
</button>
))}
</div>
{/* Tab Content */}
{tab === 'overview' && <OverviewTab />}
{tab === 'policies' && <PoliciesTab />}
{tab === 'soa' && <SoATab />}
{tab === 'objectives' && <ObjectivesTab />}
{tab === 'audits' && <AuditsTab />}
{tab === 'reviews' && <ReviewsTab />}
</div>
</div>
)
}