feat: IACE CE-Compliance Module — Normen, Risikobewertung, Production Lines

Major features:
- 215 norms library with section references + Beuth URLs (A/B1/B2/C norms)
- 173 hazard patterns with detail fields (scenario, trigger, harm, zone)
- Deterministic pattern matching: Component × Lifecycle × Pattern cross-product
- SIL/PL auto-calculation from S×E×P risk graph
- Risk assessment table with editable S/E/P dropdowns
- Production Line Dashboard with animated station flow (Running Dots)
- IACE process flow + norms coverage on start page
- Non-blocking cookie banner, ProcessFlow SSR fix
- 104 Playwright E2E tests passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-07 10:53:26 +02:00
parent 3853a0838a
commit e7f2f98da3
59 changed files with 8326 additions and 525 deletions
@@ -0,0 +1,369 @@
'use client'
import React, { useState, useEffect } from 'react'
import { NormsCoverage } from './NormsCoverage'
type ScopeStatus = 'in_scope' | 'partially' | 'not_in_scope' | 'planned'
interface ProcessStep {
number: number
title: string
description: string
actor: string
scope: ScopeStatus
toolNote?: string
}
const CE_PROCESS_STEPS: ProcessStep[] = [
{
number: 1,
title: 'Maschinenplanung',
description: 'Hersteller plant Maschine/Anlage',
actor: 'Hersteller',
scope: 'not_in_scope',
},
{
number: 2,
title: 'CE-Firma beauftragen',
description: 'Hersteller beauftragt CE-Beratungsfirma oder internes CE-Team',
actor: 'Hersteller',
scope: 'not_in_scope',
},
{
number: 3,
title: 'Grenzen definieren',
description:
'Bestimmungsgemasse Verwendung, vorhersehbare Fehlanwendung, Betriebsarten, raeumliche/zeitliche Grenzen',
actor: 'Gemeinsam',
scope: 'in_scope',
toolNote: 'Interview/Wizard tab',
},
{
number: 4,
title: 'Normenrecherche',
description:
'C-Normen (maschinenspezifisch), B-Normen (Sicherheitsfunktionen), A-Normen (ISO 12100). Harmonisierte Normen ermoeglichen Konformitaetsvermutung.',
actor: 'CE-Firma',
scope: 'in_scope',
toolNote: 'manueller Eintrag',
},
{
number: 5,
title: 'Maschinenbeschreibung',
description:
'Komponentenbaum, Energiequellen, technische Daten, Betriebsarten systematisch erfassen',
actor: 'CE-Firma',
scope: 'in_scope',
toolNote: 'Komponenten tab',
},
{
number: 6,
title: 'Gefaehrdungen identifizieren',
description:
'Systematisch pro Komponente x Lebenszyklus. Deterministisches Pattern-Matching generiert Vorschlaege.',
actor: 'CE-Firma + Tool',
scope: 'in_scope',
toolNote: 'Hazard Log',
},
{
number: 7,
title: 'Risiko bewerten',
description:
'Schwere x Exposition x Eintrittswahrscheinlichkeit. Automatische SIL/PL-Ableitung aus Risikograph.',
actor: 'CE-Firma + Tool',
scope: 'in_scope',
toolNote: 'Hazard Log',
},
{
number: 8,
title: 'Massnahmen definieren',
description:
'3-Stufen-Hierarchie (PFLICHT): 1. Design, 2. Schutzeinrichtung, 3. Information. Tool schlaegt kategorienspezifisch vor.',
actor: 'CE-Firma + Tool',
scope: 'in_scope',
toolNote: 'Massnahmen tab',
},
{
number: 9,
title: 'Massnahmen umsetzen',
description:
'Hersteller implementiert konstruktive Aenderungen, Schutzeinrichtungen, Beschilderung etc.',
actor: 'Hersteller',
scope: 'partially',
toolNote: 'Nachweis-Upload',
},
{
number: 10,
title: 'Restrisiko bewerten',
description:
'Iterativ: Nach Massnahmen-Umsetzung erneut bewerten. Akzeptabel? Wenn nein: zurueck zu Schritt 8.',
actor: 'CE-Firma',
scope: 'in_scope',
toolNote: 'Reassessment',
},
{
number: 11,
title: 'Verifikation',
description: 'Messungen, Berechnungen, Pruefungen. Nachweise den Massnahmen zuordnen.',
actor: 'CE-Firma',
scope: 'in_scope',
toolNote: 'Verifikation tab',
},
{
number: 12,
title: 'Benannte Stelle',
description:
'NUR fuer Annex-IV-Maschinen (Pressen, Holzbearbeitung, Hebezeuge): Formale Baumusterpruefung durch TUeV/DGUV Test o.ae.',
actor: 'Notified Body',
scope: 'not_in_scope',
},
{
number: 13,
title: 'Betriebsanleitung',
description:
'Restrisiken fuer Bediener dokumentieren, Sicherheitshinweise, bestimmungsgemasse Verwendung',
actor: 'CE-Firma',
scope: 'planned',
},
{
number: 14,
title: 'Technische Unterlagen',
description:
'Gesamtdossier: Plaene, Schaltbilder, Berechnungen, Risikobeurteilung, Normen, Pruefberichte, Betriebsanleitung',
actor: 'CE-Firma',
scope: 'in_scope',
toolNote: 'CE-Akte tab',
},
{
number: 15,
title: 'CE-Erklaerung',
description:
'Hersteller unterschreibt EU-Konformitaetserklaerung und bringt CE-Kennzeichnung an. Die CE-Firma gibt KEIN CE — der Hersteller traegt die Verantwortung.',
actor: 'Hersteller',
scope: 'not_in_scope',
},
]
const SCOPE_STYLES: Record<ScopeStatus, { border: string; bg: string; badge: string; badgeText: string }> = {
in_scope: {
border: 'border-l-purple-500',
bg: 'bg-purple-50 dark:bg-purple-900/10',
badge: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
badgeText: 'Im Tool',
},
partially: {
border: 'border-l-yellow-500',
bg: 'bg-yellow-50 dark:bg-yellow-900/10',
badge: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300',
badgeText: 'Teilweise',
},
not_in_scope: {
border: 'border-l-gray-300',
bg: 'bg-gray-50 dark:bg-gray-800/50',
badge: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400',
badgeText: 'Nicht im Tool',
},
planned: {
border: 'border-l-gray-300 border-dashed',
bg: 'bg-gray-50 dark:bg-gray-800/50',
badge: 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300',
badgeText: 'Geplant',
},
}
const STORAGE_KEY = 'iace-process-flow-collapsed'
function StepCard({ step }: { step: ProcessStep }) {
const style = SCOPE_STYLES[step.scope]
const muted = step.scope === 'not_in_scope' || step.scope === 'planned'
return (
<div className={`relative flex gap-4 ${muted ? 'opacity-75' : ''}`}>
{/* Timeline connector */}
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 ${
step.scope === 'in_scope'
? 'bg-purple-600 text-white'
: step.scope === 'partially'
? 'bg-yellow-500 text-white'
: 'bg-gray-300 text-gray-600 dark:bg-gray-600 dark:text-gray-300'
}`}
>
{step.number}
</div>
{step.number < 15 && (
<div className="w-0.5 flex-1 bg-gray-200 dark:bg-gray-700 mt-1" />
)}
</div>
{/* Card */}
<div
className={`flex-1 mb-3 p-4 rounded-lg border-l-4 ${style.border} ${style.bg} ${
step.scope === 'planned' ? 'border-dashed border border-gray-300 dark:border-gray-600' : ''
}`}
>
<div className="flex items-start justify-between gap-2 mb-1">
<h4 className={`font-semibold text-sm ${muted ? 'text-gray-600 dark:text-gray-400' : 'text-gray-900 dark:text-white'}`}>
{step.title}
</h4>
<div className="flex items-center gap-2 flex-shrink-0">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${style.badge}`}>
{style.badgeText}
</span>
</div>
</div>
<p className={`text-xs leading-relaxed ${muted ? 'text-gray-500 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}>
{step.description}
</p>
<div className="mt-2 flex items-center gap-3">
<span className="inline-flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
{step.actor}
</span>
{step.toolNote && (
<span className="inline-flex items-center gap-1 text-xs text-purple-600 dark:text-purple-400">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
{step.toolNote}
</span>
)}
</div>
</div>
</div>
)
}
export function ProcessFlow() {
// Default to expanded (false) — avoids SSR hydration mismatch
const [collapsed, setCollapsed] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored === 'true') {
setCollapsed(true)
}
setMounted(true)
}, [])
function toggle() {
const next = !collapsed
setCollapsed(next)
localStorage.setItem(STORAGE_KEY, String(next))
}
const inScopeCount = CE_PROCESS_STEPS.filter((s) => s.scope === 'in_scope').length
const partialCount = CE_PROCESS_STEPS.filter((s) => s.scope === 'partially').length
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header — always visible */}
<button
onClick={toggle}
className="w-full flex items-center justify-between px-6 py-4 text-left hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
>
<div>
<h2 className="text-base font-semibold text-gray-900 dark:text-white">
CE-Prozess: 15 Schritte zur Konformitaet
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{inScopeCount} Schritte im Tool abgedeckt, {partialCount} teilweise
</p>
</div>
<svg
className={`w-5 h-5 text-gray-400 transition-transform ${collapsed ? '' : 'rotate-180'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Content — collapsible */}
{!collapsed && (
<div className="px-6 pb-6 border-t border-gray-100 dark:border-gray-700">
{/* Legend */}
<div className="flex flex-wrap items-center gap-4 py-3 mb-4">
<span className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-300">
<span className="w-3 h-3 rounded-sm bg-purple-500" />
Im Tool abgedeckt
</span>
<span className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-300">
<span className="w-3 h-3 rounded-sm bg-yellow-500" />
Teilweise abgedeckt
</span>
<span className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-300">
<span className="w-3 h-3 rounded-sm bg-gray-300 dark:bg-gray-600" />
Nicht im Tool
</span>
<span className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-300">
<span className="w-3 h-3 rounded-sm border border-dashed border-gray-400" />
Geplant
</span>
</div>
{/* Timeline */}
<div className="space-y-0">
{CE_PROCESS_STEPS.map((step) => (
<StepCard key={step.number} step={step} />
))}
</div>
{/* Norms Coverage Table */}
<div className="mt-4">
<NormsCoverage />
</div>
{/* Disclaimers */}
<div className="mt-4 space-y-3">
<div className="p-3 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-lg">
<p className="text-xs text-amber-800 dark:text-amber-300 leading-relaxed">
<strong>Hinweis:</strong> Dieses Tool ersetzt NICHT die Fachkompetenz eines CE-Beraters.
Es automatisiert die systematische Dokumentation und schlaegt Gefaehrdungen/Massnahmen vor.
Die fachliche Bewertung und Verantwortung verbleibt beim CE-Experten und Hersteller.
</p>
</div>
<div className="p-3 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-xs font-semibold text-blue-800 dark:text-blue-300 mb-2">Normenrecherche Rechtliche Grundlage</p>
<div className="text-xs text-blue-700 dark:text-blue-400 leading-relaxed space-y-2">
<div>
<p className="font-medium mb-1">Was dieses Tool anzeigt:</p>
<ul className="list-disc list-inside space-y-0.5 ml-1">
<li>Normennummern (z.B. &quot;ISO 13857:2019&quot;) Identifikatoren, kein geschuetzter Text</li>
<li>Offizielle Normentitel bibliografische Information</li>
<li>Abschnittsnummern (z.B. &quot;Abschnitt 4.2, Tabelle 1&quot;) Verweisadressen</li>
<li>Eigene Zusammenfassungen des Regelungsbereichs unser Text, nicht Normtext</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">Was dieses Tool NICHT anzeigt:</p>
<ul className="list-disc list-inside space-y-0.5 ml-1">
<li>Normtext (auch nicht auszugsweise) urheberrechtlich geschuetzt durch DIN/ISO/CEN</li>
<li>Tabellenwerte oder Grenzwerte aus Normen</li>
<li>Diagramme oder Bilder aus Normen</li>
</ul>
</div>
<p className="text-blue-600 dark:text-blue-300 pt-1">
Normtexte muessen separat beschafft werden, z.B. ueber{' '}
<a href="https://www.beuth.de" target="_blank" rel="noopener noreferrer" className="underline font-medium hover:text-blue-800">
www.beuth.de
</a>{' '}
(DIN-Normen) oder{' '}
<a href="https://www.iso.org" target="_blank" rel="noopener noreferrer" className="underline font-medium hover:text-blue-800">
www.iso.org
</a>.
</p>
</div>
</div>
</div>
</div>
)}
</div>
)
}