diff --git a/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/RiskComparison.tsx b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/RiskComparison.tsx new file mode 100644 index 00000000..44e4aecb --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/RiskComparison.tsx @@ -0,0 +1,100 @@ +'use client' + +import type { RiskComparisonPair, RiskAgreement } from '../_hooks/useBenchmark' + +type Ampel = 'green' | 'yellow' | 'red' + +// EN-62061-style risk number R = S * (F + W + P) → traffic light (like the Excel). +function ampelEN(r: number): Ampel { + if (r >= 30) return 'red' + if (r >= 18) return 'yellow' + return 'green' +} + +function ampelBand(band: string): Ampel { + if (band === 'sehr hoch' || band === 'hoch') return 'red' + if (band === 'wesentlich' || band === 'moeglich') return 'yellow' + return 'green' +} + +const cellColor: Record = { + red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', + yellow: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300', + green: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', +} + +function pctColor(p: number): Ampel { + if (p >= 80) return 'green' + if (p >= 50) return 'yellow' + return 'red' +} + +function Stat({ label, pct }: { label: string; pct: number }) { + const c = pctColor(pct) + return ( +
+
{Math.round(pct)}%
+
{label}
+
+ ) +} + +export function RiskComparison({ pairs, agreement }: { pairs?: RiskComparisonPair[]; agreement?: RiskAgreement }) { + if (!pairs || pairs.length === 0) return null + + return ( +
+
+

Risikozahlen-Vergleich (Fachmann vs. Tool)

+

+ R = S × (F + W + P), Ampel wie in der Excel. Fine-Kinney (P×E×C) als zweite, US-anerkannte Bewertung. +

+
+ + {agreement && agreement.n > 0 && ( +
+ + + + + +
+ )} + +
+ + + + + + + + + + {pairs.map((p, i) => { + const engR = p.eng_severity * (p.eng_frequency + p.eng_probability + p.eng_avoidance) + return ( + + + + + + + + + + + + + + ) + })} + +
GefaehrdungFachmann · S F W P RTool · S F W P R / FK
{p.hazard_name || '—'}{p.gt_severity}{p.gt_frequency}{p.gt_probability}{p.gt_avoidance}{p.gt_risk}{p.eng_severity}{p.eng_frequency}{p.eng_probability}{p.eng_avoidance} + {engR} + FK {Math.round(p.fk_score)} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/benchmark/_hooks/useBenchmark.ts b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_hooks/useBenchmark.ts index e690ddd7..a9668067 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/benchmark/_hooks/useBenchmark.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_hooks/useBenchmark.ts @@ -48,6 +48,20 @@ export interface CategoryScore { category: string; gt_count: number; match_count: number; coverage: number } +export interface RiskComparisonPair { + hazard_name: string + gt_severity: number; gt_frequency: number; gt_probability: number; gt_avoidance: number; gt_risk: number + eng_severity: number; eng_frequency: number; eng_probability: number; eng_avoidance: number + fk_score: number; fk_band: string +} + +export interface RiskAgreement { + n: number + severity_within1: number; frequency_within1: number + probability_within1: number; avoidance_within1: number + rank_concordance: number +} + export interface BenchmarkResult { coverage_score: number measure_coverage: number @@ -58,6 +72,8 @@ export interface BenchmarkResult { extra_in_engine: HazardSummary[] category_breakdown: CategoryScore[] risk_rank_pairs: { gt_rank: number; engine_rank: number; hazard_name: string; gt_risk_score: number }[] + risk_comparison?: RiskComparisonPair[] + risk_agreement?: RiskAgreement } interface UseBenchmarkReturn { diff --git a/admin-compliance/app/sdk/iace/[projectId]/benchmark/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/benchmark/page.tsx index f1f6e03f..2e507e8c 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/benchmark/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/benchmark/page.tsx @@ -6,6 +6,7 @@ import { useBenchmark } from './_hooks/useBenchmark' import { GTImportForm } from './_components/GTImportForm' import { HazardComparisonTable } from './_components/HazardComparisonTable' import { CategoryBreakdown } from './_components/CategoryBreakdown' +import { RiskComparison } from './_components/RiskComparison' export default function BenchmarkPage() { const { projectId } = useParams<{ projectId: string }>() @@ -102,6 +103,9 @@ export default function BenchmarkPage() { {/* Category Breakdown */} + {/* Risk-number comparison (tool vs professional) with traffic lights */} + + {/* Hazard Comparison Table */} 0 { + n++ + if abs(engS-gt.S) <= 1 { + sevOK++ + } + if gt.F > 0 && abs(engF-gt.F) <= 1 { + freqOK++ + } + if gt.W > 0 && abs(engW-gt.W) <= 1 { + probOK++ + } + if gt.P > 0 && abs(engP-gt.P) <= 1 { + avoidOK++ + } + engFK = append(engFK, fk.Score) + gtR = append(gtR, float64(gt.R)) + } + } + + agg := RiskAgreement{N: n} + if n > 0 { + agg.SeverityWithin1 = pct(sevOK, n) + agg.FrequencyWithin1 = pct(freqOK, n) + agg.ProbabilityWithin1 = pct(probOK, n) + agg.AvoidanceWithin1 = pct(avoidOK, n) + agg.RankConcordance = rankConcordance(engFK, gtR) + } + return pairs, agg +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +func pct(x, total int) float64 { + if total == 0 { + return 0 + } + return 100 * float64(x) / float64(total) +} + +// rankConcordance returns the fraction of comparable hazard pairs the tool +// orders the same way the professional does (scale-invariant, 0.5 = random). +func rankConcordance(a, b []float64) float64 { + concordant, discordant := 0, 0 + for i := 0; i < len(a); i++ { + for j := i + 1; j < len(a); j++ { + da, db := a[i]-a[j], b[i]-b[j] + if da == 0 || db == 0 { + continue + } + if (da > 0) == (db > 0) { + concordant++ + } else { + discordant++ + } + } + } + if concordant+discordant == 0 { + return 0 + } + return 100 * float64(concordant) / float64(concordant+discordant) +}