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:
Sharang Parnerkar
2026-04-11 22:47:01 +02:00
parent 5cb91e88d2
commit ddcd89f26d
9 changed files with 1242 additions and 1203 deletions

View File

@@ -0,0 +1,163 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { API, SecurityObjective } from '../_types'
import { EmptyState, LoadingSpinner, StatusBadge } from './shared'
// =============================================================================
// TAB: OBJECTIVES
// =============================================================================
export function ObjectivesTab() {
const [objectives, setObjectives] = useState<SecurityObjective[]>([])
const [loading, setLoading] = useState(true)
const [showCreate, setShowCreate] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const res = await fetch(`${API}/objectives`)
if (res.ok) {
const data = await res.json()
setObjectives(data.objectives || [])
}
} catch { /* ignore */ }
setLoading(false)
}, [])
useEffect(() => { load() }, [load])
const createObjective = async (form: Record<string, unknown>) => {
try {
const res = await fetch(`${API}/objectives?created_by=admin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (res.ok) { setShowCreate(false); load() }
} catch { /* ignore */ }
}
if (loading) return <LoadingSpinner />
const active = objectives.filter(o => o.status === 'active')
const achieved = objectives.filter(o => o.status === 'achieved')
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex gap-3 text-sm text-gray-600">
<span>Aktiv: {active.length}</span>
<span>Erreicht: {achieved.length}</span>
</div>
<button onClick={() => setShowCreate(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">Neues Ziel</button>
</div>
{objectives.length === 0 ? (
<EmptyState text="Keine Sicherheitsziele definiert" action="Ziel erstellen" onAction={() => setShowCreate(true)} />
) : (
<div className="space-y-3">
{objectives.map(o => (
<div key={o.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{o.objective_id}</span>
<span className="text-sm font-medium text-gray-900">{o.title}</span>
<StatusBadge status={o.status} />
</div>
<span className="text-sm font-bold text-purple-600">{o.progress_percentage}%</span>
</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden mb-2">
<div
className={`h-full rounded-full transition-all ${o.progress_percentage >= 100 ? 'bg-green-500' : 'bg-purple-500'}`}
style={{ width: `${Math.min(100, o.progress_percentage)}%` }}
/>
</div>
<div className="flex gap-4 text-xs text-gray-500">
<span>KPI: {o.kpi_name} Ziel: {o.kpi_target} {o.kpi_unit}</span>
<span>Verantwortlich: {o.owner}</span>
<span>Zieldatum: {new Date(o.target_date).toLocaleDateString('de-DE')}</span>
<span>Messung: {o.measurement_frequency}</span>
</div>
</div>
))}
</div>
)}
{showCreate && (
<ObjectiveCreateModal onClose={() => setShowCreate(false)} onSave={createObjective} />
)}
</div>
)
}
function ObjectiveCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record<string, unknown>) => void }) {
const [form, setForm] = useState({
objective_id: '', title: '', description: '', category: 'confidentiality',
specific: '', measurable: '', achievable: '', relevant: '', time_bound: '',
kpi_name: '', kpi_target: 95, kpi_unit: '%', measurement_frequency: 'monthly',
owner: '', target_date: '', related_controls: [] as string[], related_risks: [] as string[],
})
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6">
<h3 className="text-lg font-semibold mb-4">Neues Sicherheitsziel (SMART)</h3>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Ziel-ID</label>
<input value={form.objective_id} onChange={e => setForm({ ...form, objective_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="OBJ-001" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Kategorie</label>
<select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm">
<option value="confidentiality">Vertraulichkeit</option>
<option value="integrity">Integritaet</option>
<option value="availability">Verfuegbarkeit</option>
<option value="compliance">Compliance</option>
<option value="awareness">Awareness</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-600">Titel</label>
<input value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Beschreibung</label>
<textarea value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" rows={2} />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">KPI Name</label>
<input value={form.kpi_name} onChange={e => setForm({ ...form, kpi_name: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="Patch-Rate" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Zielwert</label>
<input type="number" value={form.kpi_target} onChange={e => setForm({ ...form, kpi_target: Number(e.target.value) })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Einheit</label>
<input value={form.kpi_unit} onChange={e => setForm({ ...form, kpi_unit: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600">Verantwortlich</label>
<input value={form.owner} onChange={e => setForm({ ...form, owner: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="text-xs font-medium text-gray-600">Zieldatum</label>
<input type="date" value={form.target_date} onChange={e => setForm({ ...form, target_date: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" />
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={() => onSave(form)} className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">Erstellen</button>
</div>
</div>
</div>
)
}