Extract components and hooks from oversized pages into colocated _components/ and _hooks/ subdirectories to enforce the 500-LOC hard cap. page.tsx files reduced to 205, 121, and 136 LOC respectively. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSDK, Risk, RiskLikelihood, RiskImpact, RiskSeverity, RiskStatus, RiskMitigation, calculateRiskScore, getRiskSeverityFromScore } from '@/lib/sdk'
|
|
import { RiskFormData } from '../_components/RiskForm'
|
|
|
|
export function useRisks() {
|
|
const { state, dispatch, addRisk } = useSDK()
|
|
const [showForm, setShowForm] = useState(false)
|
|
const [editingRisk, setEditingRisk] = useState<Risk | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [matrixFilter, setMatrixFilter] = useState<{ likelihood: number; impact: number } | null>(null)
|
|
|
|
useEffect(() => {
|
|
const fetchRisks = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch('/api/sdk/v1/compliance/risks')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const backendRisks = data.risks || data
|
|
if (Array.isArray(backendRisks) && backendRisks.length > 0) {
|
|
const mapped: Risk[] = backendRisks.map((r: Record<string, unknown>) => ({
|
|
id: (r.risk_id || r.id || '') as string,
|
|
title: (r.title || '') as string,
|
|
description: (r.description || '') as string,
|
|
category: (r.category || 'technical') as string,
|
|
likelihood: (r.likelihood || 3) as RiskLikelihood,
|
|
impact: (r.impact || 3) as RiskImpact,
|
|
severity: ((r.inherent_risk || r.severity || 'MEDIUM') as string).toUpperCase() as RiskSeverity,
|
|
inherentRiskScore: (r.likelihood as number || 3) * (r.impact as number || 3),
|
|
residualRiskScore: (r.residual_likelihood as number || r.likelihood as number || 3) * (r.residual_impact as number || r.impact as number || 3),
|
|
status: (r.status || 'IDENTIFIED') as RiskStatus,
|
|
mitigation: (Array.isArray(r.mitigating_controls) ? (r.mitigating_controls as RiskMitigation[]) : []) as RiskMitigation[],
|
|
owner: (r.owner || null) as string | null,
|
|
relatedControls: [] as string[],
|
|
relatedRequirements: [] as string[],
|
|
}))
|
|
dispatch({ type: 'SET_STATE', payload: { risks: mapped } })
|
|
setError(null)
|
|
}
|
|
}
|
|
} catch {
|
|
// Backend unavailable — use SDK state as-is
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchRisks()
|
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleSubmit = async (data: RiskFormData) => {
|
|
const score = calculateRiskScore(data.likelihood, data.impact)
|
|
const severity = getRiskSeverityFromScore(score)
|
|
|
|
if (editingRisk) {
|
|
dispatch({
|
|
type: 'UPDATE_RISK',
|
|
payload: {
|
|
id: editingRisk.id,
|
|
data: { ...data, severity, inherentRiskScore: score, residualRiskScore: score },
|
|
},
|
|
})
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/risks/${editingRisk.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: data.title,
|
|
description: data.description,
|
|
category: data.category,
|
|
likelihood: data.likelihood,
|
|
impact: data.impact,
|
|
}),
|
|
})
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
} else {
|
|
const riskId = `risk-${Date.now()}`
|
|
const newRisk: Risk = {
|
|
id: riskId,
|
|
...data,
|
|
severity,
|
|
inherentRiskScore: score,
|
|
residualRiskScore: score,
|
|
status: 'IDENTIFIED',
|
|
mitigation: [],
|
|
owner: null,
|
|
relatedControls: [],
|
|
relatedRequirements: [],
|
|
}
|
|
addRisk(newRisk)
|
|
try {
|
|
await fetch('/api/sdk/v1/compliance/risks', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
risk_id: riskId,
|
|
title: data.title,
|
|
description: data.description,
|
|
category: data.category,
|
|
likelihood: data.likelihood,
|
|
impact: data.impact,
|
|
}),
|
|
})
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
setShowForm(false)
|
|
setEditingRisk(null)
|
|
}
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('Moechten Sie dieses Risiko wirklich loeschen?')) return
|
|
dispatch({ type: 'DELETE_RISK', payload: id })
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/risks/${id}`, { method: 'DELETE' })
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
const handleStatusChange = async (riskId: string, status: RiskStatus) => {
|
|
dispatch({ type: 'UPDATE_RISK', payload: { id: riskId, data: { status } } })
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/risks/${riskId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status }),
|
|
})
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
const handleEdit = (risk: Risk) => {
|
|
setEditingRisk(risk)
|
|
setShowForm(true)
|
|
}
|
|
|
|
const handleMatrixCellClick = (l: number, i: number) => {
|
|
if (matrixFilter && matrixFilter.likelihood === l && matrixFilter.impact === i) {
|
|
setMatrixFilter(null)
|
|
} else {
|
|
setMatrixFilter({ likelihood: l, impact: i })
|
|
}
|
|
}
|
|
|
|
const stats = {
|
|
totalRisks: state.risks.length,
|
|
criticalRisks: state.risks.filter(r => r.severity === 'CRITICAL').length,
|
|
highRisks: state.risks.filter(r => r.severity === 'HIGH').length,
|
|
mitigatedRisks: state.risks.filter(r => r.mitigation.length > 0).length,
|
|
}
|
|
|
|
const filteredRisks = state.risks
|
|
.filter(risk => !matrixFilter || (risk.likelihood === matrixFilter.likelihood && risk.impact === matrixFilter.impact))
|
|
.sort((a, b) => b.inherentRiskScore - a.inherentRiskScore)
|
|
|
|
return {
|
|
state,
|
|
showForm,
|
|
setShowForm,
|
|
editingRisk,
|
|
setEditingRisk,
|
|
loading,
|
|
error,
|
|
setError,
|
|
matrixFilter,
|
|
setMatrixFilter,
|
|
handleSubmit,
|
|
handleDelete,
|
|
handleStatusChange,
|
|
handleEdit,
|
|
handleMatrixCellClick,
|
|
stats,
|
|
filteredRisks,
|
|
}
|
|
}
|