Files
breakpilot-compliance/admin-compliance/app/sdk/isms/_components/OverviewTab.tsx
Sharang Parnerkar ddcd89f26d refactor(admin): split isms page.tsx into colocated components
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>
2026-04-11 22:47:01 +02:00

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">&#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>
)
}