diff --git a/admin-compliance/app/sdk/iace/[projectId]/_components/SuggestedNorms.tsx b/admin-compliance/app/sdk/iace/[projectId]/_components/SuggestedNorms.tsx index 5e6d881..28dc22e 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/_components/SuggestedNorms.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/_components/SuggestedNorms.tsx @@ -36,6 +36,9 @@ export function SuggestedNorms({ projectId }: { projectId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [collapsed, setCollapsed] = useState(false) + const [customNorms, setCustomNorms] = useState>([]) + const [customNormNumber, setCustomNormNumber] = useState('') + const [customNormTitle, setCustomNormTitle] = useState('') useEffect(() => { fetch(`/api/sdk/v1/iace/projects/${projectId}/suggested-norms`) @@ -72,9 +75,11 @@ export function SuggestedNorms({ projectId }: { projectId: string }) {

- - - +
+ + + +
{!collapsed && ( @@ -126,11 +131,62 @@ export function SuggestedNorms({ projectId }: { projectId: string }) { ) })} - {/* Disclaimer */} -
- Hinweis: Diese Normenvorschlaege basieren auf dem Maschinentyp und den identifizierten - Gefaehrdungen. Der CE-Fachmann muss die Anwendbarkeit pruefen und ggf. weitere Normen ergaenzen. - Nur Normennummern und -titel werden angezeigt — der Normtext muss separat beschafft werden (z.B. ueber Beuth/DIN). + {/* Add custom norm */} +
+

Weitere Norm ergaenzen

+
+ setCustomNormNumber(e.target.value)} + className="flex-1 px-3 py-1.5 text-xs border border-gray-300 rounded-lg focus:ring-1 focus:ring-purple-400 dark:bg-gray-700 dark:border-gray-600 dark:text-white" + /> + setCustomNormTitle(e.target.value)} + className="flex-1 px-3 py-1.5 text-xs border border-gray-300 rounded-lg focus:ring-1 focus:ring-purple-400 dark:bg-gray-700 dark:border-gray-600 dark:text-white" + /> + +
+ {customNorms.length > 0 && ( +
+ {customNorms.map((cn, i) => ( +
+ {cn.number} + {cn.title && — {cn.title}} + Manuell + +
+ ))} +
+ )} +
+ + {/* Pflicht-Erklärung + Disclaimer */} +
+
+ Pflicht bedeutet: Diese Norm ist fuer diesen Maschinentyp typischerweise zwingend anzuwenden + (z.B. ISO 12100 fuer alle Maschinen, EN 692 fuer mechanische Pressen). Die Anwendung harmonisierter Normen + erzeugt eine Konformitaetsvermutung. +
+
+ Hinweis: Diese Normenvorschlaege basieren auf dem Maschinentyp und den identifizierten + Gefaehrdungen. Der CE-Fachmann muss die Anwendbarkeit pruefen und ggf. weitere Normen ueber das Feld oben ergaenzen. + Normtexte muessen separat beschafft werden (z.B. ueber beuth.de). +
)} diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx index 0c35641..56dfccf 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx @@ -15,7 +15,7 @@ export default function HazardsPage() { const params = useParams() const projectId = params.projectId as string const h = useHazards(projectId) - const [view, setView] = useState('list') + const [view, setView] = useState('risk') if (h.loading) { return ( @@ -46,6 +46,7 @@ export default function HazardsPage() {
- +
+ {selected.size > 0 && ( + <> + {selected.size} ausgewaehlt + + + + + )} + {selected.size === 0 && ( + <> + + + + )} - -
@@ -95,76 +140,80 @@ export default function MitigationsPage() { {showForm && ( { - const ok = await handleSubmit(data) - if (ok) { setShowForm(false); setPreselectedType(undefined) } - }} - onCancel={() => { setShowForm(false); setPreselectedType(undefined) }} - hazards={hazards} - preselectedType={preselectedType} - onOpenLibrary={handleOpenLibrary} + onSubmit={async (data) => { const ok = await handleSubmit(data); if (ok) setShowForm(false) }} + onCancel={() => setShowForm(false)} hazards={hazards} preselectedType={preselectedType} onOpenLibrary={handleOpenLibrary} /> )} + {showLibrary && setShowLibrary(false)} filterType={libraryFilter} />} + {showSuggest && setShowSuggest(false)} />} - {showLibrary && ( - setShowLibrary(false)} filterType={libraryFilter} - /> - )} + {/* 3-Step Accordions */} + {(['design', 'protection', 'information'] as const).map((type) => { + const config = REDUCTION_TYPES[type] + const items = byType[type] + const isExpanded = expanded[type] + const allSelected = items.length > 0 && items.every((m) => selected.has(m.id)) - {showSuggest && ( - setShowSuggest(false)} - /> - )} + return ( +
+ {/* Accordion Header */} + - {/* 3-Column Layout */} -
- {(['design', 'protection', 'information'] as const).map((type) => { - const config = REDUCTION_TYPES[type] - const items = byType[type] - return ( -
-
- {config.icon} -
-

{config.label}

-

{config.description}

+ {/* Accordion Content — Table rows */} + {isExpanded && items.length > 0 && ( +
+ {/* Table header */} +
+
+ selectAllInType(type)} + className="accent-purple-600" title="Alle auswaehlen" /> +
+
Massnahme
+
Status
+
Gefaehrdung
- {items.length} -
-
- {config.subTypes.map((st) => ( - - {st.label} - - ))} -
-
+ {/* Rows */} {items.map((m) => ( - +
+
+ toggleSelect(m.id)} + className="accent-purple-600" /> +
+
+
{m.title || ''}
+ {m.description &&
{m.description}
} +
+
+ +
+
+ {(m.linked_hazard_names || []).join(', ') || '-'} +
+
))}
-
- - + )} + + {isExpanded && items.length === 0 && ( +
+ Keine Massnahmen in dieser Stufe
-
- ) - })} -
+ )} +
+ ) + })}
) } diff --git a/admin-compliance/app/sdk/iace/lines/page.tsx b/admin-compliance/app/sdk/iace/lines/page.tsx index 5f37b06..26baca6 100644 --- a/admin-compliance/app/sdk/iace/lines/page.tsx +++ b/admin-compliance/app/sdk/iace/lines/page.tsx @@ -12,25 +12,114 @@ interface ProductionLineItem { updated_at: string } +interface ProjectItem { + id: string + machine_name: string + machine_type: string + status: string +} + +const STATION_TYPES = [ + { value: 'press', label: 'Presse' }, + { value: 'cobot', label: 'Cobot/Roboter' }, + { value: 'motor', label: 'Motor/Antrieb' }, + { value: 'rotary_transfer', label: 'Rundtaktanlage' }, + { value: 'conveyor', label: 'Foerderer' }, + { value: 'assembly', label: 'Montage' }, + { value: 'milling', label: 'Fraese' }, + { value: 'turning', label: 'Drehmaschine' }, + { value: 'welding', label: 'Schweissen' }, + { value: 'inspection', label: 'Pruefung' }, + { value: 'packaging', label: 'Verpackung' }, +] + export default function ProductionLinesListPage() { const [lines, setLines] = useState([]) + const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(true) + const [showCreate, setShowCreate] = useState(false) + const [creating, setCreating] = useState(false) + const [lineName, setLineName] = useState('') + const [lineDesc, setLineDesc] = useState('') + const [selectedStations, setSelectedStations] = useState>([]) - useEffect(() => { - fetchLines() - }, []) + useEffect(() => { fetchLines(); fetchProjects() }, []) async function fetchLines() { try { const res = await fetch('/api/sdk/v1/iace/production-lines') if (res.ok) { const json = await res.json() - setLines(json.lines || json || []) + setLines(json.lines || []) } + } catch { /* ignore */ } + finally { setLoading(false) } + } + + async function fetchProjects() { + try { + const res = await fetch('/api/sdk/v1/iace/projects') + if (res.ok) { + const json = await res.json() + setProjects((json.projects || []).map((p: Record) => ({ + id: p.id, machine_name: p.machine_name, machine_type: p.machine_type, status: p.status, + }))) + } + } catch { /* ignore */ } + } + + function addStation() { + setSelectedStations((prev) => [...prev, { projectId: '', stationType: 'assembly' }]) + } + + function removeStation(idx: number) { + setSelectedStations((prev) => prev.filter((_, i) => i !== idx)) + } + + function updateStation(idx: number, field: 'projectId' | 'stationType', value: string) { + setSelectedStations((prev) => prev.map((s, i) => i === idx ? { ...s, [field]: value } : s)) + } + + async function handleCreate() { + if (!lineName.trim()) return + setCreating(true) + try { + // 1. Create the line + const lineRes = await fetch('/api/sdk/v1/iace/production-lines', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: lineName.trim(), description: lineDesc.trim() }), + }) + if (!lineRes.ok) return + const lineJson = await lineRes.json() + const lineId = lineJson.line?.id || lineJson.id + + // 2. Add stations + for (let i = 0; i < selectedStations.length; i++) { + const s = selectedStations[i] + if (!s.projectId) continue + const proj = projects.find((p) => p.id === s.projectId) + await fetch(`/api/sdk/v1/iace/production-lines/${lineId}/stations`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project_id: s.projectId, + station_type: s.stationType, + station_label: proj?.machine_name || '', + sort_order: i + 1, + }), + }) + } + + setShowCreate(false) + setLineName('') + setLineDesc('') + setSelectedStations([]) + await fetchLines() } catch (err) { - console.error('Failed to fetch production lines:', err) + console.error('Failed to create line:', err) } finally { - setLoading(false) + setCreating(false) } } @@ -44,90 +133,126 @@ export default function ProductionLinesListPage() { return (
- {/* Header */}
-
- - - - - IACE - -
-

- Produktionslinien -

-

- Verkettete Fertigungsstrassen mit aggregierter Risikoansicht -

+ + + + + IACE + +

Produktionslinien

+

Verkettete Fertigungsstrassen mit aggregierter Risikoansicht

- +
+ {/* Create form */} + {showCreate && ( +
+

Neue Produktionslinie erstellen

+
+
+ + setLineName(e.target.value)} + placeholder="z.B. Fertigungsstrasse Halle 3" + className="w-full px-4 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" /> +
+
+ + setLineDesc(e.target.value)} + placeholder="Optionale Beschreibung" + className="w-full px-4 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" /> +
+
+ + {/* Stations */} +
+
+ + +
+ {selectedStations.length === 0 && ( +

Noch keine Stationen. Klicken Sie "+ Station hinzufuegen" um Projekte zuzuordnen.

+ )} +
+ {selectedStations.map((s, i) => ( +
+ {i + 1}. + + + +
+ ))} +
+
+ +
+ + +
+
+ )} + {/* Lines list */} - {lines.length > 0 ? ( + {lines.length > 0 && (
{lines.map((line) => ( - -

- {line.name} -

- {line.description && ( -

- {line.description} -

- )} -
- - - - - {line.station_count} Stationen - - - Aktualisiert: {new Date(line.updated_at || line.created_at).toLocaleDateString('de-DE')} - + +

{line.name}

+ {line.description &&

{line.description}

} +
+ {line.station_count || 0} Stationen + Aktualisiert: {new Date(line.updated_at || line.created_at).toLocaleDateString('de-DE')}
))}
- ) : ( + )} + + {lines.length === 0 && !showCreate && (
-
+
-

- Noch keine Produktionslinien vorhanden -

-

+

Noch keine Produktionslinien

+

Produktionslinien verketten mehrere CE-Projekte zu einer Fertigungsstrasse. - Sie sehen auf einen Blick den Risikostatus aller Stationen und koennen - Massnahmen priorisieren.

- +
)}
diff --git a/admin-compliance/e2e/test-results/.last-run.json b/admin-compliance/e2e/test-results/.last-run.json index cbcc1fb..5fca3f8 100644 --- a/admin-compliance/e2e/test-results/.last-run.json +++ b/admin-compliance/e2e/test-results/.last-run.json @@ -1,4 +1,4 @@ { - "status": "passed", + "status": "failed", "failedTests": [] } \ No newline at end of file diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_norms.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_norms.go index 0c8cb6c..b2dc5bb 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_norms.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_norms.go @@ -28,6 +28,10 @@ func (h *IACEHandler) ListNormsLibrary(c *gin.Context) { allNorms = append(allNorms, iace.GetWoodMetalCNorms()...) allNorms = append(allNorms, iace.GetFoodPkgCNorms()...) allNorms = append(allNorms, iace.GetLiftMiscCNorms()...) + allNorms = append(allNorms, iace.GetMachiningCNorms()...) + allNorms = append(allNorms, iace.GetConveyorAutoCNorms()...) + allNorms = append(allNorms, iace.GetProcessCNorms()...) + allNorms = append(allNorms, iace.GetConstructionCNorms()...) var filtered []iace.NormReference for _, norm := range allNorms { diff --git a/ai-compliance-sdk/internal/iace/norms_engine.go b/ai-compliance-sdk/internal/iace/norms_engine.go index 6ef0f31..a7cb98c 100644 --- a/ai-compliance-sdk/internal/iace/norms_engine.go +++ b/ai-compliance-sdk/internal/iace/norms_engine.go @@ -31,6 +31,10 @@ func SuggestNorms(machineType string, hazardCategories []string, tags []string) allNorms = append(allNorms, GetWoodMetalCNorms()...) allNorms = append(allNorms, GetFoodPkgCNorms()...) allNorms = append(allNorms, GetLiftMiscCNorms()...) + allNorms = append(allNorms, GetMachiningCNorms()...) + allNorms = append(allNorms, GetConveyorAutoCNorms()...) + allNorms = append(allNorms, GetProcessCNorms()...) + allNorms = append(allNorms, GetConstructionCNorms()...) // Build lookup sets for efficient matching hazardSet := toSet(hazardCategories)