'use client' /** * Control Catalogue Page * * Features: * - List all controls with filters * - Control detail view * - Status update / Review * - Evidence linking */ import { useState, useEffect } from 'react' import Link from 'next/link' import AdminLayout from '@/components/admin/AdminLayout' interface Control { id: string control_id: string domain: string control_type: string title: string description: string pass_criteria: string implementation_guidance: string code_reference: string is_automated: boolean automation_tool: string owner: string status: string status_notes: string last_reviewed_at: string | null next_review_at: string | null evidence_count: number } const DOMAIN_LABELS: Record = { gov: 'Governance', priv: 'Datenschutz', iam: 'Identity & Access', crypto: 'Kryptografie', sdlc: 'Secure Dev', ops: 'Operations', ai: 'KI-spezifisch', cra: 'Supply Chain', aud: 'Audit', } const DOMAIN_COLORS: Record = { gov: 'bg-slate-100 text-slate-700', priv: 'bg-blue-100 text-blue-700', iam: 'bg-purple-100 text-purple-700', crypto: 'bg-yellow-100 text-yellow-700', sdlc: 'bg-green-100 text-green-700', ops: 'bg-orange-100 text-orange-700', ai: 'bg-pink-100 text-pink-700', cra: 'bg-cyan-100 text-cyan-700', aud: 'bg-indigo-100 text-indigo-700', } const STATUS_STYLES: Record = { pass: { bg: 'bg-green-100', text: 'text-green-700', icon: 'M5 13l4 4L19 7' }, partial: { bg: 'bg-yellow-100', text: 'text-yellow-700', icon: 'M12 8v4m0 4h.01' }, fail: { bg: 'bg-red-100', text: 'text-red-700', icon: 'M6 18L18 6M6 6l12 12' }, planned: { bg: 'bg-slate-100', text: 'text-slate-700', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' }, 'n/a': { bg: 'bg-slate-100', text: 'text-slate-500', icon: 'M20 12H4' }, } export default function ControlsPage() { const [controls, setControls] = useState([]) const [loading, setLoading] = useState(true) const [selectedControl, setSelectedControl] = useState(null) const [filterDomain, setFilterDomain] = useState('') const [filterStatus, setFilterStatus] = useState('') const [searchTerm, setSearchTerm] = useState('') const [reviewModalOpen, setReviewModalOpen] = useState(false) const [reviewStatus, setReviewStatus] = useState('pass') const [reviewNotes, setReviewNotes] = useState('') const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000' useEffect(() => { loadControls() }, [filterDomain, filterStatus]) const loadControls = async () => { setLoading(true) try { const params = new URLSearchParams() if (filterDomain) params.append('domain', filterDomain) if (filterStatus) params.append('status', filterStatus) if (searchTerm) params.append('search', searchTerm) const res = await fetch(`${BACKEND_URL}/api/v1/compliance/controls?${params}`) if (res.ok) { const data = await res.json() setControls(data.controls || []) } } catch (error) { console.error('Failed to load controls:', error) } finally { setLoading(false) } } const handleSearch = () => { loadControls() } const openReviewModal = (control: Control) => { setSelectedControl(control) setReviewStatus(control.status || 'planned') setReviewNotes(control.status_notes || '') setReviewModalOpen(true) } const submitReview = async () => { if (!selectedControl) return try { const res = await fetch(`${BACKEND_URL}/api/v1/compliance/controls/${selectedControl.control_id}/review`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: reviewStatus, status_notes: reviewNotes, }), }) if (res.ok) { setReviewModalOpen(false) loadControls() } else { alert('Fehler beim Speichern') } } catch (error) { console.error('Review failed:', error) alert('Fehler beim Speichern') } } const filteredControls = controls.filter((c) => { if (searchTerm) { const term = searchTerm.toLowerCase() return ( c.control_id.toLowerCase().includes(term) || c.title.toLowerCase().includes(term) || (c.description && c.description.toLowerCase().includes(term)) ) } return true }) const getDaysUntilReview = (nextReview: string | null) => { if (!nextReview) return null const days = Math.ceil((new Date(nextReview).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) return days } return ( {/* Header Actions */}
Zurueck zum Dashboard
{filteredControls.length} Controls
{/* Filters */}
setSearchTerm(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
{/* Controls Table */} {loading ? (
) : (
{filteredControls.map((control) => { const statusStyle = STATUS_STYLES[control.status] || STATUS_STYLES.planned const daysUntilReview = getDaysUntilReview(control.next_review_at) return ( ) })}
ID Domain Titel Status Automatisiert Nachweise Review Aktionen
{control.control_id} {DOMAIN_LABELS[control.domain] || control.domain}

{control.title}

{control.description && (

{control.description}

)}
{control.status} {control.is_automated ? ( {control.automation_tool} ) : ( Manuell )} {control.evidence_count || 0} {daysUntilReview !== null ? ( {daysUntilReview < 0 ? `${Math.abs(daysUntilReview)}d ueberfaellig` : `${daysUntilReview}d`} ) : ( - )}
)} {/* Review Modal */} {reviewModalOpen && selectedControl && (

Control Review: {selectedControl.control_id}

{selectedControl.title}

Pass-Kriterium:

{selectedControl.pass_criteria}

{Object.entries(STATUS_STYLES).map(([key, style]) => ( ))}