refactor(admin): split requirements page.tsx into colocated components
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>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
'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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user