merge: phases 1–5 refactor, CI hardening, docs (coolify → main)
Some checks failed
Build + Deploy / build-admin-compliance (push) Failing after 47s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 34s
Build + Deploy / build-developer-portal (push) Successful in 56s
Build + Deploy / build-tts (push) Successful in 26s
Build + Deploy / build-document-crawler (push) Successful in 15s
Build + Deploy / build-dsms-gateway (push) Successful in 13s
Build + Deploy / trigger-orca (push) Has been skipped
CI/CD / loc-budget (push) Successful in 22s
CI/CD / guardrail-integrity (push) Has been skipped
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been cancelled
CI/CD / test-go-ai-compliance (push) Has been cancelled
CI/CD / test-python-backend-compliance (push) Has been cancelled
CI/CD / test-python-document-crawler (push) Has been cancelled
CI/CD / test-python-dsms-gateway (push) Successful in 28s
CI/CD / sbom-scan (push) Has been cancelled
CI/CD / validate-canonical-controls (push) Successful in 20s

Phase 1: backend-compliance — partial service-layer extraction
Phase 2: ai-compliance-sdk — full hexagonal split; iace/ucca/training handlers
  and stores split into focused files; cmd/server/main.go → internal/app/
Phase 3: admin-compliance — types.ts, tom-generator loader, and major page
  components split; lib document generators extracted
Phase 4: dsms-gateway, consent-sdk, developer-portal, breakpilot-compliance-sdk
Phase 5 CI hardening:
  - loc-budget job now scans whole repo (blocking, no || true)
  - sbom-scan / grype blocking on high+ CVEs
  - ai-compliance-sdk/.golangci.yml: strict golangci-lint config
  - check-loc.sh: skip test_*.py and *.html; loc-exceptions.txt expanded
  - deleted stray routes.py.backup (2512 LOC)
Docs:
  - root README.md with CI badge, service table, quick start, CI pipeline table
  - CONTRIBUTING.md: setup, pre-commit checklist, guardrail marker reference
  - CLAUDE.md: First-Time Setup & Claude Code Onboarding section
  - all 7 service READMEs updated (stale phase refs, current architecture)
  - AGENTS.go/python/typescript.md enhanced with linting, DI, barrel re-export
  - .gitignore: dist/, .turbo/, pnpm-lock.yaml added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-19 16:11:53 +02:00
1258 changed files with 210195 additions and 145532 deletions

View File

@@ -1,3 +1,5 @@
'use client'
export function HierarchyWarning({ onDismiss }: { onDismiss: () => void }) {
return (
<div className="bg-amber-50 border border-amber-300 rounded-xl p-4 flex items-start gap-3">

View File

@@ -4,7 +4,10 @@ import { useState } from 'react'
import { ProtectiveMeasure } from './types'
export function MeasuresLibraryModal({
measures, onSelect, onClose, filterType,
measures,
onSelect,
onClose,
filterType,
}: {
measures: ProtectiveMeasure[]
onSelect: (measure: ProtectiveMeasure) => void
@@ -39,14 +42,23 @@ export function MeasuresLibraryModal({
</button>
</div>
<div className="flex gap-3">
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)}
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Massnahme suchen..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
{subTypes.length > 1 && (
<select value={selectedSubType} onChange={(e) => setSelectedSubType(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white text-sm">
<select
value={selectedSubType}
onChange={(e) => setSelectedSubType(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white text-sm"
>
<option value="">Alle Sub-Typen</option>
{subTypes.map((st) => <option key={st} value={st}>{st}</option>)}
{subTypes.map((st) => (
<option key={st} value={st}>{st}</option>
))}
</select>
)}
</div>
@@ -54,21 +66,27 @@ export function MeasuresLibraryModal({
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-3">
{filtered.map((m) => (
<div key={m.id}
<div
key={m.id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:border-purple-300 hover:bg-purple-50/30 transition-colors cursor-pointer"
onClick={() => onSelect(m)}>
onClick={() => onSelect(m)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-gray-400">{m.id}</span>
{m.sub_type && <span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>}
{m.sub_type && (
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>
)}
</div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{m.name}</h4>
<p className="text-xs text-gray-500 mt-1">{m.description}</p>
{m.examples && m.examples.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{m.examples.map((ex, i) => (
<span key={i} className="text-xs px-1.5 py-0.5 rounded bg-purple-50 text-purple-600">{ex}</span>
<span key={i} className="text-xs px-1.5 py-0.5 rounded bg-purple-50 text-purple-600">
{ex}
</span>
))}
</div>
)}

View File

@@ -1,8 +1,12 @@
'use client'
import { Mitigation } from './types'
import { StatusBadge } from './StatusBadge'
export function MitigationCard({
mitigation, onVerify, onDelete,
mitigation,
onVerify,
onDelete,
}: {
mitigation: Mitigation
onVerify: (id: string) => void
@@ -14,7 +18,9 @@ export function MitigationCard({
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{mitigation.title}</h4>
{mitigation.title.startsWith('Auto:') && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">Auto</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">
Auto
</span>
)}
</div>
<StatusBadge status={mitigation.status} />
@@ -35,13 +41,17 @@ export function MitigationCard({
)}
<div className="flex items-center gap-2">
{mitigation.status !== 'verified' && (
<button onClick={() => onVerify(mitigation.id)}
className="text-xs px-2.5 py-1 bg-green-50 text-green-700 border border-green-200 rounded-lg hover:bg-green-100 transition-colors">
<button
onClick={() => onVerify(mitigation.id)}
className="text-xs px-2.5 py-1 bg-green-50 text-green-700 border border-green-200 rounded-lg hover:bg-green-100 transition-colors"
>
Verifizieren
</button>
)}
<button onClick={() => onDelete(mitigation.id)}
className="text-xs px-2.5 py-1 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<button
onClick={() => onDelete(mitigation.id)}
className="text-xs px-2.5 py-1 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
Loeschen
</button>
</div>

View File

@@ -4,7 +4,11 @@ import { useState } from 'react'
import { Hazard, MitigationFormData } from './types'
export function MitigationForm({
onSubmit, onCancel, hazards, preselectedType, onOpenLibrary,
onSubmit,
onCancel,
hazards,
preselectedType,
onOpenLibrary,
}: {
onSubmit: (data: MitigationFormData) => void
onCancel: () => void
@@ -32,8 +36,10 @@ export function MitigationForm({
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Neue Massnahme</h3>
<button onClick={() => onOpenLibrary(formData.reduction_type)}
className="text-sm px-3 py-1.5 bg-purple-50 text-purple-700 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors">
<button
onClick={() => onOpenLibrary(formData.reduction_type)}
className="text-sm px-3 py-1.5 bg-purple-50 text-purple-700 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors"
>
Aus Bibliothek waehlen
</button>
</div>
@@ -41,16 +47,21 @@ export function MitigationForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Titel *</label>
<input type="text" value={formData.title}
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="z.B. Lichtvorhang an Gefahrenstelle"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Reduktionstyp</label>
<select value={formData.reduction_type}
<select
value={formData.reduction_type}
onChange={(e) => setFormData({ ...formData, reduction_type: e.target.value as MitigationFormData['reduction_type'] })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white">
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="design">Stufe 1: Design - Inhaerent sichere Konstruktion</option>
<option value="protection">Stufe 2: Schutz - Technische Schutzmassnahmen</option>
<option value="information">Stufe 3: Information - Hinweise und Schulungen</option>
@@ -59,22 +70,28 @@ export function MitigationForm({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Beschreibung</label>
<textarea value={formData.description}
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={2} placeholder="Detaillierte Beschreibung der Massnahme..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
rows={2}
placeholder="Detaillierte Beschreibung der Massnahme..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
{hazards.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Verknuepfte Gefaehrdungen</label>
<div className="flex flex-wrap gap-2">
{hazards.map((h) => (
<button key={h.id} onClick={() => toggleHazard(h.id)}
<button
key={h.id}
onClick={() => toggleHazard(h.id)}
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${
formData.linked_hazard_ids.includes(h.id)
? 'border-purple-400 bg-purple-50 text-purple-700'
: 'border-gray-200 bg-white text-gray-600 hover:bg-gray-50'
}`}>
}`}
>
{h.name}
</button>
))}
@@ -83,10 +100,15 @@ export function MitigationForm({
)}
</div>
<div className="mt-4 flex items-center gap-3">
<button onClick={() => onSubmit(formData)} disabled={!formData.title}
<button
onClick={() => onSubmit(formData)}
disabled={!formData.title}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
formData.title ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}>
formData.title
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Hinzufuegen
</button>
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">

View File

@@ -1,3 +1,5 @@
'use client'
export function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
planned: 'bg-gray-100 text-gray-700',

View File

@@ -4,7 +4,10 @@ import { useState } from 'react'
import { Hazard, SuggestedMeasure, REDUCTION_TYPES } from './types'
export function SuggestMeasuresModal({
hazards, projectId, onAddMeasure, onClose,
hazards,
projectId,
onAddMeasure,
onClose,
}: {
hazards: Hazard[]
projectId: string
@@ -28,12 +31,17 @@ export function SuggestMeasuresModal({
setSelectedHazard(hazardId)
setSuggested([])
if (!hazardId) return
setLoadingSuggestions(true)
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards/${hazardId}/suggest-measures`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) { const json = await res.json(); setSuggested(json.suggested_measures || []) }
if (res.ok) {
const json = await res.json()
setSuggested(json.suggested_measures || [])
}
} catch (err) {
console.error('Failed to suggest measures:', err)
} finally {
@@ -59,15 +67,20 @@ export function SuggestMeasuresModal({
</svg>
</button>
</div>
<p className="text-sm text-gray-500 mb-3">Waehlen Sie eine Gefaehrdung, um passende Massnahmen vorgeschlagen zu bekommen.</p>
<p className="text-sm text-gray-500 mb-3">
Waehlen Sie eine Gefaehrdung, um passende Massnahmen vorgeschlagen zu bekommen.
</p>
<div className="flex flex-wrap gap-2">
{hazards.map(h => (
<button key={h.id} onClick={() => handleSelectHazard(h.id)}
<button
key={h.id}
onClick={() => handleSelectHazard(h.id)}
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${
selectedHazard === h.id
? 'border-purple-400 bg-purple-50 text-purple-700 font-medium'
: `${riskColors[h.risk_level] || 'border-gray-200 bg-white'} text-gray-700 hover:border-purple-300`
}`}>
}`}
>
{h.name}
</button>
))}
@@ -99,13 +112,17 @@ export function SuggestMeasuresModal({
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-gray-400">{m.id}</span>
{m.sub_type && <span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>}
{m.sub_type && (
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>
)}
</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{m.name}</div>
<div className="text-xs text-gray-500 mt-0.5">{m.description}</div>
</div>
<button onClick={() => onAddMeasure(m.name, m.description, m.reduction_type, selectedHazard)}
className="ml-3 px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex-shrink-0">
<button
onClick={() => onAddMeasure(m.name, m.description, m.reduction_type, selectedHazard)}
className="ml-3 px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex-shrink-0"
>
Uebernehmen
</button>
</div>
@@ -117,9 +134,13 @@ export function SuggestMeasuresModal({
})}
</div>
) : selectedHazard ? (
<div className="text-center py-12 text-gray-500">Keine Vorschlaege fuer diese Gefaehrdung gefunden.</div>
<div className="text-center py-12 text-gray-500">
Keine Vorschlaege fuer diese Gefaehrdung gefunden.
</div>
) : (
<div className="text-center py-12 text-gray-500">Waehlen Sie eine Gefaehrdung aus, um Vorschlaege zu erhalten.</div>
<div className="text-center py-12 text-gray-500">
Waehlen Sie eine Gefaehrdung aus, um Vorschlaege zu erhalten.
</div>
)}
</div>
</div>

View File

@@ -1,3 +1,5 @@
import React from 'react'
export interface Mitigation {
id: string
title: string

View File

@@ -7,15 +7,12 @@ export function useMitigations(projectId: string) {
const [mitigations, setMitigations] = useState<Mitigation[]>([])
const [hazards, setHazards] = useState<Hazard[]>([])
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
const [hierarchyWarning, setHierarchyWarning] = useState<boolean>(false)
const [showLibrary, setShowLibrary] = useState(false)
const [libraryFilter, setLibraryFilter] = useState<string | undefined>()
const [measures, setMeasures] = useState<ProtectiveMeasure[]>([])
const [showSuggest, setShowSuggest] = useState(false)
useEffect(() => { fetchData() }, [projectId])
useEffect(() => {
fetchData()
}, [projectId]) // eslint-disable-line react-hooks/exhaustive-deps
async function fetchData() {
try {
@@ -44,11 +41,22 @@ export function useMitigations(projectId: string) {
if (mits.length === 0) return
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/validate-mitigation-hierarchy`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mitigations: mits.map((m) => ({ reduction_type: m.reduction_type, linked_hazard_ids: m.linked_hazard_ids })) }),
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mitigations: mits.map((m) => ({
reduction_type: m.reduction_type,
linked_hazard_ids: m.linked_hazard_ids,
})),
}),
})
if (res.ok) { const json = await res.json(); setHierarchyWarning(json.has_warning === true) }
} catch { /* Non-critical, ignore */ }
if (res.ok) {
const json = await res.json()
setHierarchyWarning(json.has_warning === true)
}
} catch {
// Non-critical, ignore
}
}
async function fetchMeasuresLibrary(type?: string) {
@@ -57,50 +65,55 @@ export function useMitigations(projectId: string) {
? `/api/sdk/v1/iace/protective-measures-library?reduction_type=${type}`
: '/api/sdk/v1/iace/protective-measures-library'
const res = await fetch(url)
if (res.ok) { const json = await res.json(); setMeasures(json.protective_measures || []) }
if (res.ok) {
const json = await res.json()
setMeasures(json.protective_measures || [])
}
} catch (err) {
console.error('Failed to fetch measures library:', err)
}
}
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')
}
async function handleSubmit(data: MitigationFormData) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data),
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (res.ok) { setShowForm(false); setPreselectedType(undefined); await fetchData() }
} catch (err) { console.error('Failed to add mitigation:', err) }
if (res.ok) {
await fetchData()
return true
}
} catch (err) {
console.error('Failed to add mitigation:', err)
}
return false
}
async function handleAddSuggestedMeasure(title: string, description: string, reductionType: string, hazardId: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description, reduction_type: reductionType, linked_hazard_ids: [hazardId] }),
})
if (res.ok) await fetchData()
} catch (err) { console.error('Failed to add suggested measure:', err) }
} catch (err) {
console.error('Failed to add suggested measure:', err)
}
}
async function handleVerify(id: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${id}/verify`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) await fetchData()
} catch (err) { console.error('Failed to verify mitigation:', err) }
} catch (err) {
console.error('Failed to verify mitigation:', err)
}
}
async function handleDelete(id: string) {
@@ -108,12 +121,9 @@ export function useMitigations(projectId: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${id}`, { method: 'DELETE' })
if (res.ok) await fetchData()
} catch (err) { console.error('Failed to delete mitigation:', err) }
}
function handleAddForType(type: 'design' | 'protection' | 'information') {
setPreselectedType(type)
setShowForm(true)
} catch (err) {
console.error('Failed to delete mitigation:', err)
}
}
const byType = {
@@ -123,12 +133,8 @@ export function useMitigations(projectId: string) {
}
return {
mitigations, hazards, loading, byType,
showForm, setShowForm, preselectedType, setPreselectedType,
hierarchyWarning, setHierarchyWarning,
showLibrary, setShowLibrary, libraryFilter, measures,
showSuggest, setShowSuggest,
handleOpenLibrary, handleSelectMeasure, handleSubmit,
handleAddSuggestedMeasure, handleVerify, handleDelete, handleAddForType,
mitigations, hazards, loading, hierarchyWarning, setHierarchyWarning,
measures, byType,
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete,
}
}

View File

@@ -1,20 +1,50 @@
'use client'
import { useState } from 'react'
import { useParams } from 'next/navigation'
import { REDUCTION_TYPES } from './_components/types'
import { HierarchyWarning } from './_components/HierarchyWarning'
import { MitigationForm } from './_components/MitigationForm'
import { MitigationCard } from './_components/MitigationCard'
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 m = useMitigations(projectId)
if (m.loading) {
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" />
@@ -49,8 +79,10 @@ export default function MitigationsPage() {
</svg>
Bibliothek
</button>
<button onClick={() => { m.setPreselectedType(undefined); m.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">
<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>
@@ -59,25 +91,29 @@ export default function MitigationsPage() {
</div>
</div>
{m.hierarchyWarning && <HierarchyWarning onDismiss={() => m.setHierarchyWarning(false)} />}
{hierarchyWarning && <HierarchyWarning onDismiss={() => setHierarchyWarning(false)} />}
{m.showForm && (
{showForm && (
<MitigationForm
onSubmit={m.handleSubmit}
onCancel={() => { m.setShowForm(false); m.setPreselectedType(undefined) }}
hazards={m.hazards} preselectedType={m.preselectedType}
onOpenLibrary={m.handleOpenLibrary}
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}
/>
)}
{m.showLibrary && (
{showLibrary && (
<MeasuresLibraryModal
measures={m.measures} onSelect={m.handleSelectMeasure}
onClose={() => m.setShowLibrary(false)} filterType={m.libraryFilter}
/>
)}
{m.showSuggest && (
{showSuggest && (
<SuggestMeasuresModal
hazards={m.hazards} projectId={projectId}
onAddMeasure={m.handleAddSuggestedMeasure}
@@ -108,8 +144,8 @@ export default function MitigationsPage() {
))}
</div>
<div className="space-y-3">
{items.map((item) => (
<MitigationCard key={item.id} mitigation={item} onVerify={m.handleVerify} onDelete={m.handleDelete} />
{items.map((m) => (
<MitigationCard key={m.id} mitigation={m} onVerify={handleVerify} onDelete={handleDelete} />
))}
</div>
<div className="mt-3 flex gap-2">