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,24 @@
|
||||
'use client'
|
||||
|
||||
export function HierarchyWarning({ onDismiss }: { onDismiss: () => void }) {
|
||||
return (
|
||||
<div className="bg-amber-50 border border-amber-300 rounded-xl p-4 flex items-start gap-3">
|
||||
<svg className="w-6 h-6 text-amber-600 flex-shrink-0 mt-0.5" 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>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-semibold text-amber-800">Hierarchie-Warnung: Massnahmen vom Typ "Information"</h4>
|
||||
<p className="text-sm text-amber-700 mt-1">
|
||||
Hinweismassnahmen (Stufe 3) duerfen <strong>nicht als Primaermassnahme</strong> akzeptiert werden, wenn konstruktive
|
||||
(Stufe 1) oder technische (Stufe 2) Massnahmen moeglich und zumutbar sind. Pruefen Sie, ob hoeherwertige
|
||||
Massnahmen ergaenzt werden koennen.
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={onDismiss} className="text-amber-400 hover:text-amber-600 transition-colors">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ProtectiveMeasure } from './types'
|
||||
|
||||
export function MeasuresLibraryModal({
|
||||
measures,
|
||||
onSelect,
|
||||
onClose,
|
||||
filterType,
|
||||
}: {
|
||||
measures: ProtectiveMeasure[]
|
||||
onSelect: (measure: ProtectiveMeasure) => void
|
||||
onClose: () => void
|
||||
filterType?: string
|
||||
}) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedSubType, setSelectedSubType] = useState('')
|
||||
|
||||
const filtered = measures.filter((m) => {
|
||||
if (filterType && m.reduction_type !== filterType) return false
|
||||
if (selectedSubType && m.sub_type !== selectedSubType) return false
|
||||
if (search) {
|
||||
const q = search.toLowerCase()
|
||||
return m.name.toLowerCase().includes(q) || m.description.toLowerCase().includes(q)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const subTypes = [...new Set(measures.filter((m) => !filterType || m.reduction_type === filterType).map((m) => m.sub_type))].filter(Boolean)
|
||||
|
||||
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-3xl max-h-[80vh] 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">Massnahmen-Bibliothek</h3>
|
||||
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded transition-colors">
|
||||
<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-3">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Massnahme 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"
|
||||
/>
|
||||
{subTypes.length > 1 && (
|
||||
<select
|
||||
value={selectedSubType}
|
||||
onChange={(e) => setSelectedSubType(e.target.value)}
|
||||
className="px-3 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 text-sm"
|
||||
>
|
||||
<option value="">Alle Sub-Typen</option>
|
||||
{subTypes.map((st) => (
|
||||
<option key={st} value={st}>{st}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">{filtered.length} Massnahmen</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-3">
|
||||
{filtered.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:border-purple-300 hover:bg-purple-50/30 transition-colors cursor-pointer"
|
||||
onClick={() => onSelect(m)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-mono text-gray-400">{m.id}</span>
|
||||
{m.sub_type && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{m.name}</h4>
|
||||
<p className="text-xs text-gray-500 mt-1">{m.description}</p>
|
||||
{m.examples && m.examples.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{m.examples.map((ex, i) => (
|
||||
<span key={i} className="text-xs px-1.5 py-0.5 rounded bg-purple-50 text-purple-600">
|
||||
{ex}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="ml-3 px-3 py-1.5 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors flex-shrink-0">
|
||||
Uebernehmen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">Keine Massnahmen gefunden</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { Mitigation } from './types'
|
||||
import { StatusBadge } from './StatusBadge'
|
||||
|
||||
export function MitigationCard({
|
||||
mitigation,
|
||||
onVerify,
|
||||
onDelete,
|
||||
}: {
|
||||
mitigation: Mitigation
|
||||
onVerify: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{mitigation.title}</h4>
|
||||
{mitigation.title.startsWith('Auto:') && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">
|
||||
Auto
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<StatusBadge status={mitigation.status} />
|
||||
</div>
|
||||
{mitigation.description && (
|
||||
<p className="text-xs text-gray-500 mb-3">{mitigation.description}</p>
|
||||
)}
|
||||
{mitigation.linked_hazard_names.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{mitigation.linked_hazard_names.map((name, i) => (
|
||||
<span key={i} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{mitigation.status !== 'verified' && (
|
||||
<button
|
||||
onClick={() => onVerify(mitigation.id)}
|
||||
className="text-xs px-2.5 py-1 bg-green-50 text-green-700 border border-green-200 rounded-lg hover:bg-green-100 transition-colors"
|
||||
>
|
||||
Verifizieren
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onDelete(mitigation.id)}
|
||||
className="text-xs px-2.5 py-1 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Hazard, MitigationFormData } from './types'
|
||||
|
||||
export function MitigationForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
hazards,
|
||||
preselectedType,
|
||||
onOpenLibrary,
|
||||
}: {
|
||||
onSubmit: (data: MitigationFormData) => void
|
||||
onCancel: () => void
|
||||
hazards: Hazard[]
|
||||
preselectedType?: 'design' | 'protection' | 'information'
|
||||
onOpenLibrary: (type?: string) => void
|
||||
}) {
|
||||
const [formData, setFormData] = useState<MitigationFormData>({
|
||||
title: '',
|
||||
description: '',
|
||||
reduction_type: preselectedType || 'design',
|
||||
linked_hazard_ids: [],
|
||||
})
|
||||
|
||||
function toggleHazard(id: string) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
linked_hazard_ids: prev.linked_hazard_ids.includes(id)
|
||||
? prev.linked_hazard_ids.filter((h) => h !== id)
|
||||
: [...prev.linked_hazard_ids, id],
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Neue Massnahme</h3>
|
||||
<button
|
||||
onClick={() => onOpenLibrary(formData.reduction_type)}
|
||||
className="text-sm px-3 py-1.5 bg-purple-50 text-purple-700 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
Aus Bibliothek waehlen
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<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">Titel *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="z.B. Lichtvorhang an Gefahrenstelle"
|
||||
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">Reduktionstyp</label>
|
||||
<select
|
||||
value={formData.reduction_type}
|
||||
onChange={(e) => setFormData({ ...formData, reduction_type: e.target.value as MitigationFormData['reduction_type'] })}
|
||||
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"
|
||||
>
|
||||
<option value="design">Stufe 1: Design - Inhaerent sichere Konstruktion</option>
|
||||
<option value="protection">Stufe 2: Schutz - Technische Schutzmassnahmen</option>
|
||||
<option value="information">Stufe 3: Information - Hinweise und Schulungen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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 })}
|
||||
rows={2}
|
||||
placeholder="Detaillierte Beschreibung der Massnahme..."
|
||||
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>
|
||||
{hazards.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Verknuepfte Gefaehrdungen</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{hazards.map((h) => (
|
||||
<button
|
||||
key={h.id}
|
||||
onClick={() => toggleHazard(h.id)}
|
||||
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${
|
||||
formData.linked_hazard_ids.includes(h.id)
|
||||
? 'border-purple-400 bg-purple-50 text-purple-700'
|
||||
: 'border-gray-200 bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{h.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => onSubmit(formData)}
|
||||
disabled={!formData.title}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
|
||||
formData.title
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
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,19 @@
|
||||
'use client'
|
||||
|
||||
export function StatusBadge({ status }: { status: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
planned: 'bg-gray-100 text-gray-700',
|
||||
implemented: 'bg-blue-100 text-blue-700',
|
||||
verified: 'bg-green-100 text-green-700',
|
||||
}
|
||||
const labels: Record<string, string> = {
|
||||
planned: 'Geplant',
|
||||
implemented: 'Umgesetzt',
|
||||
verified: 'Verifiziert',
|
||||
}
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${colors[status] || colors.planned}`}>
|
||||
{labels[status] || status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Hazard, SuggestedMeasure, REDUCTION_TYPES } from './types'
|
||||
|
||||
export function SuggestMeasuresModal({
|
||||
hazards,
|
||||
projectId,
|
||||
onAddMeasure,
|
||||
onClose,
|
||||
}: {
|
||||
hazards: Hazard[]
|
||||
projectId: string
|
||||
onAddMeasure: (title: string, description: string, reductionType: string, hazardId: string) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [selectedHazard, setSelectedHazard] = useState<string>('')
|
||||
const [suggested, setSuggested] = useState<SuggestedMeasure[]>([])
|
||||
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
|
||||
|
||||
const riskColors: Record<string, string> = {
|
||||
not_acceptable: 'border-red-400 bg-red-50',
|
||||
very_high: 'border-red-300 bg-red-50',
|
||||
critical: 'border-red-300 bg-red-50',
|
||||
high: 'border-orange-300 bg-orange-50',
|
||||
medium: 'border-yellow-300 bg-yellow-50',
|
||||
low: 'border-green-300 bg-green-50',
|
||||
}
|
||||
|
||||
async function handleSelectHazard(hazardId: string) {
|
||||
setSelectedHazard(hazardId)
|
||||
setSuggested([])
|
||||
if (!hazardId) return
|
||||
|
||||
setLoadingSuggestions(true)
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards/${hazardId}/suggest-measures`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setSuggested(json.suggested_measures || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to suggest measures:', err)
|
||||
} finally {
|
||||
setLoadingSuggestions(false)
|
||||
}
|
||||
}
|
||||
|
||||
const groupedByType = {
|
||||
design: suggested.filter(m => m.reduction_type === 'design'),
|
||||
protection: suggested.filter(m => m.reduction_type === 'protection'),
|
||||
information: suggested.filter(m => m.reduction_type === 'information'),
|
||||
}
|
||||
|
||||
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-3xl 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">Massnahmen-Vorschlaege</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>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Waehlen Sie eine Gefaehrdung, um passende Massnahmen vorgeschlagen zu bekommen.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{hazards.map(h => (
|
||||
<button
|
||||
key={h.id}
|
||||
onClick={() => handleSelectHazard(h.id)}
|
||||
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${
|
||||
selectedHazard === h.id
|
||||
? 'border-purple-400 bg-purple-50 text-purple-700 font-medium'
|
||||
: `${riskColors[h.risk_level] || 'border-gray-200 bg-white'} text-gray-700 hover:border-purple-300`
|
||||
}`}
|
||||
>
|
||||
{h.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{loadingSuggestions ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
) : suggested.length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
{(['design', 'protection', 'information'] as const).map(type => {
|
||||
const items = groupedByType[type]
|
||||
if (items.length === 0) return null
|
||||
const config = REDUCTION_TYPES[type]
|
||||
return (
|
||||
<div key={type}>
|
||||
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${config.headerColor} mb-3`}>
|
||||
{config.icon}
|
||||
<span className="text-sm font-semibold">{config.label}</span>
|
||||
<span className="ml-auto text-sm font-bold">{items.length}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{items.map(m => (
|
||||
<div key={m.id} className="border border-gray-200 rounded-lg p-3 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-mono text-gray-400">{m.id}</span>
|
||||
{m.sub_type && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{m.name}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{m.description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onAddMeasure(m.name, m.description, m.reduction_type, selectedHazard)}
|
||||
className="ml-3 px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex-shrink-0"
|
||||
>
|
||||
Uebernehmen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : selectedHazard ? (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
Keine Vorschlaege fuer diese Gefaehrdung gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
Waehlen Sie eine Gefaehrdung aus, um Vorschlaege zu erhalten.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface Mitigation {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
reduction_type: 'design' | 'protection' | 'information'
|
||||
status: 'planned' | 'implemented' | 'verified'
|
||||
linked_hazard_ids: string[]
|
||||
linked_hazard_names: string[]
|
||||
created_at: string
|
||||
verified_at: string | null
|
||||
verified_by: string | null
|
||||
source?: string
|
||||
}
|
||||
|
||||
export interface Hazard {
|
||||
id: string
|
||||
name: string
|
||||
risk_level: string
|
||||
category?: string
|
||||
}
|
||||
|
||||
export interface ProtectiveMeasure {
|
||||
id: string
|
||||
reduction_type: string
|
||||
sub_type: string
|
||||
name: string
|
||||
description: string
|
||||
hazard_category: string
|
||||
examples: string[]
|
||||
}
|
||||
|
||||
export interface SuggestedMeasure {
|
||||
id: string
|
||||
reduction_type: string
|
||||
sub_type: string
|
||||
name: string
|
||||
description: string
|
||||
hazard_category: string
|
||||
examples: string[]
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface MitigationFormData {
|
||||
title: string
|
||||
description: string
|
||||
reduction_type: 'design' | 'protection' | 'information'
|
||||
linked_hazard_ids: string[]
|
||||
}
|
||||
|
||||
export const REDUCTION_TYPES = {
|
||||
design: {
|
||||
label: 'Stufe 1: Design',
|
||||
description: 'Inhaerent sichere Konstruktion',
|
||||
color: 'border-blue-200 bg-blue-50',
|
||||
headerColor: 'bg-blue-100 text-blue-800',
|
||||
subTypes: [
|
||||
{ value: 'geometry', label: 'Geometrie & Anordnung' },
|
||||
{ value: 'force_energy', label: 'Kraft & Energie' },
|
||||
{ value: 'material', label: 'Material & Stabilitaet' },
|
||||
{ value: 'ergonomics', label: 'Ergonomie' },
|
||||
{ value: 'control_design', label: 'Steuerungstechnik' },
|
||||
{ value: 'fluid_design', label: 'Pneumatik / Hydraulik' },
|
||||
],
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
protection: {
|
||||
label: 'Stufe 2: Schutz',
|
||||
description: 'Technische Schutzmassnahmen',
|
||||
color: 'border-green-200 bg-green-50',
|
||||
headerColor: 'bg-green-100 text-green-800',
|
||||
subTypes: [
|
||||
{ value: 'fixed_guard', label: 'Feststehende Schutzeinrichtung' },
|
||||
{ value: 'movable_guard', label: 'Bewegliche Schutzeinrichtung' },
|
||||
{ value: 'electro_sensitive', label: 'Optoelektronisch' },
|
||||
{ value: 'pressure_sensitive', label: 'Druckempfindlich' },
|
||||
{ value: 'emergency_stop', label: 'Not-Halt' },
|
||||
{ value: 'electrical_protection', label: 'Elektrischer Schutz' },
|
||||
{ value: 'thermal_protection', label: 'Thermischer Schutz' },
|
||||
{ value: 'fluid_protection', label: 'Hydraulik/Pneumatik-Schutz' },
|
||||
{ value: 'extraction', label: 'Absaugung / Kapselung' },
|
||||
],
|
||||
icon: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
),
|
||||
},
|
||||
information: {
|
||||
label: 'Stufe 3: Information',
|
||||
description: 'Hinweise und Schulungen',
|
||||
color: 'border-yellow-200 bg-yellow-50',
|
||||
headerColor: 'bg-yellow-100 text-yellow-800',
|
||||
subTypes: [
|
||||
{ value: 'signage', label: 'Beschilderung & Kennzeichnung' },
|
||||
{ value: 'manual', label: 'Betriebsanleitung' },
|
||||
{ value: 'training', label: 'Schulung & Unterweisung' },
|
||||
{ value: 'ppe', label: 'PSA (Schutzausruestung)' },
|
||||
{ value: 'organizational', label: 'Organisatorisch' },
|
||||
{ value: 'marking', label: 'Markierung & Codierung' },
|
||||
],
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user