- {/* Step Header */}
- {/* Error Banner */}
{error && (
{error}
@@ -522,7 +196,6 @@ export default function AIActPage() {
)}
- {/* Add/Edit System Form */}
{showAddForm && (
)}
- {/* Stats */}
KI-Systeme gesamt
@@ -551,10 +223,8 @@ export default function AIActPage() {
- {/* Risk Pyramid */}
- {/* Filter */}
Filter:
{['all', 'high-risk', 'limited-risk', 'minimal-risk', 'unclassified', 'compliant', 'non-compliant'].map(f => (
@@ -562,9 +232,7 @@ export default function AIActPage() {
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
- filter === f
- ? 'bg-purple-600 text-white'
- : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
+ filter === f ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
@@ -577,10 +245,8 @@ export default function AIActPage() {
))}
- {/* Loading */}
{loading &&
}
- {/* AI Systems List */}
{!loading && (
{filteredSystems.map(system => (
diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/HazardForm.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/HazardForm.tsx
new file mode 100644
index 0000000..fb26a3b
--- /dev/null
+++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/HazardForm.tsx
@@ -0,0 +1,129 @@
+'use client'
+
+import { useState } from 'react'
+import { HazardFormData, HAZARD_CATEGORIES, CATEGORY_LABELS, getRiskLevel, getRiskColor } from './types'
+import { RiskBadge } from './RiskBadge'
+
+interface HazardFormProps {
+ onSubmit: (data: HazardFormData) => void
+ onCancel: () => void
+}
+
+export function HazardForm({ onSubmit, onCancel }: HazardFormProps) {
+ const [formData, setFormData] = useState
({
+ name: '',
+ description: '',
+ category: 'mechanical',
+ component_id: '',
+ severity: 3,
+ exposure: 3,
+ probability: 3,
+ })
+
+ const rInherent = formData.severity * formData.exposure * formData.probability
+ const riskLevel = getRiskLevel(rInherent)
+
+ return (
+
+
Neue Gefaehrdung
+
+
+
+
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder="z.B. Quetschung durch Roboterarm"
+ 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"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
Risikobewertung (S x E x P)
+
+
+
+
setFormData({ ...formData, severity: Number(e.target.value) })}
+ className="w-full accent-purple-600"
+ />
+
GeringToedlich
+
+
+
+
setFormData({ ...formData, exposure: Number(e.target.value) })}
+ className="w-full accent-purple-600"
+ />
+
SeltenStaendig
+
+
+
+
setFormData({ ...formData, probability: Number(e.target.value) })}
+ className="w-full accent-purple-600"
+ />
+
UnwahrscheinlichSehr wahrscheinlich
+
+
+
+
+
+
R_inherent = S x E x P
+
+ {rInherent}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/HazardTable.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/HazardTable.tsx
new file mode 100644
index 0000000..13de838
--- /dev/null
+++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/HazardTable.tsx
@@ -0,0 +1,68 @@
+'use client'
+
+import { Hazard, CATEGORY_LABELS, STATUS_LABELS } from './types'
+import { RiskBadge } from './RiskBadge'
+
+interface HazardTableProps {
+ hazards: Hazard[]
+ onDelete: (id: string) => void
+}
+
+export function HazardTable({ hazards, onDelete }: HazardTableProps) {
+ return (
+
+
+
+
+
+ | Bezeichnung |
+ Kategorie |
+ Komponente |
+ S |
+ E |
+ P |
+ R |
+ Risiko |
+ Status |
+ Aktionen |
+
+
+
+ {hazards
+ .sort((a, b) => b.r_inherent - a.r_inherent)
+ .map((hazard) => (
+
+ |
+ {hazard.name}
+ {hazard.description && (
+ {hazard.description}
+ )}
+ |
+ {CATEGORY_LABELS[hazard.category] || hazard.category} |
+ {hazard.component_name || '--'} |
+ {hazard.severity} |
+ {hazard.exposure} |
+ {hazard.probability} |
+ {hazard.r_inherent} |
+ |
+
+ {STATUS_LABELS[hazard.status] || hazard.status}
+ |
+
+
+ |
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/LibraryModal.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/LibraryModal.tsx
new file mode 100644
index 0000000..2ab02f7
--- /dev/null
+++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/LibraryModal.tsx
@@ -0,0 +1,81 @@
+'use client'
+
+import { useState } from 'react'
+import { LibraryHazard, HAZARD_CATEGORIES, CATEGORY_LABELS } from './types'
+
+interface LibraryModalProps {
+ library: LibraryHazard[]
+ onAdd: (item: LibraryHazard) => void
+ onClose: () => void
+}
+
+export function LibraryModal({ library, onAdd, onClose }: LibraryModalProps) {
+ const [search, setSearch] = useState('')
+ const [filterCat, setFilterCat] = useState('')
+
+ const filtered = library.filter((h) => {
+ const matchSearch = !search || h.name.toLowerCase().includes(search.toLowerCase()) || h.description.toLowerCase().includes(search.toLowerCase())
+ const matchCat = !filterCat || h.category === filterCat
+ return matchSearch && matchCat
+ })
+
+ return (
+
+
+
+
+
Gefaehrdungsbibliothek
+
+
+
+ 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"
+ />
+
+
+
+
+ {filtered.length > 0 ? (
+ filtered.map((item) => (
+
+
+
{item.name}
+
{item.description}
+
+ {CATEGORY_LABELS[item.category] || item.category}
+ S:{item.default_severity} E:{item.default_exposure} P:{item.default_probability}
+
+
+
+
+ ))
+ ) : (
+
Keine Eintraege gefunden
+ )}
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskBadge.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskBadge.tsx
new file mode 100644
index 0000000..d405a74
--- /dev/null
+++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskBadge.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import { getRiskColor, getRiskLevelLabel } from './types'
+
+export function RiskBadge({ level }: { level: string }) {
+ return (
+
+ {getRiskLevelLabel(level)}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts
new file mode 100644
index 0000000..ed90338
--- /dev/null
+++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts
@@ -0,0 +1,92 @@
+export interface Hazard {
+ id: string
+ name: string
+ description: string
+ component_id: string | null
+ component_name: string | null
+ category: string
+ status: string
+ severity: number
+ exposure: number
+ probability: number
+ r_inherent: number
+ risk_level: string
+ created_at: string
+}
+
+export interface LibraryHazard {
+ id: string
+ name: string
+ description: string
+ category: string
+ default_severity: number
+ default_exposure: number
+ default_probability: number
+}
+
+export interface HazardFormData {
+ name: string
+ description: string
+ category: string
+ component_id: string
+ severity: number
+ exposure: number
+ probability: number
+}
+
+export const HAZARD_CATEGORIES = [
+ 'mechanical', 'electrical', 'thermal', 'noise', 'vibration',
+ 'radiation', 'material', 'ergonomic', 'software', 'ai_specific',
+ 'cybersecurity', 'functional_safety', 'environmental',
+]
+
+export const CATEGORY_LABELS: Record = {
+ mechanical: 'Mechanisch',
+ electrical: 'Elektrisch',
+ thermal: 'Thermisch',
+ noise: 'Laerm',
+ vibration: 'Vibration',
+ radiation: 'Strahlung',
+ material: 'Stoffe/Materialien',
+ ergonomic: 'Ergonomie',
+ software: 'Software',
+ ai_specific: 'KI-spezifisch',
+ cybersecurity: 'Cybersecurity',
+ functional_safety: 'Funktionale Sicherheit',
+ environmental: 'Umgebung',
+}
+
+export const STATUS_LABELS: Record = {
+ identified: 'Identifiziert',
+ assessed: 'Bewertet',
+ mitigated: 'Gemindert',
+ accepted: 'Akzeptiert',
+ closed: 'Geschlossen',
+}
+
+export function getRiskColor(level: string): string {
+ switch (level) {
+ case 'critical': return 'bg-red-100 text-red-700 border-red-200'
+ case 'high': return 'bg-orange-100 text-orange-700 border-orange-200'
+ case 'medium': return 'bg-yellow-100 text-yellow-700 border-yellow-200'
+ case 'low': return 'bg-green-100 text-green-700 border-green-200'
+ default: return 'bg-gray-100 text-gray-700 border-gray-200'
+ }
+}
+
+export function getRiskLevel(r: number): string {
+ if (r >= 100) return 'critical'
+ if (r >= 50) return 'high'
+ if (r >= 20) return 'medium'
+ return 'low'
+}
+
+export function getRiskLevelLabel(level: string): string {
+ switch (level) {
+ case 'critical': return 'Kritisch'
+ case 'high': return 'Hoch'
+ case 'medium': return 'Mittel'
+ case 'low': return 'Niedrig'
+ default: return level
+ }
+}
diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx
index 2e35968..bdbeb2e 100644
--- a/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx
+++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/page.tsx
@@ -2,338 +2,10 @@
import React, { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
-
-interface Hazard {
- id: string
- name: string
- description: string
- component_id: string | null
- component_name: string | null
- category: string
- status: string
- severity: number
- exposure: number
- probability: number
- r_inherent: number
- risk_level: string
- created_at: string
-}
-
-interface LibraryHazard {
- id: string
- name: string
- description: string
- category: string
- default_severity: number
- default_exposure: number
- default_probability: number
-}
-
-const HAZARD_CATEGORIES = [
- 'mechanical', 'electrical', 'thermal', 'noise', 'vibration',
- 'radiation', 'material', 'ergonomic', 'software', 'ai_specific',
- 'cybersecurity', 'functional_safety', 'environmental',
-]
-
-const CATEGORY_LABELS: Record = {
- mechanical: 'Mechanisch',
- electrical: 'Elektrisch',
- thermal: 'Thermisch',
- noise: 'Laerm',
- vibration: 'Vibration',
- radiation: 'Strahlung',
- material: 'Stoffe/Materialien',
- ergonomic: 'Ergonomie',
- software: 'Software',
- ai_specific: 'KI-spezifisch',
- cybersecurity: 'Cybersecurity',
- functional_safety: 'Funktionale Sicherheit',
- environmental: 'Umgebung',
-}
-
-const STATUS_LABELS: Record = {
- identified: 'Identifiziert',
- assessed: 'Bewertet',
- mitigated: 'Gemindert',
- accepted: 'Akzeptiert',
- closed: 'Geschlossen',
-}
-
-function getRiskColor(level: string): string {
- switch (level) {
- case 'critical': return 'bg-red-100 text-red-700 border-red-200'
- case 'high': return 'bg-orange-100 text-orange-700 border-orange-200'
- case 'medium': return 'bg-yellow-100 text-yellow-700 border-yellow-200'
- case 'low': return 'bg-green-100 text-green-700 border-green-200'
- default: return 'bg-gray-100 text-gray-700 border-gray-200'
- }
-}
-
-function getRiskLevel(r: number): string {
- if (r >= 100) return 'critical'
- if (r >= 50) return 'high'
- if (r >= 20) return 'medium'
- return 'low'
-}
-
-function getRiskLevelLabel(level: string): string {
- switch (level) {
- case 'critical': return 'Kritisch'
- case 'high': return 'Hoch'
- case 'medium': return 'Mittel'
- case 'low': return 'Niedrig'
- default: return level
- }
-}
-
-function RiskBadge({ level }: { level: string }) {
- return (
-
- {getRiskLevelLabel(level)}
-
- )
-}
-
-interface HazardFormData {
- name: string
- description: string
- category: string
- component_id: string
- severity: number
- exposure: number
- probability: number
-}
-
-function HazardForm({
- onSubmit,
- onCancel,
-}: {
- onSubmit: (data: HazardFormData) => void
- onCancel: () => void
-}) {
- const [formData, setFormData] = useState({
- name: '',
- description: '',
- category: 'mechanical',
- component_id: '',
- severity: 3,
- exposure: 3,
- probability: 3,
- })
-
- const rInherent = formData.severity * formData.exposure * formData.probability
- const riskLevel = getRiskLevel(rInherent)
-
- return (
-
-
Neue Gefaehrdung
-
-
-
-
- setFormData({ ...formData, name: e.target.value })}
- placeholder="z.B. Quetschung durch Roboterarm"
- 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"
- />
-
-
-
-
-
-
-
-
-
-
-
- {/* S/E/P Sliders */}
-
-
Risikobewertung (S x E x P)
-
-
-
-
setFormData({ ...formData, severity: Number(e.target.value) })}
- className="w-full accent-purple-600"
- />
-
- Gering
- Toedlich
-
-
-
-
-
setFormData({ ...formData, exposure: Number(e.target.value) })}
- className="w-full accent-purple-600"
- />
-
- Selten
- Staendig
-
-
-
-
-
setFormData({ ...formData, probability: Number(e.target.value) })}
- className="w-full accent-purple-600"
- />
-
- Unwahrscheinlich
- Sehr wahrscheinlich
-
-
-
-
-
-
-
R_inherent = S x E x P
-
- {rInherent}
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-function LibraryModal({
- library,
- onAdd,
- onClose,
-}: {
- library: LibraryHazard[]
- onAdd: (item: LibraryHazard) => void
- onClose: () => void
-}) {
- const [search, setSearch] = useState('')
- const [filterCat, setFilterCat] = useState('')
-
- const filtered = library.filter((h) => {
- const matchSearch = !search || h.name.toLowerCase().includes(search.toLowerCase()) || h.description.toLowerCase().includes(search.toLowerCase())
- const matchCat = !filterCat || h.category === filterCat
- return matchSearch && matchCat
- })
-
- return (
-
-
-
-
-
Gefaehrdungsbibliothek
-
-
-
- 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"
- />
-
-
-
-
- {filtered.length > 0 ? (
- filtered.map((item) => (
-
-
-
{item.name}
-
{item.description}
-
- {CATEGORY_LABELS[item.category] || item.category}
- S:{item.default_severity} E:{item.default_exposure} P:{item.default_probability}
-
-
-
-
- ))
- ) : (
-
Keine Eintraege gefunden
- )}
-
-
-
- )
-}
+import { Hazard, LibraryHazard, HazardFormData } from './_components/types'
+import { HazardForm } from './_components/HazardForm'
+import { LibraryModal } from './_components/LibraryModal'
+import { HazardTable } from './_components/HazardTable'
export default function HazardsPage() {
const params = useParams()
@@ -390,9 +62,7 @@ export default function HazardsPage() {
probability: item.default_probability,
}),
})
- if (res.ok) {
- await fetchHazards()
- }
+ if (res.ok) await fetchHazards()
} catch (err) {
console.error('Failed to add from library:', err)
}
@@ -421,9 +91,7 @@ export default function HazardsPage() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
- if (res.ok) {
- await fetchHazards()
- }
+ if (res.ok) await fetchHazards()
} catch (err) {
console.error('Failed to get AI suggestions:', err)
} finally {
@@ -435,9 +103,7 @@ export default function HazardsPage() {
if (!confirm('Gefaehrdung wirklich loeschen?')) return
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards/${id}`, { method: 'DELETE' })
- if (res.ok) {
- await fetchHazards()
- }
+ if (res.ok) await fetchHazards()
} catch (err) {
console.error('Failed to delete hazard:', err)
}
@@ -523,12 +189,10 @@ export default function HazardsPage() {
)}
- {/* Form */}
{showForm && (
setShowForm(false)} />
)}
- {/* Library Modal */}
{showLibrary && (
)}
- {/* Hazard Table */}
{hazards.length > 0 ? (
-
-
-
-
-
- | Bezeichnung |
- Kategorie |
- Komponente |
- S |
- E |
- P |
- R |
- Risiko |
- Status |
- Aktionen |
-
-
-
- {hazards
- .sort((a, b) => b.r_inherent - a.r_inherent)
- .map((hazard) => (
-
- |
- {hazard.name}
- {hazard.description && (
- {hazard.description}
- )}
- |
- {CATEGORY_LABELS[hazard.category] || hazard.category} |
- {hazard.component_name || '--'} |
- {hazard.severity} |
- {hazard.exposure} |
- {hazard.probability} |
- {hazard.r_inherent} |
- |
-
- {STATUS_LABELS[hazard.status] || hazard.status}
- |
-
-
- |
-
- ))}
-
-
-
-
+
) : (
!showForm && (
@@ -607,16 +217,10 @@ export default function HazardsPage() {
oder KI-Vorschlaege als Ausgangspunkt.
-