feat: 7 Analyse-Module auf 100% — Backend-Endpoints, DB-Model, Frontend-Persistenz
Alle 7 Analyse-Module (Requirements → Report) von ~80% auf 100% gebracht: - Modul 1 (Requirements): POST/DELETE Endpoints + Frontend-Anbindung + Rollback - Modul 2 (Controls): Evidence-Linking UI mit Validity-Badge - Modul 3 (Evidence): Pagination (Frontend + Backend) - Modul 4 (Risk Matrix): Mitigation-UI, Residual Risk, Status-Workflow - Modul 5 (AI Act): AISystemDB Model, 6 CRUD-Endpoints, Backend-Persistenz - Modul 6 (Audit Checklist): PDF-Download + Session-History - Modul 7 (Audit Report): Detail-Seite mit Checklist Sign-Off + Navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -257,12 +257,14 @@ function AddRequirementForm({
|
||||
function RequirementCard({
|
||||
requirement,
|
||||
onStatusChange,
|
||||
onDelete,
|
||||
expanded,
|
||||
onToggleDetails,
|
||||
linkedControls,
|
||||
}: {
|
||||
requirement: DisplayRequirement
|
||||
onStatusChange: (status: RequirementStatus) => void
|
||||
onDelete: () => void
|
||||
expanded: boolean
|
||||
onToggleDetails: () => void
|
||||
linkedControls: { id: string; name: string }[]
|
||||
@@ -345,19 +347,27 @@ function RequirementCard({
|
||||
<p className="text-sm text-gray-400">Keine Kontrollen zugeordnet</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Status-Historie</h4>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
requirement.displayStatus === 'compliant' ? 'bg-green-100 text-green-700' :
|
||||
requirement.displayStatus === 'partial' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{requirement.status === 'NOT_STARTED' ? 'Nicht begonnen' :
|
||||
requirement.status === 'IN_PROGRESS' ? 'In Bearbeitung' :
|
||||
requirement.status === 'IMPLEMENTED' ? 'Implementiert' : 'Verifiziert'}
|
||||
</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Status-Historie</h4>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
requirement.displayStatus === 'compliant' ? 'bg-green-100 text-green-700' :
|
||||
requirement.displayStatus === 'partial' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{requirement.status === 'NOT_STARTED' ? 'Nicht begonnen' :
|
||||
requirement.status === 'IN_PROGRESS' ? 'In Bearbeitung' :
|
||||
requirement.status === 'IMPLEMENTED' ? 'Implementiert' : 'Verifiziert'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 border border-red-200 rounded-lg transition-colors"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -493,6 +503,7 @@ export default function RequirementsPage() {
|
||||
const nonCompliantCount = displayRequirements.filter(r => r.displayStatus === 'non-compliant').length
|
||||
|
||||
const handleStatusChange = async (requirementId: string, status: RequirementStatus) => {
|
||||
const previousStatus = state.requirements.find(r => r.id === requirementId)?.status
|
||||
dispatch({
|
||||
type: 'UPDATE_REQUIREMENT',
|
||||
payload: { id: requirementId, data: { status } },
|
||||
@@ -500,17 +511,94 @@ export default function RequirementsPage() {
|
||||
|
||||
// Persist to backend
|
||||
try {
|
||||
await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status }),
|
||||
body: JSON.stringify({ implementation_status: status.toLowerCase() }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
// Rollback on failure
|
||||
if (previousStatus) {
|
||||
dispatch({ type: 'UPDATE_REQUIREMENT', payload: { id: requirementId, data: { status: previousStatus } } })
|
||||
}
|
||||
setError('Status-Aenderung konnte nicht gespeichert werden')
|
||||
}
|
||||
} catch {
|
||||
// Silently fail — SDK state is already updated
|
||||
if (previousStatus) {
|
||||
dispatch({ type: 'UPDATE_REQUIREMENT', payload: { id: requirementId, data: { status: previousStatus } } })
|
||||
}
|
||||
setError('Backend nicht erreichbar — Aenderung zurueckgesetzt')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddRequirement = (data: { regulation: string; article: string; title: string; description: string; criticality: RiskSeverity }) => {
|
||||
const handleDeleteRequirement = async (requirementId: string) => {
|
||||
if (!confirm('Anforderung wirklich loeschen?')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (res.ok) {
|
||||
dispatch({ type: 'SET_STATE', payload: { requirements: state.requirements.filter(r => r.id !== requirementId) } })
|
||||
} else {
|
||||
setError('Loeschen fehlgeschlagen')
|
||||
}
|
||||
} catch {
|
||||
setError('Backend nicht erreichbar')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddRequirement = async (data: { regulation: string; article: string; title: string; description: string; criticality: RiskSeverity }) => {
|
||||
// Try to resolve regulation_id from backend
|
||||
let regulationId = ''
|
||||
try {
|
||||
const regRes = await fetch(`/api/sdk/v1/compliance/regulations/${data.regulation}`)
|
||||
if (regRes.ok) {
|
||||
const regData = await regRes.json()
|
||||
regulationId = regData.id
|
||||
}
|
||||
} catch {
|
||||
// Regulation not found — still add locally
|
||||
}
|
||||
|
||||
const priorityMap: Record<string, number> = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 }
|
||||
|
||||
if (regulationId) {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/compliance/requirements', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
regulation_id: regulationId,
|
||||
article: data.article,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
priority: priorityMap[data.criticality] || 2,
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
const created = await res.json()
|
||||
const newReq: SDKRequirement = {
|
||||
id: created.id,
|
||||
regulation: data.regulation,
|
||||
article: data.article,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
criticality: data.criticality,
|
||||
applicableModules: [],
|
||||
status: 'NOT_STARTED',
|
||||
controls: [],
|
||||
}
|
||||
dispatch({ type: 'ADD_REQUIREMENT', payload: newReq })
|
||||
setShowAddForm(false)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Fall through to local-only add
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: add locally only
|
||||
const newReq: SDKRequirement = {
|
||||
id: `req-${Date.now()}`,
|
||||
regulation: data.regulation,
|
||||
@@ -651,6 +739,7 @@ export default function RequirementsPage() {
|
||||
key={requirement.id}
|
||||
requirement={requirement}
|
||||
onStatusChange={(status) => handleStatusChange(requirement.id, status)}
|
||||
onDelete={() => handleDeleteRequirement(requirement.id)}
|
||||
expanded={expandedId === requirement.id}
|
||||
onToggleDetails={() => setExpandedId(expandedId === requirement.id ? null : requirement.id)}
|
||||
linkedControls={linkedControls}
|
||||
|
||||
Reference in New Issue
Block a user