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>
872 lines
36 KiB
TypeScript
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>
|
|
)
|
|
}
|