fix: Erstbewertung aus risk_assessment + Pagination + Projektname
- Erstbewertung S/E/P liest jetzt aus risk_assessment statt hazard - Hazards: Pagination 50 pro Seite mit < > Navigation - Massnahmen: Lazy-Load 50 pro Accordion mit "Mehr laden" - Sidebar: Projektname (z.B. "Kniehebelpresse HP-500") prominent - Uebersicht: Nur 2 API-Calls (keine schweren Listen) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-7
@@ -139,13 +139,28 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
|||||||
finally { setSaving(null) }
|
finally { setSaving(null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
const sorted = [...hazards].sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
|
const sorted = [...hazards].sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
|
||||||
|
const totalPages = Math.ceil(sorted.length / PAGE_SIZE)
|
||||||
|
const paged = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Risikobewertungstabelle (ISO 12100)</h2>
|
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Risikobewertungstabelle (ISO 12100)</h2>
|
||||||
<span className="text-xs text-gray-500">{hazards.length} Gefaehrdungen</span>
|
<div className="flex items-center gap-3">
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
<button onClick={() => setPage(Math.max(0, page - 1))} disabled={page === 0}
|
||||||
|
className="px-2 py-1 rounded border border-gray-200 hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed"><</button>
|
||||||
|
<span className="px-2 text-gray-600">Seite {page + 1} / {totalPages}</span>
|
||||||
|
<button onClick={() => setPage(Math.min(totalPages - 1, page + 1))} disabled={page >= totalPages - 1}
|
||||||
|
className="px-2 py-1 rounded border border-gray-200 hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed">></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-gray-500">{hazards.length} Gefaehrdungen</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -185,15 +200,20 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
{sorted.map(h => {
|
{paged.map(h => {
|
||||||
|
const ra = (h as Record<string, unknown>).risk_assessment as Record<string, number> | null
|
||||||
|
const initS = ra?.severity || h.severity || 3
|
||||||
|
const initE = ra?.exposure || h.exposure || 3
|
||||||
|
const initP = ra?.probability || h.probability || 3
|
||||||
|
const initA = h.avoidance || 0
|
||||||
const e = edits[h.id]
|
const e = edits[h.id]
|
||||||
const initRpz = h.r_inherent || rpz(h.severity, h.exposure, h.probability, h.avoidance)
|
const initRpz = rpz(initS, initE, initP, initA)
|
||||||
const afterRpz = e ? rpz(e.severity, e.exposure, e.probability, e.avoidance) : initRpz
|
const afterRpz = e ? rpz(e.severity, e.exposure, e.probability, e.avoidance) : initRpz
|
||||||
const afterLevel = getRiskLevelISO(afterRpz)
|
const afterLevel = getRiskLevelISO(afterRpz)
|
||||||
const sil = silFromRpz(afterRpz)
|
const sil = silFromRpz(afterRpz)
|
||||||
const pl = plFromRpz(afterRpz)
|
const pl = plFromRpz(afterRpz)
|
||||||
const mc = mitCounts[h.id] || 0
|
const mc = mitCounts[h.id] || 0
|
||||||
const changed = e && (e.severity !== h.severity || e.exposure !== h.exposure || e.probability !== h.probability || e.avoidance !== (h.avoidance || 3))
|
const changed = e && (e.severity !== initS || e.exposure !== initE || e.probability !== initP)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={h.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
<tr key={h.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
||||||
@@ -208,9 +228,9 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{/* Initial S/E/P/RPZ/Risk */}
|
{/* Initial S/E/P/RPZ/Risk */}
|
||||||
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{h.severity}</td>
|
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initS}</td>
|
||||||
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{h.exposure}</td>
|
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initE}</td>
|
||||||
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{h.probability}</td>
|
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initP}</td>
|
||||||
<td className="px-2 py-2 text-center font-bold text-gray-900 dark:text-white">{initRpz}</td>
|
<td className="px-2 py-2 text-center font-bold text-gray-900 dark:text-white">{initRpz}</td>
|
||||||
<td className="px-2 py-2 text-center border-r border-gray-200 dark:border-gray-600">
|
<td className="px-2 py-2 text-center border-r border-gray-200 dark:border-gray-600">
|
||||||
<span className={`inline-block px-1.5 py-0.5 rounded-full text-[10px] font-medium border ${getRiskColor(h.risk_level)}`}>
|
<span className={`inline-block px-1.5 py-0.5 rounded-full text-[10px] font-medium border ${getRiskColor(h.risk_level)}`}>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function MitigationsPage() {
|
|||||||
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 [expanded, setExpanded] = useState<Record<string, boolean>>({ design: true, protection: true, information: true })
|
||||||
|
const [mitPages, setMitPages] = useState<Record<string, number>>({ design: 1, protection: 1, information: 1 })
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||||
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
|
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
|
||||||
|
|
||||||
@@ -183,8 +184,8 @@ export default function MitigationsPage() {
|
|||||||
<div>Gefaehrdung</div>
|
<div>Gefaehrdung</div>
|
||||||
<div>Status</div>
|
<div>Status</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rows */}
|
{/* Rows — paginated */}
|
||||||
{items.map((m) => (
|
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => (
|
||||||
<div key={m.id}
|
<div key={m.id}
|
||||||
className={`grid grid-cols-[24px_2fr_1fr_80px] 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' : ''}`}>
|
className={`grid grid-cols-[24px_2fr_1fr_80px] 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="pt-0.5">
|
<div className="pt-0.5">
|
||||||
@@ -203,6 +204,12 @@ export default function MitigationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{items.length > (mitPages[type] || 1) * 50 && (
|
||||||
|
<button onClick={() => setMitPages(prev => ({ ...prev, [type]: (prev[type] || 1) + 1 }))}
|
||||||
|
className="w-full py-2 text-xs text-purple-600 hover:bg-purple-50 border-t border-gray-100 transition-colors">
|
||||||
|
Weitere {Math.min(50, items.length - (mitPages[type] || 1) * 50)} von {items.length} laden...
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,15 @@ export default function IACELayout({ children }: { children: React.ReactNode })
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const projectId = params?.projectId as string | undefined
|
const projectId = params?.projectId as string | undefined
|
||||||
|
const [projectName, setProjectName] = React.useState('')
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!projectId) return
|
||||||
|
fetch(`/api/sdk/v1/iace/projects/${projectId}`)
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(d => { if (d?.machine_name) setProjectName(d.machine_name) })
|
||||||
|
.catch(() => {})
|
||||||
|
}, [projectId])
|
||||||
|
|
||||||
const basePath = projectId ? `/sdk/iace/${projectId}` : ''
|
const basePath = projectId ? `/sdk/iace/${projectId}` : ''
|
||||||
|
|
||||||
@@ -127,9 +136,12 @@ export default function IACELayout({ children }: { children: React.ReactNode })
|
|||||||
</svg>
|
</svg>
|
||||||
Alle Projekte
|
Alle Projekte
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mt-2">
|
{projectName && (
|
||||||
CE-Compliance
|
<p className="text-xs font-bold text-purple-700 dark:text-purple-400 mt-2 truncate" title={projectName}>
|
||||||
</h2>
|
{projectName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-gray-400 mt-0.5">CE-Compliance</p>
|
||||||
<Link
|
<Link
|
||||||
href="/sdk/iace/lines"
|
href="/sdk/iace/lines"
|
||||||
className="mt-2 flex items-center gap-1.5 text-xs text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400 transition-colors"
|
className="mt-2 flex items-center gap-1.5 text-xs text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400 transition-colors"
|
||||||
|
|||||||
Reference in New Issue
Block a user