feat: Normen-Referenzen in Hazards + Massnahmen + Normenrecherche-Tab

- Hazard Log: Top 2 relevante Normen pro Kategorie unter dem Kategorie-Badge
- Massnahmen: Normen-Referenzen aus measures_library inline anzeigen
- Navigation: Neuer Normenrecherche-Tab (zwischen Grenzen und Komponenten)
- Normenrecherche-Seite: SuggestedNorms + A/B/C Erklaerung

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-08 00:18:45 +02:00
parent 21c01d6405
commit 136dc4d553
4 changed files with 95 additions and 1 deletions
@@ -80,6 +80,25 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
const [mitCounts, setMitCounts] = useState<Record<string, number>>({})
const [edits, setEdits] = useState<Record<string, EditState>>({})
const [saving, setSaving] = useState<string | null>(null)
const [normsByCategory, setNormsByCategory] = useState<Record<string, string[]>>({})
// Fetch norms library and build category→norm-numbers map
useEffect(() => {
fetch(`/api/sdk/v1/iace/norms-library`)
.then(r => r.ok ? r.json() : null)
.then(json => {
if (!json?.norms) return
const map: Record<string, string[]> = {}
for (const n of json.norms) {
for (const cat of (n.hazard_cats || [])) {
if (!map[cat]) map[cat] = []
if (map[cat].length < 3) map[cat].push(n.number)
}
}
setNormsByCategory(map)
})
.catch(() => {})
}, [])
// Fetch mitigation counts per hazard
useEffect(() => {
@@ -226,6 +245,11 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
<span className="inline-block px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-[10px] font-medium">
{CATEGORY_LABELS[h.category] || h.category}
</span>
{normsByCategory[h.category]?.length > 0 && (
<div className="text-[9px] text-blue-500 mt-0.5 truncate" title={normsByCategory[h.category].join(', ')}>
{normsByCategory[h.category].slice(0, 2).join(', ')}
</div>
)}
</td>
{/* Initial S/E/P/RPZ/Risk */}
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initS}</td>
@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { REDUCTION_TYPES, Mitigation } from './_components/types'
import { HierarchyWarning } from './_components/HierarchyWarning'
@@ -21,6 +21,24 @@ export default function MitigationsPage() {
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete,
} = useMitigations(projectId)
const [measureNorms, setMeasureNorms] = useState<Record<string, string[]>>({})
useEffect(() => {
fetch('/api/sdk/v1/iace/protective-measures-library')
.then(r => r.ok ? r.json() : null)
.then(json => {
if (!json?.protective_measures) return
const map: Record<string, string[]> = {}
for (const m of json.protective_measures) {
if (m.norm_references?.length > 0) {
map[(m.name || '').toLowerCase()] = m.norm_references
}
}
setMeasureNorms(map)
})
.catch(() => {})
}, [])
const [showForm, setShowForm] = useState(false)
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
const [showLibrary, setShowLibrary] = useState(false)
@@ -195,6 +213,12 @@ export default function MitigationsPage() {
<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(', ') || '-'}
@@ -0,0 +1,39 @@
'use client'
import { useParams } from 'next/navigation'
import { SuggestedNorms } from '../_components/SuggestedNorms'
export default function NormsPage() {
const params = useParams()
const projectId = params.projectId as string
return (
<div className="space-y-6">
{/* Page header */}
<div>
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Normenrecherche</h1>
<p className="text-sm text-gray-500 mt-1">
Relevante Normen fuer Ihr Produkt, automatisch ermittelt aus Maschinentyp, Gefaehrdungen
und Komponenten. Ergaenzen Sie bei Bedarf weitere Normen manuell.
</p>
</div>
{/* Info banner */}
<div className="p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-xs text-blue-800 dark:text-blue-300">
<strong>A-Normen</strong> (z.B. ISO 12100) gelten fuer alle Maschinen.{' '}
<strong>B-Normen</strong> decken Sicherheitsaspekte ab (B1: Grundnormen, B2: Schutzeinrichtungen).{' '}
<strong>C-Normen</strong> sind maschinenspezifisch und erzeugen eine Konformitaetsvermutung.
</div>
</div>
</div>
{/* Suggested norms component — rendered expanded (not collapsed by default) */}
<SuggestedNorms projectId={projectId} />
</div>
)
}