All 4 page.tsx files reduced well below 500 LOC (235/181/158/262) by extracting components and hooks into colocated _components/ and _hooks/ subdirectories. Zero behavior changes — logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
182 lines
7.9 KiB
TypeScript
182 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useParams } from 'next/navigation'
|
|
import { REDUCTION_TYPES } from './_components/types'
|
|
import { HierarchyWarning } from './_components/HierarchyWarning'
|
|
import { MeasuresLibraryModal } from './_components/MeasuresLibraryModal'
|
|
import { SuggestMeasuresModal } from './_components/SuggestMeasuresModal'
|
|
import { MitigationForm } from './_components/MitigationForm'
|
|
import { MitigationCard } from './_components/MitigationCard'
|
|
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 [showForm, setShowForm] = useState(false)
|
|
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
|
|
const [showLibrary, setShowLibrary] = useState(false)
|
|
const [libraryFilter, setLibraryFilter] = useState<string | undefined>()
|
|
const [showSuggest, setShowSuggest] = useState(false)
|
|
|
|
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')
|
|
}
|
|
|
|
function handleAddForType(type: 'design' | 'protection' | 'information') {
|
|
setPreselectedType(type)
|
|
setShowForm(true)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Massnahmen</h1>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Risikominderung nach dem 3-Stufen-Verfahren: Design → Schutz → Information.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{hazards.length > 0 && (
|
|
<button
|
|
onClick={() => setShowSuggest(true)}
|
|
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
</svg>
|
|
Vorschlaege
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => handleOpenLibrary()}
|
|
className="flex items-center gap-2 px-4 py-2 bg-white border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
</svg>
|
|
Bibliothek
|
|
</button>
|
|
<button
|
|
onClick={() => { setPreselectedType(undefined); setShowForm(true) }}
|
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Massnahme hinzufuegen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{hierarchyWarning && <HierarchyWarning onDismiss={() => setHierarchyWarning(false)} />}
|
|
|
|
{showForm && (
|
|
<MitigationForm
|
|
onSubmit={async (data) => {
|
|
const ok = await handleSubmit(data)
|
|
if (ok) { setShowForm(false); setPreselectedType(undefined) }
|
|
}}
|
|
onCancel={() => { setShowForm(false); setPreselectedType(undefined) }}
|
|
hazards={hazards}
|
|
preselectedType={preselectedType}
|
|
onOpenLibrary={handleOpenLibrary}
|
|
/>
|
|
)}
|
|
|
|
{showLibrary && (
|
|
<MeasuresLibraryModal
|
|
measures={measures}
|
|
onSelect={handleSelectMeasure}
|
|
onClose={() => setShowLibrary(false)}
|
|
filterType={libraryFilter}
|
|
/>
|
|
)}
|
|
|
|
{showSuggest && (
|
|
<SuggestMeasuresModal
|
|
hazards={hazards}
|
|
projectId={projectId}
|
|
onAddMeasure={handleAddSuggestedMeasure}
|
|
onClose={() => setShowSuggest(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* 3-Column Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{(['design', 'protection', 'information'] as const).map((type) => {
|
|
const config = REDUCTION_TYPES[type]
|
|
const items = byType[type]
|
|
return (
|
|
<div key={type} className={`rounded-xl border ${config.color} p-4`}>
|
|
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${config.headerColor} mb-3`}>
|
|
{config.icon}
|
|
<div>
|
|
<h3 className="text-sm font-semibold">{config.label}</h3>
|
|
<p className="text-xs opacity-75">{config.description}</p>
|
|
</div>
|
|
<span className="ml-auto text-sm font-bold">{items.length}</span>
|
|
</div>
|
|
<div className="mb-3 flex flex-wrap gap-1">
|
|
{config.subTypes.map((st) => (
|
|
<span key={st.value} className="text-xs px-1.5 py-0.5 rounded bg-white/60 text-gray-500 border border-gray-200/50">
|
|
{st.label}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="space-y-3">
|
|
{items.map((m) => (
|
|
<MitigationCard key={m.id} mitigation={m} onVerify={handleVerify} onDelete={handleDelete} />
|
|
))}
|
|
</div>
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={() => handleAddForType(type)}
|
|
className="flex-1 py-2 text-sm text-gray-500 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors"
|
|
>
|
|
+ Hinzufuegen
|
|
</button>
|
|
<button
|
|
onClick={() => handleOpenLibrary(type)}
|
|
className="py-2 px-3 text-sm text-gray-400 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors"
|
|
title="Aus Bibliothek waehlen"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|