'use client' import React, { useState, useEffect, useRef } from 'react' import { useSDK, Evidence as SDKEvidence, EvidenceType } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' // ============================================================================= // TYPES // ============================================================================= type DisplayEvidenceType = 'document' | 'screenshot' | 'log' | 'audit-report' | 'certificate' type DisplayFormat = 'pdf' | 'image' | 'text' | 'json' type DisplayStatus = 'valid' | 'expired' | 'pending-review' interface DisplayEvidence { id: string name: string description: string displayType: DisplayEvidenceType format: DisplayFormat controlId: string linkedRequirements: string[] linkedControls: string[] uploadedBy: string uploadedAt: Date validFrom: Date validUntil: Date | null status: DisplayStatus fileSize: string fileUrl: string | null } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function mapEvidenceTypeToDisplay(type: EvidenceType): DisplayEvidenceType { switch (type) { case 'DOCUMENT': return 'document' case 'SCREENSHOT': return 'screenshot' case 'LOG': return 'log' case 'CERTIFICATE': return 'certificate' case 'AUDIT_REPORT': return 'audit-report' default: return 'document' } } function getEvidenceStatus(validUntil: Date | null): DisplayStatus { if (!validUntil) return 'pending-review' const now = new Date() if (validUntil < now) return 'expired' return 'valid' } // ============================================================================= // FALLBACK TEMPLATES // ============================================================================= interface EvidenceTemplate { id: string name: string description: string type: EvidenceType displayType: DisplayEvidenceType format: DisplayFormat controlId: string linkedRequirements: string[] linkedControls: string[] uploadedBy: string validityDays: number fileSize: string } const evidenceTemplates: EvidenceTemplate[] = [ { id: 'ev-dse-001', name: 'Datenschutzerklaerung v2.3', description: 'Aktuelle Datenschutzerklaerung fuer Website und App', type: 'DOCUMENT', displayType: 'document', format: 'pdf', controlId: 'ctrl-org-001', linkedRequirements: ['req-gdpr-13', 'req-gdpr-14'], linkedControls: ['ctrl-org-001'], uploadedBy: 'DSB', validityDays: 365, fileSize: '245 KB', }, { id: 'ev-pentest-001', name: 'Penetrationstest Report Q4/2024', description: 'Externer Penetrationstest durch Security-Partner', type: 'AUDIT_REPORT', displayType: 'audit-report', format: 'pdf', controlId: 'ctrl-tom-001', linkedRequirements: ['req-gdpr-32', 'req-iso-a12'], linkedControls: ['ctrl-tom-001', 'ctrl-tom-002', 'ctrl-det-001'], uploadedBy: 'IT Security Team', validityDays: 365, fileSize: '2.1 MB', }, { id: 'ev-iso-cert', name: 'ISO 27001 Zertifikat', description: 'Zertifizierung des ISMS', type: 'CERTIFICATE', displayType: 'certificate', format: 'pdf', controlId: 'ctrl-tom-001', linkedRequirements: ['req-iso-4.1', 'req-iso-5.1'], linkedControls: [], uploadedBy: 'QM Abteilung', validityDays: 365, fileSize: '156 KB', }, { id: 'ev-schulung-001', name: 'Schulungsnachweis Datenschutz 2024', description: 'Teilnehmerliste und Schulungsinhalt', type: 'DOCUMENT', displayType: 'document', format: 'pdf', controlId: 'ctrl-org-001', linkedRequirements: ['req-gdpr-39'], linkedControls: ['ctrl-org-001'], uploadedBy: 'HR Team', validityDays: 365, fileSize: '890 KB', }, { id: 'ev-rbac-001', name: 'Access Control Screenshot', description: 'Nachweis der RBAC-Konfiguration', type: 'SCREENSHOT', displayType: 'screenshot', format: 'image', controlId: 'ctrl-tom-001', linkedRequirements: ['req-gdpr-32'], linkedControls: ['ctrl-tom-001'], uploadedBy: 'Admin', validityDays: 0, fileSize: '1.2 MB', }, { id: 'ev-log-001', name: 'Audit Log Export', description: 'Monatlicher Audit-Log Export', type: 'LOG', displayType: 'log', format: 'json', controlId: 'ctrl-det-001', linkedRequirements: ['req-gdpr-32'], linkedControls: ['ctrl-det-001'], uploadedBy: 'System', validityDays: 90, fileSize: '4.5 MB', }, ] // ============================================================================= // COMPONENTS // ============================================================================= function EvidenceCard({ evidence, onDelete, onView, onDownload }: { evidence: DisplayEvidence; onDelete: () => void; onView: () => void; onDownload: () => void }) { const typeIcons = { document: ( ), screenshot: ( ), log: ( ), 'audit-report': ( ), certificate: ( ), } const statusColors = { valid: 'bg-green-100 text-green-700 border-green-200', expired: 'bg-red-100 text-red-700 border-red-200', 'pending-review': 'bg-yellow-100 text-yellow-700 border-yellow-200', } const statusLabels = { valid: 'Gueltig', expired: 'Abgelaufen', 'pending-review': 'Pruefung ausstehend', } return (
{typeIcons[evidence.displayType]}

{evidence.name}

{statusLabels[evidence.status]}

{evidence.description}

Hochgeladen: {evidence.uploadedAt.toLocaleDateString('de-DE')} {evidence.validUntil && ( Gueltig bis: {evidence.validUntil.toLocaleDateString('de-DE')} )} {evidence.fileSize}
{evidence.linkedRequirements.map(req => ( {req} ))} {evidence.linkedControls.map(ctrl => ( {ctrl} ))}
Hochgeladen von: {evidence.uploadedBy}
) } function LoadingSkeleton() { return (
{[1, 2, 3].map(i => (
))}
) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function EvidencePage() { const { state, dispatch } = useSDK() const [filter, setFilter] = useState('all') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [uploading, setUploading] = useState(false) const fileInputRef = useRef(null) const [page, setPage] = useState(1) const [pageSize] = useState(20) const [total, setTotal] = useState(0) // Fetch evidence from backend on mount and when page changes 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) => ({ 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 // Convert SDK evidence to display evidence 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 { // Use the first control as default, or a generic one 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() // Add to SDK state 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) => { const file = e.target.files?.[0] if (file) { handleUpload(file) e.target.value = '' // Reset input } } const stepInfo = STEP_EXPLANATIONS['evidence'] return (
{/* Hidden file input */} {/* Step Header */} {/* Error Banner */} {error && (
{error}
)} {/* Controls Alert */} {state.controls.length === 0 && !loading && (

Keine Kontrollen definiert

Bitte definieren Sie zuerst Kontrollen, um die zugehoerigen Nachweise zu laden.

)} {/* Stats */}
Gesamt
{displayEvidence.length}
Gueltig
{validCount}
Abgelaufen
{expiredCount}
Pruefung ausstehend
{pendingCount}
{/* Filter */}
Filter: {['all', 'valid', 'expired', 'pending-review', 'document', 'certificate', 'audit-report'].map(f => ( ))}
{/* Loading State */} {loading && } {/* Evidence List */} {!loading && (
{filteredEvidence.map(ev => ( handleDelete(ev.id)} onView={() => handleView(ev)} onDownload={() => handleDownload(ev)} /> ))}
)} {/* Pagination */} {!loading && total > pageSize && (
Zeige {((page - 1) * pageSize) + 1}–{Math.min(page * pageSize, total)} von {total} Nachweisen
Seite {page} von {Math.ceil(total / pageSize)}
)} {!loading && filteredEvidence.length === 0 && state.controls.length > 0 && (

Keine Nachweise gefunden

Passen Sie den Filter an oder laden Sie neue Nachweise hoch.

)}
) }