'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, fetchData, fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleDelete, handleDeleteSilent, handleSetRelevant, } = 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 [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 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})

{hierarchyWarning && setHierarchyWarning(false)} />} {/* Reinitialisieren-Warnung: nach manuellem Loeschen wuerde ein Reinit die geloeschten Engine-Vorschlaege wiederherstellen. */}
Hinweis: Markiere jede Maßnahme als Relevant (☑) oder lösche sie aus dem Projekt (🗑). Nur als relevant markierte Maßnahmen erscheinen in der Verifikation. Achtung: nach dem Löschen kein Neu initialisieren mehr drücken — sonst werden die gelöschten Vorschläge aus den Engine-Daten wiederhergestellt.
{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] 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 */}
Relev.
Lösch.
Massnahme
Gefährdungen
Status (P · I · V)
{visibleGroups.map(({ title, instances }) => { const groupKey = `${type}:${title}` const isGroupOpen = expandedGroup.has(groupKey) // (legacy bulk-select removed — Relevant-checkbox is now the primary mass-action) 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] const relevantInGroup = instances.filter((m) => m.is_relevant).length const allRelevant = relevantInGroup === instances.length return (
{/* Group header row */}
toggleGroup(groupKey)} className={`grid grid-cols-[36px_36px_2fr_120px_110px] 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`}>
e.stopPropagation()}> { if (el) el.indeterminate = !allRelevant && relevantInGroup > 0 }} onChange={async (e) => { const target = e.target.checked for (const m of instances) { if (m.is_relevant !== target) await handleSetRelevant(m.id, target) } }} className="accent-purple-600" title={`${relevantInGroup}/${instances.length} als relevant markiert. Klick: alle als ${allRelevant ? 'nicht relevant' : 'relevant'} markieren.`} />
e.stopPropagation()}>
{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-[36px_36px_2fr_120px_110px] 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 ${m.is_relevant ? 'bg-emerald-50/40 dark:bg-emerald-900/10' : ''}`}>
e.stopPropagation()}> handleSetRelevant(m.id, !m.is_relevant)} className="accent-purple-600" title="Als relevant markieren" />
e.stopPropagation()}>
{(m.linked_hazard_names || []).join(', ') || '— (keine Gefaehrdung verknuepft)'}
{m.is_customer_standard ? 'Kundenstandard' : ''}
{isDetailOpen && (
)}
) })}
)}
) })} {groups.length > visibleGroups.length && ( )}
) })()} {isExpanded && items.length === 0 && (
Keine Massnahmen in dieser Stufe
)}
) })}
) }