This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(admin)/compliance/requirements/page.tsx
BreakPilot Dev 660295e218 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>
2026-02-08 23:40:15 -08:00

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>
)
}