feat(cra): Eingangstür-Frontend — neutrales Verdict + Hersteller-Typ + Presets
ReadinessCheck erweitert: Hersteller-Typ-Weiche (Komponente/Endgeraet/Anlage- Maschine/Software-App), Verkauf-ab-2027- und Kunden-Nachfrage-Fragen, Checkliste vorhandener Nachweise. Neuer Ergebnis-View (ReadinessResult): 3-Tier-Verdict (zwingend/ratsam/nicht betroffen, Co-Pilot-Ton ohne Panik-Rot) + Reifegrad-% + fehlende Nachweise + gefundene digitale Elemente + Pflichten-Uebersicht. Zwei Demo-Presets (OWIS PS90+ Komponente, ZwickRoell roboTest Anlage+SW). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
'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
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
export function ReadinessResultView({ result, onCreateProject }: { result: ReadinessResult; onCreateProject?: () => void }) {
|
||||
const v = result.verdict
|
||||
const m = result.maturity
|
||||
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user