From c6ebe6116218603c71b1377f3b90faeab600402e Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 9 Jun 2026 15:40:59 +0200 Subject: [PATCH] feat(iace-frontend): Risikobewertung tab with dual risk model + live formula MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tab /sdk/iace/[projectId]/risikobewertung. Per hazard it shows BOTH models side by side — EN-62061-style (S/F/W/P) and Fine-Kinney (P/E/C) — with BreakPilot's justified suggested values from public data, the visible formula, and editable fields that recompute the score + risk band live. The professional adjusts the values (e.g. from his own licensed DIN/Beuth data); we only supply the formula + inputs, reproduce no norm table. Consumes GET .../hazards/:hid/risk-suggestion. Registered in IACE_NAV_ITEMS. Co-Authored-By: Claude Opus 4.7 --- .../_components/RiskModelCard.tsx | 160 ++++++++++++++++++ .../_hooks/useRiskAssessment.ts | 85 ++++++++++ .../iace/[projectId]/risikobewertung/page.tsx | 41 +++++ admin-compliance/app/sdk/iace/layout.tsx | 1 + 4 files changed, 287 insertions(+) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_components/RiskModelCard.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_hooks/useRiskAssessment.ts create mode 100644 admin-compliance/app/sdk/iace/[projectId]/risikobewertung/page.tsx diff --git a/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_components/RiskModelCard.tsx b/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_components/RiskModelCard.tsx new file mode 100644 index 00000000..ba331a89 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_components/RiskModelCard.tsx @@ -0,0 +1,160 @@ +'use client' + +import { useEffect, useState } from 'react' +import type { HazardLite, RiskSuggestion } from '../_hooks/useRiskAssessment' + +function enLevel(idx: number): string { + if (idx >= 45) return 'kritisch' + if (idx >= 30) return 'hoch' + if (idx >= 18) return 'mittel' + if (idx >= 9) return 'gering' + return 'vernachlaessigbar' +} + +function fkBand(score: number): string { + if (score > 400) return 'sehr hoch' + if (score > 200) return 'hoch' + if (score > 70) return 'wesentlich' + if (score > 20) return 'moeglich' + return 'gering' +} + +function badgeColor(label: string): string { + switch (label) { + case 'kritisch': + case 'sehr hoch': + return 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' + case 'hoch': + return 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300' + case 'mittel': + case 'wesentlich': + return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300' + case 'gering': + case 'moeglich': + return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' + default: + return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' + } +} + +function Field({ + label, + value, + step, + justification, + onChange, +}: { + label: string + value: number + step?: number + justification: string + onChange: (v: number) => void +}) { + return ( +
+
+
{label}
+
+ {justification} +
+
+ onChange(parseFloat(e.target.value) || 0)} + className="w-16 px-2 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-right" + /> +
+ ) +} + +function Panel({ + title, + formula, + score, + badge, + children, +}: { + title: string + formula: string + score: number + badge: string + children: React.ReactNode +}) { + return ( +
+
+
{title}
+ {badge} +
+
{children}
+
+ {formula} + + R = {Number.isInteger(score) ? score : score.toFixed(1)} + +
+
+ ) +} + +export function RiskModelCard({ + hazard, + suggestion, +}: { + hazard: HazardLite + suggestion?: RiskSuggestion +}) { + const [en, setEn] = useState({ s: 0, f: 0, w: 0, p: 0 }) + const [fk, setFk] = useState({ p: 0, e: 0, c: 0 }) + + useEffect(() => { + if (!suggestion) return + setEn({ + s: suggestion.en62061.severity.value, + f: suggestion.en62061.frequency.value, + w: suggestion.en62061.probability.value, + p: suggestion.en62061.avoidance.value, + }) + setFk({ + p: suggestion.fine_kinney.probability.value, + e: suggestion.fine_kinney.exposure.value, + c: suggestion.fine_kinney.consequence.value, + }) + }, [suggestion]) + + if (!suggestion) { + return ( +
+ {hazard.name} — keine Bewertung verfuegbar +
+ ) + } + + const enScore = en.s * (en.f + en.w + en.p) + const fkScore = fk.p * fk.e * fk.c + + return ( +
+
+
{hazard.name}
+ Kontaktart: {suggestion.contact_mode} +
+
+ + setEn({ ...en, s: v })} /> + setEn({ ...en, f: v })} /> + setEn({ ...en, w: v })} /> + setEn({ ...en, p: v })} /> + + + setFk({ ...fk, p: v })} /> + setFk({ ...fk, e: v })} /> + setFk({ ...fk, c: v })} /> + +
+

{suggestion.note}

+
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_hooks/useRiskAssessment.ts b/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_hooks/useRiskAssessment.ts new file mode 100644 index 00000000..b9e323e4 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/_hooks/useRiskAssessment.ts @@ -0,0 +1,85 @@ +'use client' + +import { useEffect, useState } from 'react' + +export interface SuggestedValue { + value: number + justification: string +} + +export interface RiskSuggestion { + hazard_id: string + contact_mode: string + en62061: { + severity: SuggestedValue + frequency: SuggestedValue + probability: SuggestedValue + avoidance: SuggestedValue + score: number + level: string + formula: string + } + fine_kinney: { + probability: SuggestedValue + exposure: SuggestedValue + consequence: SuggestedValue + score: number + band: string + action: string + formula: string + } + note: string +} + +export interface HazardLite { + id: string + name: string + category: string + scenario?: string +} + +export function useRiskAssessment(projectId: string) { + const [hazards, setHazards] = useState([]) + const [suggestions, setSuggestions] = useState>({}) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let cancelled = false + async function load() { + setLoading(true) + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`) + const json = res.ok ? await res.json() : {} + const hz: HazardLite[] = json.hazards || json || [] + if (cancelled) return + setHazards(hz) + const entries = await Promise.all( + hz.map(async (h) => { + try { + const r = await fetch( + `/api/sdk/v1/iace/projects/${projectId}/hazards/${h.id}/risk-suggestion`, + ) + return r.ok ? ([h.id, (await r.json()) as RiskSuggestion] as const) : null + } catch { + return null + } + }), + ) + if (cancelled) return + const map: Record = {} + for (const e of entries) if (e) map[e[0]] = e[1] + setSuggestions(map) + } catch (err) { + console.error('Failed to load risk assessment:', err) + } finally { + if (!cancelled) setLoading(false) + } + } + load() + return () => { + cancelled = true + } + }, [projectId]) + + return { hazards, suggestions, loading } +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/page.tsx new file mode 100644 index 00000000..8e954d36 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/risikobewertung/page.tsx @@ -0,0 +1,41 @@ +'use client' + +import { useParams } from 'next/navigation' +import { useRiskAssessment } from './_hooks/useRiskAssessment' +import { RiskModelCard } from './_components/RiskModelCard' + +export default function RisikobewertungPage() { + const params = useParams<{ projectId: string }>() + const projectId = params.projectId + const { hazards, suggestions, loading } = useRiskAssessment(projectId) + + return ( +
+
+

Risikobewertung

+

+ Zwei Modelle pro Gefaehrdung: EN-62061-Stil (F/W/P/S) und{' '} + Fine-Kinney (P/E/C, US-anerkannt). BreakPilot schlaegt begruendete + Werte aus oeffentlichen Datenquellen vor (ESAW/NIOSH/OSHA) — passen Sie sie nach Ihrer + Erfahrung bzw. Ihren eigenen Normdaten an; das Tool rechnet die Formel live aus. +

+
+ + {loading && ( +
Lade Gefaehrdungen…
+ )} + + {!loading && hazards.length === 0 && ( +
+ Keine Gefaehrdungen vorhanden. Bitte zuerst im Hazard Log erzeugen. +
+ )} + +
+ {hazards.map((h) => ( + + ))} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/iace/layout.tsx b/admin-compliance/app/sdk/iace/layout.tsx index bd302267..01ed8031 100644 --- a/admin-compliance/app/sdk/iace/layout.tsx +++ b/admin-compliance/app/sdk/iace/layout.tsx @@ -13,6 +13,7 @@ const IACE_NAV_ITEMS = [ { id: 'norms', label: 'Normenrecherche', href: '/norms', icon: 'book' }, { id: 'components', label: 'Komponenten', href: '/components', icon: 'cube' }, { id: 'hazards', label: 'Hazard Log', href: '/hazards', icon: 'warning' }, + { id: 'risikobewertung', label: 'Risikobewertung', href: '/risikobewertung', icon: 'activity' }, { id: 'mitigations', label: 'Massnahmen', href: '/mitigations', icon: 'shield' }, { id: 'clarifications', label: 'Klärungen', href: '/clarifications', icon: 'chat' }, { id: 'verification', label: 'Verifikation', href: '/verification', icon: 'check' },