Break 838-line page.tsx into _types.ts, _data.ts (templates),
_components/{AddRequirementForm,RequirementCard,LoadingSkeleton}.tsx,
and _hooks/useRequirementsData.ts. page.tsx is now 246 LOC (wiring
only). No behavior changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
247 lines
8.7 KiB
TypeScript
247 lines
8.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSDK, Requirement as SDKRequirement, RequirementStatus, RiskSeverity } from '@/lib/sdk'
|
|
import { AddRequirementData } from '../_types'
|
|
import { requirementTemplates } from '../_data'
|
|
|
|
export function useRequirementsData() {
|
|
const { state, dispatch } = useSDK()
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [ragExtracting, setRagExtracting] = useState(false)
|
|
const [ragResult, setRagResult] = useState<{ created: number; skipped_duplicates: number; message: string } | null>(null)
|
|
|
|
const extractFromRAG = async () => {
|
|
setRagExtracting(true)
|
|
setRagResult(null)
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/compliance/extract-requirements-from-rag', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ max_per_query: 20 }),
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setRagResult({ created: data.created, skipped_duplicates: data.skipped_duplicates, message: data.message })
|
|
// Reload requirements list
|
|
const listRes = await fetch('/api/sdk/v1/compliance/requirements')
|
|
if (listRes.ok) {
|
|
const listData = await listRes.json()
|
|
const reqs = listData.requirements || listData
|
|
if (Array.isArray(reqs) && reqs.length > 0) {
|
|
const mapped = reqs.map((r: Record<string, unknown>) => ({
|
|
id: (r.requirement_id || r.id) as string,
|
|
regulation: (r.regulation_code || r.regulation || '') as string,
|
|
article: (r.article || '') as string,
|
|
title: (r.title || '') as string,
|
|
description: (r.description || '') as string,
|
|
criticality: ((r.criticality || r.priority || 'MEDIUM') as string).toUpperCase() as RiskSeverity,
|
|
applicableModules: [] as string[],
|
|
status: 'NOT_STARTED' as RequirementStatus,
|
|
controls: [] as string[],
|
|
}))
|
|
dispatch({ type: 'SET_STATE', payload: { requirements: mapped } })
|
|
}
|
|
}
|
|
} else {
|
|
setRagResult({ created: 0, skipped_duplicates: 0, message: 'RAG-Extraktion fehlgeschlagen' })
|
|
}
|
|
} catch {
|
|
setRagResult({ created: 0, skipped_duplicates: 0, message: 'RAG-Extraktion nicht erreichbar' })
|
|
} finally {
|
|
setRagExtracting(false)
|
|
}
|
|
}
|
|
|
|
// Fetch requirements from backend on mount
|
|
useEffect(() => {
|
|
const fetchRequirements = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch('/api/sdk/v1/compliance/requirements')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const backendRequirements = data.requirements || data
|
|
if (Array.isArray(backendRequirements) && backendRequirements.length > 0) {
|
|
// Map backend data to SDK format and load into state
|
|
const mapped: SDKRequirement[] = backendRequirements.map((r: Record<string, unknown>) => ({
|
|
id: (r.requirement_id || r.id) as string,
|
|
regulation: (r.regulation_code || r.regulation || '') as string,
|
|
article: (r.article || '') as string,
|
|
title: (r.title || '') as string,
|
|
description: (r.description || '') as string,
|
|
criticality: ((r.criticality || r.priority || 'MEDIUM') as string).toUpperCase() as RiskSeverity,
|
|
applicableModules: (r.applicable_modules || []) as string[],
|
|
status: (r.status || 'NOT_STARTED') as RequirementStatus,
|
|
controls: (r.controls || []) as string[],
|
|
}))
|
|
dispatch({ type: 'SET_STATE', payload: { requirements: mapped } })
|
|
setError(null)
|
|
return
|
|
}
|
|
}
|
|
// If backend returns empty or fails, fall back to templates
|
|
loadFromTemplates()
|
|
} catch {
|
|
// Backend unavailable — use templates
|
|
loadFromTemplates()
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadFromTemplates = () => {
|
|
if (state.requirements.length > 0) return // Already have data
|
|
if (state.modules.length === 0) return // No modules yet
|
|
|
|
const activeModuleIds = state.modules.map(m => m.id)
|
|
const relevantRequirements = requirementTemplates.filter(r =>
|
|
r.applicableModules.some(m => activeModuleIds.includes(m))
|
|
)
|
|
|
|
relevantRequirements.forEach(req => {
|
|
const sdkRequirement: SDKRequirement = {
|
|
id: req.id,
|
|
regulation: req.regulation,
|
|
article: req.article,
|
|
title: req.title,
|
|
description: req.description,
|
|
criticality: req.criticality,
|
|
applicableModules: req.applicableModules,
|
|
status: 'NOT_STARTED',
|
|
controls: [],
|
|
}
|
|
dispatch({ type: 'ADD_REQUIREMENT', payload: sdkRequirement })
|
|
})
|
|
}
|
|
|
|
fetchRequirements()
|
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleStatusChange = async (requirementId: string, status: RequirementStatus) => {
|
|
const previousStatus = state.requirements.find(r => r.id === requirementId)?.status
|
|
dispatch({
|
|
type: 'UPDATE_REQUIREMENT',
|
|
payload: { id: requirementId, data: { status } },
|
|
})
|
|
|
|
// Persist to backend
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ implementation_status: status.toLowerCase() }),
|
|
})
|
|
if (!res.ok) {
|
|
// Rollback on failure
|
|
if (previousStatus) {
|
|
dispatch({ type: 'UPDATE_REQUIREMENT', payload: { id: requirementId, data: { status: previousStatus } } })
|
|
}
|
|
setError('Status-Aenderung konnte nicht gespeichert werden')
|
|
}
|
|
} catch {
|
|
if (previousStatus) {
|
|
dispatch({ type: 'UPDATE_REQUIREMENT', payload: { id: requirementId, data: { status: previousStatus } } })
|
|
}
|
|
setError('Backend nicht erreichbar — Aenderung zurueckgesetzt')
|
|
}
|
|
}
|
|
|
|
const handleDeleteRequirement = async (requirementId: string) => {
|
|
if (!confirm('Anforderung wirklich loeschen?')) return
|
|
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
|
|
method: 'DELETE',
|
|
})
|
|
if (res.ok) {
|
|
dispatch({ type: 'SET_STATE', payload: { requirements: state.requirements.filter(r => r.id !== requirementId) } })
|
|
} else {
|
|
setError('Loeschen fehlgeschlagen')
|
|
}
|
|
} catch {
|
|
setError('Backend nicht erreichbar')
|
|
}
|
|
}
|
|
|
|
const handleAddRequirement = async (data: AddRequirementData): Promise<boolean> => {
|
|
// Try to resolve regulation_id from backend
|
|
let regulationId = ''
|
|
try {
|
|
const regRes = await fetch(`/api/sdk/v1/compliance/regulations/${data.regulation}`)
|
|
if (regRes.ok) {
|
|
const regData = await regRes.json()
|
|
regulationId = regData.id
|
|
}
|
|
} catch {
|
|
// Regulation not found — still add locally
|
|
}
|
|
|
|
const priorityMap: Record<string, number> = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 }
|
|
|
|
if (regulationId) {
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/compliance/requirements', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
regulation_id: regulationId,
|
|
article: data.article,
|
|
title: data.title,
|
|
description: data.description,
|
|
priority: priorityMap[data.criticality] || 2,
|
|
}),
|
|
})
|
|
if (res.ok) {
|
|
const created = await res.json()
|
|
const newReq: SDKRequirement = {
|
|
id: created.id,
|
|
regulation: data.regulation,
|
|
article: data.article,
|
|
title: data.title,
|
|
description: data.description,
|
|
criticality: data.criticality,
|
|
applicableModules: [],
|
|
status: 'NOT_STARTED',
|
|
controls: [],
|
|
}
|
|
dispatch({ type: 'ADD_REQUIREMENT', payload: newReq })
|
|
return true
|
|
}
|
|
} catch {
|
|
// Fall through to local-only add
|
|
}
|
|
}
|
|
|
|
// Fallback: add locally only
|
|
const newReq: SDKRequirement = {
|
|
id: `req-${Date.now()}`,
|
|
regulation: data.regulation,
|
|
article: data.article,
|
|
title: data.title,
|
|
description: data.description,
|
|
criticality: data.criticality,
|
|
applicableModules: [],
|
|
status: 'NOT_STARTED',
|
|
controls: [],
|
|
}
|
|
dispatch({ type: 'ADD_REQUIREMENT', payload: newReq })
|
|
return true
|
|
}
|
|
|
|
return {
|
|
state,
|
|
loading,
|
|
error,
|
|
setError,
|
|
ragExtracting,
|
|
ragResult,
|
|
setRagResult,
|
|
extractFromRAG,
|
|
handleStatusChange,
|
|
handleDeleteRequirement,
|
|
handleAddRequirement,
|
|
}
|
|
}
|