Merge remote gitea/main — resolve conflicts keeping local (origin) state
Build + Deploy / build-admin-compliance (push) Successful in 2m29s
Build + Deploy / build-backend-compliance (push) Successful in 3m23s
Build + Deploy / build-ai-sdk (push) Failing after 47s
Build + Deploy / build-developer-portal (push) Successful in 1m19s
Build + Deploy / build-tts (push) Failing after 1m29s
Build + Deploy / build-document-crawler (push) Successful in 43s
Build + Deploy / build-dsms-gateway (push) Successful in 25s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m17s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 48s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 31s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 18s
Build + Deploy / build-admin-compliance (push) Successful in 2m29s
Build + Deploy / build-backend-compliance (push) Successful in 3m23s
Build + Deploy / build-ai-sdk (push) Failing after 47s
Build + Deploy / build-developer-portal (push) Successful in 1m19s
Build + Deploy / build-tts (push) Failing after 1m29s
Build + Deploy / build-document-crawler (push) Successful in 43s
Build + Deploy / build-dsms-gateway (push) Successful in 25s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m17s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 48s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 31s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 18s
Local origin is 20+ commits ahead of remote gitea. All conflicts resolved by keeping HEAD (our version) which includes the full 56→138 check expansion and doc_checks package split. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,9 @@ export function SuggestedNorms({ projectId }: { projectId: string }) {
|
|||||||
const [data, setData] = useState<NormResult | null>(null)
|
const [data, setData] = useState<NormResult | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
|
const [customNorms, setCustomNorms] = useState<Array<{ number: string; title: string }>>([])
|
||||||
|
const [customNormNumber, setCustomNormNumber] = useState('')
|
||||||
|
const [customNormTitle, setCustomNormTitle] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/suggested-norms`)
|
fetch(`/api/sdk/v1/iace/projects/${projectId}/suggested-norms`)
|
||||||
@@ -72,9 +75,11 @@ export function SuggestedNorms({ projectId }: { projectId: string }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<svg className={`w-5 h-5 text-gray-400 transition-transform ${collapsed ? '' : 'rotate-180'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<svg className={`w-5 h-5 text-gray-400 transition-transform ${collapsed ? '' : 'rotate-180'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
@@ -126,11 +131,62 @@ export function SuggestedNorms({ projectId }: { projectId: string }) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Disclaimer */}
|
{/* Add custom norm */}
|
||||||
<div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-xs text-amber-800">
|
<div className="p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600">
|
||||||
<strong>Hinweis:</strong> Diese Normenvorschlaege basieren auf dem Maschinentyp und den identifizierten
|
<p className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">Weitere Norm ergaenzen</p>
|
||||||
Gefaehrdungen. Der CE-Fachmann muss die Anwendbarkeit pruefen und ggf. weitere Normen ergaenzen.
|
<div className="flex gap-2">
|
||||||
Nur Normennummern und -titel werden angezeigt — der Normtext muss separat beschafft werden (z.B. ueber Beuth/DIN).
|
<input
|
||||||
|
type="text" placeholder="z.B. ISO 13857:2019"
|
||||||
|
value={customNormNumber} onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text" placeholder="Titel (optional)"
|
||||||
|
value={customNormTitle} onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (customNormNumber.trim()) {
|
||||||
|
setCustomNorms((prev) => [...prev, { number: customNormNumber.trim(), title: customNormTitle.trim() }])
|
||||||
|
setCustomNormNumber('')
|
||||||
|
setCustomNormTitle('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!customNormNumber.trim()}
|
||||||
|
className="px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
+ Hinzufuegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{customNorms.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{customNorms.map((cn, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="font-mono font-semibold text-gray-800 dark:text-gray-200">{cn.number}</span>
|
||||||
|
{cn.title && <span className="text-gray-500">— {cn.title}</span>}
|
||||||
|
<span className="px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">Manuell</span>
|
||||||
|
<button onClick={() => setCustomNorms((prev) => prev.filter((_, j) => j !== i))} className="text-red-400 hover:text-red-600">
|
||||||
|
<svg className="w-3.5 h-3.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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pflicht-Erklärung + Disclaimer */}
|
||||||
|
<div className="space-y-2 text-xs">
|
||||||
|
<div className="p-3 rounded-lg bg-red-50 border border-red-200 text-red-800">
|
||||||
|
<strong>Pflicht</strong> 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.
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-amber-800">
|
||||||
|
<strong>Hinweis:</strong> 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 <a href="https://www.beuth.de" target="_blank" rel="noopener noreferrer" className="underline font-medium">beuth.de</a>).
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function HazardsPage() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const projectId = params.projectId as string
|
const projectId = params.projectId as string
|
||||||
const h = useHazards(projectId)
|
const h = useHazards(projectId)
|
||||||
const [view, setView] = useState<ViewMode>('list')
|
const [view, setView] = useState<ViewMode>('risk')
|
||||||
|
|
||||||
if (h.loading) {
|
if (h.loading) {
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +46,7 @@ export default function HazardsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={h.handlePatternMatching} disabled={h.matchingPatterns}
|
<button onClick={h.handlePatternMatching} disabled={h.matchingPatterns}
|
||||||
|
title="Erkennt automatisch Gefaehrdungen anhand der Komponenten-Tags und Lebensphasen"
|
||||||
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors disabled:opacity-50 text-sm">
|
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors disabled:opacity-50 text-sm">
|
||||||
{h.matchingPatterns ? (
|
{h.matchingPatterns ? (
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600" />
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600" />
|
||||||
@@ -54,18 +55,7 @@ export default function HazardsPage() {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
Auto-Erkennung
|
Gefaehrdungen erkennen
|
||||||
</button>
|
|
||||||
<button onClick={h.handleAISuggestions} disabled={h.suggestingAI}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors disabled:opacity-50 text-sm">
|
|
||||||
{h.suggestingAI ? (
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-600" />
|
|
||||||
) : (
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
KI-Vorschlaege
|
|
||||||
</button>
|
</button>
|
||||||
<button onClick={h.fetchLibrary}
|
<button onClick={h.fetchLibrary}
|
||||||
className="flex items-center gap-2 px-3 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm">
|
className="flex items-center gap-2 px-3 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm">
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { REDUCTION_TYPES } from './_components/types'
|
import { REDUCTION_TYPES, Mitigation } from './_components/types'
|
||||||
import { HierarchyWarning } from './_components/HierarchyWarning'
|
import { HierarchyWarning } from './_components/HierarchyWarning'
|
||||||
import { MeasuresLibraryModal } from './_components/MeasuresLibraryModal'
|
import { MeasuresLibraryModal } from './_components/MeasuresLibraryModal'
|
||||||
import { SuggestMeasuresModal } from './_components/SuggestMeasuresModal'
|
import { SuggestMeasuresModal } from './_components/SuggestMeasuresModal'
|
||||||
import { MitigationForm } from './_components/MitigationForm'
|
import { MitigationForm } from './_components/MitigationForm'
|
||||||
import { MitigationCard } from './_components/MitigationCard'
|
import { StatusBadge } from './_components/StatusBadge'
|
||||||
import { ProtectiveMeasure } from './_components/types'
|
import { ProtectiveMeasure } from './_components/types'
|
||||||
import { useMitigations } from './_hooks/useMitigations'
|
import { useMitigations } from './_hooks/useMitigations'
|
||||||
|
|
||||||
@@ -26,6 +26,47 @@ export default function MitigationsPage() {
|
|||||||
const [showLibrary, setShowLibrary] = useState(false)
|
const [showLibrary, setShowLibrary] = useState(false)
|
||||||
const [libraryFilter, setLibraryFilter] = useState<string | undefined>()
|
const [libraryFilter, setLibraryFilter] = useState<string | undefined>()
|
||||||
const [showSuggest, setShowSuggest] = useState(false)
|
const [showSuggest, setShowSuggest] = useState(false)
|
||||||
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({ design: true, protection: true, information: true })
|
||||||
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||||
|
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
|
||||||
|
|
||||||
|
function toggleSection(type: string) {
|
||||||
|
setExpanded((prev) => ({ ...prev, [type]: !prev[type] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelect(id: string) {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) next.delete(id); else next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllInType(type: string) {
|
||||||
|
const items = byType[type as keyof typeof byType]
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
const allSelected = items.every((m) => next.has(m.id))
|
||||||
|
if (allSelected) { items.forEach((m) => next.delete(m.id)) }
|
||||||
|
else { items.forEach((m) => next.add(m.id)) }
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchVerify() {
|
||||||
|
setBatchAction('verify')
|
||||||
|
for (const id of selected) { await handleVerify(id) }
|
||||||
|
setSelected(new Set())
|
||||||
|
setBatchAction(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
if (!confirm(`${selected.size} Massnahmen wirklich loeschen?`)) return
|
||||||
|
setBatchAction('delete')
|
||||||
|
for (const id of selected) { await handleDelete(id) }
|
||||||
|
setSelected(new Set())
|
||||||
|
setBatchAction(null)
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpenLibrary(type?: string) {
|
function handleOpenLibrary(type?: string) {
|
||||||
setLibraryFilter(type)
|
setLibraryFilter(type)
|
||||||
@@ -39,11 +80,6 @@ export default function MitigationsPage() {
|
|||||||
setPreselectedType(measure.reduction_type as 'design' | 'protection' | 'information')
|
setPreselectedType(measure.reduction_type as 'design' | 'protection' | 'information')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddForType(type: 'design' | 'protection' | 'information') {
|
|
||||||
setPreselectedType(type)
|
|
||||||
setShowForm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@@ -52,42 +88,51 @@ export default function MitigationsPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalMeasures = byType.design.length + byType.protection.length + byType.information.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Massnahmen</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Massnahmen</h1>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
Risikominderung nach dem 3-Stufen-Verfahren: Design → Schutz → Information.
|
{totalMeasures} Massnahmen nach 3-Stufen-Verfahren: Design ({byType.design.length}) → Schutz ({byType.protection.length}) → Information ({byType.information.length})
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
{hazards.length > 0 && (
|
{selected.size > 0 && (
|
||||||
<button onClick={() => setShowSuggest(true)}
|
<>
|
||||||
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm">
|
<span className="text-xs text-gray-500">{selected.size} ausgewaehlt</span>
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<button onClick={handleBatchVerify} disabled={batchAction !== null}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
className="px-3 py-1.5 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50">
|
||||||
</svg>
|
{batchAction === 'verify' ? 'Verifiziere...' : 'Verifizieren'}
|
||||||
Vorschlaege
|
</button>
|
||||||
</button>
|
<button onClick={handleBatchDelete} disabled={batchAction !== null}
|
||||||
|
className="px-3 py-1.5 text-xs bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setSelected(new Set())} className="px-2 py-1.5 text-xs text-gray-500 hover:text-gray-700">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selected.size === 0 && (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setShowSuggest(true)}
|
||||||
|
className="px-3 py-1.5 text-xs border border-green-300 text-green-700 rounded-lg hover:bg-green-50">
|
||||||
|
Vorschlaege
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleOpenLibrary()}
|
||||||
|
className="px-3 py-1.5 text-xs border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50">
|
||||||
|
Bibliothek
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setPreselectedType(undefined); setShowForm(true) }}
|
||||||
|
className="px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700">
|
||||||
|
+ Hinzufuegen
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => handleOpenLibrary()}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-white border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 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 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
||||||
</svg>
|
|
||||||
Bibliothek
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { setPreselectedType(undefined); setShowForm(true) }}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
Massnahme hinzufuegen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,76 +140,80 @@ export default function MitigationsPage() {
|
|||||||
|
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<MitigationForm
|
<MitigationForm
|
||||||
onSubmit={async (data) => {
|
onSubmit={async (data) => { const ok = await handleSubmit(data); if (ok) setShowForm(false) }}
|
||||||
const ok = await handleSubmit(data)
|
onCancel={() => setShowForm(false)} hazards={hazards} preselectedType={preselectedType} onOpenLibrary={handleOpenLibrary}
|
||||||
if (ok) { setShowForm(false); setPreselectedType(undefined) }
|
|
||||||
}}
|
|
||||||
onCancel={() => { setShowForm(false); setPreselectedType(undefined) }}
|
|
||||||
hazards={hazards}
|
|
||||||
preselectedType={preselectedType}
|
|
||||||
onOpenLibrary={handleOpenLibrary}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showLibrary && <MeasuresLibraryModal measures={measures} onSelect={handleSelectMeasure} onClose={() => setShowLibrary(false)} filterType={libraryFilter} />}
|
||||||
|
{showSuggest && <SuggestMeasuresModal hazards={hazards} projectId={projectId} onAddMeasure={handleAddSuggestedMeasure} onClose={() => setShowSuggest(false)} />}
|
||||||
|
|
||||||
{showLibrary && (
|
{/* 3-Step Accordions */}
|
||||||
<MeasuresLibraryModal
|
{(['design', 'protection', 'information'] as const).map((type) => {
|
||||||
measures={measures} onSelect={handleSelectMeasure}
|
const config = REDUCTION_TYPES[type]
|
||||||
onClose={() => setShowLibrary(false)} filterType={libraryFilter}
|
const items = byType[type]
|
||||||
/>
|
const isExpanded = expanded[type]
|
||||||
)}
|
const allSelected = items.length > 0 && items.every((m) => selected.has(m.id))
|
||||||
|
|
||||||
{showSuggest && (
|
return (
|
||||||
<SuggestMeasuresModal
|
<div key={type} className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
hazards={hazards} projectId={projectId}
|
{/* Accordion Header */}
|
||||||
onAddMeasure={handleAddSuggestedMeasure}
|
<button onClick={() => toggleSection(type)}
|
||||||
onClose={() => setShowSuggest(false)}
|
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${config.headerColor}`}>
|
||||||
/>
|
<svg className={`w-4 h-4 transition-transform ${isExpanded ? '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>
|
||||||
|
{config.icon}
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-sm font-semibold">{config.label}</span>
|
||||||
|
<span className="ml-2 text-xs opacity-75">{config.description}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold">{items.length}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* 3-Column Layout */}
|
{/* Accordion Content — Table rows */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
{isExpanded && items.length > 0 && (
|
||||||
{(['design', 'protection', 'information'] as const).map((type) => {
|
<div className="border-t border-gray-100 dark:border-gray-700">
|
||||||
const config = REDUCTION_TYPES[type]
|
{/* Table header */}
|
||||||
const items = byType[type]
|
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 dark:bg-gray-750 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
return (
|
<div className="w-6">
|
||||||
<div key={type} className={`rounded-xl border ${config.color} p-4`}>
|
<input type="checkbox" checked={allSelected} onChange={() => selectAllInType(type)}
|
||||||
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${config.headerColor} mb-3`}>
|
className="accent-purple-600" title="Alle auswaehlen" />
|
||||||
{config.icon}
|
</div>
|
||||||
<div>
|
<div className="flex-1">Massnahme</div>
|
||||||
<h3 className="text-sm font-semibold">{config.label}</h3>
|
<div className="w-24">Status</div>
|
||||||
<p className="text-xs opacity-75">{config.description}</p>
|
<div className="w-32">Gefaehrdung</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-auto text-sm font-bold">{items.length}</span>
|
{/* Rows */}
|
||||||
</div>
|
|
||||||
<div className="mb-3 flex flex-wrap gap-1">
|
|
||||||
{config.subTypes.map((st) => (
|
|
||||||
<span key={st.value} className="text-xs px-1.5 py-0.5 rounded bg-white/60 text-gray-500 border border-gray-200/50">
|
|
||||||
{st.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{items.map((m) => (
|
{items.map((m) => (
|
||||||
<MitigationCard key={m.id} mitigation={m} onVerify={handleVerify} onDelete={handleDelete} />
|
<div key={m.id}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 border-t border-gray-50 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors ${selected.has(m.id) ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
|
||||||
|
<div className="w-6">
|
||||||
|
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
|
||||||
|
className="accent-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm text-gray-900 dark:text-white truncate">{m.title || ''}</div>
|
||||||
|
{m.description && <div className="text-xs text-gray-400 truncate">{m.description}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="w-24">
|
||||||
|
<StatusBadge status={m.status} />
|
||||||
|
</div>
|
||||||
|
<div className="w-32 text-xs text-gray-500 truncate">
|
||||||
|
{(m.linked_hazard_names || []).join(', ') || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex gap-2">
|
)}
|
||||||
<button onClick={() => handleAddForType(type)}
|
|
||||||
className="flex-1 py-2 text-sm text-gray-500 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors">
|
{isExpanded && items.length === 0 && (
|
||||||
+ Hinzufuegen
|
<div className="px-4 py-6 text-center text-sm text-gray-400 border-t border-gray-100">
|
||||||
</button>
|
Keine Massnahmen in dieser Stufe
|
||||||
<button onClick={() => handleOpenLibrary(type)}
|
|
||||||
className="py-2 px-3 text-sm text-gray-400 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors"
|
|
||||||
title="Aus Bibliothek waehlen">
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)
|
</div>
|
||||||
})}
|
)
|
||||||
</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,25 +12,114 @@ interface ProductionLineItem {
|
|||||||
updated_at: string
|
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() {
|
export default function ProductionLinesListPage() {
|
||||||
const [lines, setLines] = useState<ProductionLineItem[]>([])
|
const [lines, setLines] = useState<ProductionLineItem[]>([])
|
||||||
|
const [projects, setProjects] = useState<ProjectItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
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<Array<{ projectId: string; stationType: string }>>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { fetchLines(); fetchProjects() }, [])
|
||||||
fetchLines()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
async function fetchLines() {
|
async function fetchLines() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/sdk/v1/iace/production-lines')
|
const res = await fetch('/api/sdk/v1/iace/production-lines')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const json = await res.json()
|
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<string, unknown>) => ({
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch production lines:', err)
|
console.error('Failed to create line:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setCreating(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,90 +133,126 @@ export default function ProductionLinesListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-6xl mx-auto">
|
<div className="space-y-6 max-w-6xl mx-auto">
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<Link href="/sdk/iace" className="text-xs text-purple-600 hover:text-purple-700 font-medium flex items-center gap-1 mb-1">
|
||||||
<Link
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
href="/sdk/iace"
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
className="text-xs text-purple-600 hover:text-purple-700 font-medium flex items-center gap-1"
|
</svg>
|
||||||
>
|
IACE
|
||||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
</Link>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Produktionslinien</h1>
|
||||||
</svg>
|
<p className="mt-1 text-sm text-gray-500">Verkettete Fertigungsstrassen mit aggregierter Risikoansicht</p>
|
||||||
IACE
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
Produktionslinien
|
|
||||||
</h1>
|
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Verkettete Fertigungsstrassen mit aggregierter Risikoansicht
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<button onClick={() => setShowCreate(true)}
|
||||||
href="/sdk/iace/lines/new"
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex-shrink-0"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
Neue Produktionslinie
|
Neue Produktionslinie
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Create form */}
|
||||||
|
{showCreate && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-purple-200 p-6 space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Neue Produktionslinie erstellen</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={lineName} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Beschreibung</label>
|
||||||
|
<input type="text" value={lineDesc} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stations */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Stationen (Projekte zuordnen)</label>
|
||||||
|
<button onClick={addStation} className="text-xs text-purple-600 hover:text-purple-700 font-medium">+ Station hinzufuegen</button>
|
||||||
|
</div>
|
||||||
|
{selectedStations.length === 0 && (
|
||||||
|
<p className="text-xs text-gray-400 italic">Noch keine Stationen. Klicken Sie "+ Station hinzufuegen" um Projekte zuzuordnen.</p>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedStations.map((s, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-gray-400 w-6">{i + 1}.</span>
|
||||||
|
<select value={s.projectId} onChange={(e) => updateStation(i, 'projectId', e.target.value)}
|
||||||
|
className="flex-1 px-2 py-1.5 text-sm border border-gray-300 rounded-lg dark:bg-gray-600 dark:border-gray-500 dark:text-white">
|
||||||
|
<option value="">-- Projekt waehlen --</option>
|
||||||
|
{projects.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>{p.machine_name} ({p.machine_type})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select value={s.stationType} onChange={(e) => updateStation(i, 'stationType', e.target.value)}
|
||||||
|
className="w-40 px-2 py-1.5 text-sm border border-gray-300 rounded-lg dark:bg-gray-600 dark:border-gray-500 dark:text-white">
|
||||||
|
{STATION_TYPES.map((t) => (
|
||||||
|
<option key={t.value} value={t.value}>{t.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button onClick={() => removeStation(i)} className="p-1 text-red-400 hover:text-red-600">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<button onClick={handleCreate} disabled={!lineName.trim() || creating}
|
||||||
|
className={`px-6 py-2 rounded-lg font-medium transition-colors ${lineName.trim() && !creating ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'}`}>
|
||||||
|
{creating ? 'Wird erstellt...' : 'Produktionslinie erstellen'}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowCreate(false); setSelectedStations([]) }}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Lines list */}
|
{/* Lines list */}
|
||||||
{lines.length > 0 ? (
|
{lines.length > 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{lines.map((line) => (
|
{lines.map((line) => (
|
||||||
<Link
|
<Link key={line.id} href={`/sdk/iace/lines/${line.id}`}
|
||||||
key={line.id}
|
className="block bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md hover:border-purple-300 transition-all">
|
||||||
href={`/sdk/iace/lines/${line.id}`}
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">{line.name}</h3>
|
||||||
className="block bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md hover:border-purple-300 transition-all"
|
{line.description && <p className="text-sm text-gray-500 mb-3 line-clamp-2">{line.description}</p>}
|
||||||
>
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
<span>{line.station_count || 0} Stationen</span>
|
||||||
{line.name}
|
<span>Aktualisiert: {new Date(line.updated_at || line.created_at).toLocaleDateString('de-DE')}</span>
|
||||||
</h3>
|
|
||||||
{line.description && (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3 line-clamp-2">
|
|
||||||
{line.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
||||||
</svg>
|
|
||||||
{line.station_count} Stationen
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Aktualisiert: {new Date(line.updated_at || line.created_at).toLocaleDateString('de-DE')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{lines.length === 0 && !showCreate && (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||||
<div className="w-16 h-16 mx-auto bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mb-4">
|
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||||
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Noch keine Produktionslinien</h3>
|
||||||
Noch keine Produktionslinien vorhanden
|
<p className="mt-2 text-gray-500 max-w-lg mx-auto">
|
||||||
</h3>
|
|
||||||
<p className="mt-2 text-gray-500 dark:text-gray-400 max-w-lg mx-auto">
|
|
||||||
Produktionslinien verketten mehrere CE-Projekte zu einer Fertigungsstrasse.
|
Produktionslinien verketten mehrere CE-Projekte zu einer Fertigungsstrasse.
|
||||||
Sie sehen auf einen Blick den Risikostatus aller Stationen und koennen
|
|
||||||
Massnahmen priorisieren.
|
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<button onClick={() => { setShowCreate(true); addStation() }}
|
||||||
href="/sdk/iace/lines/new"
|
className="mt-6 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium">
|
||||||
className="inline-block mt-6 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
Erste Produktionslinie erstellen
|
Erste Produktionslinie erstellen
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"status": "passed",
|
"status": "failed",
|
||||||
"failedTests": []
|
"failedTests": []
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,10 @@ func (h *IACEHandler) ListNormsLibrary(c *gin.Context) {
|
|||||||
allNorms = append(allNorms, iace.GetWoodMetalCNorms()...)
|
allNorms = append(allNorms, iace.GetWoodMetalCNorms()...)
|
||||||
allNorms = append(allNorms, iace.GetFoodPkgCNorms()...)
|
allNorms = append(allNorms, iace.GetFoodPkgCNorms()...)
|
||||||
allNorms = append(allNorms, iace.GetLiftMiscCNorms()...)
|
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
|
var filtered []iace.NormReference
|
||||||
for _, norm := range allNorms {
|
for _, norm := range allNorms {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ func SuggestNorms(machineType string, hazardCategories []string, tags []string)
|
|||||||
allNorms = append(allNorms, GetWoodMetalCNorms()...)
|
allNorms = append(allNorms, GetWoodMetalCNorms()...)
|
||||||
allNorms = append(allNorms, GetFoodPkgCNorms()...)
|
allNorms = append(allNorms, GetFoodPkgCNorms()...)
|
||||||
allNorms = append(allNorms, GetLiftMiscCNorms()...)
|
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
|
// Build lookup sets for efficient matching
|
||||||
hazardSet := toSet(hazardCategories)
|
hazardSet := toSet(hazardCategories)
|
||||||
|
|||||||
Reference in New Issue
Block a user