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>
This commit is contained in:
107
admin-compliance/app/sdk/isms/_components/SoATab.tsx
Normal file
107
admin-compliance/app/sdk/isms/_components/SoATab.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user