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,53 @@
'use client'
import { OshaAnchor, MinimumDistance } from '../_hooks/useMinimumDistances'
const RELATION_LABEL: Record<string, string> = {
value_source: 'Wertquelle',
public_domain_crossref: 'Public-Domain-Pendant',
}
function distanceLine(d: MinimumDistance): string {
if (d.formula_description) return d.formula_description
if (d.recommended_min_mm && d.recommended_max_mm)
return `${d.recommended_min_mm}${d.recommended_max_mm} mm (empfohlen, sicherheitsseitig gerundet)`
if (d.recommended_mm) return `${d.recommended_mm} mm (empfohlen, sicherheitsseitig gerundet)`
return d.context
}
/** Renders the OSHA safety-distance anchor for one measure (audit view). */
export function OshaDistanceNote({ entry }: { entry: OshaAnchor }) {
const { link, distances } = entry
if (!distances.length) return null
return (
<div className="px-12 pb-2">
<div className="rounded border border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-900/15 p-2">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] font-semibold uppercase tracking-wide text-amber-700 dark:text-amber-300">
OSHA-Sicherheitsabstand
</span>
<span className="text-[10px] text-amber-600/80 dark:text-amber-400/80">
{RELATION_LABEL[link.relation] || link.relation}
</span>
</div>
{distances.map((d) => (
<div key={d.id} className="text-[11px] text-gray-600 dark:text-gray-300 mb-1">
<span className="font-medium">{distanceLine(d)}</span>
<span className="text-gray-400">
{' · '}
{d.source_cfr} · {d.license}
</span>
{(d.eu_norm_hints || []).map((h, i) => (
<span key={i} className="block text-[10px] text-gray-400 mt-0.5">
EU: {h.norm}
{h.din_comparison_note ? `${h.din_comparison_note}` : ''}
</span>
))}
</div>
))}
{link.note && <p className="text-[10px] text-gray-400 italic mt-0.5">{link.note}</p>}
</div>
</div>
)
}