refactor(admin): split evidence, import, portfolio pages
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>
This commit is contained in:
184
admin-compliance/app/sdk/risks/_hooks/useRisks.ts
Normal file
184
admin-compliance/app/sdk/risks/_hooks/useRisks.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
'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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user