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:
@@ -80,6 +80,25 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
|||||||
const [mitCounts, setMitCounts] = useState<Record<string, number>>({})
|
const [mitCounts, setMitCounts] = useState<Record<string, number>>({})
|
||||||
const [edits, setEdits] = useState<Record<string, EditState>>({})
|
const [edits, setEdits] = useState<Record<string, EditState>>({})
|
||||||
const [saving, setSaving] = useState<string | null>(null)
|
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
|
// Fetch mitigation counts per hazard
|
||||||
useEffect(() => {
|
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">
|
<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}
|
{CATEGORY_LABELS[h.category] || h.category}
|
||||||
</span>
|
</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>
|
</td>
|
||||||
{/* Initial S/E/P/RPZ/Risk */}
|
{/* Initial S/E/P/RPZ/Risk */}
|
||||||
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initS}</td>
|
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initS}</td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { REDUCTION_TYPES, Mitigation } from './_components/types'
|
import { REDUCTION_TYPES, Mitigation } from './_components/types'
|
||||||
import { HierarchyWarning } from './_components/HierarchyWarning'
|
import { HierarchyWarning } from './_components/HierarchyWarning'
|
||||||
@@ -21,6 +21,24 @@ export default function MitigationsPage() {
|
|||||||
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete,
|
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete,
|
||||||
} = useMitigations(projectId)
|
} = 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 [showForm, setShowForm] = useState(false)
|
||||||
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
|
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
|
||||||
const [showLibrary, setShowLibrary] = useState(false)
|
const [showLibrary, setShowLibrary] = useState(false)
|
||||||
@@ -195,6 +213,12 @@ export default function MitigationsPage() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
|
<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>}
|
{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>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
{(m.linked_hazard_names || []).join(', ') || '-'}
|
{(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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ const IACE_NAV_ITEMS = [
|
|||||||
{ id: 'overview', label: 'Uebersicht', href: '', icon: 'grid' },
|
{ id: 'overview', label: 'Uebersicht', href: '', icon: 'grid' },
|
||||||
{ id: 'order', label: 'Auftrag', href: '/order', icon: 'briefcase' },
|
{ id: 'order', label: 'Auftrag', href: '/order', icon: 'briefcase' },
|
||||||
{ id: 'interview', label: 'Grenzen & Verwendung', href: '/interview', icon: 'chat' },
|
{ id: 'interview', label: 'Grenzen & Verwendung', href: '/interview', icon: 'chat' },
|
||||||
|
{ id: 'norms', label: 'Normenrecherche', href: '/norms', icon: 'book' },
|
||||||
{ id: 'components', label: 'Komponenten', href: '/components', icon: 'cube' },
|
{ id: 'components', label: 'Komponenten', href: '/components', icon: 'cube' },
|
||||||
{ id: 'hazards', label: 'Hazard Log', href: '/hazards', icon: 'warning' },
|
{ id: 'hazards', label: 'Hazard Log', href: '/hazards', icon: 'warning' },
|
||||||
{ id: 'mitigations', label: 'Massnahmen', href: '/mitigations', icon: 'shield' },
|
{ id: 'mitigations', label: 'Massnahmen', href: '/mitigations', icon: 'shield' },
|
||||||
@@ -91,6 +92,12 @@ function NavIcon({ icon, className }: { icon: string; className?: string }) {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'book':
|
||||||
|
return (
|
||||||
|
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user