feat(iace-ui): component presence/CE review + machine-type dropdown
- Components view: three presence sections (Vorhanden / Nicht vorhanden / Geloescht) with bidirectional move + soft-delete (audit-visible, restorable), so the expert corrects the engine's best-effort negation in both directions. - CE marking per component (bought robot/actuator/SPS) with a clear "validate the integrated safety function (PL/SIL)" note when also safety-relevant. Safe semantics: hazards are not suppressed, only provenance is surfaced. - Project-create form: machine type is now a grouped dropdown from the engine's controlled vocabulary (GET /machine-types) instead of free text. - Knowledge graph: component→hazard edges use the real component_id. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+38
-1
@@ -10,13 +10,19 @@ export function ComponentTreeNode({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAddChild,
|
||||
onMarkAbsent,
|
||||
onToggleCE,
|
||||
}: {
|
||||
component: Component
|
||||
depth: number
|
||||
onEdit: (c: Component) => void
|
||||
onDelete: (id: string) => void
|
||||
onAddChild: (parentId: string) => void
|
||||
onMarkAbsent?: (id: string) => void
|
||||
onToggleCE?: (id: string, value: boolean) => void
|
||||
}) {
|
||||
const ceMarked = !!component.ce_marked
|
||||
const safetyRelevant = !!(component.is_safety_relevant ?? component.safety_relevant)
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const hasChildren = component.children && component.children.length > 0
|
||||
|
||||
@@ -53,6 +59,18 @@ export function ComponentTreeNode({
|
||||
Bibliothek
|
||||
</span>
|
||||
)}
|
||||
{ceMarked && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700"
|
||||
title="Zugekauft mit eigener CE / Konformitaetserklaerung — Nachweis via Hersteller-DoC">
|
||||
CE
|
||||
</span>
|
||||
)}
|
||||
{ceMarked && safetyRelevant && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800"
|
||||
title="Die CE der Box deckt die integrierte Sicherheitsfunktion NICHT — PL/SIL nach EN ISO 13849-1 / IEC 62061 muss validiert werden">
|
||||
⚠ Sicherheitsfunktion validieren (PL/SIL)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{component.description && (
|
||||
@@ -74,7 +92,24 @@ export function ComponentTreeNode({
|
||||
<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"
|
||||
{onToggleCE && (
|
||||
<button onClick={() => onToggleCE(component.id, !ceMarked)}
|
||||
title={ceMarked ? 'CE-Markierung entfernen' : 'Als zugekaufte CE-Komponente markieren (Roboter/Aktor/SPS)'}
|
||||
className={`p-1 rounded transition-colors ${ceMarked ? 'text-blue-600 bg-blue-50' : 'text-gray-400 hover:text-blue-600 hover:bg-blue-50'}`}>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{onMarkAbsent && (
|
||||
<button onClick={() => onMarkAbsent(component.id)} title="Als nicht vorhanden markieren"
|
||||
className="p-1 text-gray-400 hover:text-amber-600 hover:bg-amber-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="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => onDelete(component.id)} title="Loeschen (in Geloescht verschieben)"
|
||||
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" />
|
||||
@@ -93,6 +128,8 @@ export function ComponentTreeNode({
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onAddChild={onAddChild}
|
||||
onMarkAbsent={onMarkAbsent}
|
||||
onToggleCE={onToggleCE}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { Component } from './types'
|
||||
|
||||
interface Action {
|
||||
label: string
|
||||
onClick: (id: string) => void
|
||||
variant: 'primary' | 'danger' | 'neutral'
|
||||
}
|
||||
|
||||
const VARIANT: Record<string, string> = {
|
||||
primary: 'text-green-700 border-green-300 hover:bg-green-50',
|
||||
danger: 'text-red-700 border-red-300 hover:bg-red-50',
|
||||
neutral: 'text-gray-600 border-gray-300 hover:bg-gray-50',
|
||||
}
|
||||
|
||||
// PresenceSection renders a flat list of components in one presence state
|
||||
// (nicht_vorhanden / geloescht) with the moves the expert can apply to each.
|
||||
export function PresenceSection({
|
||||
title, hint, accent, components, actions,
|
||||
}: {
|
||||
title: string
|
||||
hint: string
|
||||
accent: string
|
||||
components: Component[]
|
||||
actions: Action[]
|
||||
}) {
|
||||
if (components.length === 0) return null
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className={`px-4 py-3 border-l-4 ${accent} bg-gray-50 dark:bg-gray-750`}>
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{title} <span className="text-gray-400">({components.length})</span>
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{hint}</p>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{components.map((c) => (
|
||||
<li key={c.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<div className="min-w-0">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{c.name}</span>
|
||||
{c.type && <span className="ml-2 text-xs text-gray-400">{c.type}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{actions.map((a) => (
|
||||
<button key={a.label} onClick={() => a.onClick(c.id)}
|
||||
className={`px-2.5 py-1 text-xs font-medium border rounded-md transition-colors ${VARIANT[a.variant]}`}>
|
||||
{a.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export type PresenceStatus = 'vorhanden' | 'nicht_vorhanden' | 'geloescht'
|
||||
|
||||
export interface Component {
|
||||
id: string
|
||||
name: string
|
||||
@@ -5,7 +7,16 @@ export interface Component {
|
||||
version: string
|
||||
description: string
|
||||
safety_relevant: boolean
|
||||
// is_safety_relevant is the backend's field name (the form's `safety_relevant`
|
||||
// does not currently round-trip). Read this when deriving CE obligations.
|
||||
is_safety_relevant?: boolean
|
||||
// ce_marked: bought component carrying its own CE / DoC (robot, actuator, SPS).
|
||||
// Safe semantics — no hazard suppression; drives provenance + the PL/SIL
|
||||
// validation note when also safety-relevant.
|
||||
ce_marked?: boolean
|
||||
// presence_status: 'vorhanden' (default) | 'nicht_vorhanden' (engine negation
|
||||
// verdict, awaiting expert review) | 'geloescht' (soft-deleted, restorable).
|
||||
presence_status?: PresenceStatus
|
||||
parent_id: string | null
|
||||
children: Component[]
|
||||
library_component_id?: string
|
||||
|
||||
Reference in New Issue
Block a user