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:
169
admin-compliance/app/sdk/isms/_components/PoliciesTab.tsx
Normal file
169
admin-compliance/app/sdk/isms/_components/PoliciesTab.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { API, ISMSPolicy } from '../_types'
|
||||
import { EmptyState, LoadingSpinner, StatusBadge } from './shared'
|
||||
|
||||
// =============================================================================
|
||||
// TAB: POLICIES
|
||||
// =============================================================================
|
||||
|
||||
export function PoliciesTab() {
|
||||
const [policies, setPolicies] = useState<ISMSPolicy[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [filter, setFilter] = useState<string>('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const url = filter ? `${API}/policies?policy_type=${filter}` : `${API}/policies`
|
||||
const res = await fetch(url)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setPolicies(data.policies || [])
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [filter])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
const createPolicy = async (form: Record<string, unknown>) => {
|
||||
try {
|
||||
const res = await fetch(`${API}/policies`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form),
|
||||
})
|
||||
if (res.ok) { setShowCreate(false); load() }
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const approvePolicy = async (policyId: string) => {
|
||||
try {
|
||||
await fetch(`${API}/policies/${policyId}/approve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reviewed_by: 'admin',
|
||||
approved_by: 'admin',
|
||||
effective_date: new Date().toISOString().split('T')[0],
|
||||
}),
|
||||
})
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (loading) return <LoadingSpinner />
|
||||
|
||||
const policyTypes = ['master', 'topic', 'operational', 'standard']
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setFilter('')} className={`px-3 py-1.5 rounded-lg text-sm ${!filter ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>Alle</button>
|
||||
{policyTypes.map(t => (
|
||||
<button key={t} onClick={() => setFilter(t)} className={`px-3 py-1.5 rounded-lg text-sm capitalize ${filter === t ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>{t}</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={() => setShowCreate(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">Neue Policy</button>
|
||||
</div>
|
||||
|
||||
{policies.length === 0 ? (
|
||||
<EmptyState text="Keine Policies vorhanden" action="Policy erstellen" onAction={() => setShowCreate(true)} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{policies.map(p => (
|
||||
<div key={p.id} className="bg-white border rounded-xl p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-gray-500">{p.policy_id}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{p.title}</span>
|
||||
<StatusBadge status={p.status} />
|
||||
<span className="text-xs text-gray-400">v{p.version}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1 line-clamp-2">{p.description}</p>
|
||||
<div className="flex gap-3 mt-1 text-xs text-gray-500">
|
||||
<span>Typ: {p.policy_type}</span>
|
||||
<span>Review: alle {p.review_frequency_months} Monate</span>
|
||||
{p.next_review_date && <span>Naechste Review: {new Date(p.next_review_date).toLocaleDateString('de-DE')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{p.status === 'draft' && (
|
||||
<button onClick={() => approvePolicy(p.id)} className="px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xs">Genehmigen</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Modal */}
|
||||
{showCreate && (
|
||||
<PolicyCreateModal onClose={() => setShowCreate(false)} onSave={createPolicy} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PolicyCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record<string, unknown>) => void }) {
|
||||
const [form, setForm] = useState({
|
||||
policy_id: '', title: '', policy_type: 'topic', description: '', policy_text: '',
|
||||
applies_to: ['Alle Mitarbeiter'], review_frequency_months: 12, related_controls: [] as string[],
|
||||
authored_by: 'admin',
|
||||
})
|
||||
|
||||
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">Neue ISMS Policy</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Policy-ID</label>
|
||||
<input value={form.policy_id} onChange={e => setForm({ ...form, policy_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="z.B. POL-SEC-001" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Typ</label>
|
||||
<select value={form.policy_type} onChange={e => setForm({ ...form, policy_type: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm">
|
||||
<option value="master">Master Policy</option>
|
||||
<option value="topic">Topic Policy</option>
|
||||
<option value="operational">Operational</option>
|
||||
<option value="standard">Standard</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>
|
||||
<label className="text-xs font-medium text-gray-600">Policy-Text</label>
|
||||
<textarea value={form.policy_text} onChange={e => setForm({ ...form, policy_text: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" rows={5} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Review-Frequenz (Monate)</label>
|
||||
<input type="number" value={form.review_frequency_months} onChange={e => setForm({ ...form, review_frequency_months: 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">Autor</label>
|
||||
<input value={form.authored_by} onChange={e => setForm({ ...form, authored_by: 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user