Split 1260-LOC client page into _types.ts and six tab components under _components/ (Overview, Policies, SoA, Objectives, Audits, Reviews) plus a shared helpers module. Behavior preserved exactly; page.tsx is now a thin wiring shell. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
8.6 KiB
TypeScript
197 lines
8.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
import { API, ISMSOverview, ISMSScope, ReadinessCheck } from '../_types'
|
|
import { LoadingSpinner, StatCard, StatusBadge } from './shared'
|
|
|
|
// =============================================================================
|
|
// TAB: OVERVIEW
|
|
// =============================================================================
|
|
|
|
export 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">✖</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">⚠</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>
|
|
)
|
|
}
|