feat(iace): show ESAW evidence panel in risk view (B1)
The Risikobewertung page only mentioned the data sources as static prose. Add a collapsible "Datenquellen & Evidenz" panel sourced from /iace/risk-data-sources: the real Eurostat ESAW 2023 contact-mode shares per mode, with license + ready-to-print attribution, and the note that tiers anchor the ordering while values stay GT-calibrated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { RiskDataSources as RiskDataSourcesData } from '../_hooks/useRiskDataSources'
|
||||
|
||||
/**
|
||||
* Collapsible evidence panel: the real public-statistics figures (Eurostat ESAW
|
||||
* 2023) that anchor the W/S tiers, with license + ready-to-print attribution.
|
||||
* Confidence-aware tonality — informs the source, does not alarm.
|
||||
*/
|
||||
export function RiskDataSources({ data }: { data: RiskDataSourcesData }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
if (!data.evidence?.length) return null
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Datenquellen & Evidenz <span className="text-gray-400 font-normal">({data.evidence.length} belegte Kontaktmodi)</span>
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs">{open ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-4 pb-4 space-y-3">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{data.note}</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
|
||||
<th className="py-1.5 pr-3">Kontaktmodus</th>
|
||||
<th className="py-1.5 pr-3">Belegte Quote</th>
|
||||
<th className="py-1.5 pr-3">Quelle</th>
|
||||
<th className="py-1.5">Lizenz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.evidence.map((e) => (
|
||||
<tr key={e.mode} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
|
||||
<td className="py-1.5 pr-3 text-gray-700 dark:text-gray-300 font-medium">{e.label}</td>
|
||||
<td className="py-1.5 pr-3 text-gray-600 dark:text-gray-300 tabular-nums">{e.stat}</td>
|
||||
<td className="py-1.5 pr-3 text-gray-500">{e.source}</td>
|
||||
<td className="py-1.5 text-gray-500">{e.license}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400">
|
||||
{data.evidence[0]?.attribution} · Tiers verankern die Quoten-<em>Ordnung</em>; die Werte sind an
|
||||
BreakPilot-Ground-Truth kalibriert (keine Norm-Tabelle reproduziert).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export interface RiskEvidence {
|
||||
mode: string
|
||||
label: string
|
||||
stat: string
|
||||
source: string
|
||||
license: string
|
||||
attribution: string
|
||||
retrieved: string
|
||||
}
|
||||
|
||||
export interface RiskDataSources {
|
||||
note: string
|
||||
evidence: RiskEvidence[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the license-tagged public-statistics evidence behind the risk-frequency
|
||||
* anchors (Eurostat ESAW hsw_ph3_08, 2023). Global, not per project — so an
|
||||
* auditor can see WHERE the W/S tiers come from and the source is cited.
|
||||
*/
|
||||
export function useRiskDataSources() {
|
||||
const [data, setData] = useState<RiskDataSources | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/iace/risk-data-sources')
|
||||
if (!res.ok) return
|
||||
const json = (await res.json()) as RiskDataSources
|
||||
if (!cancelled) setData(json)
|
||||
} catch (err) {
|
||||
console.error('Failed to load risk data sources:', err)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { data }
|
||||
}
|
||||
@@ -3,14 +3,17 @@
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useRiskAssessment } from './_hooks/useRiskAssessment'
|
||||
import { useRiskMatrix } from './_hooks/useRiskMatrix'
|
||||
import { useRiskDataSources } from './_hooks/useRiskDataSources'
|
||||
import { RiskModelCard } from './_components/RiskModelCard'
|
||||
import { RiskMatrix } from './_components/RiskMatrix'
|
||||
import { RiskDataSources } from './_components/RiskDataSources'
|
||||
|
||||
export default function RisikobewertungPage() {
|
||||
const params = useParams<{ projectId: string }>()
|
||||
const projectId = params.projectId
|
||||
const { hazards, suggestions, loading } = useRiskAssessment(projectId)
|
||||
const { data: matrix } = useRiskMatrix(projectId)
|
||||
const { data: dataSources } = useRiskDataSources()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -26,6 +29,8 @@ export default function RisikobewertungPage() {
|
||||
|
||||
{matrix && matrix.total > 0 && <RiskMatrix data={matrix} />}
|
||||
|
||||
{dataSources && <RiskDataSources data={dataSources} />}
|
||||
|
||||
{loading && (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Lade Gefaehrdungen…</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user