Files
breakpilot-compliance/admin-compliance/app/sdk/iace/library/_components/MeasuresTab.tsx
T
Benjamin Admin a708d139ab feat: IACE Bibliotheks-Browser — 751 Normen, 1000 Patterns, 200 Massnahmen
Neue Seite /sdk/iace/library mit 3 Tabs:
- Normen: Suche + Filter A/B/C + Pflicht + Beuth-Links
- Patterns: Suche + Filter Kategorie/Prioritaet + Details aufklappbar
- Massnahmen: Suche + Filter Design/Schutz/Information
Alle mit Pagination (50/Seite) und Zaehler-Badges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 00:09:31 +02:00

130 lines
5.4 KiB
TypeScript

'use client'
import React, { useMemo, useState, useRef, useEffect } from 'react'
import { SearchInput, FilterDropdown, Pagination, ExpandableRow } from './LibraryTable'
export interface ProtectiveMeasure {
id: string
reduction_type: string
sub_type: string
name: string
description: string
hazard_category: string
examples: string[]
norm_references: string[]
}
const PER_PAGE = 50
const TYPE_OPTIONS = [
{ value: '', label: 'Alle Typen' },
{ value: 'Design', label: 'Design' },
{ value: 'Schutz', label: 'Schutz' },
{ value: 'Information', label: 'Information' },
]
function typeColor(t: string): string {
switch (t) {
case 'Design': return 'bg-blue-100 text-blue-800'
case 'Schutz': return 'bg-green-100 text-green-800'
case 'Information': return 'bg-yellow-100 text-yellow-800'
default: return 'bg-gray-100 text-gray-700'
}
}
interface Props { measures: ProtectiveMeasure[] }
export default function MeasuresTab({ measures }: Props) {
const [search, setSearch] = useState('')
const [debounced, setDebounced] = useState('')
const [typeFilter, setTypeFilter] = useState('')
const [page, setPage] = useState(1)
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
timer.current = setTimeout(() => setDebounced(search), 300)
return () => { if (timer.current) clearTimeout(timer.current) }
}, [search])
useEffect(() => { setPage(1) }, [debounced, typeFilter])
const filtered = useMemo(() => {
const q = debounced.toLowerCase()
return measures.filter((m) => {
if (q && !m.name.toLowerCase().includes(q) && !m.description.toLowerCase().includes(q)) return false
if (typeFilter && m.reduction_type !== typeFilter) return false
return true
})
}, [measures, debounced, typeFilter])
const totalPages = Math.ceil(filtered.length / PER_PAGE)
const pageItems = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE)
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
<div className="flex-1 min-w-[200px]">
<SearchInput value={search} onChange={setSearch} placeholder="Name oder Beschreibung suchen..." />
</div>
<FilterDropdown label="Typ" value={typeFilter} options={TYPE_OPTIONS} onChange={setTypeFilter} />
<span className="text-sm text-gray-500 dark:text-gray-400 ml-auto">
{measures.length} Massnahmen{filtered.length !== measures.length && ` (${filtered.length} gefiltert)`}
</span>
</div>
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
<table className="w-full text-left">
<thead className="bg-gray-100 dark:bg-gray-800">
<tr>
{['ID', 'Name', 'Typ', 'Subtyp', 'Kategorie', 'Normen'].map((h) => (
<th key={h} className="px-4 py-2.5 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">{h}</th>
))}
<th className="w-8" />
</tr>
</thead>
<tbody>
{pageItems.map((m) => (
<ExpandableRow
key={m.id}
colSpan={6}
cells={[
<span key="id" className="font-mono text-xs text-gray-500">{m.id.slice(0, 8)}</span>,
<span key="name" className="max-w-xs truncate block">{m.name}</span>,
<span key="type" className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${typeColor(m.reduction_type)}`}>{m.reduction_type}</span>,
<span key="sub" className="text-xs">{m.sub_type || '-'}</span>,
<span key="cat" className="text-xs">{m.hazard_category?.replace(/_/g, ' ') || '-'}</span>,
<span key="norms" className="text-xs text-gray-500">{m.norm_references.length > 0 ? m.norm_references.slice(0, 2).join(', ') : '-'}{m.norm_references.length > 2 ? ` +${m.norm_references.length - 2}` : ''}</span>,
]}
expandedContent={
<div className="space-y-2">
{m.description && <div><span className="font-medium text-gray-700 dark:text-gray-300">Beschreibung:</span> {m.description}</div>}
{m.examples.length > 0 && (
<div>
<span className="font-medium text-gray-700 dark:text-gray-300">Beispiele:</span>
<ul className="list-disc ml-5 mt-1 space-y-0.5">
{m.examples.map((ex, i) => <li key={i}>{ex}</li>)}
</ul>
</div>
)}
{m.norm_references.length > 0 && (
<div className="flex flex-wrap gap-1">
{m.norm_references.map((nr) => (
<span key={nr} className="px-1.5 py-0.5 rounded text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-mono">{nr}</span>
))}
</div>
)}
</div>
}
/>
))}
{pageItems.length === 0 && (
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-gray-400">Keine Massnahmen gefunden</td></tr>
)}
</tbody>
</table>
</div>
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
</div>
)
}