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:
228
admin-compliance/app/sdk/evidence/_hooks/useEvidence.ts
Normal file
228
admin-compliance/app/sdk/evidence/_hooks/useEvidence.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useSDK, Evidence as SDKEvidence, EvidenceType } from '@/lib/sdk'
|
||||
import {
|
||||
DisplayEvidence,
|
||||
mapEvidenceTypeToDisplay,
|
||||
getEvidenceStatus,
|
||||
evidenceTemplates,
|
||||
} from '../_components/EvidenceTypes'
|
||||
|
||||
export function useEvidence() {
|
||||
const { state, dispatch } = useSDK()
|
||||
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)
|
||||
|
||||
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 mapped: SDKEvidence[] = backendEvidence.map((e: Record<string, unknown>) => ({
|
||||
id: (e.id || '') as string,
|
||||
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(),
|
||||
}))
|
||||
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]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const displayEvidence: DisplayEvidence[] = state.evidence.map(ev => {
|
||||
const template = evidenceTemplates.find(t => t.id === 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,
|
||||
}
|
||||
})
|
||||
|
||||
const filteredEvidence = filter === 'all'
|
||||
? displayEvidence
|
||||
: displayEvidence.filter(e => e.status === filter || e.displayType === filter)
|
||||
|
||||
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 — SDK state is already updated
|
||||
}
|
||||
}
|
||||
|
||||
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: 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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
filter,
|
||||
setFilter,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
uploading,
|
||||
fileInputRef,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
total,
|
||||
displayEvidence,
|
||||
filteredEvidence,
|
||||
validCount,
|
||||
expiredCount,
|
||||
pendingCount,
|
||||
handleDelete,
|
||||
handleView,
|
||||
handleDownload,
|
||||
handleUploadClick,
|
||||
handleFileChange,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user