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>
206 lines
7.9 KiB
TypeScript
206 lines
7.9 KiB
TypeScript
'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 { useEvidence } from './_hooks/useEvidence'
|
||
|
||
export default function EvidencePage() {
|
||
const {
|
||
state,
|
||
filter,
|
||
setFilter,
|
||
loading,
|
||
error,
|
||
setError,
|
||
uploading,
|
||
fileInputRef,
|
||
page,
|
||
setPage,
|
||
pageSize,
|
||
total,
|
||
displayEvidence,
|
||
filteredEvidence,
|
||
validCount,
|
||
expiredCount,
|
||
pendingCount,
|
||
handleDelete,
|
||
handleView,
|
||
handleDownload,
|
||
handleUploadClick,
|
||
handleFileChange,
|
||
} = useEvidence()
|
||
|
||
const stepInfo = STEP_EXPLANATIONS['evidence']
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Hidden file input */}
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
className="hidden"
|
||
onChange={handleFileChange}
|
||
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.json,.csv,.txt"
|
||
/>
|
||
|
||
{/* Step Header */}
|
||
<StepHeader
|
||
stepId="evidence"
|
||
title={stepInfo.title}
|
||
description={stepInfo.description}
|
||
explanation={stepInfo.explanation}
|
||
tips={stepInfo.tips}
|
||
>
|
||
<button
|
||
onClick={handleUploadClick}
|
||
disabled={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"
|
||
>
|
||
{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>
|
||
|
||
{/* Error Banner */}
|
||
{error && (
|
||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
|
||
<span>{error}</span>
|
||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">×</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Controls Alert */}
|
||
{state.controls.length === 0 && !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>
|
||
)}
|
||
|
||
{/* Stats */}
|
||
<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">{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">{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">{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">{pendingCount}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filter */}
|
||
<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={() => setFilter(f)}
|
||
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||
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>
|
||
))}
|
||
</div>
|
||
|
||
{/* Loading State */}
|
||
{loading && <LoadingSkeleton />}
|
||
|
||
{/* Evidence List */}
|
||
{!loading && (
|
||
<div className="space-y-4">
|
||
{filteredEvidence.map(ev => (
|
||
<EvidenceCard
|
||
key={ev.id}
|
||
evidence={ev}
|
||
onDelete={() => handleDelete(ev.id)}
|
||
onView={() => handleView(ev)}
|
||
onDownload={() => handleDownload(ev)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Pagination */}
|
||
{!loading && total > 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 {((page - 1) * pageSize) + 1}–{Math.min(page * pageSize, total)} von {total} Nachweisen
|
||
</span>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||
disabled={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 {page} von {Math.ceil(total / pageSize)}
|
||
</span>
|
||
<button
|
||
onClick={() => setPage(p => Math.min(Math.ceil(total / pageSize), p + 1))}
|
||
disabled={page >= Math.ceil(total / 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>
|
||
)}
|
||
|
||
{!loading && filteredEvidence.length === 0 && 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>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|