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:
Benjamin Admin
2026-05-07 17:51:59 +02:00
parent 313ee5073b
commit 58a3fb285f
3 changed files with 51 additions and 12 deletions
@@ -139,14 +139,29 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
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 totalPages = Math.ceil(sorted.length / PAGE_SIZE)
const paged = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
return (
<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">
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Risikobewertungstabelle (ISO 12100)</h2>
<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">&lt;</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">&gt;</button>
</div>
)}
<span className="text-xs text-gray-500">{hazards.length} Gefaehrdungen</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs whitespace-nowrap">
@@ -185,15 +200,20 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
</tr>
</thead>
<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 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 afterLevel = getRiskLevelISO(afterRpz)
const sil = silFromRpz(afterRpz)
const pl = plFromRpz(afterRpz)
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 (
<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>
</td>
{/* 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">{h.exposure}</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">{initS}</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">{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 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)}`}>
@@ -27,6 +27,7 @@ export default function MitigationsPage() {
const [libraryFilter, setLibraryFilter] = useState<string | undefined>()
const [showSuggest, setShowSuggest] = useState(false)
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 [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
@@ -183,8 +184,8 @@ export default function MitigationsPage() {
<div>Gefaehrdung</div>
<div>Status</div>
</div>
{/* Rows */}
{items.map((m) => (
{/* Rows — paginated */}
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => (
<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' : ''}`}>
<div className="pt-0.5">
@@ -203,6 +204,12 @@ export default function MitigationsPage() {
</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>
)}
+15 -3
View File
@@ -100,6 +100,15 @@ export default function IACELayout({ children }: { children: React.ReactNode })
const pathname = usePathname()
const params = useParams()
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}` : ''
@@ -127,9 +136,12 @@ export default function IACELayout({ children }: { children: React.ReactNode })
</svg>
Alle Projekte
</Link>
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mt-2">
CE-Compliance
</h2>
{projectName && (
<p className="text-xs font-bold text-purple-700 dark:text-purple-400 mt-2 truncate" title={projectName}>
{projectName}
</p>
)}
<p className="text-[10px] text-gray-400 mt-0.5">CE-Compliance</p>
<Link
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"