Files
breakpilot-compliance/admin-compliance/app/sdk/isms/_components/SoATab.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

108 lines
4.8 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { API, SoAEntry } from '../_types'
import { EmptyState, LoadingSpinner, StatCard, StatusBadge } from './shared'
// =============================================================================
// TAB: SOA (Statement of Applicability)
// =============================================================================
export 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>
)
}