62fafaaec5
3-Tier-MaschinenVO-Verdict (direkt / sicherheitsrelevant / nicht relevant) aus Personengefährdungs-Signal: eine Komponente ist keine Maschine, aber wenn ihre Funktion bei Fehler ODER Manipulation Personen gefaehrden kann (Bewegung, Laser/ Auge, Kraft, Temperatur, elektrisch), ist sie sicherheitsrelevant — Pflicht trifft den Maschinenbauer, Zulieferer liefert Nachweise, und ein Cyber-Angriff kann die Sicherheitsfunktion aushebeln (Cyber-Safety-Bruecke). OWIS-mit-Laser landet so korrekt als 'sicherheitsrelevante Komponente'. Engine + /readiness additiv; Frontend: Gefährdungs-Frage + -Typen, MaschinenVO-Ergebnisblock. Presets aktualisiert (OWIS: Laser+Bewegung, Zwick: Bewegung). 22 Tests gruen. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
204 lines
9.5 KiB
TypeScript
204 lines
9.5 KiB
TypeScript
'use client'
|
|
|
|
interface GuidelineItem {
|
|
req_id: string
|
|
title: string
|
|
annex_anchor: string
|
|
measures: { id: string; name: string }[]
|
|
source?: string
|
|
}
|
|
interface EvidenceItem { key: string; label: string }
|
|
export interface ReadinessResult {
|
|
in_scope: boolean
|
|
classification: string
|
|
rationale: string[]
|
|
conformity_path_hint: string
|
|
regulations: string[]
|
|
guideline: { code: GuidelineItem[]; process: GuidelineItem[]; document: GuidelineItem[] }
|
|
counts: { code: number; process: number; document: number }
|
|
total_effort_days: number
|
|
deadlines: { date: string; label: string }[]
|
|
verdict?: {
|
|
tier: string
|
|
label: string
|
|
in_scope: boolean
|
|
market_pull: boolean
|
|
cra_class: string
|
|
cutoff: string
|
|
reasons: string[]
|
|
}
|
|
maturity?: { pct: number; present: EvidenceItem[]; missing: EvidenceItem[]; total: number }
|
|
digital_elements?: string[]
|
|
producer_type?: string
|
|
machinery_verdict?: {
|
|
tier: string
|
|
label: string
|
|
hazards: { key: string; label: string }[]
|
|
cyber_safety_bridge: boolean
|
|
reasons: string[]
|
|
}
|
|
}
|
|
|
|
const CLASS_LABEL: Record<string, string> = {
|
|
CRITICAL: 'Kritisch', IMPORTANT_II: 'Wichtig (Klasse II)', IMPORTANT_I: 'Wichtig (Klasse I)',
|
|
STANDARD: 'Standard', NOT_IN_SCOPE: 'Nicht im CRA-Anwendungsbereich',
|
|
}
|
|
const BUCKETS: { key: 'code' | 'process' | 'document'; label: string; hint: string }[] = [
|
|
{ key: 'code', label: 'Code / Technik', hint: 'im Produkt umzusetzen' },
|
|
{ key: 'process', label: 'Prozesse', hint: 'organisatorisch zu etablieren' },
|
|
{ key: 'document', label: 'Dokumentation', hint: 'nachzuweisen / beizulegen' },
|
|
]
|
|
// Neutral, Co-Pilot tone — no panic red. zwingend = attention (amber), ratsam =
|
|
// advisory (blue), nicht betroffen = positive (emerald).
|
|
const TIER_STYLE: Record<string, string> = {
|
|
zwingend: 'border-amber-300 bg-amber-50 dark:bg-amber-900/20 text-amber-900 dark:text-amber-200',
|
|
ratsam: 'border-blue-300 bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200',
|
|
nicht_betroffen: 'border-emerald-300 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-900 dark:text-emerald-200',
|
|
}
|
|
const MV_STYLE: Record<string, string> = {
|
|
direkt: 'border-amber-300 bg-amber-50 dark:bg-amber-900/20 text-amber-900 dark:text-amber-200',
|
|
sicherheitsrelevant: 'border-indigo-300 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-900 dark:text-indigo-200',
|
|
nicht_relevant: 'border-gray-200 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300',
|
|
}
|
|
|
|
export function ReadinessResultView({ result, onCreateProject }: { result: ReadinessResult; onCreateProject?: () => void }) {
|
|
const v = result.verdict
|
|
const m = result.maturity
|
|
const mv = result.machinery_verdict
|
|
|
|
return (
|
|
<div className="mt-5 space-y-4">
|
|
{/* Neutrales Verdict — Rechtspflicht vs. Marktzwang */}
|
|
{v && (
|
|
<div className={`rounded-xl border p-4 ${TIER_STYLE[v.tier] || TIER_STYLE.ratsam}`}>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-base font-semibold">{v.label}</span>
|
|
{v.market_pull && (
|
|
<span className="rounded bg-white/60 dark:bg-black/20 px-2 py-0.5 text-xs font-medium">Markt-Druck: Kunden fordern Nachweise</span>
|
|
)}
|
|
</div>
|
|
<ul className="mt-2 space-y-0.5 text-sm">
|
|
{v.reasons.map((r, i) => <li key={i}>• {r}</li>)}
|
|
</ul>
|
|
<p className="mt-2 text-xs opacity-80">
|
|
Hinweis: Maßgeblich ist das <strong>Inverkehrbringen</strong> (ab {v.cutoff}), nicht der Entwicklungszeitpunkt.
|
|
Das ist eine erste Einschätzung zur Klärung mit DSB/Anwalt — keine Rechtsberatung.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Maschinenverordnung — Personensicherheit + Cyber-Safety-Brücke */}
|
|
{mv && mv.tier !== 'nicht_relevant' && (
|
|
<div className={`rounded-xl border p-4 ${MV_STYLE[mv.tier] || MV_STYLE.sicherheitsrelevant}`}>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-base font-semibold">Maschinenverordnung: {mv.label}</span>
|
|
{mv.cyber_safety_bridge && (
|
|
<span className="rounded bg-white/60 dark:bg-black/20 px-2 py-0.5 text-xs font-medium">Cyber → Safety aktiv</span>
|
|
)}
|
|
</div>
|
|
{mv.hazards.length > 0 && (
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{mv.hazards.map((h) => (
|
|
<span key={h.key} className="rounded bg-white/70 dark:bg-black/20 px-2 py-0.5 text-xs font-medium">{h.label}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
<ul className="mt-2 space-y-0.5 text-sm">
|
|
{mv.reasons.map((r, i) => <li key={i}>• {r}</li>)}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Reifegrad + digitale Elemente */}
|
|
<div className="grid md:grid-cols-2 gap-3">
|
|
{m && (
|
|
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
|
|
<div className="flex items-baseline justify-between">
|
|
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Reifegrad (CRA-Nachweise)</h3>
|
|
<span className="text-2xl font-bold text-gray-900 dark:text-gray-100">{m.pct}%</span>
|
|
</div>
|
|
<div className="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
<div className="h-2 bg-emerald-500" style={{ width: `${m.pct}%` }} />
|
|
</div>
|
|
{m.missing.length > 0 && (
|
|
<div className="mt-2">
|
|
<p className="text-xs text-gray-500 mb-1">Fehlende Nachweise:</p>
|
|
<ul className="space-y-0.5">
|
|
{m.missing.map((e) => (
|
|
<li key={e.key} className="text-xs text-gray-700 dark:text-gray-300">• {e.label}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{result.digital_elements && result.digital_elements.length > 0 && (
|
|
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
|
|
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
|
Gefundene digitale Elemente <span className="text-gray-400 font-normal">({result.digital_elements.length})</span>
|
|
</h3>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{result.digital_elements.map((d) => (
|
|
<span key={d} className="rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 px-2 py-0.5 text-xs">{d}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Einstufung + Pflichten-Übersicht (wie bisher) */}
|
|
{result.in_scope && (
|
|
<>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm text-gray-600 dark:text-gray-300">CRA-Einstufung:</span>
|
|
<span className="rounded px-2 py-0.5 text-xs font-semibold bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300">
|
|
{CLASS_LABEL[result.classification] || result.classification}
|
|
</span>
|
|
<span className="text-xs text-gray-500">· Konformität: {result.conformity_path_hint}</span>
|
|
{result.regulations.map((r) => (
|
|
<span key={r} className="rounded px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">{r}</span>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
{result.counts.code + result.counts.process + result.counts.document} Pflichten · grobe Schätzung
|
|
~{result.total_effort_days} Personentage.
|
|
</p>
|
|
<div className="grid md:grid-cols-3 gap-3">
|
|
{BUCKETS.map((b) => (
|
|
<div key={b.key} className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
|
|
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200">{b.label}
|
|
<span className="ml-1 text-[10px] font-normal text-gray-400">({result.counts[b.key]} · {b.hint})</span>
|
|
</h3>
|
|
<ul className="mt-2 space-y-1.5">
|
|
{result.guideline[b.key].map((it) => (
|
|
<li key={it.req_id} className="text-[11px] text-gray-600 dark:text-gray-300">
|
|
{it.source === 'Maschinen-VO' && (
|
|
<span className="inline-block rounded bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300 px-1 py-0.5 text-[9px] font-medium mr-1">MaschVO</span>
|
|
)}
|
|
<span className="font-medium text-gray-800 dark:text-gray-200">{it.title}</span>
|
|
<span className="text-gray-400"> · {it.annex_anchor}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex flex-wrap gap-3 text-[11px] text-gray-500">
|
|
<span className="font-medium text-gray-600 dark:text-gray-300">CRA-Fristen:</span>
|
|
{result.deadlines.map((d) => (
|
|
<span key={d.date}><span className="font-mono">{d.date}</span> {d.label}</span>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{onCreateProject && (
|
|
<button onClick={onCreateProject}
|
|
className="rounded bg-purple-600 hover:bg-purple-700 text-white text-sm px-4 py-2">
|
|
Projekt anlegen & Cyber-Akte aufbauen — wir setzen es mit Ihnen um
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|