e7f2f98da3
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>
370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
'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. "ISO 13857:2019") — Identifikatoren, kein geschuetzter Text</li>
|
|
<li>Offizielle Normentitel — bibliografische Information</li>
|
|
<li>Abschnittsnummern (z.B. "Abschnitt 4.2, Tabelle 1") — 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>
|
|
)
|
|
}
|