feat(iace): surface OSHA distance anchor in Maßnahmen tab (name-resolved)

Makes the OSHA minimum-distance anchor visible per measure in a project
without a DB schema change or re-seed: persisted mitigations store the
measure NAME verbatim (not the catalog ID), and measure names are unique
across the 578-entry library (pinned by test), so a name→ID resolver
bridges the gap.

Backend: MeasureIDByName + MinimumDistancesForMeasureName/LinksForMeasureName;
/iace/minimum-distances now accepts ?measure_name=; link table enriched with
measure_name for one-request UI matching.
Frontend: useMinimumDistances loads the link table once and keys it by name;
OshaDistanceNote renders the anchor (value/CFR/license/EU-hint/relation) on the
matching measure group in the Maßnahmen tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 13:39:48 +02:00
parent 76be96556d
commit 6b41eec176
6 changed files with 226 additions and 8 deletions
@@ -0,0 +1,77 @@
'use client'
import { useEffect, useState } from 'react'
export interface EuNormHint {
norm: string
section?: string
din_comparison_note?: string
}
export interface MinimumDistance {
id: string
source_cfr?: string
source_table?: string
license: string
context: string
body_part?: string
recommended_mm?: number
recommended_min_mm?: number
recommended_max_mm?: number
formula_description?: string
formula_mm_per_second?: number
rounding_note?: string
eu_norm_hints?: EuNormHint[]
}
export interface MeasureDistanceLink {
measure_id: string
measure_name?: string
distance_ids: string[]
relation: string // "value_source" | "public_domain_crossref"
note?: string
}
export interface OshaAnchor {
link: MeasureDistanceLink
distances: MinimumDistance[]
}
/**
* Loads the OSHA minimum-distance link table ONCE and returns a lookup keyed by
* (lower-cased) measure name. A persisted mitigation stores the measure name
* verbatim, so the Maßnahmen tab can surface the OSHA anchor by matching on name
* — no per-row request, no catalog ID needed.
*/
export function useMinimumDistances() {
const [byName, setByName] = useState<Record<string, OshaAnchor>>({})
useEffect(() => {
let cancelled = false
async function load() {
try {
const res = await fetch('/api/sdk/v1/iace/minimum-distances')
if (!res.ok) return
const json = (await res.json()) as { distances: MinimumDistance[]; links: MeasureDistanceLink[] }
const byId = Object.fromEntries((json.distances || []).map((d) => [d.id, d]))
const map: Record<string, OshaAnchor> = {}
for (const link of json.links || []) {
if (!link.measure_name) continue
map[link.measure_name.toLowerCase()] = {
link,
distances: link.distance_ids.map((id) => byId[id]).filter(Boolean),
}
}
if (!cancelled) setByName(map)
} catch (err) {
console.error('Failed to load minimum distances:', err)
}
}
load()
return () => {
cancelled = true
}
}, [])
return { byName }
}