refactor(admin): split control-library, iace/mitigations, iace/components, controls pages
All 4 page.tsx files reduced well below 500 LOC (235/181/158/262) by extracting components and hooks into colocated _components/ and _hooks/ subdirectories. Zero behavior changes — logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Component, ComponentFormData, COMPONENT_TYPES } from './types'
|
||||
|
||||
export function ComponentForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
initialData,
|
||||
parentId,
|
||||
}: {
|
||||
onSubmit: (data: ComponentFormData) => void
|
||||
onCancel: () => void
|
||||
initialData?: Component | null
|
||||
parentId?: string | null
|
||||
}) {
|
||||
const [formData, setFormData] = useState<ComponentFormData>({
|
||||
name: initialData?.name || '',
|
||||
type: initialData?.type || 'SW',
|
||||
version: initialData?.version || '',
|
||||
description: initialData?.description || '',
|
||||
safety_relevant: initialData?.safety_relevant || false,
|
||||
parent_id: parentId || initialData?.parent_id || null,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{initialData ? 'Komponente bearbeiten' : parentId ? 'Unterkomponente hinzufuegen' : 'Neue Komponente'}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="z.B. Bildverarbeitungsmodul"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Typ</label>
|
||||
<select
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
{COMPONENT_TYPES.map((t) => (
|
||||
<option key={t.value} value={t.value}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Version</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.version}
|
||||
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
|
||||
placeholder="z.B. 1.2.0"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-6">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.safety_relevant}
|
||||
onChange={(e) => setFormData({ ...formData, safety_relevant: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-purple-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-red-500" />
|
||||
</label>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Sicherheitsrelevant</span>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Kurze Beschreibung der Komponente..."
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => onSubmit(formData)}
|
||||
disabled={!formData.name}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
|
||||
formData.name
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{initialData ? 'Aktualisieren' : 'Hinzufuegen'}
|
||||
</button>
|
||||
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { LibraryComponent, EnergySource, LIBRARY_CATEGORIES } from './types'
|
||||
import { ComponentTypeIcon } from './ComponentTypeIcon'
|
||||
|
||||
export function ComponentLibraryModal({
|
||||
onAdd,
|
||||
onClose,
|
||||
}: {
|
||||
onAdd: (components: LibraryComponent[], energySources: EnergySource[]) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [libraryComponents, setLibraryComponents] = useState<LibraryComponent[]>([])
|
||||
const [energySources, setEnergySources] = useState<EnergySource[]>([])
|
||||
const [selectedComponents, setSelectedComponents] = useState<Set<string>>(new Set())
|
||||
const [selectedEnergySources, setSelectedEnergySources] = useState<Set<string>>(new Set())
|
||||
const [search, setSearch] = useState('')
|
||||
const [filterCategory, setFilterCategory] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<'components' | 'energy'>('components')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [compRes, enRes] = await Promise.all([
|
||||
fetch('/api/sdk/v1/iace/component-library'),
|
||||
fetch('/api/sdk/v1/iace/energy-sources'),
|
||||
])
|
||||
if (compRes.ok) {
|
||||
const json = await compRes.json()
|
||||
setLibraryComponents(json.components || [])
|
||||
}
|
||||
if (enRes.ok) {
|
||||
const json = await enRes.json()
|
||||
setEnergySources(json.energy_sources || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch library:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
function toggleComponent(id: string) {
|
||||
setSelectedComponents(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function toggleEnergySource(id: string) {
|
||||
setSelectedEnergySources(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function toggleAllInCategory(category: string) {
|
||||
const items = libraryComponents.filter(c => c.category === category)
|
||||
const allIds = items.map(i => i.id)
|
||||
const allSelected = allIds.every(id => selectedComponents.has(id))
|
||||
setSelectedComponents(prev => {
|
||||
const next = new Set(prev)
|
||||
allIds.forEach(id => allSelected ? next.delete(id) : next.add(id))
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
const selComps = libraryComponents.filter(c => selectedComponents.has(c.id))
|
||||
const selEnergy = energySources.filter(e => selectedEnergySources.has(e.id))
|
||||
onAdd(selComps, selEnergy)
|
||||
}
|
||||
|
||||
const categories = Object.keys(LIBRARY_CATEGORIES)
|
||||
const filtered = libraryComponents.filter(c => {
|
||||
if (filterCategory && c.category !== filterCategory) return false
|
||||
if (search) {
|
||||
const q = search.toLowerCase()
|
||||
return c.name_de.toLowerCase().includes(q) || c.name_en.toLowerCase().includes(q) || c.description_de.toLowerCase().includes(q)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const grouped = filtered.reduce<Record<string, LibraryComponent[]>>((acc, c) => {
|
||||
if (!acc[c.category]) acc[c.category] = []
|
||||
acc[c.category].push(c)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const totalSelected = selectedComponents.size + selectedEnergySources.size
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto" />
|
||||
<p className="mt-3 text-sm text-gray-500">Bibliothek wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-4xl max-h-[85vh] flex flex-col">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Komponentenbibliothek</h3>
|
||||
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button onClick={() => setActiveTab('components')}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === 'components' ? 'bg-purple-100 text-purple-700' : 'text-gray-500 hover:bg-gray-100'}`}>
|
||||
Komponenten ({libraryComponents.length})
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('energy')}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === 'energy' ? 'bg-purple-100 text-purple-700' : 'text-gray-500 hover:bg-gray-100'}`}>
|
||||
Energiequellen ({energySources.length})
|
||||
</button>
|
||||
</div>
|
||||
{activeTab === 'components' && (
|
||||
<div className="flex gap-3">
|
||||
<input type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Suchen..."
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
|
||||
<select value={filterCategory} onChange={e => setFilterCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="">Alle Kategorien</option>
|
||||
{categories.map(cat => <option key={cat} value={cat}>{LIBRARY_CATEGORIES[cat]}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{activeTab === 'components' ? (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(grouped)
|
||||
.sort(([a], [b]) => categories.indexOf(a) - categories.indexOf(b))
|
||||
.map(([category, items]) => (
|
||||
<div key={category}>
|
||||
<div className="flex items-center gap-2 mb-2 sticky top-0 bg-white dark:bg-gray-800 py-1 z-10">
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">{LIBRARY_CATEGORIES[category] || category}</h4>
|
||||
<span className="text-xs text-gray-400">({items.length})</span>
|
||||
<button onClick={() => toggleAllInCategory(category)} className="text-xs text-purple-600 hover:text-purple-700 ml-auto">
|
||||
{items.every(i => selectedComponents.has(i.id)) ? 'Alle abwaehlen' : 'Alle waehlen'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{items.map(comp => (
|
||||
<label key={comp.id}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedComponents.has(comp.id) ? 'border-purple-400 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 hover:bg-gray-50 dark:border-gray-700'
|
||||
}`}>
|
||||
<input type="checkbox" checked={selectedComponents.has(comp.id)} onChange={() => toggleComponent(comp.id)} className="mt-0.5 accent-purple-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-gray-400">{comp.id}</span>
|
||||
<ComponentTypeIcon type={comp.maps_to_component_type} />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{comp.name_de}</div>
|
||||
{comp.description_de && <div className="text-xs text-gray-500 mt-0.5 line-clamp-2">{comp.description_de}</div>}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filtered.length === 0 && <div className="text-center py-8 text-gray-500">Keine Komponenten gefunden</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{energySources.map(es => (
|
||||
<label key={es.id}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedEnergySources.has(es.id) ? 'border-purple-400 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 hover:bg-gray-50 dark:border-gray-700'
|
||||
}`}>
|
||||
<input type="checkbox" checked={selectedEnergySources.has(es.id)} onChange={() => toggleEnergySource(es.id)} className="mt-0.5 accent-purple-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2"><span className="text-xs font-mono text-gray-400">{es.id}</span></div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{es.name_de}</div>
|
||||
{es.description_de && <div className="text-xs text-gray-500 mt-0.5 line-clamp-2">{es.description_de}</div>}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">
|
||||
{selectedComponents.size} Komponenten, {selectedEnergySources.size} Energiequellen ausgewaehlt
|
||||
</span>
|
||||
<div className="flex gap-3">
|
||||
<button onClick={onClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">Abbrechen</button>
|
||||
<button onClick={handleAdd} disabled={totalSelected === 0}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${totalSelected > 0 ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'}`}>
|
||||
{totalSelected > 0 ? `${totalSelected} hinzufuegen` : 'Auswaehlen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Component } from './types'
|
||||
import { ComponentTypeIcon } from './ComponentTypeIcon'
|
||||
|
||||
export function ComponentTreeNode({
|
||||
component,
|
||||
depth,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAddChild,
|
||||
}: {
|
||||
component: Component
|
||||
depth: number
|
||||
onEdit: (c: Component) => void
|
||||
onDelete: (id: string) => void
|
||||
onAddChild: (parentId: string) => void
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const hasChildren = component.children && component.children.length > 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center gap-2 py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 group transition-colors"
|
||||
style={{ paddingLeft: `${depth * 24 + 12}px` }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className={`w-5 h-5 flex items-center justify-center text-gray-400 ${hasChildren ? 'visible' : 'invisible'}`}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<ComponentTypeIcon type={component.type} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{component.name}</span>
|
||||
{component.version && <span className="ml-2 text-xs text-gray-400">v{component.version}</span>}
|
||||
{component.safety_relevant && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">
|
||||
Sicherheitsrelevant
|
||||
</span>
|
||||
)}
|
||||
{component.library_component_id && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">
|
||||
Bibliothek
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{component.description && (
|
||||
<span className="text-xs text-gray-400 truncate max-w-[200px] hidden lg:block">
|
||||
{component.description}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button onClick={() => onAddChild(component.id)} title="Unterkomponente hinzufuegen"
|
||||
className="p-1 text-gray-400 hover:text-purple-600 hover:bg-purple-50 rounded transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => onEdit(component)} title="Bearbeiten"
|
||||
className="p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => onDelete(component.id)} title="Loeschen"
|
||||
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && hasChildren && (
|
||||
<div>
|
||||
{component.children.map((child) => (
|
||||
<ComponentTreeNode
|
||||
key={child.id}
|
||||
component={child}
|
||||
depth={depth + 1}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onAddChild={onAddChild}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
|
||||
export function ComponentTypeIcon({ type }: { type: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
SW: 'bg-blue-100 text-blue-700',
|
||||
FW: 'bg-indigo-100 text-indigo-700',
|
||||
AI: 'bg-purple-100 text-purple-700',
|
||||
HMI: 'bg-pink-100 text-pink-700',
|
||||
SENSOR: 'bg-cyan-100 text-cyan-700',
|
||||
ACTUATOR: 'bg-orange-100 text-orange-700',
|
||||
CONTROLLER: 'bg-green-100 text-green-700',
|
||||
NETWORK: 'bg-yellow-100 text-yellow-700',
|
||||
MECHANICAL: 'bg-gray-100 text-gray-700',
|
||||
ELECTRICAL: 'bg-red-100 text-red-700',
|
||||
OTHER: 'bg-gray-100 text-gray-500',
|
||||
}
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${colors[type] || colors.OTHER}`}>
|
||||
{type}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
export interface Component {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
version: string
|
||||
description: string
|
||||
safety_relevant: boolean
|
||||
parent_id: string | null
|
||||
children: Component[]
|
||||
library_component_id?: string
|
||||
energy_source_ids?: string[]
|
||||
}
|
||||
|
||||
export interface LibraryComponent {
|
||||
id: string
|
||||
name_de: string
|
||||
name_en: string
|
||||
category: string
|
||||
description_de: string
|
||||
typical_hazard_categories: string[]
|
||||
typical_energy_sources: string[]
|
||||
maps_to_component_type: string
|
||||
tags: string[]
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface EnergySource {
|
||||
id: string
|
||||
name_de: string
|
||||
name_en: string
|
||||
description_de: string
|
||||
typical_components: string[]
|
||||
typical_hazard_categories: string[]
|
||||
tags: string[]
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface ComponentFormData {
|
||||
name: string
|
||||
type: string
|
||||
version: string
|
||||
description: string
|
||||
safety_relevant: boolean
|
||||
parent_id: string | null
|
||||
}
|
||||
|
||||
export const LIBRARY_CATEGORIES: Record<string, string> = {
|
||||
mechanical: 'Mechanik',
|
||||
structural: 'Struktur',
|
||||
drive: 'Antrieb',
|
||||
hydraulic: 'Hydraulik',
|
||||
pneumatic: 'Pneumatik',
|
||||
electrical: 'Elektrik',
|
||||
control: 'Steuerung',
|
||||
sensor: 'Sensorik',
|
||||
actuator: 'Aktorik',
|
||||
safety: 'Sicherheit',
|
||||
it_network: 'IT/Netzwerk',
|
||||
}
|
||||
|
||||
export const COMPONENT_TYPES = [
|
||||
{ value: 'SW', label: 'Software (SW)' },
|
||||
{ value: 'FW', label: 'Firmware (FW)' },
|
||||
{ value: 'AI', label: 'KI-Modul (AI)' },
|
||||
{ value: 'HMI', label: 'Mensch-Maschine-Schnittstelle (HMI)' },
|
||||
{ value: 'SENSOR', label: 'Sensor' },
|
||||
{ value: 'ACTUATOR', label: 'Aktor' },
|
||||
{ value: 'CONTROLLER', label: 'Steuerung' },
|
||||
{ value: 'NETWORK', label: 'Netzwerk' },
|
||||
{ value: 'MECHANICAL', label: 'Mechanik' },
|
||||
{ value: 'ELECTRICAL', label: 'Elektrik' },
|
||||
{ value: 'OTHER', label: 'Sonstiges' },
|
||||
]
|
||||
|
||||
export function buildTree(components: Component[]): Component[] {
|
||||
const map = new Map<string, Component>()
|
||||
const roots: Component[] = []
|
||||
|
||||
components.forEach((c) => {
|
||||
map.set(c.id, { ...c, children: [] })
|
||||
})
|
||||
|
||||
components.forEach((c) => {
|
||||
const node = map.get(c.id)!
|
||||
if (c.parent_id && map.has(c.parent_id)) {
|
||||
map.get(c.parent_id)!.children.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
}
|
||||
Reference in New Issue
Block a user