fix(admin-v2): Restore complete admin-v2 application
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>
This commit is contained in:
446
admin-v2/app/(admin)/compliance/requirements/page.tsx
Normal file
446
admin-v2/app/(admin)/compliance/requirements/page.tsx
Normal file
@@ -0,0 +1,446 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user