fix: Fehlende Dateien fuer Grenzen-Formular + Report-Export

Interview: LimitsFormSections, FormFields, SectionCard, _types
Tech-File: ReportPrintView, report-types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-07 15:56:28 +02:00
parent f51671737a
commit 5244500af6
6 changed files with 1097 additions and 75 deletions
@@ -0,0 +1,125 @@
'use client'
interface TextInputProps {
label: string
value: string
onChange: (value: string) => void
placeholder?: string
helpText?: string
disabled?: boolean
}
export function TextInput({ label, value, onChange, placeholder, helpText, disabled }: TextInputProps) {
return (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
{helpText && <p className="text-xs text-gray-400 mb-1.5">{helpText}</p>}
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:text-gray-400"
/>
</div>
)
}
interface TextAreaProps {
label: string
value: string
onChange: (value: string) => void
placeholder?: string
helpText?: string
rows?: number
}
export function TextArea({ label, value, onChange, placeholder, helpText, rows = 3 }: TextAreaProps) {
return (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
{helpText && <p className="text-xs text-gray-400 mb-1.5">{helpText}</p>}
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
rows={rows}
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-y"
/>
</div>
)
}
interface SelectInputProps {
label: string
value: string
onChange: (value: string) => void
options: string[]
placeholder?: string
helpText?: string
}
export function SelectInput({ label, value, onChange, options, placeholder, helpText }: SelectInputProps) {
return (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
{helpText && <p className="text-xs text-gray-400 mb-1.5">{helpText}</p>}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="">{placeholder || '-- Bitte waehlen --'}</option>
{options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
)
}
interface CheckboxGroupProps {
label: string
values: string[]
onChange: (values: string[]) => void
options: string[]
helpText?: string
}
export function CheckboxGroup({ label, values, onChange, options, helpText }: CheckboxGroupProps) {
const toggle = (opt: string) => {
if (values.includes(opt)) {
onChange(values.filter((v) => v !== opt))
} else {
onChange([...values, opt])
}
}
return (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
{helpText && <p className="text-xs text-gray-400 mb-1.5">{helpText}</p>}
<div className="flex flex-wrap gap-2">
{options.map((opt) => (
<label
key={opt}
className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg border cursor-pointer transition-colors ${
values.includes(opt)
? 'bg-purple-50 border-purple-300 text-purple-700 dark:bg-purple-900/40 dark:border-purple-600 dark:text-purple-300'
: 'bg-white border-gray-200 text-gray-700 hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300'
}`}
>
<input
type="checkbox"
checked={values.includes(opt)}
onChange={() => toggle(opt)}
className="w-3.5 h-3.5 text-purple-600 rounded"
/>
{opt}
</label>
))}
</div>
</div>
)
}
@@ -0,0 +1,209 @@
'use client'
import { SectionCard } from './SectionCard'
import { TextInput, TextArea, SelectInput, CheckboxGroup } from './FormFields'
import {
FORM_SECTIONS,
AREA_OF_USE_OPTIONS,
OPERATING_MODE_OPTIONS,
PERSON_GROUP_OPTIONS,
type LimitsFormData,
} from '../_types'
interface LimitsFormSectionsProps {
data: LimitsFormData
onChange: (field: keyof LimitsFormData, value: string | string[]) => void
prefilled: { machine_name?: string; machine_type?: string; manufacturer?: string }
}
export function LimitsFormSections({ data, onChange, prefilled }: LimitsFormSectionsProps) {
return (
<div className="space-y-3">
{/* Section 1: Allgemeine Produktbeschreibung */}
<SectionCard section={FORM_SECTIONS[0]} defaultOpen>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TextInput
label="Maschinenbezeichnung *"
value={data.machine_designation || prefilled.machine_name || ''}
onChange={(v) => onChange('machine_designation', v)}
placeholder="z.B. Roboterzelle RZ-500"
helpText={prefilled.machine_name ? `Vorausgefuellt aus Projekt: ${prefilled.machine_name}` : undefined}
/>
<TextInput
label="Maschinentyp *"
value={data.machine_type || prefilled.machine_type || ''}
onChange={(v) => onChange('machine_type', v)}
placeholder="z.B. Roboterzelle / CNC-Maschine"
helpText={prefilled.machine_type ? `Vorausgefuellt aus Projekt: ${prefilled.machine_type}` : undefined}
/>
<TextInput
label="Hersteller *"
value={data.manufacturer || prefilled.manufacturer || ''}
onChange={(v) => onChange('manufacturer', v)}
placeholder="z.B. Mueller Maschinenbau GmbH"
helpText={prefilled.manufacturer ? `Vorausgefuellt aus Projekt: ${prefilled.manufacturer}` : undefined}
/>
<TextInput
label="Baujahr"
value={data.year_of_construction}
onChange={(v) => onChange('year_of_construction', v)}
placeholder="z.B. 2026"
/>
<TextInput
label="Seriennummer"
value={data.serial_number}
onChange={(v) => onChange('serial_number', v)}
placeholder="z.B. SN-2026-001"
/>
</div>
<TextArea
label="Allgemeine Beschreibung *"
value={data.general_description}
onChange={(v) => onChange('general_description', v)}
placeholder="Die EIGENBAU-Zelle ist ein Arbeitstisch mit integriertem Roboterarm, der Bauteile aus einem Magazin entnimmt und dem Bearbeitungszentrum zufuehrt..."
helpText="Beschreiben Sie Aufbau, Funktion und Arbeitsweise der Maschine/Anlage ausfuehrlich."
rows={5}
/>
</SectionCard>
{/* Section 2: Bestimmungsgemasse Verwendung */}
<SectionCard section={FORM_SECTIONS[1]}>
<TextArea
label="Verwendungszweck *"
value={data.intended_purpose}
onChange={(v) => onChange('intended_purpose', v)}
placeholder="Zum Einsatz an Bearbeitungszentren, zur Zufuehrung von Bauteilen aus einem Magazin in die Bearbeitungsmaschine..."
helpText="Beschreiben Sie den bestimmungsgemassen Einsatzzweck der Maschine."
rows={4}
/>
<SelectInput
label="Einsatzbereich *"
value={data.area_of_use}
onChange={(v) => onChange('area_of_use', v)}
options={AREA_OF_USE_OPTIONS}
/>
<CheckboxGroup
label="Betriebsarten"
values={data.operating_modes}
onChange={(v) => onChange('operating_modes', v)}
options={OPERATING_MODE_OPTIONS}
helpText="Waehlen Sie alle zutreffenden Betriebsarten."
/>
<TextArea
label="Varianten"
value={data.variants}
onChange={(v) => onChange('variants', v)}
placeholder="Variante A: nicht-kollaborierend mit Schutzzaun&#10;Variante B: kollaborierend mit Kraft-/Leistungsbegrenzung"
helpText="Beschreiben Sie verschiedene Ausbauvarianten oder Konfigurationen der Maschine."
rows={3}
/>
</SectionCard>
{/* Section 3: Vorhersehbare Fehlanwendung */}
<SectionCard section={FORM_SECTIONS[2]}>
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3 mb-2">
<p className="text-xs text-amber-700 dark:text-amber-300">
Dokumentieren Sie alle vernuenftigerweise vorhersehbaren Fehlanwendungen gemaess ISO 12100 Abschnitt 5.4. Beruecksichtigen Sie dabei reflexartiges Verhalten, mangelnde Konzentration und Verhaltensweisen nach dem Grundsatz des geringsten Widerstandes.
</p>
</div>
<TextArea
label="Vorhersehbare Fehlanwendungen *"
value={data.foreseeable_misuses}
onChange={(v) => onChange('foreseeable_misuses', v)}
placeholder="- Eingriff in laufende Maschine bei Stoerung&#10;- Umgehung von Schutzeinrichtungen (Tuerschalter ueberbrueckt)&#10;- Betrieb mit offenem Schutzzaun&#10;- Unqualifiziertes Personal fuehrt Wartungsarbeiten durch&#10;- Verwendung nicht freigegebener Werkzeuge/Materialien"
helpText="Jeweils eine Fehlanwendung pro Zeile, mit Stichpunkt-Aufzaehlung."
rows={6}
/>
</SectionCard>
{/* Section 4: Grenzen der Maschine */}
<SectionCard section={FORM_SECTIONS[3]}>
<TextArea
label="Raeumliche Grenzen *"
value={data.spatial_limits}
onChange={(v) => onChange('spatial_limits', v)}
placeholder="Abmessungen: 2000 x 1500 x 1800 mm (LxBxH)&#10;Arbeitsraum Roboter: Radius 850mm&#10;Zugangsbereich: nur von vorne&#10;Sicherheitsabstand: min. 500mm zum Schutzzaun"
helpText="Abmessungen, Arbeitsraum, Zugangsbereich, Sicherheitsabstaende."
rows={4}
/>
<TextArea
label="Zeitliche Grenzen"
value={data.temporal_limits}
onChange={(v) => onChange('temporal_limits', v)}
placeholder="Geplante Lebensdauer: 15 Jahre&#10;Wartungsintervall: alle 2000 Betriebsstunden&#10;Max. Betriebsdauer pro Tag: 16 Stunden (2-Schicht)"
helpText="Lebensdauer, Wartungsintervalle, Nutzungsdauer pro Tag/Woche."
rows={3}
/>
<TextArea
label="Betriebsbedingungen"
value={data.operating_conditions}
onChange={(v) => onChange('operating_conditions', v)}
placeholder="Temperatur: +5 bis +40 Grad C&#10;Luftfeuchtigkeit: max. 80% (nicht kondensierend)&#10;Hoehenlage: bis 1000m ue.NN&#10;Keine explosionsgefaehrdete Atmosphaere"
helpText="Temperatur, Feuchtigkeit, Staub, Vibrationen, besondere Umgebungsbedingungen."
rows={4}
/>
<TextArea
label="Energieversorgung"
value={data.energy_supply}
onChange={(v) => onChange('energy_supply', v)}
placeholder="Elektrisch: 400V/50Hz, 32A Absicherung&#10;Druckluft: 6 bar, oelfrei&#10;Pneumatik: 6 bar Betriebsdruck"
helpText="Spannung, Absicherung, Druckluftversorgung, weitere Energiequellen."
rows={3}
/>
</SectionCard>
{/* Section 5: Schnittstellen */}
<SectionCard section={FORM_SECTIONS[4]}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TextArea
label="Mechanische Schnittstellen"
value={data.mechanical_interfaces}
onChange={(v) => onChange('mechanical_interfaces', v)}
placeholder="- Flanschverbindung zum Bearbeitungszentrum&#10;- Magazin-Andockstation&#10;- Greifer-Wechselsystem"
rows={3}
/>
<TextArea
label="Elektrische Schnittstellen"
value={data.electrical_interfaces}
onChange={(v) => onChange('electrical_interfaces', v)}
placeholder="- ProfiNET Steuerungsbus&#10;- 24V Sicherheitskreis&#10;- E/A-Module fuer Sensorik"
rows={3}
/>
<TextArea
label="Software-Schnittstellen"
value={data.software_interfaces}
onChange={(v) => onChange('software_interfaces', v)}
placeholder="- OPC UA Server&#10;- REST API fuer MES-Anbindung&#10;- HMI Webinterface"
rows={3}
/>
<TextArea
label="Pneumatische/Hydraulische Schnittstellen"
value={data.pneumatic_hydraulic_interfaces}
onChange={(v) => onChange('pneumatic_hydraulic_interfaces', v)}
placeholder="- 6mm Druckluftanschluss&#10;- Wartungseinheit mit Filter/Regler&#10;- Abluft ueber Schalldaempfer"
rows={3}
/>
</div>
</SectionCard>
{/* Section 6: Betroffene Personen */}
<SectionCard section={FORM_SECTIONS[5]}>
<CheckboxGroup
label="Personengruppen"
values={data.person_groups}
onChange={(v) => onChange('person_groups', v)}
options={PERSON_GROUP_OPTIONS}
helpText="Waehlen Sie alle Personengruppen, die mit der Maschine in Beruehrung kommen koennen."
/>
<TextArea
label="Qualifikationsanforderungen"
value={data.qualification_requirements}
onChange={(v) => onChange('qualification_requirements', v)}
placeholder="Bedienpersonal: Unterweisung gemaess Betriebsanleitung, min. 18 Jahre&#10;Einrichter: Facharbeiter Mechatronik + Herstellerschulung&#10;Wartungspersonal: Elektrofachkraft fuer Elektroanschluss, Mechatroniker fuer mechanische Wartung"
helpText="Mindestqualifikation je Personengruppe mit Verweis auf erforderliche Schulungen."
rows={4}
/>
</SectionCard>
</div>
)
}
@@ -0,0 +1,91 @@
'use client'
import { useState, type ReactNode } from 'react'
import type { FormSection } from '../_types'
function SectionIcon({ icon, className }: { icon: FormSection['icon']; className?: string }) {
const cls = className || 'w-5 h-5'
switch (icon) {
case 'clipboard':
return (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
)
case 'target':
return (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
)
case 'alert':
return (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
)
case 'box':
return (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
)
case 'link':
return (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
)
case 'users':
return (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
)
default:
return null
}
}
interface SectionCardProps {
section: FormSection
defaultOpen?: boolean
children: ReactNode
}
export function SectionCard({ section, defaultOpen = false, children }: SectionCardProps) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<button
type="button"
onClick={() => setOpen(!open)}
className="w-full flex items-center gap-4 px-6 py-4 text-left hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
>
<div className="w-10 h-10 bg-purple-50 dark:bg-purple-900/30 rounded-lg flex items-center justify-center text-purple-600 flex-shrink-0">
<SectionIcon icon={section.icon} className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-gray-900 dark:text-white">
{section.number}. {section.title}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{section.description}
</div>
</div>
<svg
className={`w-5 h-5 text-gray-400 transition-transform flex-shrink-0 ${open ? '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>
</button>
{open && (
<div className="px-6 pb-6 pt-2 border-t border-gray-100 dark:border-gray-700 space-y-4">
{children}
</div>
)}
</div>
)
}
@@ -1,85 +1,142 @@
// IACE Interview Types — structured questions based on CE risk assessment document structure // IACE Limits & Intended Use Form Types — CE Risk Assessment Step 3
// Based on ISO 12100 Sections 5.3 (Intended Use) and 5.4 (Limits)
export interface InterviewQuestion { /** Full form data structure stored in project metadata.limits_form */
id: string export interface LimitsFormData {
section: number // Section 1: Allgemeine Produktbeschreibung
sectionTitle: string machine_designation: string
question: string machine_type: string
type: 'text' | 'textarea' | 'select' | 'multiselect' | 'number' manufacturer: string
options?: string[] year_of_construction: string
placeholder?: string serial_number: string
helpText?: string general_description: string
required?: boolean
// Section 2: Bestimmungsgemasse Verwendung
intended_purpose: string
area_of_use: string
operating_modes: string[]
variants: string
// Section 3: Vorhersehbare Fehlanwendung
foreseeable_misuses: string
// Section 4: Grenzen der Maschine
spatial_limits: string
temporal_limits: string
operating_conditions: string
energy_supply: string
// Section 5: Schnittstellen
mechanical_interfaces: string
electrical_interfaces: string
software_interfaces: string
pneumatic_hydraulic_interfaces: string
// Section 6: Betroffene Personen
person_groups: string[]
qualification_requirements: string
} }
export interface InterviewAnswer { export const EMPTY_LIMITS_FORM: LimitsFormData = {
questionId: string machine_designation: '',
value: string | string[] | number machine_type: '',
manufacturer: '',
year_of_construction: '',
serial_number: '',
general_description: '',
intended_purpose: '',
area_of_use: '',
operating_modes: [],
variants: '',
foreseeable_misuses: '',
spatial_limits: '',
temporal_limits: '',
operating_conditions: '',
energy_supply: '',
mechanical_interfaces: '',
electrical_interfaces: '',
software_interfaces: '',
pneumatic_hydraulic_interfaces: '',
person_groups: [],
qualification_requirements: '',
} }
export const INTERVIEW_QUESTIONS: InterviewQuestion[] = [ export const AREA_OF_USE_OPTIONS = [
// Section 1: Maschinenbeschreibung 'Industriell',
{ id: 'machine_name', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Wie heisst die Maschine / Anlage?', type: 'text', placeholder: 'z.B. Kniehebelpresse HP-500', required: true }, 'Gewerblich',
{ id: 'machine_type', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Welcher Maschinentyp ist es?', type: 'select', options: ['Presse', 'Roboter', 'CNC-Maschine', 'Foerderanlage', 'Verpackungsmaschine', 'Schweissanlage', 'Montageanlage', 'Sondermaschine'], required: true }, 'Privat',
{ id: 'manufacturer', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Wer ist der Hersteller?', type: 'text', placeholder: 'z.B. Mueller Maschinenbau GmbH' }, 'Oeffentlich',
{ id: 'description', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Beschreiben Sie die Anlage und ihre Funktion:', type: 'textarea', placeholder: 'Die Anlage ist eine vollautomatische...', helpText: 'Beschreiben Sie den Zweck, die Arbeitsweise und den Aufbau der Maschine.', required: true },
{ id: 'components', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Aus welchen Baugruppen besteht die Anlage?', type: 'multiselect', options: ['Zufuehrung', 'Presse/Umformung', 'Transferanlage', 'Foerderband', 'Roboter', 'Absaugung', 'Schmieranlage', 'Schutzumhausung', 'Aufzug/Hubwerk', 'Schaltschrank/Steuerung', 'Kuehlung', 'Heizung', 'Hydraulik', 'Pneumatik'] },
// Section 2: Lebensphasen
{ id: 'lifecycle_operation', section: 2, sectionTitle: 'Lebensphasen', question: 'Wie laeuft der Normalbetrieb ab?', type: 'textarea', placeholder: 'Die Bearbeitung erfolgt vollautomatisch...', helpText: 'Beschreiben Sie den typischen Produktionszyklus.' },
{ id: 'lifecycle_setup', section: 2, sectionTitle: 'Lebensphasen', question: 'Welche Arbeiten fallen beim Einrichten/Umruesten an?', type: 'textarea', placeholder: 'Werkzeugwechsel, Parameteranpassung...' },
{ id: 'lifecycle_maintenance', section: 2, sectionTitle: 'Lebensphasen', question: 'Welche Wartungs- und Reinigungsarbeiten sind noetig?', type: 'textarea', placeholder: 'Woechentliche Schmierung, Filter reinigen...' },
// Section 3: Bestimmungsgemäße Verwendung
{ id: 'intended_use', section: 3, sectionTitle: 'Bestimmungsgemäße Verwendung', question: 'Wozu dient die Maschine (bestimmungsgemäße Verwendung)?', type: 'textarea', placeholder: 'Die Anlage dient der automatischen...', required: true },
// Section 4: Vorhersehbare Fehlanwendung
{ id: 'misuse', section: 4, sectionTitle: 'Vorhersehbare Fehlanwendung', question: 'Welche vorhersehbaren Fehlanwendungen sind moeglich?', type: 'multiselect', options: ['Ueberschreiten von Belastungsgrenzen', 'Verwendung ungeeigneter Materialien', 'Betrieb in explosionsgefaehrdeter Atmosphaere', 'Betrieb bei Leckagen', 'Betrieb ohne PSA', 'Umgehung von Sicherheitseinrichtungen', 'Bedienung ohne Einweisung', 'Manipulation der Steuerung'], helpText: 'Waehlen Sie alle zutreffenden oder ergaenzen Sie.' },
// Section 5: Qualifikation
{ id: 'operator_qualification', section: 5, sectionTitle: 'Qualifikation der Benutzer', question: 'Welche Qualifikation hat das Bedienpersonal?', type: 'select', options: ['Eingewiesenes Personal ohne Fachkenntnisse', 'Angelernte Mitarbeiter', 'Facharbeiter mit Berufsausbildung', 'Ingenieure/Techniker', 'Elektrofachkraefte'] },
{ id: 'maintenance_qualification', section: 5, sectionTitle: 'Qualifikation der Benutzer', question: 'Wer fuehrt Wartung/Instandhaltung durch?', type: 'select', options: ['Eigenes Fachpersonal', 'Hersteller-Service', 'Fremdfirma', 'Nicht separat betrachtet (CE-Erklaerung Lieferant)'] },
// Section 6: Grenzen
{ id: 'spatial_limits', section: 6, sectionTitle: 'Raeumliche und zeitliche Grenzen', question: 'Welche Gefahrenbereiche gibt es?', type: 'textarea', placeholder: 'Werkzeugeinbauraum, Zufuehrbereich, Auslaufbereich...', helpText: 'Listen Sie alle Bereiche auf, in denen Personen gefaehrdet sein koennten.' },
{ id: 'safety_measures_org', section: 6, sectionTitle: 'Raeumliche und zeitliche Grenzen', question: 'Welche organisatorischen Schutzmassnahmen gelten?', type: 'multiselect', options: ['Sicherheitsschuhe Pflicht', 'Gehoerschutz Pflicht', 'Handschuhe Pflicht', 'Schutzbrille Pflicht', 'Zutrittsbeschraenkung', 'Unterweisung vor Zugang'] },
// Section 7: Technische Daten
{ id: 'force_pressure', section: 7, sectionTitle: 'Technische Daten', question: 'Welche Kraefte/Druecke wirken? (kN, bar, Tonnen)', type: 'text', placeholder: 'z.B. 20000 kN, 250 bar' },
{ id: 'voltage', section: 7, sectionTitle: 'Technische Daten', question: 'Welche Spannungen sind vorhanden? (V)', type: 'text', placeholder: 'z.B. 400V Hauptstrom, 24V Steuerung' },
{ id: 'temperature', section: 7, sectionTitle: 'Technische Daten', question: 'Treten erhoehte Temperaturen auf? (°C)', type: 'text', placeholder: 'z.B. 130°C Werkstuecktemperatur' },
{ id: 'speed_rpm', section: 7, sectionTitle: 'Technische Daten', question: 'Welche Geschwindigkeiten/Drehzahlen gibt es? (/min, m/s)', type: 'text', placeholder: 'z.B. 736 /min Schwungrad, 36 Huebe/min' },
{ id: 'energy', section: 7, sectionTitle: 'Technische Daten', question: 'Welches Arbeitsvermoegen hat die Maschine? (kJ, kW)', type: 'text', placeholder: 'z.B. 400 kJ, 3 kW Motor' },
// Section 8: Umgebung
{ id: 'environment', section: 8, sectionTitle: 'Umgebungsbedingungen', question: 'Unter welchen Umgebungsbedingungen wird die Maschine betrieben?', type: 'textarea', placeholder: '+5 bis +40°C, max. 95% Luftfeuchte, bis 1000m ueNN', helpText: 'Temperatur, Luftfeuchte, Hoehenlage, besondere Bedingungen.' },
] ]
export function answersToNarrativeText(answers: InterviewAnswer[]): string { export const OPERATING_MODE_OPTIONS = [
const parts: string[] = [] 'Automatikbetrieb',
const getVal = (id: string) => { 'Einrichtbetrieb',
const a = answers.find(a => a.questionId === id) 'Handbetrieb',
if (!a) return '' 'Sonderbetrieb',
return Array.isArray(a.value) ? (a.value as string[]).join(', ') : String(a.value) 'Reinigung',
} 'Wartung',
]
parts.push(`Maschinenname: ${getVal('machine_name')}. Maschinentyp: ${getVal('machine_type')}. Hersteller: ${getVal('manufacturer')}.`) export const PERSON_GROUP_OPTIONS = [
if (getVal('description')) parts.push(getVal('description')) 'Bedienpersonal',
if (getVal('components')) parts.push(`Baugruppen: ${getVal('components')}.`) 'Einrichter',
if (getVal('lifecycle_operation')) parts.push(`Betrieb: ${getVal('lifecycle_operation')}`) 'Wartungspersonal',
if (getVal('lifecycle_setup')) parts.push(`Einrichten: ${getVal('lifecycle_setup')}`) 'Reinigungspersonal',
if (getVal('lifecycle_maintenance')) parts.push(`Wartung: ${getVal('lifecycle_maintenance')}`) 'Auszubildende',
if (getVal('intended_use')) parts.push(`Bestimmungsgemäße Verwendung: ${getVal('intended_use')}`) 'Besucher',
if (getVal('misuse')) parts.push(`Vorhersehbare Fehlanwendung: ${getVal('misuse')}`) 'Fremdfirmenpersonal',
if (getVal('operator_qualification')) parts.push(`Bedienpersonal: ${getVal('operator_qualification')}`) ]
if (getVal('spatial_limits')) parts.push(`Gefahrenbereiche: ${getVal('spatial_limits')}`)
if (getVal('safety_measures_org')) parts.push(`Organisatorische Massnahmen: ${getVal('safety_measures_org')}`)
if (getVal('force_pressure')) parts.push(getVal('force_pressure'))
if (getVal('voltage')) parts.push(getVal('voltage'))
if (getVal('temperature')) parts.push(getVal('temperature'))
if (getVal('speed_rpm')) parts.push(getVal('speed_rpm'))
if (getVal('energy')) parts.push(getVal('energy'))
if (getVal('environment')) parts.push(`Umgebung: ${getVal('environment')}`)
return parts.join('\n') /** Section definition for rendering collapsible form cards */
export interface FormSection {
id: string
number: number
title: string
description: string
icon: 'clipboard' | 'target' | 'alert' | 'box' | 'link' | 'users'
} }
export const FORM_SECTIONS: FormSection[] = [
{
id: 'product_description',
number: 1,
title: 'Allgemeine Produktbeschreibung',
description: 'Grundlegende Angaben zur Maschine/Anlage',
icon: 'clipboard',
},
{
id: 'intended_use',
number: 2,
title: 'Bestimmungsgemasse Verwendung',
description: 'Verwendungszweck, Einsatzbereich und Betriebsarten',
icon: 'target',
},
{
id: 'foreseeable_misuse',
number: 3,
title: 'Vorhersehbare Fehlanwendung',
description: 'Vernuenftigerweise vorhersehbare Fehlanwendungen gemaess ISO 12100 Abschnitt 5.4',
icon: 'alert',
},
{
id: 'machine_limits',
number: 4,
title: 'Grenzen der Maschine',
description: 'Raeumliche, zeitliche und betriebliche Grenzen',
icon: 'box',
},
{
id: 'interfaces',
number: 5,
title: 'Schnittstellen',
description: 'Mechanische, elektrische, Software- und pneumatische/hydraulische Schnittstellen',
icon: 'link',
},
{
id: 'affected_persons',
number: 6,
title: 'Betroffene Personen',
description: 'Personengruppen und Qualifikationsanforderungen',
icon: 'users',
},
]
@@ -0,0 +1,376 @@
'use client'
import React from 'react'
import {
ReportData, rpz, plFromRpz, silFromRpz, riskLevelLabel, riskLevelColor,
CATEGORY_LABELS, REDUCTION_LABELS, STATUS_LABELS,
} from './report-types'
interface ReportPrintViewProps {
data: ReportData
}
function formatDate(iso: string): string {
try { return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) }
catch { return iso }
}
const NORM_TYPE_LABELS: Record<string, string> = {
a_norms: 'A-Normen (Grundnormen)',
b1_norms: 'B1-Normen (Sicherheitsgrundnormen)',
b2_norms: 'B2-Normen (Sicherheitsfachgrundnormen)',
c_norms: 'C-Normen (Maschinenspezifisch)',
}
/** Print-optimized CE report rendered as HTML for window.print(). */
export function ReportPrintView({ data }: ReportPrintViewProps) {
const { project, hazards, mitigations, norms, triggers, riskSummary } = data
const sortedHazards = [...hazards].sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
const byDesign = mitigations.filter(m => m.reduction_type === 'design')
const byProtection = mitigations.filter(m => m.reduction_type === 'protection')
const byInfo = mitigations.filter(m => m.reduction_type === 'information')
const openMitigations = mitigations.filter(m => m.status !== 'verified')
const highRiskCount = (riskSummary.critical || 0) + (riskSummary.high || 0)
return (
<div className="report-print-view">
<style>{`
.report-print-view {
font-family: 'Segoe UI', Arial, sans-serif;
color: #1a1a1a;
font-size: 10pt;
line-height: 1.5;
max-width: 210mm;
margin: 0 auto;
}
.report-print-view h1 { font-size: 20pt; margin: 0 0 4pt; color: #1e1b4b; }
.report-print-view h2 {
font-size: 13pt; margin: 20pt 0 8pt; padding-bottom: 4pt;
border-bottom: 2pt solid #7c3aed; color: #1e1b4b;
}
.report-print-view h3 { font-size: 11pt; margin: 12pt 0 6pt; color: #374151; }
.report-print-view table {
width: 100%; border-collapse: collapse; margin: 8pt 0;
font-size: 8.5pt; page-break-inside: auto;
}
.report-print-view th, .report-print-view td {
border: 0.5pt solid #d1d5db; padding: 3pt 5pt; text-align: left;
}
.report-print-view th {
background: #f3f4f6; font-weight: 600; color: #374151;
}
.report-print-view tr { page-break-inside: avoid; }
.report-print-view .cover {
text-align: center; padding: 60pt 20pt 40pt;
border-bottom: 3pt solid #7c3aed;
}
.report-print-view .cover .subtitle {
font-size: 14pt; color: #6b7280; margin-top: 8pt;
}
.report-print-view .cover .meta {
margin-top: 30pt; font-size: 10pt; color: #374151;
}
.report-print-view .cover .meta td { border: none; padding: 2pt 8pt; }
.report-print-view .cover .meta td:first-child { font-weight: 600; text-align: right; }
.report-print-view .toc { margin: 16pt 0; }
.report-print-view .toc li { padding: 3pt 0; color: #374151; }
.report-print-view .risk-cell { font-weight: 600; text-align: center; }
.report-print-view .badge {
display: inline-block; padding: 1pt 6pt; border-radius: 3pt;
font-size: 7.5pt; font-weight: 600;
}
.report-print-view .section-break { page-break-before: always; }
.report-print-view .summary-box {
border: 1pt solid #d1d5db; border-radius: 4pt; padding: 12pt;
margin: 8pt 0; background: #f9fafb;
}
.report-print-view .footer-line {
margin-top: 24pt; padding-top: 8pt; border-top: 1pt solid #d1d5db;
font-size: 8pt; color: #9ca3af; text-align: center;
}
@media print {
.report-print-view { margin: 0; max-width: none; }
.report-print-view .section-break { page-break-before: always; }
@page { size: A4; margin: 15mm 12mm 18mm; }
}
`}</style>
{/* 1. Deckblatt */}
<div className="cover">
<h1>CE-Akte / Risikobeurteilung</h1>
<div className="subtitle">{project.machine_name}</div>
<table className="meta" style={{ margin: '30pt auto 0', textAlign: 'left' }}>
<tbody>
<tr><td>Maschinentyp:</td><td>{project.machine_type || '-'}</td></tr>
<tr><td>Hersteller:</td><td>{project.manufacturer || '-'}</td></tr>
<tr><td>Projektstatus:</td><td>{project.status}</td></tr>
<tr><td>Erstelldatum:</td><td>{formatDate(project.created_at)}</td></tr>
<tr><td>Letzte Aktualisierung:</td><td>{formatDate(project.updated_at)}</td></tr>
<tr><td>Vollstaendigkeit:</td><td>{project.completeness_pct}%</td></tr>
</tbody>
</table>
</div>
{/* 2. Inhaltsverzeichnis */}
<div className="section-break">
<h2>Inhaltsverzeichnis</h2>
<ol className="toc">
<li>Maschinenbeschreibung</li>
<li>Angewandte Normen</li>
<li>Gefaehrdungsliste</li>
<li>Risikobewertung</li>
<li>Massnahmenliste</li>
<li>Compliance-Hinweise</li>
<li>Zusammenfassung</li>
</ol>
</div>
{/* 3. Maschinenbeschreibung */}
<div className="section-break">
<h2>1. Maschinenbeschreibung</h2>
<table>
<tbody>
<tr><td style={{ fontWeight: 600, width: '35%' }}>Maschinenbezeichnung</td><td>{project.machine_name}</td></tr>
<tr><td style={{ fontWeight: 600 }}>Maschinentyp</td><td>{project.machine_type || '-'}</td></tr>
<tr><td style={{ fontWeight: 600 }}>Hersteller</td><td>{project.manufacturer || '-'}</td></tr>
<tr><td style={{ fontWeight: 600 }}>Anzahl Komponenten</td><td>{project.component_count}</td></tr>
<tr><td style={{ fontWeight: 600 }}>Anzahl Gefaehrdungen</td><td>{project.hazard_count}</td></tr>
<tr><td style={{ fontWeight: 600 }}>Anzahl Massnahmen</td><td>{project.mitigation_count}</td></tr>
</tbody>
</table>
</div>
{/* 4. Angewandte Normen */}
<div className="section-break">
<h2>2. Angewandte Normen</h2>
{norms && norms.total > 0 ? (
(['a_norms', 'b1_norms', 'b2_norms', 'c_norms'] as const).map(key => {
const items = norms[key]
if (!items || items.length === 0) return null
return (
<div key={key}>
<h3>{NORM_TYPE_LABELS[key]}</h3>
<table>
<thead>
<tr><th style={{ width: '20%' }}>Nummer</th><th style={{ width: '45%' }}>Titel</th><th>Abschnitte / Grund</th></tr>
</thead>
<tbody>
{items.map((ns, i) => (
<tr key={i}>
<td>{ns.norm.number}</td>
<td>{ns.norm.title_de}</td>
<td>{ns.reason}</td>
</tr>
))}
</tbody>
</table>
</div>
)
})
) : (
<p style={{ color: '#6b7280' }}>Keine Normenvorschlaege vorhanden.</p>
)}
</div>
{/* 5. Gefaehrdungsliste */}
<div className="section-break">
<h2>3. Gefaehrdungsliste</h2>
<table>
<thead>
<tr>
<th style={{ width: '5%' }}>Nr.</th>
<th style={{ width: '15%' }}>Komponente</th>
<th style={{ width: '20%' }}>Gefaehrdung</th>
<th style={{ width: '12%' }}>Kategorie</th>
<th style={{ width: '24%' }}>Szenario</th>
<th style={{ width: '12%' }}>Lebensphase</th>
<th style={{ width: '12%' }}>Risiko</th>
</tr>
</thead>
<tbody>
{sortedHazards.map((h, i) => (
<tr key={h.id}>
<td>{i + 1}</td>
<td>{h.component_name || '-'}</td>
<td>{h.name}</td>
<td>{CATEGORY_LABELS[h.category] || h.category}</td>
<td>{h.possible_harm || h.trigger_event || '-'}</td>
<td>{h.lifecycle_phase || '-'}</td>
<td className="risk-cell" style={{ color: riskLevelColor(h.risk_level) }}>
{riskLevelLabel(h.risk_level)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 6. Risikobewertung */}
<div className="section-break">
<h2>4. Risikobewertung</h2>
<table>
<thead>
<tr>
<th>Nr.</th>
<th>Gefaehrdung</th>
<th style={{ textAlign: 'center' }}>S</th>
<th style={{ textAlign: 'center' }}>E</th>
<th style={{ textAlign: 'center' }}>P</th>
<th style={{ textAlign: 'center' }}>RPZ</th>
<th style={{ textAlign: 'center' }}>SIL</th>
<th style={{ textAlign: 'center' }}>PL</th>
<th>Risiko</th>
<th style={{ textAlign: 'center' }}>Akzeptabel</th>
</tr>
</thead>
<tbody>
{sortedHazards.map((h, i) => {
const r = rpz(h.severity, h.exposure, h.probability, h.avoidance)
const sil = silFromRpz(r)
const pl = plFromRpz(r)
const acceptable = r <= 20
return (
<tr key={h.id}>
<td>{i + 1}</td>
<td>{h.name}</td>
<td style={{ textAlign: 'center' }}>{h.severity}</td>
<td style={{ textAlign: 'center' }}>{h.exposure}</td>
<td style={{ textAlign: 'center' }}>{h.probability}</td>
<td className="risk-cell" style={{ color: riskLevelColor(h.risk_level) }}>{r}</td>
<td style={{ textAlign: 'center' }}>{sil}</td>
<td style={{ textAlign: 'center' }}>{pl}</td>
<td style={{ color: riskLevelColor(h.risk_level) }}>{riskLevelLabel(h.risk_level)}</td>
<td style={{ textAlign: 'center', color: acceptable ? '#16a34a' : '#dc2626', fontWeight: 600 }}>
{acceptable ? 'Ja' : 'Nein'}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{/* 7. Massnahmenliste */}
<div className="section-break">
<h2>5. Massnahmenliste</h2>
<p style={{ marginBottom: '8pt', color: '#374151' }}>
Gesamt: {mitigations.length} Massnahmen
(Design: {byDesign.length}, Schutz: {byProtection.length}, Information: {byInfo.length})
</p>
<table>
<thead>
<tr>
<th style={{ width: '5%' }}>Nr.</th>
<th style={{ width: '25%' }}>Massnahme</th>
<th style={{ width: '15%' }}>Typ</th>
<th style={{ width: '30%' }}>Zugeordnete Gefaehrdungen</th>
<th style={{ width: '12%' }}>Status</th>
</tr>
</thead>
<tbody>
{mitigations.map((m, i) => (
<tr key={m.id}>
<td>{i + 1}</td>
<td>{m.title}</td>
<td>{REDUCTION_LABELS[m.reduction_type] || m.reduction_type}</td>
<td>{m.linked_hazard_names?.join(', ') || '-'}</td>
<td>
<span className="badge" style={{
background: m.status === 'verified' ? '#dcfce7' : m.status === 'implemented' ? '#dbeafe' : '#fef3c7',
color: m.status === 'verified' ? '#166534' : m.status === 'implemented' ? '#1e40af' : '#92400e',
}}>
{STATUS_LABELS[m.status] || m.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 8. Compliance-Hinweise */}
{triggers.length > 0 && (
<div className="section-break">
<h2>6. Compliance-Hinweise</h2>
<table>
<thead>
<tr>
<th style={{ width: '12%' }}>Regulation</th>
<th style={{ width: '12%' }}>Artikel</th>
<th style={{ width: '25%' }}>Titel</th>
<th style={{ width: '10%' }}>Schwere</th>
<th>Grund</th>
</tr>
</thead>
<tbody>
{triggers.map((t) => (
<tr key={t.id}>
<td>{t.regulation}</td>
<td>{t.article}</td>
<td>{t.title}</td>
<td>
<span className="badge" style={{
background: t.severity === 'high' ? '#fecaca' : t.severity === 'medium' ? '#fef3c7' : '#dbeafe',
color: t.severity === 'high' ? '#991b1b' : t.severity === 'medium' ? '#92400e' : '#1e40af',
}}>
{t.severity === 'high' ? 'HOCH' : t.severity === 'medium' ? 'MITTEL' : 'NIEDRIG'}
</span>
</td>
<td>{t.reason}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* 9. Zusammenfassung */}
<div className="section-break">
<h2>7. Zusammenfassung</h2>
<div className="summary-box">
<h3 style={{ marginTop: 0 }}>Gesamtrisiko</h3>
<table>
<thead>
<tr>
<th>Risikostufe</th>
<th style={{ textAlign: 'center' }}>Anzahl</th>
</tr>
</thead>
<tbody>
{[
['Kritisch / Sehr hoch', (riskSummary.critical || 0) + (riskSummary.high || 0), '#dc2626'],
['Mittel', riskSummary.medium || 0, '#ca8a04'],
['Niedrig', riskSummary.low || 0, '#16a34a'],
].map(([label, count, color]) => (
<tr key={String(label)}>
<td style={{ color: String(color), fontWeight: 600 }}>{String(label)}</td>
<td style={{ textAlign: 'center' }}>{String(count)}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="summary-box">
<h3 style={{ marginTop: 0 }}>Offene Massnahmen</h3>
<p>{openMitigations.length} von {mitigations.length} Massnahmen noch nicht verifiziert.</p>
</div>
<div className="summary-box">
<h3 style={{ marginTop: 0 }}>Empfehlung</h3>
<p style={{ fontWeight: 600, color: highRiskCount > 0 ? '#dc2626' : '#16a34a' }}>
{highRiskCount > 0
? `Es bestehen ${highRiskCount} Gefaehrdungen mit hohem/kritischem Risiko. Massnahmen muessen umgesetzt und verifiziert werden, bevor die Maschine in Verkehr gebracht werden darf.`
: openMitigations.length > 0
? 'Alle identifizierten Risiken liegen im akzeptablen Bereich. Offene Massnahmen sollten zeitnah abgeschlossen und verifiziert werden.'
: 'Alle Risiken liegen im akzeptablen Bereich und alle Massnahmen sind verifiziert. Die Maschine kann in Verkehr gebracht werden.'}
</p>
</div>
</div>
<div className="footer-line">
Erstellt mit BreakPilot ComplAI am {formatDate(new Date().toISOString())} | CE-Akte: {project.machine_name}
</div>
</div>
)
}
@@ -0,0 +1,164 @@
// Types shared between ReportGenerator and ReportPrintView
export interface ProjectData {
id: string
machine_name: string
machine_type: string
manufacturer: string
status: string
completeness_pct: number
created_at: string
updated_at: string
component_count: number
hazard_count: number
mitigation_count: number
}
export interface HazardData {
id: string
name: string
description: string
component_name: string | null
category: string
lifecycle_phase: string
trigger_event: string
affected_person: string
possible_harm: string
severity: number
exposure: number
probability: number
avoidance: number
r_inherent: number
risk_level: string
status: string
}
export interface MitigationData {
id: string
title: string
description: string
reduction_type: 'design' | 'protection' | 'information'
status: 'planned' | 'implemented' | 'verified'
linked_hazard_ids: string[]
linked_hazard_names: string[]
}
export interface NormSuggestion {
norm: {
id: string
number: string
title_de: string
norm_type: string
scope_de: string
mandatory: boolean
}
reason: string
confidence: number
}
export interface NormResult {
a_norms: NormSuggestion[]
b1_norms: NormSuggestion[]
b2_norms: NormSuggestion[]
c_norms: NormSuggestion[]
total: number
}
export interface ComplianceTrigger {
id: string
regulation: string
article: string
title: string
severity: 'high' | 'medium' | 'low'
reason: string
affected_hazard_count?: number
module_path: string
module_label: string
}
export interface RiskSummary {
critical?: number
high?: number
medium?: number
low?: number
total?: number
}
export interface ReportData {
project: ProjectData
hazards: HazardData[]
mitigations: MitigationData[]
norms: NormResult | null
triggers: ComplianceTrigger[]
riskSummary: RiskSummary
}
// Helpers shared by report views
export function rpz(s: number, e: number, p: number, a: number): number {
return a >= 1 ? s * e * p * a : s * e * p
}
export function plFromRpz(r: number): string {
if (r > 300) return 'e'
if (r >= 151) return 'd'
if (r >= 61) return 'c'
if (r >= 21) return 'b'
return 'a'
}
export function silFromRpz(r: number): number {
if (r > 300) return 3
if (r >= 151) return 2
if (r >= 61) return 1
return 0
}
export function riskLevelLabel(level: string): string {
const labels: Record<string, string> = {
not_acceptable: 'Nicht akzeptabel',
very_high: 'Sehr hoch',
critical: 'Kritisch',
high: 'Hoch',
medium: 'Mittel',
low: 'Niedrig',
}
return labels[level] || level
}
export function riskLevelColor(level: string): string {
const colors: Record<string, string> = {
not_acceptable: '#dc2626',
very_high: '#dc2626',
critical: '#dc2626',
high: '#ea580c',
medium: '#ca8a04',
low: '#16a34a',
}
return colors[level] || '#6b7280'
}
export const CATEGORY_LABELS: Record<string, string> = {
mechanical: 'Mechanisch',
electrical: 'Elektrisch',
thermal: 'Thermisch',
pneumatic_hydraulic: 'Pneumatik/Hydraulik',
noise_vibration: 'Laerm/Vibration',
ergonomic: 'Ergonomie',
material_environmental: 'Stoffe/Umwelt',
software_control: 'Software/Steuerung',
cyber_network: 'Cyber/Netzwerk',
ai_specific: 'KI-spezifisch',
}
export const REDUCTION_LABELS: Record<string, string> = {
design: 'Stufe 1: Design',
protection: 'Stufe 2: Schutz',
information: 'Stufe 3: Information',
}
export const STATUS_LABELS: Record<string, string> = {
planned: 'Geplant',
implemented: 'Umgesetzt',
verified: 'Verifiziert',
}