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:
@@ -0,0 +1,172 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface NormStats {
|
||||
total: number
|
||||
byType: Record<string, number>
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
const TYPE_INFO: Record<string, { label: string; color: string }> = {
|
||||
A: { label: 'A-Normen (Grundnormen)', color: 'bg-red-50 text-red-800 border-red-200' },
|
||||
B1: { label: 'B1-Normen (Sicherheitsgrundnormen)', color: 'bg-blue-50 text-blue-800 border-blue-200' },
|
||||
B2: { label: 'B2-Normen (Sicherheitsfachgrundnormen)', color: 'bg-green-50 text-green-800 border-green-200' },
|
||||
C: { label: 'C-Normen (Maschinenspezifisch)', color: 'bg-purple-50 text-purple-800 border-purple-200' },
|
||||
}
|
||||
|
||||
const CATEGORY_DESCRIPTIONS: Record<string, string> = {
|
||||
A: 'ISO 12100 (Grundnorm Risikobeurteilung)',
|
||||
B1: 'ISO 13849-1/2, IEC 62061, IEC 61508 (SIL/PL, Funktionale Sicherheit)',
|
||||
B2: 'Elektrik, Ergonomie, Vibration, Laerm, Brandschutz, Hydraulik/Pneumatik, Software-Safety, Emissionen, Schutzeinrichtungen, Zugaenge, Signale',
|
||||
C: '',
|
||||
}
|
||||
|
||||
export function NormsCoverage() {
|
||||
const [stats, setStats] = useState<NormStats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/sdk/v1/iace/norms-library')
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((json) => {
|
||||
if (!json?.norms) return
|
||||
const norms = json.norms as Array<{ norm_type: string; machine_types?: string[] }>
|
||||
const byType: Record<string, number> = {}
|
||||
const machineTypes = new Set<string>()
|
||||
for (const n of norms) {
|
||||
byType[n.norm_type] = (byType[n.norm_type] || 0) + 1
|
||||
if (n.machine_types) {
|
||||
for (const mt of n.machine_types) machineTypes.add(mt)
|
||||
}
|
||||
}
|
||||
// Group machine types into readable categories
|
||||
const catMap: Record<string, string> = {
|
||||
press: 'Pressen', hydraulic_press: 'Pressen', mechanical_press: 'Pressen', press_brake: 'Pressen',
|
||||
robot: 'Roboter', industrial_robot: 'Roboter', robot_cell: 'Roboter',
|
||||
collaborative_robot: 'Kollaborierende Roboter', cobot: 'Kollaborierende Roboter',
|
||||
woodworking: 'Holzbearbeitung', saw: 'Holzbearbeitung', circular_saw: 'Holzbearbeitung',
|
||||
panel_saw: 'Holzbearbeitung', table_saw: 'Holzbearbeitung', miter_saw: 'Holzbearbeitung',
|
||||
log_saw: 'Holzbearbeitung', planer: 'Holzbearbeitung', router: 'Holzbearbeitung',
|
||||
lathe: 'Metallbearbeitung', turning_machine: 'Metallbearbeitung', large_lathe: 'Metallbearbeitung',
|
||||
small_lathe: 'Metallbearbeitung', milling_machine: 'Metallbearbeitung', drilling_machine: 'Metallbearbeitung',
|
||||
grinding_machine: 'Metallbearbeitung', metal_saw: 'Metallbearbeitung', band_saw: 'Metallbearbeitung',
|
||||
cold_saw: 'Metallbearbeitung', shearing_machine: 'Metallbearbeitung', bending_machine: 'Metallbearbeitung',
|
||||
cnc: 'Metallbearbeitung', machining_center: 'Metallbearbeitung', transfer_machine: 'Metallbearbeitung',
|
||||
injection_molding: 'Kunststoff/Gummi', plastics_machine: 'Kunststoff/Gummi',
|
||||
compression_molding: 'Kunststoff/Gummi', blow_molding: 'Kunststoff/Gummi',
|
||||
extruder: 'Kunststoff/Gummi', plastics_press: 'Kunststoff/Gummi',
|
||||
rubber_machine: 'Kunststoff/Gummi', two_roll_mill: 'Kunststoff/Gummi',
|
||||
reaction_molding: 'Kunststoff/Gummi', calender: 'Kunststoff/Gummi',
|
||||
food_machine: 'Lebensmittel', meat_grinder: 'Lebensmittel', bread_slicer: 'Lebensmittel',
|
||||
bakery: 'Lebensmittel', mixer: 'Lebensmittel', cooker: 'Lebensmittel',
|
||||
cutter: 'Lebensmittel', food_cutter: 'Lebensmittel', filling_machine: 'Lebensmittel',
|
||||
packaging_machine: 'Verpackung', palletizer: 'Verpackung', pallet_wrapper: 'Verpackung',
|
||||
wrapping_machine: 'Verpackung', strapping_machine: 'Verpackung',
|
||||
textile_machine: 'Textilmaschinen', spinning_machine: 'Textilmaschinen',
|
||||
weaving_machine: 'Textilmaschinen', dyeing_machine: 'Textilmaschinen',
|
||||
nonwoven_machine: 'Textilmaschinen',
|
||||
agricultural_machine: 'Landmaschinen', combine_harvester: 'Landmaschinen',
|
||||
mower: 'Landmaschinen', baler: 'Landmaschinen', sprayer: 'Landmaschinen', tiller: 'Landmaschinen',
|
||||
crane: 'Krane/Hebezeuge', bridge_crane: 'Krane/Hebezeuge', gantry_crane: 'Krane/Hebezeuge',
|
||||
tower_crane: 'Krane/Hebezeuge', mobile_crane: 'Krane/Hebezeuge', hoist: 'Krane/Hebezeuge',
|
||||
winch: 'Krane/Hebezeuge', slewing_crane: 'Krane/Hebezeuge',
|
||||
elevator: 'Aufzuege', lift: 'Aufzuege', construction_hoist: 'Aufzuege',
|
||||
conveyor: 'Foerdertechnik', belt_conveyor: 'Foerdertechnik', screw_conveyor: 'Foerdertechnik',
|
||||
transfer_system: 'Foerdertechnik', rotary_transfer_machine: 'Foerdertechnik',
|
||||
forklift: 'Flurfoerderzeuge', industrial_truck: 'Flurfoerderzeuge',
|
||||
earth_moving: 'Erdbaumaschinen', excavator: 'Erdbaumaschinen',
|
||||
wheel_loader: 'Erdbaumaschinen', bulldozer: 'Erdbaumaschinen',
|
||||
welding_machine: 'Schweissmaschinen', arc_welder: 'Schweissmaschinen',
|
||||
printing_press: 'Druckmaschinen', coating_machine: 'Druckmaschinen',
|
||||
pump: 'Pumpen/Kompressoren', compressor: 'Pumpen/Kompressoren', vacuum_pump: 'Pumpen/Kompressoren',
|
||||
foundry_machine: 'Giesserei', casting_machine: 'Giesserei', die_casting: 'Giesserei',
|
||||
industrial_furnace: 'Industrieoefen', heat_treatment: 'Industrieoefen',
|
||||
dryer: 'Trockner/Oefen', oven: 'Trockner/Oefen', kiln: 'Trockner/Oefen',
|
||||
paper_machine: 'Papiermaschinen', slitter_rewinder: 'Papiermaschinen', pulper: 'Papiermaschinen',
|
||||
centrifuge: 'Zentrifugen',
|
||||
aerial_platform: 'Hubarbeitsbuehnen', cherry_picker: 'Hubarbeitsbuehnen',
|
||||
scissor_lift: 'Hubtische', lift_table: 'Hubtische',
|
||||
powered_gate: 'Tore/Tueren', industrial_door: 'Tore/Tueren',
|
||||
laser_machine: 'Lasermaschinen', laser_cutter: 'Lasermaschinen',
|
||||
silo: 'Schuettgutanlagen', bunker: 'Schuettgutanlagen',
|
||||
suspended_platform: 'Haengebuehnen', scaffold: 'Haengebuehnen',
|
||||
storage_retrieval: 'Lagertechnik', automated_warehouse: 'Lagertechnik',
|
||||
pressure_vessel: 'Druckbehaelter', hydraulic_accumulator: 'Druckbehaelter',
|
||||
}
|
||||
const cats = new Set<string>()
|
||||
for (const mt of machineTypes) {
|
||||
cats.add(catMap[mt] || mt)
|
||||
}
|
||||
const sortedCats = Array.from(cats).sort()
|
||||
setStats({ total: norms.length, byType, categories: sortedCats })
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
if (loading || !stats) return null
|
||||
|
||||
const cDesc = stats.categories.join(', ')
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/10 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||
<button onClick={() => setExpanded(!expanded)} className="w-full text-left">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<span className="text-sm font-semibold text-purple-800 dark:text-purple-300">
|
||||
Normen-Bibliothek: {stats.total} Normen in {stats.categories.length} Branchen
|
||||
</span>
|
||||
</div>
|
||||
<svg className={`w-4 h-4 text-purple-400 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-purple-200 dark:border-purple-700">
|
||||
<th className="text-left py-1.5 px-2 font-semibold text-purple-800 dark:text-purple-300 w-48">Typ</th>
|
||||
<th className="text-center py-1.5 px-2 font-semibold text-purple-800 dark:text-purple-300 w-16">Anzahl</th>
|
||||
<th className="text-left py-1.5 px-2 font-semibold text-purple-800 dark:text-purple-300">Abdeckung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(['A', 'B1', 'B2', 'C'] as const).map((type) => {
|
||||
const info = TYPE_INFO[type]
|
||||
const count = stats.byType[type] || 0
|
||||
const desc = type === 'C' ? cDesc : CATEGORY_DESCRIPTIONS[type]
|
||||
return (
|
||||
<tr key={type} className="border-b border-purple-100 dark:border-purple-800/50">
|
||||
<td className="py-2 px-2">
|
||||
<span className={`inline-block px-2 py-0.5 rounded border text-xs font-medium ${info.color}`}>
|
||||
{info.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-2 text-center font-bold text-purple-900 dark:text-purple-200">{count}</td>
|
||||
<td className="py-2 px-2 text-gray-700 dark:text-gray-300">{desc}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="pt-2 text-xs text-purple-600 dark:text-purple-400">
|
||||
Alle Normen mit Abschnittsnummern und{' '}
|
||||
<a href="https://www.beuth.de" target="_blank" rel="noopener noreferrer" className="underline font-medium hover:text-purple-800">
|
||||
Beuth-Kauflinks
|
||||
</a>{' '}
|
||||
hinterlegt. Die vollstaendige Bibliothek ist unter "Normenrecherche" in jedem Projekt einsehbar.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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. "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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user