controls/page.tsx 840→211 LOC — extracted StatsCards, FilterBar, ControlCard, AddControlForm, RAGPanel, LoadingSkeleton to _components/; useControlsData, useRAGSuggestions to _hooks/; shared types to _types.ts. dsr/[requestId]/page.tsx 854→172 LOC — extracted detail panels and timeline components to _components/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSDK, Control as SDKControl, ControlType, ImplementationStatus } from '@/lib/sdk'
|
|
|
|
export function useControlsData() {
|
|
const { state, dispatch } = useSDK()
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Track effectiveness locally as it's not in the SDK state type
|
|
const [effectivenessMap, setEffectivenessMap] = useState<Record<string, number>>({})
|
|
// Track linked evidence per control
|
|
const [evidenceMap, setEvidenceMap] = useState<Record<string, { id: string; title: string; status: string }[]>>({})
|
|
|
|
const fetchEvidenceForControls = async (_controlIds: string[]) => {
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/compliance/evidence')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const allEvidence = data.evidence || data
|
|
if (Array.isArray(allEvidence)) {
|
|
const map: Record<string, { id: string; title: string; status: string }[]> = {}
|
|
for (const ev of allEvidence) {
|
|
const ctrlId = ev.control_id || ''
|
|
if (!map[ctrlId]) map[ctrlId] = []
|
|
map[ctrlId].push({
|
|
id: ev.id,
|
|
title: ev.title || ev.name || 'Nachweis',
|
|
status: ev.status || 'pending',
|
|
})
|
|
}
|
|
setEvidenceMap(map)
|
|
}
|
|
}
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
// Fetch controls from backend on mount
|
|
useEffect(() => {
|
|
const fetchControls = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch('/api/sdk/v1/compliance/controls')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const backendControls = data.controls || data
|
|
if (Array.isArray(backendControls) && backendControls.length > 0) {
|
|
const mapped: SDKControl[] = backendControls.map((c: Record<string, unknown>) => ({
|
|
id: (c.control_id || c.id) as string,
|
|
name: (c.name || c.title || '') as string,
|
|
description: (c.description || '') as string,
|
|
type: ((c.type || c.control_type || 'TECHNICAL') as string).toUpperCase() as ControlType,
|
|
category: (c.category || '') as string,
|
|
implementationStatus: ((c.implementation_status || c.status || 'NOT_IMPLEMENTED') as string).toUpperCase() as ImplementationStatus,
|
|
effectiveness: (c.effectiveness || 'LOW') as 'LOW' | 'MEDIUM' | 'HIGH',
|
|
evidence: (c.evidence || []) as string[],
|
|
owner: (c.owner || null) as string | null,
|
|
dueDate: c.due_date ? new Date(c.due_date as string) : null,
|
|
}))
|
|
dispatch({ type: 'SET_STATE', payload: { controls: mapped } })
|
|
setError(null)
|
|
fetchEvidenceForControls(mapped.map(c => c.id))
|
|
return
|
|
}
|
|
}
|
|
} catch {
|
|
// API not available — show empty state
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchControls()
|
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleStatusChange = async (controlId: string, status: ImplementationStatus) => {
|
|
dispatch({
|
|
type: 'UPDATE_CONTROL',
|
|
payload: { id: controlId, data: { implementationStatus: status } },
|
|
})
|
|
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ implementation_status: status }),
|
|
})
|
|
} catch {
|
|
// Silently fail — SDK state is already updated
|
|
}
|
|
}
|
|
|
|
const handleEffectivenessChange = async (controlId: string, effectiveness: number) => {
|
|
setEffectivenessMap(prev => ({ ...prev, [controlId]: effectiveness }))
|
|
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ effectiveness_score: effectiveness }),
|
|
})
|
|
} catch {
|
|
// Silently fail — local state is already updated
|
|
}
|
|
}
|
|
|
|
const handleAddControl = (data: { name: string; description: string; type: ControlType; category: string; owner: string }) => {
|
|
const newControl: SDKControl = {
|
|
id: `ctrl-${Date.now()}`,
|
|
name: data.name,
|
|
description: data.description,
|
|
type: data.type,
|
|
category: data.category,
|
|
implementationStatus: 'NOT_IMPLEMENTED',
|
|
effectiveness: 'LOW',
|
|
evidence: [],
|
|
owner: data.owner || null,
|
|
dueDate: null,
|
|
}
|
|
dispatch({ type: 'ADD_CONTROL', payload: newControl })
|
|
}
|
|
|
|
return {
|
|
state,
|
|
dispatch,
|
|
loading,
|
|
error,
|
|
setError,
|
|
effectivenessMap,
|
|
evidenceMap,
|
|
handleStatusChange,
|
|
handleEffectivenessChange,
|
|
handleAddControl,
|
|
}
|
|
}
|