Files
breakpilot-compliance/admin-compliance/app/sdk/evidence/page.tsx
Sharang Parnerkar 1fcd8244b1 refactor(admin): split evidence, process-tasks, iace/hazards pages
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>
2026-04-16 17:12:15 +02:00

214 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React from 'react'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { EvidenceCard } from './_components/EvidenceCard'
import { LoadingSkeleton } from './_components/LoadingSkeleton'
import { ReviewModal } from './_components/ReviewModal'
import { RejectModal } from './_components/RejectModal'
import { AuditTrailPanel } from './_components/AuditTrailPanel'
import { ChecksTab } from './_components/ChecksTab'
import { MappingTab } from './_components/MappingTab'
import { ReportTab } from './_components/ReportTab'
import { confidenceFilterColors } from './_components/EvidenceTypes'
import { useEvidence } from './_hooks/useEvidence'
const evidenceTabs = [
{ key: 'evidence' as const, label: 'Nachweise' },
{ key: 'checks' as const, label: 'Automatische Checks' },
{ key: 'mapping' as const, label: 'Control-Mapping' },
{ key: 'report' as const, label: 'Report' },
]
export default function EvidencePage() {
const ev = useEvidence()
const stepInfo = STEP_EXPLANATIONS['evidence']
return (
<div className="space-y-6">
<input ref={ev.fileInputRef} type="file" className="hidden" onChange={ev.handleFileChange}
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.json,.csv,.txt" />
<StepHeader stepId="evidence" title={stepInfo.title} description={stepInfo.description}
explanation={stepInfo.explanation} tips={stepInfo.tips}>
<button onClick={ev.handleUploadClick} disabled={ev.uploading}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50">
{ev.uploading ? (
<>
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Wird hochgeladen...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
Nachweis hochladen
</>
)}
</button>
</StepHeader>
<div className="bg-white rounded-xl shadow-sm border">
<div className="flex border-b">
{evidenceTabs.map(tab => (
<button key={tab.key} onClick={() => ev.setActiveTab(tab.key)}
className={`px-6 py-3 text-sm font-medium transition-colors ${
ev.activeTab === tab.key ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-500 hover:text-gray-700'
}`}>
{tab.label}
</button>
))}
</div>
</div>
{ev.error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{ev.error}</span>
<button onClick={() => ev.setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{ev.activeTab === 'evidence' && (
<>
{ev.state.controls.length === 0 && !ev.loading && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-amber-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<h4 className="font-medium text-amber-800">Keine Kontrollen definiert</h4>
<p className="text-sm text-amber-700 mt-1">Bitte definieren Sie zuerst Kontrollen, um die zugehoerigen Nachweise zu laden.</p>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Gesamt</div>
<div className="text-3xl font-bold text-gray-900">{ev.displayEvidence.length}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Gueltig</div>
<div className="text-3xl font-bold text-green-600">{ev.validCount}</div>
</div>
<div className="bg-white rounded-xl border border-red-200 p-6">
<div className="text-sm text-red-600">Abgelaufen</div>
<div className="text-3xl font-bold text-red-600">{ev.expiredCount}</div>
</div>
<div className="bg-white rounded-xl border border-yellow-200 p-6">
<div className="text-sm text-yellow-600">Pruefung ausstehend</div>
<div className="text-3xl font-bold text-yellow-600">{ev.pendingCount}</div>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'valid', 'expired', 'pending-review', 'document', 'certificate', 'audit-report'].map(f => (
<button key={f} onClick={() => ev.setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
ev.filter === f ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}>
{f === 'all' ? 'Alle' : f === 'valid' ? 'Gueltig' : f === 'expired' ? 'Abgelaufen' :
f === 'pending-review' ? 'Ausstehend' : f === 'document' ? 'Dokumente' :
f === 'certificate' ? 'Zertifikate' : 'Audit-Berichte'}
</button>
))}
<span className="text-gray-300 mx-1">|</span>
{['E0', 'E1', 'E2', 'E3', 'E4'].map(level => (
<button key={level} onClick={() => ev.setConfidenceFilter(ev.confidenceFilter === level ? null : level)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
ev.confidenceFilter === level ? confidenceFilterColors[level] : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}>
{level}
</button>
))}
</div>
{ev.loading && <LoadingSkeleton />}
{!ev.loading && (
<div className="space-y-4">
{ev.filteredEvidence.map(e => (
<EvidenceCard key={e.id} evidence={e}
onDelete={() => ev.handleDelete(e.id)}
onView={() => ev.handleView(e)}
onDownload={() => ev.handleDownload(e)}
onReview={() => ev.setReviewEvidence(e)}
onReject={() => ev.setRejectEvidence(e)}
onShowHistory={() => ev.setAuditTrailId(e.id)}
/>
))}
</div>
)}
{!ev.loading && ev.total > ev.pageSize && (
<div className="flex items-center justify-between bg-white rounded-xl border border-gray-200 p-4">
<span className="text-sm text-gray-500">
Zeige {((ev.page - 1) * ev.pageSize) + 1}{Math.min(ev.page * ev.pageSize, ev.total)} von {ev.total} Nachweisen
</span>
<div className="flex items-center gap-2">
<button onClick={() => ev.setPage(p => Math.max(1, p - 1))} disabled={ev.page <= 1}
className="px-3 py-1 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed">
Zurueck
</button>
<span className="text-sm text-gray-700">Seite {ev.page} von {Math.ceil(ev.total / ev.pageSize)}</span>
<button onClick={() => ev.setPage(p => Math.min(Math.ceil(ev.total / ev.pageSize), p + 1))}
disabled={ev.page >= Math.ceil(ev.total / ev.pageSize)}
className="px-3 py-1 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed">
Weiter
</button>
</div>
</div>
)}
{!ev.loading && ev.filteredEvidence.length === 0 && ev.state.controls.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Nachweise gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder laden Sie neue Nachweise hoch.</p>
</div>
)}
</>
)}
{ev.activeTab === 'checks' && (
<ChecksTab checks={ev.checks} checksLoading={ev.checksLoading} checkResults={ev.checkResults}
runningCheckId={ev.runningCheckId} seedingChecks={ev.seedingChecks}
onRun={ev.runCheck} onLoadResults={ev.loadCheckResults} onSeed={ev.seedChecks} />
)}
{ev.activeTab === 'mapping' && (
<MappingTab mappings={ev.mappings} coverageReport={ev.coverageReport} />
)}
{ev.activeTab === 'report' && (
<ReportTab coverageReport={ev.coverageReport} checks={ev.checks}
displayEvidenceLength={ev.displayEvidence.length}
validCount={ev.validCount} expiredCount={ev.expiredCount} pendingCount={ev.pendingCount} />
)}
{ev.reviewEvidence && (
<ReviewModal evidence={ev.reviewEvidence} onClose={() => ev.setReviewEvidence(null)}
onSuccess={() => { ev.setReviewEvidence(null); ev.setRefreshKey(k => k + 1) }} />
)}
{ev.rejectEvidence && (
<RejectModal evidence={ev.rejectEvidence} onClose={() => ev.setRejectEvidence(null)}
onSuccess={() => { ev.setRejectEvidence(null); ev.setRefreshKey(k => k + 1) }} />
)}
{ev.auditTrailId && (
<AuditTrailPanel evidenceId={ev.auditTrailId} onClose={() => ev.setAuditTrailId(null)} />
)}
</div>
)
}