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:
Benjamin Admin
2026-05-08 01:02:41 +02:00
parent c4532049d8
commit 89af88ef7d
8 changed files with 184 additions and 280 deletions
@@ -48,6 +48,7 @@ export default function MitigationsPage() {
const [mitPages, setMitPages] = useState<Record<string, number>>({ design: 1, protection: 1, information: 1 })
const [selected, setSelected] = useState<Set<string>>(new Set())
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
const [expandedMeasure, setExpandedMeasure] = useState<string | null>(null)
function toggleSection(type: string) {
setExpanded((prev) => ({ ...prev, [type]: !prev[type] }))
@@ -203,31 +204,45 @@ export default function MitigationsPage() {
<div>Status</div>
</div>
{/* Rows — paginated */}
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => (
<div key={m.id}
className={`grid grid-cols-[24px_2fr_1fr_80px] gap-2 px-4 py-2 border-t border-gray-50 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors ${selected.has(m.id) ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
<div className="pt-0.5">
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
className="accent-purple-600" />
</div>
<div className="min-w-0">
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
{m.description && <div className="text-xs text-gray-400 mt-0.5">{m.description}</div>}
{(() => {
const refs = measureNorms[(m.title || '').toLowerCase()]
return refs?.length > 0 ? (
<div className="text-[9px] text-blue-500 mt-0.5">Normen: {refs.join(', ')}</div>
) : null
})()}
</div>
<div className="text-xs text-gray-500">
{(m.linked_hazard_names || []).join(', ') || '-'}
</div>
<div>
<StatusBadge status={m.status} />
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => {
const isDetailOpen = expandedMeasure === m.id
const catMatch = (m.description || '').match(/Kategorie\s+(\S+)/)
const category = catMatch?.[1]
const refs = measureNorms[(m.title || '').toLowerCase()]
return (
<div key={m.id}>
<div onClick={() => setExpandedMeasure(isDetailOpen ? null : m.id)}
className={`grid grid-cols-[24px_2fr_1fr_80px] gap-2 px-4 py-2 border-t border-gray-50 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors cursor-pointer ${selected.has(m.id) ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
<div className="pt-0.5" onClick={(e) => e.stopPropagation()}>
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
className="accent-purple-600" />
</div>
<div className="min-w-0 flex items-start gap-1">
<svg className={`w-3 h-3 mt-1 shrink-0 text-gray-400 transition-transform ${isDetailOpen ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div>
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
{!isDetailOpen && category && <div className="text-[10px] text-gray-400 mt-0.5">Kategorie: {category}</div>}
</div>
</div>
<div className="text-xs text-gray-500">
{(m.linked_hazard_names || []).join(', ') || '-'}
</div>
<div>
<StatusBadge status={m.status} />
</div>
</div>
{isDetailOpen && (
<div className="px-12 py-3 bg-gray-50 dark:bg-gray-750 border-t border-gray-100 dark:border-gray-700 text-xs space-y-1">
{m.description && <p className="text-gray-600 dark:text-gray-300">{m.description}</p>}
{category && <p className="text-purple-600">Diese Massnahme gilt fuer alle Gefaehrdungen der Kategorie <strong>{category}</strong>.</p>}
{refs?.length > 0 && <p className="text-blue-500">Normen: {refs.join(', ')}</p>}
</div>
)}
</div>
))}
)
})}
{items.length > (mitPages[type] || 1) * 50 && (
<button onClick={() => setMitPages(prev => ({ ...prev, [type]: (prev[type] || 1) + 1 }))}
className="w-full py-2 text-xs text-purple-600 hover:bg-purple-50 border-t border-gray-100 transition-colors">