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>
)
}