feat: Fortschritts-Tracker + Verifikation-Endpoints + Tech-File Erweiterung
- Übersicht: Completeness Gates durch Projektfortschritts-Tracker ersetzt (6 CE-Prozessschritte mit Status + Naechster-Schritt Empfehlung) - Verifikation: GET/POST/DELETE /verifications Endpoints + Alias-Handler - Tech-File: Anhang IV Struktur-Erweiterung - Maßnahmen: Expandable Details vorbereitet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ interface ProjectOverview {
|
||||
completeness_pct: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
gates: Gate[]
|
||||
metadata?: { limits_form?: Record<string, unknown> }
|
||||
risk_summary?: {
|
||||
critical?: number
|
||||
high?: number
|
||||
@@ -28,14 +28,6 @@ interface ProjectOverview {
|
||||
mitigation_count: number
|
||||
}
|
||||
|
||||
interface Gate {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
passed: boolean | null
|
||||
required: boolean
|
||||
}
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
{ href: '/components', label: 'Komponenten verwalten', icon: 'cube', description: 'SW/FW/AI/HMI Baum bearbeiten' },
|
||||
{ href: '/classification', label: 'Klassifikation pruefen', icon: 'tag', description: 'AI Act, MVO, CRA, NIS2' },
|
||||
@@ -47,33 +39,6 @@ const QUICK_ACTIONS = [
|
||||
{ href: '/monitoring', label: 'Monitoring', icon: 'activity', description: 'Post-Market Ueberwachung' },
|
||||
]
|
||||
|
||||
function GateIndicator({ gate }: { gate: Gate }) {
|
||||
const color = gate.passed === true
|
||||
? 'bg-green-500'
|
||||
: gate.passed === false
|
||||
? 'bg-red-500'
|
||||
: 'bg-gray-300'
|
||||
|
||||
const textColor = gate.passed === true
|
||||
? 'text-green-700'
|
||||
: gate.passed === false
|
||||
? 'text-red-700'
|
||||
: 'text-gray-500'
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className={`w-3 h-3 rounded-full ${color} flex-shrink-0`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm font-medium ${textColor}`}>{gate.name}</div>
|
||||
<div className="text-xs text-gray-400">{gate.description}</div>
|
||||
</div>
|
||||
{gate.required && (
|
||||
<span className="text-xs text-gray-400 flex-shrink-0">Pflicht</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RiskGauge({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
|
||||
const pct = max > 0 ? Math.round((value / max) * 100) : 0
|
||||
return (
|
||||
@@ -150,12 +115,15 @@ export default function ProjectOverviewPage() {
|
||||
mitCount = live.total_mitigations || 0
|
||||
}
|
||||
|
||||
// Calculate dynamic completeness percentage
|
||||
// Calculate dynamic completeness percentage from CE process steps
|
||||
const compCount = json.components?.length || 0
|
||||
const gates = (json.completeness_gates || json.gates || [])
|
||||
const gatesPassed = gates.filter((g: Record<string, unknown>) => g.passed === true).length
|
||||
const gatesTotal = gates.length || 1
|
||||
const completeness = Math.round((gatesPassed / gatesTotal) * 100)
|
||||
const limitsForm = json.metadata?.limits_form || {}
|
||||
const hasLimits = Object.keys(limitsForm).length > 0
|
||||
const hasComponents = compCount > 0
|
||||
const hasHazards = hazCount > 0
|
||||
const hasMitigations = mitCount > 0
|
||||
const stepsComplete = [hasLimits, hasComponents, hasHazards, hasMitigations].filter(Boolean).length
|
||||
const completeness = Math.round((stepsComplete / 6) * 100)
|
||||
|
||||
setProject({
|
||||
...json,
|
||||
@@ -163,6 +131,7 @@ export default function ProjectOverviewPage() {
|
||||
component_count: compCount,
|
||||
hazard_count: hazCount,
|
||||
mitigation_count: mitCount,
|
||||
metadata: json.metadata,
|
||||
risk_summary: {
|
||||
critical: rs.critical || 0,
|
||||
high: rs.high || 0,
|
||||
@@ -170,13 +139,6 @@ export default function ProjectOverviewPage() {
|
||||
low: rs.low || 0,
|
||||
total: rs.total || hazCount,
|
||||
},
|
||||
gates: gates.map((g: Record<string, unknown>) => ({
|
||||
id: g.id,
|
||||
name: g.name || g.label || '',
|
||||
description: g.description || g.details || '',
|
||||
passed: g.passed,
|
||||
required: g.required,
|
||||
})),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch project:', err)
|
||||
@@ -327,15 +289,44 @@ export default function ProjectOverviewPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Completeness Gates */}
|
||||
{/* Progress Tracker */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">Completeness Gates</h2>
|
||||
<div className="space-y-1">
|
||||
{project.gates && project.gates.length > 0 ? (
|
||||
project.gates.map((gate) => <GateIndicator key={gate.id} gate={gate} />)
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">Keine Gates definiert</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Projektfortschritt</h2>
|
||||
<span className="text-sm font-bold text-purple-600">{project.completeness_pct}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mb-4">
|
||||
<div className="bg-purple-500 h-2.5 rounded-full transition-all" style={{ width: `${project.completeness_pct}%` }} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{(() => {
|
||||
const hasLimits = Object.keys(project.metadata?.limits_form || {}).length > 0
|
||||
const steps = [
|
||||
{ done: hasLimits, label: 'Grenzen definiert', detail: hasLimits ? 'Felder ausgefuellt' : 'ausstehend' },
|
||||
{ done: project.component_count > 0, label: 'Komponenten erfasst', detail: `${project.component_count} Komponenten` },
|
||||
{ done: project.hazard_count > 0, label: 'Gefaehrdungen identifiziert', detail: `${project.hazard_count} bewertet` },
|
||||
{ done: project.mitigation_count > 0, label: 'Massnahmen zugeordnet', detail: `${project.mitigation_count} Massnahmen` },
|
||||
{ done: false, label: 'Verifikation', detail: 'ausstehend' },
|
||||
{ done: false, label: 'CE-Akte', detail: 'ausstehend' },
|
||||
]
|
||||
const firstPending = steps.find((s) => !s.done)
|
||||
return (
|
||||
<>
|
||||
{steps.map((step) => (
|
||||
<div key={step.label} className="flex items-center gap-2">
|
||||
<span className={`text-sm flex-shrink-0 ${step.done ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
{step.done ? '\u2713' : '\u25CB'}
|
||||
</span>
|
||||
<span className={`text-sm flex-1 ${step.done ? 'text-gray-900 dark:text-white' : 'text-gray-400'}`}>{step.label}</span>
|
||||
<span className="text-xs text-gray-400">{step.detail}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500">
|
||||
Naechster Schritt: {firstPending?.label || 'Alle Schritte abgeschlossen'}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user