The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
447 lines
21 KiB
TypeScript
447 lines
21 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Requirements Page - Alle Compliance-Anforderungen mit Implementation-Status
|
|
*
|
|
* Features:
|
|
* - Liste aller 19 Verordnungen mit URLs zu Originaldokumenten
|
|
* - 558+ Requirements mit Implementation-Status
|
|
* - Filterung nach Regulation, Status, Prioritaet
|
|
* - Detail-Ansicht mit Breakpilot-Interpretation
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
|
|
// Types
|
|
interface Regulation {
|
|
id: string
|
|
code: string
|
|
name: string
|
|
full_name: string
|
|
regulation_type: string
|
|
source_url: string
|
|
local_pdf_path?: string
|
|
effective_date?: string
|
|
description: string
|
|
is_active: boolean
|
|
requirement_count: number
|
|
}
|
|
|
|
interface Requirement {
|
|
id: string
|
|
regulation_id: string
|
|
regulation_code: string
|
|
article: string
|
|
paragraph?: string
|
|
title: string
|
|
description?: string
|
|
requirement_text?: string
|
|
breakpilot_interpretation?: string
|
|
implementation_status: 'not_started' | 'in_progress' | 'implemented' | 'verified' | 'not_applicable'
|
|
implementation_details?: string
|
|
code_references?: Array<{ file: string; line?: number; description?: string }>
|
|
evidence_description?: string
|
|
priority: number
|
|
is_applicable: boolean
|
|
controls_count: number
|
|
}
|
|
|
|
const STATUS_CONFIG: Record<string, { bg: string; text: string; label: string }> = {
|
|
not_started: { bg: 'bg-slate-100', text: 'text-slate-700', label: 'Nicht begonnen' },
|
|
in_progress: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'In Arbeit' },
|
|
implemented: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Implementiert' },
|
|
verified: { bg: 'bg-green-100', text: 'text-green-700', label: 'Verifiziert' },
|
|
not_applicable: { bg: 'bg-slate-50', text: 'text-slate-500', label: 'N/A' },
|
|
}
|
|
|
|
const PRIORITY_CONFIG: Record<number, { bg: string; text: string; label: string }> = {
|
|
1: { bg: 'bg-red-100', text: 'text-red-700', label: 'Kritisch' },
|
|
2: { bg: 'bg-orange-100', text: 'text-orange-700', label: 'Hoch' },
|
|
3: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Mittel' },
|
|
}
|
|
|
|
const REGULATION_TYPE_LABELS: Record<string, string> = {
|
|
eu_regulation: 'EU-Verordnung',
|
|
eu_directive: 'EU-Richtlinie',
|
|
de_law: 'DE Gesetz',
|
|
bsi_standard: 'BSI Standard',
|
|
}
|
|
|
|
export default function RequirementsPage() {
|
|
const [regulations, setRegulations] = useState<Regulation[]>([])
|
|
const [requirements, setRequirements] = useState<Requirement[]>([])
|
|
const [selectedRegulation, setSelectedRegulation] = useState<string | null>(null)
|
|
const [selectedRequirement, setSelectedRequirement] = useState<Requirement | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [requirementsLoading, setRequirementsLoading] = useState(false)
|
|
|
|
// Filters
|
|
const [statusFilter, setStatusFilter] = useState<string>('')
|
|
const [priorityFilter, setPriorityFilter] = useState<string>('')
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
useEffect(() => {
|
|
loadRegulations()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (selectedRegulation) {
|
|
loadRequirements(selectedRegulation)
|
|
}
|
|
}, [selectedRegulation])
|
|
|
|
const loadRegulations = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch('/api/admin/compliance/regulations')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setRegulations(data.regulations || data || [])
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load regulations:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadRequirements = async (regulationCode: string) => {
|
|
setRequirementsLoading(true)
|
|
try {
|
|
const params = new URLSearchParams({ regulation_code: regulationCode })
|
|
if (statusFilter) params.set('status', statusFilter)
|
|
if (priorityFilter) params.set('priority', priorityFilter)
|
|
if (searchQuery) params.set('search', searchQuery)
|
|
|
|
const res = await fetch(`/api/admin/compliance/requirements?${params}`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setRequirements(data.requirements || data || [])
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load requirements:', err)
|
|
} finally {
|
|
setRequirementsLoading(false)
|
|
}
|
|
}
|
|
|
|
const filteredRequirements = requirements.filter(req => {
|
|
if (statusFilter && req.implementation_status !== statusFilter) return false
|
|
if (priorityFilter && req.priority !== parseInt(priorityFilter)) return false
|
|
if (searchQuery) {
|
|
const query = searchQuery.toLowerCase()
|
|
return (
|
|
req.title.toLowerCase().includes(query) ||
|
|
req.article.toLowerCase().includes(query) ||
|
|
req.description?.toLowerCase().includes(query)
|
|
)
|
|
}
|
|
return true
|
|
})
|
|
|
|
const totalRequirements = regulations.reduce((sum, r) => sum + (r.requirement_count || 0), 0)
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PagePurpose
|
|
title="Requirements & Anforderungen"
|
|
purpose="Uebersicht aller 558+ Compliance-Anforderungen aus 19 Verordnungen (DSGVO, AI Act, CRA, BSI-TR-03161, etc.). Sehen Sie den Implementation-Status und wie Breakpilot jede Anforderung erfuellt."
|
|
audience={['DSB', 'Compliance Officer', 'Entwickler', 'Auditoren']}
|
|
gdprArticles={['Art. 5 (Rechenschaftspflicht)', 'Art. 24 (Verantwortung)']}
|
|
architecture={{
|
|
services: ['Python Backend', 'PostgreSQL'],
|
|
databases: ['compliance_regulations', 'compliance_requirements', 'compliance_control_mappings'],
|
|
}}
|
|
relatedPages={[
|
|
{ name: 'Compliance Hub', href: '/compliance/hub', description: 'Dashboard & Uebersicht' },
|
|
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Massnahmen' },
|
|
{ name: 'Audit Checklist', href: '/compliance/audit-checklist', description: 'Audit durchfuehren' },
|
|
]}
|
|
/>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-xl shadow-sm border p-4">
|
|
<p className="text-3xl font-bold text-purple-600">{regulations.length}</p>
|
|
<p className="text-sm text-slate-600">Verordnungen</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl shadow-sm border p-4">
|
|
<p className="text-3xl font-bold text-blue-600">{totalRequirements}</p>
|
|
<p className="text-sm text-slate-600">Anforderungen</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl shadow-sm border p-4">
|
|
<p className="text-3xl font-bold text-green-600">
|
|
{regulations.filter(r => r.regulation_type === 'eu_regulation').length}
|
|
</p>
|
|
<p className="text-sm text-slate-600">EU-Verordnungen</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl shadow-sm border p-4">
|
|
<p className="text-3xl font-bold text-orange-600">
|
|
{regulations.filter(r => r.regulation_type === 'bsi_standard').length}
|
|
</p>
|
|
<p className="text-sm text-slate-600">BSI Standards</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Regulations List */}
|
|
<div className="lg:col-span-1 space-y-4">
|
|
<h2 className="text-lg font-semibold text-slate-900">Verordnungen & Standards</h2>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 max-h-[70vh] overflow-y-auto pr-2">
|
|
{regulations.map((reg) => (
|
|
<div
|
|
key={reg.id}
|
|
onClick={() => setSelectedRegulation(reg.code)}
|
|
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
|
|
selectedRegulation === reg.code
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-slate-200 hover:border-slate-300 bg-white'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-mono font-bold text-purple-600">{reg.code}</span>
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
|
reg.regulation_type === 'eu_regulation' ? 'bg-blue-100 text-blue-700' :
|
|
reg.regulation_type === 'eu_directive' ? 'bg-indigo-100 text-indigo-700' :
|
|
reg.regulation_type === 'bsi_standard' ? 'bg-orange-100 text-orange-700' :
|
|
'bg-slate-100 text-slate-700'
|
|
}`}>
|
|
{REGULATION_TYPE_LABELS[reg.regulation_type] || reg.regulation_type}
|
|
</span>
|
|
</div>
|
|
<h3 className="font-medium text-slate-900 text-sm">{reg.name}</h3>
|
|
<p className="text-xs text-slate-500 mt-1 line-clamp-2">{reg.description}</p>
|
|
|
|
<div className="flex items-center justify-between mt-3">
|
|
<span className="text-xs text-slate-600">
|
|
{reg.requirement_count || 0} Anforderungen
|
|
</span>
|
|
{reg.source_url && (
|
|
<a
|
|
href={reg.source_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="text-xs text-purple-600 hover:text-purple-700 flex items-center gap-1"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
Original
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Requirements List */}
|
|
<div className="lg:col-span-2 space-y-4">
|
|
{!selectedRegulation ? (
|
|
<div className="bg-white rounded-xl shadow-sm border p-12 text-center">
|
|
<svg className="w-16 h-16 mx-auto text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<h3 className="text-lg font-medium text-slate-900">Waehlen Sie eine Verordnung</h3>
|
|
<p className="text-slate-500 mt-2">Waehlen Sie eine Verordnung aus der Liste um deren Anforderungen zu sehen.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-4 items-center">
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Suche in Anforderungen..."
|
|
className="flex-1 min-w-[200px] px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
<select
|
|
value={statusFilter}
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
|
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
<option value="">Alle Status</option>
|
|
<option value="not_started">Nicht begonnen</option>
|
|
<option value="in_progress">In Arbeit</option>
|
|
<option value="implemented">Implementiert</option>
|
|
<option value="verified">Verifiziert</option>
|
|
<option value="not_applicable">N/A</option>
|
|
</select>
|
|
<select
|
|
value={priorityFilter}
|
|
onChange={(e) => setPriorityFilter(e.target.value)}
|
|
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
<option value="">Alle Prioritaeten</option>
|
|
<option value="1">Kritisch</option>
|
|
<option value="2">Hoch</option>
|
|
<option value="3">Mittel</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Requirements Table */}
|
|
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
|
{requirementsLoading ? (
|
|
<div className="flex justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
) : filteredRequirements.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-500">
|
|
{requirements.length === 0 ? (
|
|
<>
|
|
<p className="font-medium">Keine Anforderungen gefunden</p>
|
|
<p className="text-sm mt-2">Starten Sie den Scraper um Anforderungen zu extrahieren.</p>
|
|
<button
|
|
onClick={() => {/* TODO: Trigger scraper */}}
|
|
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
|
>
|
|
Anforderungen extrahieren
|
|
</button>
|
|
</>
|
|
) : (
|
|
<p>Keine Anforderungen entsprechen den Filterkriterien</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-slate-200">
|
|
{filteredRequirements.map((req) => {
|
|
const statusConfig = STATUS_CONFIG[req.implementation_status] || STATUS_CONFIG.not_started
|
|
const priorityConfig = PRIORITY_CONFIG[req.priority] || PRIORITY_CONFIG[2]
|
|
|
|
return (
|
|
<div
|
|
key={req.id}
|
|
className={`p-4 hover:bg-slate-50 cursor-pointer transition-colors ${
|
|
selectedRequirement?.id === req.id ? 'bg-purple-50' : ''
|
|
}`}
|
|
onClick={() => setSelectedRequirement(selectedRequirement?.id === req.id ? null : req)}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-mono text-sm font-medium text-purple-600">
|
|
{req.article}
|
|
</span>
|
|
{req.paragraph && (
|
|
<span className="text-xs text-slate-500">({req.paragraph})</span>
|
|
)}
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${priorityConfig.bg} ${priorityConfig.text}`}>
|
|
{priorityConfig.label}
|
|
</span>
|
|
</div>
|
|
<h4 className="font-medium text-slate-900">{req.title}</h4>
|
|
{req.description && (
|
|
<p className="text-sm text-slate-600 mt-1 line-clamp-2">{req.description}</p>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col items-end gap-2 ml-4">
|
|
<span className={`px-3 py-1 text-xs rounded-full ${statusConfig.bg} ${statusConfig.text}`}>
|
|
{statusConfig.label}
|
|
</span>
|
|
{req.controls_count > 0 && (
|
|
<span className="text-xs text-slate-500">
|
|
{req.controls_count} Controls
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded Details */}
|
|
{selectedRequirement?.id === req.id && (
|
|
<div className="mt-4 pt-4 border-t border-slate-200 space-y-4">
|
|
{req.requirement_text && (
|
|
<div>
|
|
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Originaltext</h5>
|
|
<p className="text-sm text-slate-700 bg-slate-50 p-3 rounded-lg">
|
|
{req.requirement_text}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{req.breakpilot_interpretation && (
|
|
<div>
|
|
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Breakpilot Interpretation</h5>
|
|
<p className="text-sm text-slate-700 bg-purple-50 p-3 rounded-lg border border-purple-100">
|
|
{req.breakpilot_interpretation}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{req.implementation_details && (
|
|
<div>
|
|
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Implementation</h5>
|
|
<p className="text-sm text-slate-700 bg-green-50 p-3 rounded-lg border border-green-100">
|
|
{req.implementation_details}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{req.code_references && req.code_references.length > 0 && (
|
|
<div>
|
|
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Code-Referenzen</h5>
|
|
<div className="space-y-1">
|
|
{req.code_references.map((ref, idx) => (
|
|
<div key={idx} className="text-sm font-mono bg-slate-100 p-2 rounded">
|
|
<span className="text-purple-600">{ref.file}</span>
|
|
{ref.line && <span className="text-slate-500">:{ref.line}</span>}
|
|
{ref.description && (
|
|
<span className="text-slate-600 ml-2">- {ref.description}</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{req.evidence_description && (
|
|
<div>
|
|
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Nachweis</h5>
|
|
<p className="text-sm text-slate-700">{req.evidence_description}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
<button className="px-3 py-1.5 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">
|
|
Bearbeiten
|
|
</button>
|
|
<button className="px-3 py-1.5 text-sm border border-purple-600 text-purple-600 rounded-lg hover:bg-purple-50">
|
|
Mit Claude interpretieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
{requirements.length > 0 && (
|
|
<div className="bg-slate-50 rounded-lg p-4 text-sm text-slate-600">
|
|
<p>
|
|
<strong>{filteredRequirements.length}</strong> von <strong>{requirements.length}</strong> Anforderungen angezeigt
|
|
{statusFilter && <span> (Status: {STATUS_CONFIG[statusFilter]?.label})</span>}
|
|
{priorityFilter && <span> (Prioritaet: {PRIORITY_CONFIG[parseInt(priorityFilter)]?.label})</span>}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|