Files
breakpilot-lehrer/website/app/admin/compliance/audit-workspace/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

872 lines
36 KiB
TypeScript

'use client'
/**
* Audit Workspace - Collaborative Audit Management
*
* Features:
* - View all requirements with original text from regulations
* - Document implementation details for each requirement
* - Link to source documents (PDFs, EUR-Lex)
* - Track audit status (pending, in_review, approved, rejected)
* - Add code references and evidence
* - Auditor notes and sign-off
*/
import { useState, useEffect } from 'react'
import Link from 'next/link'
import AdminLayout from '@/components/admin/AdminLayout'
// Types
interface Regulation {
id: string
code: string
name: string
full_name: string
regulation_type: string
source_url: string | null
local_pdf_path: string | null
requirement_count: number
}
interface Requirement {
id: string
regulation_id: string
regulation_code?: string
article: string
paragraph: string | null
title: string
description: string | null
requirement_text: string | null
breakpilot_interpretation: string | null
implementation_status: string
implementation_details: string | null
code_references: Array<{ file: string; line?: number; description: string }> | null
evidence_description: string | null
audit_status: string
auditor_notes: string | null
is_applicable: boolean
applicability_reason: string | null
priority: number
source_page: number | null
source_section: string | null
}
interface RequirementUpdate {
implementation_status?: string
implementation_details?: string
code_references?: Array<{ file: string; line?: number; description: string }>
evidence_description?: string
audit_status?: string
auditor_notes?: string
is_applicable?: boolean
applicability_reason?: string
}
const IMPLEMENTATION_STATUS = {
not_started: { label: 'Nicht gestartet', color: 'bg-slate-400' },
in_progress: { label: 'In Arbeit', color: 'bg-yellow-500' },
implemented: { label: 'Implementiert', color: 'bg-blue-500' },
verified: { label: 'Verifiziert', color: 'bg-green-500' },
}
const AUDIT_STATUS = {
pending: { label: 'Ausstehend', color: 'bg-slate-400' },
in_review: { label: 'In Pruefung', color: 'bg-yellow-500' },
approved: { label: 'Genehmigt', color: 'bg-green-500' },
rejected: { label: 'Abgelehnt', color: 'bg-red-500' },
}
const PRIORITY_LABELS: Record<number, { label: string; color: string }> = {
1: { label: 'Kritisch', color: 'text-red-600' },
2: { label: 'Hoch', color: 'text-orange-600' },
3: { label: 'Mittel', color: 'text-yellow-600' },
}
export default function AuditWorkspacePage() {
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 [saving, setSaving] = useState(false)
const [filterAuditStatus, setFilterAuditStatus] = useState<string>('all')
const [filterImplStatus, setFilterImplStatus] = useState<string>('all')
const [searchQuery, setSearchQuery] = useState('')
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
useEffect(() => {
loadRegulations()
}, [])
useEffect(() => {
if (selectedRegulation) {
loadRequirements(selectedRegulation)
}
}, [selectedRegulation])
const loadRegulations = async () => {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations`)
if (res.ok) {
const data = await res.json()
setRegulations(data.regulations || [])
// Select first regulation by default
if (data.regulations?.length > 0) {
setSelectedRegulation(data.regulations[0].code)
}
}
} catch (err) {
console.error('Failed to load regulations:', err)
} finally {
setLoading(false)
}
}
const loadRequirements = async (regCode: string) => {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations/${regCode}/requirements`)
if (res.ok) {
const data = await res.json()
setRequirements(data.requirements || [])
}
} catch (err) {
console.error('Failed to load requirements:', err)
}
}
const updateRequirement = async (reqId: string, updates: RequirementUpdate) => {
setSaving(true)
try {
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/requirements/${reqId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
})
if (res.ok) {
const updated = await res.json()
setRequirements(prev => prev.map(r => r.id === reqId ? { ...r, ...updates } : r))
if (selectedRequirement?.id === reqId) {
setSelectedRequirement({ ...selectedRequirement, ...updates })
}
}
} catch (err) {
console.error('Failed to update requirement:', err)
} finally {
setSaving(false)
}
}
const filteredRequirements = requirements.filter(req => {
if (filterAuditStatus !== 'all' && req.audit_status !== filterAuditStatus) return false
if (filterImplStatus !== 'all' && req.implementation_status !== filterImplStatus) return false
if (searchQuery) {
const query = searchQuery.toLowerCase()
return (
req.title.toLowerCase().includes(query) ||
req.article.toLowerCase().includes(query) ||
req.requirement_text?.toLowerCase().includes(query)
)
}
return true
})
const currentRegulation = regulations.find(r => r.code === selectedRegulation)
// Statistics
const stats = {
total: requirements.length,
verified: requirements.filter(r => r.implementation_status === 'verified').length,
approved: requirements.filter(r => r.audit_status === 'approved').length,
pending: requirements.filter(r => r.audit_status === 'pending').length,
}
return (
<AdminLayout title="Audit Workspace" description="Gemeinsam mit Pruefern arbeiten">
{/* Header with back link */}
<div className="mb-6 flex items-center justify-between">
<Link
href="/admin/compliance"
className="text-sm text-slate-600 hover:text-slate-900 flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zu Compliance
</Link>
<div className="flex items-center gap-4">
<div className="text-sm text-slate-500">
{stats.approved}/{stats.total} genehmigt | {stats.verified}/{stats.total} verifiziert
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-6">
{/* Left Sidebar - Regulation & Requirement List */}
<div className="col-span-4 space-y-4">
{/* Regulation Selector */}
<div className="bg-white rounded-lg border border-slate-200 p-4">
<label className="block text-sm font-medium text-slate-700 mb-2">
Verordnung / Standard
</label>
<select
value={selectedRegulation || ''}
onChange={(e) => setSelectedRegulation(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
{regulations.map(reg => (
<option key={reg.code} value={reg.code}>
{reg.code} - {reg.name} ({reg.requirement_count})
</option>
))}
</select>
{currentRegulation?.source_url && (
<a
href={currentRegulation.source_url}
target="_blank"
rel="noopener noreferrer"
className="mt-2 text-sm text-primary-600 hover:text-primary-800 flex items-center gap-1"
>
<svg className="w-4 h-4" 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>
Originaldokument oeffnen
</a>
)}
{currentRegulation?.local_pdf_path && (
<a
href={`/docs/${currentRegulation.local_pdf_path}`}
target="_blank"
className="mt-1 text-sm text-slate-600 hover:text-slate-800 flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Lokale PDF
</a>
)}
</div>
{/* Filters */}
<div className="bg-white rounded-lg border border-slate-200 p-4 space-y-3">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Suche</label>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Artikel, Titel..."
className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Audit-Status</label>
<select
value={filterAuditStatus}
onChange={(e) => setFilterAuditStatus(e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-slate-300 rounded-lg"
>
<option value="all">Alle</option>
{Object.entries(AUDIT_STATUS).map(([key, { label }]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Impl.-Status</label>
<select
value={filterImplStatus}
onChange={(e) => setFilterImplStatus(e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-slate-300 rounded-lg"
>
<option value="all">Alle</option>
{Object.entries(IMPLEMENTATION_STATUS).map(([key, { label }]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
</div>
</div>
{/* Requirements List */}
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="p-3 border-b border-slate-200 bg-slate-50">
<span className="text-sm font-medium text-slate-700">
Anforderungen ({filteredRequirements.length})
</span>
</div>
<div className="max-h-[500px] overflow-y-auto">
{filteredRequirements.map(req => (
<button
key={req.id}
onClick={() => setSelectedRequirement(req)}
className={`w-full text-left p-3 border-b border-slate-100 hover:bg-slate-50 transition-colors ${
selectedRequirement?.id === req.id ? 'bg-primary-50 border-l-4 border-l-primary-500' : ''
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-slate-600">
{req.article}{req.paragraph ? ` ${req.paragraph}` : ''}
</span>
<span className={`text-xs ${PRIORITY_LABELS[req.priority]?.color || 'text-slate-500'}`}>
{PRIORITY_LABELS[req.priority]?.label || ''}
</span>
</div>
<p className="text-sm text-slate-900 truncate mt-0.5">{req.title}</p>
</div>
<div className="flex flex-col items-end gap-1">
<span className={`w-2 h-2 rounded-full ${AUDIT_STATUS[req.audit_status as keyof typeof AUDIT_STATUS]?.color || 'bg-slate-300'}`} />
<span className={`w-2 h-2 rounded-full ${IMPLEMENTATION_STATUS[req.implementation_status as keyof typeof IMPLEMENTATION_STATUS]?.color || 'bg-slate-300'}`} />
</div>
</div>
</button>
))}
</div>
</div>
</div>
{/* Right Panel - Requirement Detail */}
<div className="col-span-8">
{selectedRequirement ? (
<RequirementDetailPanel
requirement={selectedRequirement}
regulation={currentRegulation}
onUpdate={(updates) => updateRequirement(selectedRequirement.id, updates)}
saving={saving}
/>
) : (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center">
<svg className="w-12 h-12 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>
<p className="text-slate-500">Waehlen Sie eine Anforderung aus der Liste</p>
</div>
)}
</div>
</div>
</AdminLayout>
)
}
// AI Interpretation Types
interface AIInterpretation {
summary: string
applicability: string
technical_measures: string[]
affected_modules: string[]
risk_level: string
implementation_hints: string[]
confidence_score: number
error?: string
}
// Requirement Detail Panel Component
function RequirementDetailPanel({
requirement,
regulation,
onUpdate,
saving,
}: {
requirement: Requirement
regulation: Regulation | undefined
onUpdate: (updates: RequirementUpdate) => void
saving: boolean
}) {
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
const [editMode, setEditMode] = useState(false)
const [aiLoading, setAiLoading] = useState(false)
const [aiInterpretation, setAiInterpretation] = useState<AIInterpretation | null>(null)
const [showAiPanel, setShowAiPanel] = useState(false)
const [localData, setLocalData] = useState({
implementation_status: requirement.implementation_status,
implementation_details: requirement.implementation_details || '',
evidence_description: requirement.evidence_description || '',
audit_status: requirement.audit_status,
auditor_notes: requirement.auditor_notes || '',
is_applicable: requirement.is_applicable,
applicability_reason: requirement.applicability_reason || '',
})
const [newCodeRef, setNewCodeRef] = useState({ file: '', line: '', description: '' })
useEffect(() => {
setLocalData({
implementation_status: requirement.implementation_status,
implementation_details: requirement.implementation_details || '',
evidence_description: requirement.evidence_description || '',
audit_status: requirement.audit_status,
auditor_notes: requirement.auditor_notes || '',
is_applicable: requirement.is_applicable,
applicability_reason: requirement.applicability_reason || '',
})
setEditMode(false)
setAiInterpretation(null)
setShowAiPanel(false)
}, [requirement.id])
const generateAiInterpretation = async () => {
setAiLoading(true)
setShowAiPanel(true)
try {
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/ai/interpret`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requirement_id: requirement.id }),
})
if (res.ok) {
const data = await res.json()
setAiInterpretation(data)
} else {
const err = await res.json()
setAiInterpretation({
summary: '', applicability: '', technical_measures: [],
affected_modules: [], risk_level: 'unknown', implementation_hints: [],
confidence_score: 0, error: err.detail || 'Fehler bei AI-Analyse'
})
}
} catch (err) {
setAiInterpretation({
summary: '', applicability: '', technical_measures: [],
affected_modules: [], risk_level: 'unknown', implementation_hints: [],
confidence_score: 0, error: 'Netzwerkfehler bei AI-Analyse'
})
} finally {
setAiLoading(false)
}
}
const handleSave = () => {
onUpdate(localData)
setEditMode(false)
}
const addCodeReference = () => {
if (!newCodeRef.file) return
const refs = requirement.code_references || []
onUpdate({
code_references: [...refs, {
file: newCodeRef.file,
line: newCodeRef.line ? parseInt(newCodeRef.line) : undefined,
description: newCodeRef.description,
}],
})
setNewCodeRef({ file: '', line: '', description: '' })
}
return (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-slate-200 bg-slate-50">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold text-slate-900">
{requirement.article}{requirement.paragraph ? ` ${requirement.paragraph}` : ''}
</span>
<span className={`px-2 py-0.5 text-xs font-medium rounded ${
AUDIT_STATUS[requirement.audit_status as keyof typeof AUDIT_STATUS]?.color || 'bg-slate-200'
} text-white`}>
{AUDIT_STATUS[requirement.audit_status as keyof typeof AUDIT_STATUS]?.label || requirement.audit_status}
</span>
<span className={`px-2 py-0.5 text-xs font-medium rounded ${
IMPLEMENTATION_STATUS[requirement.implementation_status as keyof typeof IMPLEMENTATION_STATUS]?.color || 'bg-slate-200'
} text-white`}>
{IMPLEMENTATION_STATUS[requirement.implementation_status as keyof typeof IMPLEMENTATION_STATUS]?.label || requirement.implementation_status}
</span>
</div>
<h2 className="text-lg font-medium text-slate-900 mt-1">{requirement.title}</h2>
</div>
<div className="flex gap-2">
{editMode ? (
<>
<button
onClick={() => setEditMode(false)}
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-800"
>
Abbrechen
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</>
) : (
<button
onClick={() => setEditMode(true)}
className="px-3 py-1.5 text-sm bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200"
>
Bearbeiten
</button>
)}
</div>
</div>
</div>
<div className="p-4 space-y-6">
{/* Original Requirement Text */}
<section>
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide mb-2 flex items-center gap-2">
<svg className="w-4 h-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>
Originaler Anforderungstext
</h3>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4">
<p className="text-sm text-slate-700 whitespace-pre-wrap">
{requirement.requirement_text || 'Kein Originaltext hinterlegt'}
</p>
{requirement.source_page && (
<p className="text-xs text-slate-500 mt-2">
Quelle: {regulation?.code} Seite {requirement.source_page}
{requirement.source_section ? `, ${requirement.source_section}` : ''}
</p>
)}
</div>
</section>
{/* Applicability */}
<section>
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide mb-2">
Anwendbarkeit auf Breakpilot
</h3>
{editMode ? (
<div className="space-y-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={localData.is_applicable}
onChange={(e) => setLocalData({ ...localData, is_applicable: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-slate-700">Anwendbar</span>
</label>
<textarea
value={localData.applicability_reason}
onChange={(e) => setLocalData({ ...localData, applicability_reason: e.target.value })}
placeholder="Begruendung fuer Anwendbarkeit..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
rows={2}
/>
</div>
) : (
<div className="flex items-start gap-3">
<span className={`px-2 py-1 text-xs font-medium rounded ${
requirement.is_applicable ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'
}`}>
{requirement.is_applicable ? 'Anwendbar' : 'Nicht anwendbar'}
</span>
{requirement.applicability_reason && (
<p className="text-sm text-slate-600">{requirement.applicability_reason}</p>
)}
</div>
)}
</section>
{/* Breakpilot Interpretation & AI Analysis */}
<section>
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Interpretation
</h3>
<button
onClick={generateAiInterpretation}
disabled={aiLoading}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-gradient-to-r from-purple-600 to-pink-600 rounded-lg hover:from-purple-700 hover:to-pink-700 disabled:opacity-50 transition-all"
>
{aiLoading ? (
<>
<svg className="animate-spin h-3 w-3" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
AI analysiert...
</>
) : (
<>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
AI Analyse
</>
)}
</button>
</div>
{/* Existing interpretation */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-3">
<p className="text-sm text-blue-800">
{requirement.breakpilot_interpretation || 'Keine Interpretation hinterlegt'}
</p>
</div>
{/* AI Interpretation Panel */}
{showAiPanel && (
<div className="bg-gradient-to-br from-purple-50 to-pink-50 border border-purple-200 rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold text-purple-800 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
AI-generierte Analyse
</h4>
{aiInterpretation?.confidence_score && (
<span className="text-xs text-purple-600">
Konfidenz: {Math.round(aiInterpretation.confidence_score * 100)}%
</span>
)}
</div>
{aiLoading && (
<div className="text-center py-4">
<div className="animate-pulse text-purple-600">Claude analysiert die Anforderung...</div>
</div>
)}
{aiInterpretation?.error && (
<div className="bg-red-100 text-red-700 p-3 rounded text-sm">
{aiInterpretation.error}
</div>
)}
{aiInterpretation && !aiInterpretation.error && !aiLoading && (
<div className="space-y-3 text-sm">
{/* Summary */}
{aiInterpretation.summary && (
<div>
<div className="font-medium text-purple-700 mb-1">Zusammenfassung</div>
<p className="text-slate-700">{aiInterpretation.summary}</p>
</div>
)}
{/* Applicability */}
{aiInterpretation.applicability && (
<div>
<div className="font-medium text-purple-700 mb-1">Anwendbarkeit auf Breakpilot</div>
<p className="text-slate-700">{aiInterpretation.applicability}</p>
</div>
)}
{/* Risk Level */}
{aiInterpretation.risk_level && (
<div className="flex items-center gap-2">
<span className="font-medium text-purple-700">Risiko:</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
aiInterpretation.risk_level === 'critical' ? 'bg-red-100 text-red-700' :
aiInterpretation.risk_level === 'high' ? 'bg-orange-100 text-orange-700' :
aiInterpretation.risk_level === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
{aiInterpretation.risk_level}
</span>
</div>
)}
{/* Technical Measures */}
{aiInterpretation.technical_measures?.length > 0 && (
<div>
<div className="font-medium text-purple-700 mb-1">Technische Massnahmen</div>
<ul className="list-disc list-inside text-slate-700 space-y-1">
{aiInterpretation.technical_measures.map((m, i) => (
<li key={i}>{m}</li>
))}
</ul>
</div>
)}
{/* Affected Modules */}
{aiInterpretation.affected_modules?.length > 0 && (
<div>
<div className="font-medium text-purple-700 mb-1">Betroffene Module</div>
<div className="flex flex-wrap gap-1">
{aiInterpretation.affected_modules.map((m, i) => (
<span key={i} className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">
{m}
</span>
))}
</div>
</div>
)}
{/* Implementation Hints */}
{aiInterpretation.implementation_hints?.length > 0 && (
<div>
<div className="font-medium text-purple-700 mb-1">Implementierungshinweise</div>
<ul className="list-disc list-inside text-slate-700 space-y-1">
{aiInterpretation.implementation_hints.map((h, i) => (
<li key={i}>{h}</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
)}
</section>
{/* Implementation Details */}
<section>
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide mb-2 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Umsetzung (fuer Auditor)
</h3>
{editMode ? (
<div className="space-y-3">
<div>
<label className="block text-xs text-slate-500 mb-1">Implementierungsstatus</label>
<select
value={localData.implementation_status}
onChange={(e) => setLocalData({ ...localData, implementation_status: e.target.value })}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
{Object.entries(IMPLEMENTATION_STATUS).map(([key, { label }]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<textarea
value={localData.implementation_details}
onChange={(e) => setLocalData({ ...localData, implementation_details: e.target.value })}
placeholder="Beschreiben Sie, wie diese Anforderung in Breakpilot umgesetzt wurde..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
rows={4}
/>
</div>
) : (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-sm text-green-800 whitespace-pre-wrap">
{requirement.implementation_details || 'Noch keine Umsetzungsdetails dokumentiert'}
</p>
</div>
)}
</section>
{/* Code References */}
<section>
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide mb-2">
Code-Referenzen
</h3>
<div className="space-y-2">
{(requirement.code_references || []).map((ref, idx) => (
<div key={idx} className="flex items-center gap-2 bg-slate-50 p-2 rounded-lg text-sm">
<code className="text-primary-600">{ref.file}{ref.line ? `:${ref.line}` : ''}</code>
<span className="text-slate-500">-</span>
<span className="text-slate-700">{ref.description}</span>
</div>
))}
{editMode && (
<div className="flex gap-2">
<input
type="text"
value={newCodeRef.file}
onChange={(e) => setNewCodeRef({ ...newCodeRef, file: e.target.value })}
placeholder="Datei (z.B. backend/auth.py)"
className="flex-1 px-2 py-1.5 text-sm border border-slate-300 rounded"
/>
<input
type="text"
value={newCodeRef.line}
onChange={(e) => setNewCodeRef({ ...newCodeRef, line: e.target.value })}
placeholder="Zeile"
className="w-20 px-2 py-1.5 text-sm border border-slate-300 rounded"
/>
<input
type="text"
value={newCodeRef.description}
onChange={(e) => setNewCodeRef({ ...newCodeRef, description: e.target.value })}
placeholder="Beschreibung"
className="flex-1 px-2 py-1.5 text-sm border border-slate-300 rounded"
/>
<button
onClick={addCodeReference}
className="px-3 py-1.5 bg-primary-600 text-white rounded text-sm"
>
+
</button>
</div>
)}
</div>
</section>
{/* Evidence */}
<section>
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide mb-2 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
Nachweis / Evidence
</h3>
{editMode ? (
<textarea
value={localData.evidence_description}
onChange={(e) => setLocalData({ ...localData, evidence_description: e.target.value })}
placeholder="Welche Nachweise belegen die Erfuellung dieser Anforderung?"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
rows={3}
/>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-sm text-amber-800">
{requirement.evidence_description || 'Keine Nachweise beschrieben'}
</p>
</div>
)}
</section>
{/* Auditor Section */}
<section className="border-t border-slate-200 pt-4">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide mb-2 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Auditor-Bereich
</h3>
{editMode ? (
<div className="space-y-3">
<div>
<label className="block text-xs text-slate-500 mb-1">Audit-Status</label>
<select
value={localData.audit_status}
onChange={(e) => setLocalData({ ...localData, audit_status: e.target.value })}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
{Object.entries(AUDIT_STATUS).map(([key, { label }]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<textarea
value={localData.auditor_notes}
onChange={(e) => setLocalData({ ...localData, auditor_notes: e.target.value })}
placeholder="Notizen des Auditors..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
rows={3}
/>
</div>
) : (
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4">
<p className="text-sm text-slate-700">
{requirement.auditor_notes || 'Keine Auditor-Notizen'}
</p>
</div>
)}
</section>
</div>
</div>
)
}