'use client' import { useState, useEffect } from 'react' import { useParams } from 'next/navigation' import { REDUCTION_TYPES, Mitigation } from './_components/types' import { HierarchyWarning } from './_components/HierarchyWarning' import { MeasuresLibraryModal } from './_components/MeasuresLibraryModal' import { SuggestMeasuresModal } from './_components/SuggestMeasuresModal' import { MitigationForm } from './_components/MitigationForm' import { StatusBadge } from './_components/StatusBadge' import { MitigationHints } from './_components/MitigationHints' import { ProtectiveMeasure } from './_components/types' import { useMitigations } from './_hooks/useMitigations' export default function MitigationsPage() { const params = useParams() const projectId = params.projectId as string const { hazards, loading, hierarchyWarning, setHierarchyWarning, measures, byType, fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete, } = useMitigations(projectId) const [measureNorms, setMeasureNorms] = useState>({}) useEffect(() => { fetch('/api/sdk/v1/iace/protective-measures-library') .then(r => r.ok ? r.json() : null) .then(json => { if (!json?.protective_measures) return const map: Record = {} for (const m of json.protective_measures) { if (m.norm_references?.length > 0) { map[(m.name || '').toLowerCase()] = m.norm_references } } setMeasureNorms(map) }) .catch(() => {}) }, []) const [showForm, setShowForm] = useState(false) const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>() const [showLibrary, setShowLibrary] = useState(false) const [libraryFilter, setLibraryFilter] = useState() const [showSuggest, setShowSuggest] = useState(false) const [expanded, setExpanded] = useState>({ design: true, protection: true, information: true }) const [mitPages, setMitPages] = useState>({ design: 1, protection: 1, information: 1 }) const [selected, setSelected] = useState>(new Set()) const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null) const [expandedMeasure, setExpandedMeasure] = useState(null) // Group-Expand: key = `${type}:${title}` so the same title in different // reduction stages stays independently togglable. const [expandedGroup, setExpandedGroup] = useState>(new Set()) function toggleGroup(key: string) { setExpandedGroup((prev) => { const next = new Set(prev) if (next.has(key)) next.delete(key); else next.add(key) return next }) } // Mitigations sharing the same title (e.g. "Sicherheitszeichen nach ISO 7010" // applied to 21 hazards) collapse into a single group row. Each instance // keeps its own DB id, status and notes — the grouping is presentation-only. function groupByTitle(items: Mitigation[]): Array<{ title: string; instances: Mitigation[] }> { const map = new Map() for (const m of items) { const key = (m.title || '').trim() || '(ohne Titel)' const arr = map.get(key) if (arr) arr.push(m); else map.set(key, [m]) } return Array.from(map.entries()).map(([title, instances]) => ({ title, instances })) } // Compact status distribution: returns counts for the three known states. function statusCounts(instances: Mitigation[]) { const c = { planned: 0, implemented: 0, verified: 0 } for (const m of instances) { if (m.status === 'planned') c.planned++ else if (m.status === 'implemented') c.implemented++ else if (m.status === 'verified') c.verified++ } return c } 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) { setLibraryFilter(type) fetchMeasuresLibrary(type) setShowLibrary(true) } function handleSelectMeasure(measure: ProtectiveMeasure) { setShowLibrary(false) setShowForm(true) setPreselectedType(measure.reduction_type as 'design' | 'protection' | 'information') } if (loading) { return (
) } const totalMeasures = byType.design.length + byType.protection.length + byType.information.length return (
{/* Header */}

Massnahmen

{totalMeasures} Massnahmen nach 3-Stufen-Verfahren: Design ({byType.design.length}) → Schutz ({byType.protection.length}) → Information ({byType.information.length})

{selected.size > 0 && ( <> {selected.size} ausgewaehlt )} {selected.size === 0 && ( <> )}
{hierarchyWarning && setHierarchyWarning(false)} />} {showForm && ( { const ok = await handleSubmit(data); if (ok) setShowForm(false) }} onCancel={() => setShowForm(false)} hazards={hazards} preselectedType={preselectedType} onOpenLibrary={handleOpenLibrary} /> )} {showLibrary && setShowLibrary(false)} filterType={libraryFilter} />} {showSuggest && setShowSuggest(false)} />} {/* 3-Step Accordions */} {(['design', 'protection', 'information'] as const).map((type) => { const config = REDUCTION_TYPES[type] const items = byType[type] const isExpanded = expanded[type] const allSelected = items.length > 0 && items.every((m) => selected.has(m.id)) return (
{/* Accordion Header */} {/* Accordion Content — grouped by measure title */} {isExpanded && items.length > 0 && (() => { const groups = groupByTitle(items) const visibleGroups = groups.slice(0, (mitPages[type] || 1) * 50) return (
{/* Table header */}
selectAllInType(type)} className="accent-purple-600" title="Alle auswaehlen" />
Massnahme
Gefaehrdungen
Status (P · I · V)
{visibleGroups.map(({ title, instances }) => { const groupKey = `${type}:${title}` const isGroupOpen = expandedGroup.has(groupKey) const allInGroupSelected = instances.length > 0 && instances.every((m) => selected.has(m.id)) const counts = statusCounts(instances) const refs = measureNorms[title.toLowerCase()] const first = instances[0] const description = first?.description || '' const catMatch = description.match(/Kategorie\s+(\S+)/) const category = catMatch?.[1] return (
{/* Group header row */}
toggleGroup(groupKey)} className={`grid grid-cols-[24px_2fr_140px_120px] 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 cursor-pointer ${allInGroupSelected ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
e.stopPropagation()}> { setSelected((prev) => { const next = new Set(prev) if (allInGroupSelected) instances.forEach((m) => next.delete(m.id)) else instances.forEach((m) => next.add(m.id)) return next }) }} className="accent-purple-600" title={`Alle ${instances.length} Instanzen auswaehlen`} />
{title}
{category &&
Kategorie: {category}
}
{instances.length}
{counts.planned} · {counts.implemented} · {counts.verified}
{/* Group children — one row per instance (hazard) */} {isGroupOpen && (
{description && (

{description}

)} {refs?.length > 0 && (

Normen: {refs.join(', ')}

)} {instances.map((m) => { const isDetailOpen = expandedMeasure === m.id return (
setExpandedMeasure(isDetailOpen ? null : m.id)} className={`grid grid-cols-[40px_24px_2fr_140px] gap-2 px-4 py-1.5 border-t border-gray-100 dark:border-gray-700 hover:bg-white dark:hover:bg-gray-800 transition-colors cursor-pointer ${selected.has(m.id) ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
e.stopPropagation()}> toggleSelect(m.id)} className="accent-purple-600" />
{(m.linked_hazard_names || []).join(', ') || '— (keine Gefaehrdung verknuepft)'}
{isDetailOpen && (
)}
) })}
)}
) })} {groups.length > visibleGroups.length && ( )}
) })()} {isExpanded && items.length === 0 && (
Keine Massnahmen in dieser Stufe
)}
) })}
) }