a708d139ab
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>
142 lines
6.4 KiB
TypeScript
142 lines
6.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useMemo, useState, useRef, useEffect } from 'react'
|
|
import { SearchInput, FilterDropdown, Pagination, ExpandableRow } from './LibraryTable'
|
|
|
|
export interface HazardPattern {
|
|
id: string
|
|
name_de: string
|
|
name_en: string
|
|
priority: number
|
|
required_component_tags: string[]
|
|
generated_hazard_categories: string[]
|
|
scenario_de: string
|
|
trigger_de: string
|
|
harm_de: string
|
|
affected_de?: string
|
|
zone_de?: string
|
|
}
|
|
|
|
const PER_PAGE = 50
|
|
const PRIORITY_OPTIONS = [
|
|
{ value: '', label: 'Alle Prioritaeten' },
|
|
{ value: '90', label: 'Prioritaet > 90' },
|
|
{ value: '70', label: 'Prioritaet > 70' },
|
|
{ value: '50', label: 'Prioritaet > 50' },
|
|
]
|
|
|
|
function priorityColor(p: number): string {
|
|
if (p >= 90) return 'bg-red-100 text-red-800'
|
|
if (p >= 70) return 'bg-orange-100 text-orange-800'
|
|
if (p >= 50) return 'bg-yellow-100 text-yellow-800'
|
|
return 'bg-gray-100 text-gray-700'
|
|
}
|
|
|
|
interface Props { patterns: HazardPattern[] }
|
|
|
|
export default function PatternsTab({ patterns }: Props) {
|
|
const [search, setSearch] = useState('')
|
|
const [debounced, setDebounced] = useState('')
|
|
const [catFilter, setCatFilter] = useState('')
|
|
const [prioFilter, setPrioFilter] = 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, catFilter, prioFilter])
|
|
|
|
const categories = useMemo(() => {
|
|
const set = new Set<string>()
|
|
patterns.forEach((p) => p.generated_hazard_categories.forEach((c) => set.add(c)))
|
|
return Array.from(set).sort()
|
|
}, [patterns])
|
|
|
|
const catOptions = useMemo(() => [
|
|
{ value: '', label: 'Alle Kategorien' },
|
|
...categories.map((c) => ({ value: c, label: c.replace(/_/g, ' ') })),
|
|
], [categories])
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = debounced.toLowerCase()
|
|
const prioMin = prioFilter ? parseInt(prioFilter) : 0
|
|
return patterns.filter((p) => {
|
|
if (q && !p.name_de.toLowerCase().includes(q) && !p.scenario_de.toLowerCase().includes(q) && !p.harm_de.toLowerCase().includes(q)) return false
|
|
if (catFilter && !p.generated_hazard_categories.includes(catFilter)) return false
|
|
if (prioMin && p.priority <= prioMin) return false
|
|
return true
|
|
})
|
|
}, [patterns, debounced, catFilter, prioFilter])
|
|
|
|
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, Szenario oder Schaden suchen..." />
|
|
</div>
|
|
<FilterDropdown label="Kategorie" value={catFilter} options={catOptions} onChange={setCatFilter} />
|
|
<FilterDropdown label="Prioritaet" value={prioFilter} options={PRIORITY_OPTIONS} onChange={setPrioFilter} />
|
|
<span className="text-sm text-gray-500 dark:text-gray-400 ml-auto">
|
|
{patterns.length} Patterns{filtered.length !== patterns.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', 'Kategorie', 'Prioritaet', 'Schaden', 'Tags'].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((p) => (
|
|
<ExpandableRow
|
|
key={p.id}
|
|
colSpan={6}
|
|
cells={[
|
|
<span key="id" className="font-mono text-xs text-gray-500">{p.id.slice(0, 8)}</span>,
|
|
<span key="name" className="max-w-xs truncate block">{p.name_de}</span>,
|
|
<span key="cat" className="text-xs">{p.generated_hazard_categories[0]?.replace(/_/g, ' ') || '-'}</span>,
|
|
<span key="prio" className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${priorityColor(p.priority)}`}>{p.priority}</span>,
|
|
<span key="harm" className="max-w-[160px] truncate block text-xs">{p.harm_de || '-'}</span>,
|
|
<span key="tags" className="text-xs text-gray-500">{p.required_component_tags.length > 0 ? p.required_component_tags.slice(0, 2).join(', ') : '-'}{p.required_component_tags.length > 2 ? ` +${p.required_component_tags.length - 2}` : ''}</span>,
|
|
]}
|
|
expandedContent={
|
|
<div className="space-y-2">
|
|
{p.scenario_de && <div><span className="font-medium text-gray-700 dark:text-gray-300">Szenario:</span> {p.scenario_de}</div>}
|
|
{p.trigger_de && <div><span className="font-medium text-gray-700 dark:text-gray-300">Ausloeser:</span> {p.trigger_de}</div>}
|
|
{p.harm_de && <div><span className="font-medium text-gray-700 dark:text-gray-300">Schaden:</span> {p.harm_de}</div>}
|
|
{p.affected_de && <div><span className="font-medium text-gray-700 dark:text-gray-300">Betroffene:</span> {p.affected_de}</div>}
|
|
{p.zone_de && <div><span className="font-medium text-gray-700 dark:text-gray-300">Zone:</span> {p.zone_de}</div>}
|
|
{p.generated_hazard_categories.length > 0 && (
|
|
<div className="flex flex-wrap gap-1">
|
|
{p.generated_hazard_categories.map((c) => (
|
|
<span key={c} className="px-1.5 py-0.5 rounded text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300">{c.replace(/_/g, ' ')}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
}
|
|
/>
|
|
))}
|
|
{pageItems.length === 0 && (
|
|
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-gray-400">Keine Patterns gefunden</td></tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
|
|
</div>
|
|
)
|
|
}
|