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>
108 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|