Extract components and hooks into _components/ and _hooks/ subdirectories to reduce each page.tsx to under 500 LOC (was 1545/1383/1316). Final line counts: evidence=213, process-tasks=304, hazards=157. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useRef } from 'react'
|
|
import { useSDK, Evidence as SDKEvidence, EvidenceType } from '@/lib/sdk'
|
|
import {
|
|
DisplayEvidence,
|
|
EvidenceCheck,
|
|
CheckResult,
|
|
EvidenceMapping,
|
|
CoverageReport,
|
|
EvidenceTabKey,
|
|
mapEvidenceTypeToDisplay,
|
|
getEvidenceStatus,
|
|
evidenceTemplates,
|
|
CHECK_API,
|
|
} from '../_components/EvidenceTypes'
|
|
|
|
type AntiFakeMeta = Record<string, {
|
|
confidenceLevel: string | null
|
|
truthStatus: string | null
|
|
generationMode: string | null
|
|
approvalStatus: string | null
|
|
requiresFourEyes: boolean
|
|
}>
|
|
|
|
export function useEvidence() {
|
|
const { state, dispatch } = useSDK()
|
|
const [activeTab, setActiveTab] = useState<EvidenceTabKey>('evidence')
|
|
const [filter, setFilter] = useState<string>('all')
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [uploading, setUploading] = useState(false)
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
const [page, setPage] = useState(1)
|
|
const [pageSize] = useState(20)
|
|
const [total, setTotal] = useState(0)
|
|
const [antiFakeMeta, setAntiFakeMeta] = useState<AntiFakeMeta>({})
|
|
|
|
// Evidence Checks state
|
|
const [checks, setChecks] = useState<EvidenceCheck[]>([])
|
|
const [checksLoading, setChecksLoading] = useState(false)
|
|
const [runningCheckId, setRunningCheckId] = useState<string | null>(null)
|
|
const [checkResults, setCheckResults] = useState<Record<string, CheckResult[]>>({})
|
|
|
|
// Mappings state
|
|
const [mappings, setMappings] = useState<EvidenceMapping[]>([])
|
|
const [coverageReport, setCoverageReport] = useState<CoverageReport | null>(null)
|
|
const [seedingChecks, setSeedingChecks] = useState(false)
|
|
|
|
// Phase 3: Review/Reject/AuditTrail state
|
|
const [reviewEvidence, setReviewEvidence] = useState<DisplayEvidence | null>(null)
|
|
const [rejectEvidence, setRejectEvidence] = useState<DisplayEvidence | null>(null)
|
|
const [auditTrailId, setAuditTrailId] = useState<string | null>(null)
|
|
const [confidenceFilter, setConfidenceFilter] = useState<string | null>(null)
|
|
const [refreshKey, setRefreshKey] = useState(0)
|
|
|
|
useEffect(() => {
|
|
const fetchEvidence = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch(`/api/sdk/v1/compliance/evidence?page=${page}&limit=${pageSize}`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
if (data.total !== undefined) setTotal(data.total)
|
|
const backendEvidence = data.evidence || data
|
|
if (Array.isArray(backendEvidence) && backendEvidence.length > 0) {
|
|
const metaMap: AntiFakeMeta = {}
|
|
const mapped: SDKEvidence[] = backendEvidence.map((e: Record<string, unknown>) => {
|
|
const id = (e.id || '') as string
|
|
metaMap[id] = {
|
|
confidenceLevel: (e.confidence_level || null) as string | null,
|
|
truthStatus: (e.truth_status || null) as string | null,
|
|
generationMode: (e.generation_mode || null) as string | null,
|
|
approvalStatus: (e.approval_status || null) as string | null,
|
|
requiresFourEyes: !!e.requires_four_eyes,
|
|
}
|
|
return {
|
|
id,
|
|
controlId: (e.control_id || '') as string,
|
|
type: ((e.evidence_type || 'DOCUMENT') as string).toUpperCase() as EvidenceType,
|
|
name: (e.title || e.name || '') as string,
|
|
description: (e.description || '') as string,
|
|
fileUrl: (e.artifact_url || null) as string | null,
|
|
validFrom: e.valid_from ? new Date(e.valid_from as string) : new Date(),
|
|
validUntil: e.valid_until ? new Date(e.valid_until as string) : null,
|
|
uploadedBy: (e.uploaded_by || 'System') as string,
|
|
uploadedAt: e.created_at ? new Date(e.created_at as string) : new Date(),
|
|
}
|
|
})
|
|
setAntiFakeMeta(metaMap)
|
|
dispatch({ type: 'SET_STATE', payload: { evidence: mapped } })
|
|
setError(null)
|
|
return
|
|
}
|
|
}
|
|
loadFromTemplates()
|
|
} catch {
|
|
loadFromTemplates()
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadFromTemplates = () => {
|
|
if (state.evidence.length > 0) return
|
|
if (state.controls.length === 0) return
|
|
const relevantEvidence = evidenceTemplates.filter(e =>
|
|
state.controls.some(c => c.id === e.controlId || e.linkedControls.includes(c.id))
|
|
)
|
|
const now = new Date()
|
|
relevantEvidence.forEach(template => {
|
|
const validFrom = new Date(now)
|
|
validFrom.setMonth(validFrom.getMonth() - 1)
|
|
const validUntil = template.validityDays > 0
|
|
? new Date(validFrom.getTime() + template.validityDays * 24 * 60 * 60 * 1000)
|
|
: null
|
|
const sdkEvidence: SDKEvidence = {
|
|
id: template.id,
|
|
controlId: template.controlId,
|
|
type: template.type,
|
|
name: template.name,
|
|
description: template.description,
|
|
fileUrl: null,
|
|
validFrom,
|
|
validUntil,
|
|
uploadedBy: template.uploadedBy,
|
|
uploadedAt: validFrom,
|
|
}
|
|
dispatch({ type: 'ADD_EVIDENCE', payload: sdkEvidence })
|
|
})
|
|
}
|
|
|
|
fetchEvidence()
|
|
}, [page, pageSize, refreshKey]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const displayEvidence: DisplayEvidence[] = state.evidence.map(ev => {
|
|
const template = evidenceTemplates.find(t => t.id === ev.id)
|
|
const meta = antiFakeMeta[ev.id]
|
|
return {
|
|
id: ev.id,
|
|
name: ev.name,
|
|
description: ev.description,
|
|
displayType: mapEvidenceTypeToDisplay(ev.type),
|
|
format: template?.format || 'pdf',
|
|
controlId: ev.controlId,
|
|
linkedRequirements: template?.linkedRequirements || [],
|
|
linkedControls: template?.linkedControls || [ev.controlId],
|
|
uploadedBy: ev.uploadedBy,
|
|
uploadedAt: ev.uploadedAt,
|
|
validFrom: ev.validFrom,
|
|
validUntil: ev.validUntil,
|
|
status: getEvidenceStatus(ev.validUntil),
|
|
fileSize: template?.fileSize || 'Unbekannt',
|
|
fileUrl: ev.fileUrl,
|
|
confidenceLevel: meta?.confidenceLevel || null,
|
|
truthStatus: meta?.truthStatus || null,
|
|
generationMode: meta?.generationMode || null,
|
|
approvalStatus: meta?.approvalStatus || null,
|
|
requiresFourEyes: meta?.requiresFourEyes || false,
|
|
}
|
|
})
|
|
|
|
const filteredEvidence = (filter === 'all'
|
|
? displayEvidence
|
|
: displayEvidence.filter(e => e.status === filter || e.displayType === filter)
|
|
).filter(e => !confidenceFilter || e.confidenceLevel === confidenceFilter)
|
|
|
|
const validCount = displayEvidence.filter(e => e.status === 'valid').length
|
|
const expiredCount = displayEvidence.filter(e => e.status === 'expired').length
|
|
const pendingCount = displayEvidence.filter(e => e.status === 'pending-review').length
|
|
|
|
const handleDelete = async (evidenceId: string) => {
|
|
if (!confirm('Moechten Sie diesen Nachweis wirklich loeschen?')) return
|
|
dispatch({ type: 'DELETE_EVIDENCE', payload: evidenceId })
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/evidence/${evidenceId}`, { method: 'DELETE' })
|
|
} catch { /* Silently fail */ }
|
|
}
|
|
|
|
const handleUpload = async (file: File) => {
|
|
setUploading(true)
|
|
setError(null)
|
|
try {
|
|
const controlId = state.controls.length > 0 ? state.controls[0].id : 'GENERIC'
|
|
const params = new URLSearchParams({ control_id: controlId, evidence_type: 'document', title: file.name })
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
const res = await fetch(`/api/sdk/v1/compliance/evidence/upload?${params}`, { method: 'POST', body: formData })
|
|
if (!res.ok) {
|
|
const errData = await res.json().catch(() => ({ error: 'Upload fehlgeschlagen' }))
|
|
throw new Error(errData.error || errData.detail || 'Upload fehlgeschlagen')
|
|
}
|
|
const data = await res.json()
|
|
const newEvidence: SDKEvidence = {
|
|
id: data.id || `ev-${Date.now()}`,
|
|
controlId,
|
|
type: 'DOCUMENT',
|
|
name: file.name,
|
|
description: `Hochgeladen am ${new Date().toLocaleDateString('de-DE')}`,
|
|
fileUrl: data.artifact_url || null,
|
|
validFrom: new Date(),
|
|
validUntil: null,
|
|
uploadedBy: 'Aktueller Benutzer',
|
|
uploadedAt: new Date(),
|
|
}
|
|
dispatch({ type: 'ADD_EVIDENCE', payload: newEvidence })
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen')
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
const handleView = (ev: DisplayEvidence) => {
|
|
if (ev.fileUrl) window.open(ev.fileUrl, '_blank')
|
|
else alert('Keine Datei vorhanden')
|
|
}
|
|
|
|
const handleDownload = (ev: DisplayEvidence) => {
|
|
if (!ev.fileUrl) return
|
|
const a = document.createElement('a')
|
|
a.href = ev.fileUrl
|
|
a.download = ev.name
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
}
|
|
|
|
const handleUploadClick = () => fileInputRef.current?.click()
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) { handleUpload(file); e.target.value = '' }
|
|
}
|
|
|
|
const loadChecks = async () => {
|
|
setChecksLoading(true)
|
|
try {
|
|
const res = await fetch(`${CHECK_API}?limit=50`)
|
|
if (res.ok) { const data = await res.json(); setChecks(data.checks || []) }
|
|
} catch { /* silent */ }
|
|
finally { setChecksLoading(false) }
|
|
}
|
|
|
|
const runCheck = async (checkId: string) => {
|
|
setRunningCheckId(checkId)
|
|
try {
|
|
const res = await fetch(`${CHECK_API}/${checkId}/run`, { method: 'POST' })
|
|
if (res.ok) {
|
|
const result = await res.json()
|
|
setCheckResults(prev => ({ ...prev, [checkId]: [result, ...(prev[checkId] || [])].slice(0, 5) }))
|
|
loadChecks()
|
|
}
|
|
} catch { /* silent */ }
|
|
finally { setRunningCheckId(null) }
|
|
}
|
|
|
|
const loadCheckResults = async (checkId: string) => {
|
|
try {
|
|
const res = await fetch(`${CHECK_API}/${checkId}/results?limit=5`)
|
|
if (res.ok) { const data = await res.json(); setCheckResults(prev => ({ ...prev, [checkId]: data.results || [] })) }
|
|
} catch { /* silent */ }
|
|
}
|
|
|
|
const seedChecks = async () => {
|
|
setSeedingChecks(true)
|
|
try { await fetch(`${CHECK_API}/seed`, { method: 'POST' }); loadChecks() }
|
|
catch { /* silent */ }
|
|
finally { setSeedingChecks(false) }
|
|
}
|
|
|
|
const loadMappings = async () => {
|
|
try {
|
|
const res = await fetch(`${CHECK_API}/mappings`)
|
|
if (res.ok) { const data = await res.json(); setMappings(data.mappings || []) }
|
|
} catch { /* silent */ }
|
|
}
|
|
|
|
const loadCoverageReport = async () => {
|
|
try {
|
|
const res = await fetch(`${CHECK_API}/mappings/report`)
|
|
if (res.ok) setCoverageReport(await res.json())
|
|
} catch { /* silent */ }
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'checks' && checks.length === 0) loadChecks()
|
|
if (activeTab === 'mapping') { loadMappings(); loadCoverageReport() }
|
|
if (activeTab === 'report') loadCoverageReport()
|
|
}, [activeTab]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
return {
|
|
state,
|
|
activeTab,
|
|
setActiveTab,
|
|
filter,
|
|
setFilter,
|
|
loading,
|
|
error,
|
|
setError,
|
|
uploading,
|
|
fileInputRef,
|
|
page,
|
|
setPage,
|
|
pageSize,
|
|
total,
|
|
displayEvidence,
|
|
filteredEvidence,
|
|
validCount,
|
|
expiredCount,
|
|
pendingCount,
|
|
confidenceFilter,
|
|
setConfidenceFilter,
|
|
reviewEvidence,
|
|
setReviewEvidence,
|
|
rejectEvidence,
|
|
setRejectEvidence,
|
|
auditTrailId,
|
|
setAuditTrailId,
|
|
setRefreshKey,
|
|
checks,
|
|
checksLoading,
|
|
checkResults,
|
|
runningCheckId,
|
|
seedingChecks,
|
|
mappings,
|
|
coverageReport,
|
|
handleDelete,
|
|
handleView,
|
|
handleDownload,
|
|
handleUploadClick,
|
|
handleFileChange,
|
|
runCheck,
|
|
loadCheckResults,
|
|
seedChecks,
|
|
}
|
|
}
|