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:
134
admin-compliance/app/sdk/evidence/_components/EvidenceCard.tsx
Normal file
134
admin-compliance/app/sdk/evidence/_components/EvidenceCard.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import type { DisplayEvidence, DisplayEvidenceType } from './EvidenceTypes'
|
||||
|
||||
const typeIcons: Record<DisplayEvidenceType, React.ReactNode> = {
|
||||
document: (
|
||||
<svg className="w-6 h-6" 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>
|
||||
),
|
||||
screenshot: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
log: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
),
|
||||
'audit-report': (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
|
||||
),
|
||||
certificate: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
const typeIconBg: Record<DisplayEvidenceType, string> = {
|
||||
certificate: 'bg-yellow-100 text-yellow-600',
|
||||
'audit-report': 'bg-purple-100 text-purple-600',
|
||||
screenshot: 'bg-blue-100 text-blue-600',
|
||||
log: 'bg-green-100 text-green-600',
|
||||
document: 'bg-gray-100 text-gray-600',
|
||||
}
|
||||
|
||||
export function EvidenceCard({
|
||||
evidence,
|
||||
onDelete,
|
||||
onView,
|
||||
onDownload,
|
||||
}: {
|
||||
evidence: DisplayEvidence
|
||||
onDelete: () => void
|
||||
onView: () => void
|
||||
onDownload: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className={`bg-white rounded-xl border-2 p-6 ${
|
||||
evidence.status === 'expired' ? 'border-red-200' :
|
||||
evidence.status === 'pending-review' ? 'border-yellow-200' : 'border-gray-200'
|
||||
}`}>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${typeIconBg[evidence.displayType]}`}>
|
||||
{typeIcons[evidence.displayType]}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{evidence.name}</h3>
|
||||
<span className={`px-3 py-1 text-xs rounded-full ${statusColors[evidence.status]}`}>
|
||||
{statusLabels[evidence.status]}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">{evidence.description}</p>
|
||||
|
||||
<div className="mt-3 flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>Hochgeladen: {evidence.uploadedAt.toLocaleDateString('de-DE')}</span>
|
||||
{evidence.validUntil && (
|
||||
<span className={evidence.status === 'expired' ? 'text-red-600' : ''}>
|
||||
Gueltig bis: {evidence.validUntil.toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
)}
|
||||
<span>{evidence.fileSize}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 flex-wrap">
|
||||
{evidence.linkedRequirements.map(req => (
|
||||
<span key={req} className="px-2 py-0.5 text-xs bg-blue-50 text-blue-600 rounded">
|
||||
{req}
|
||||
</span>
|
||||
))}
|
||||
{evidence.linkedControls.map(ctrl => (
|
||||
<span key={ctrl} className="px-2 py-0.5 text-xs bg-green-50 text-green-600 rounded">
|
||||
{ctrl}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">Hochgeladen von: {evidence.uploadedBy}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onView}
|
||||
disabled={!evidence.fileUrl}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Anzeigen
|
||||
</button>
|
||||
<button
|
||||
onClick={onDownload}
|
||||
disabled={!evidence.fileUrl}
|
||||
className="px-3 py-1 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Herunterladen
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
143
admin-compliance/app/sdk/evidence/_components/EvidenceTypes.ts
Normal file
143
admin-compliance/app/sdk/evidence/_components/EvidenceTypes.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import type { EvidenceType } from '@/lib/sdk'
|
||||
|
||||
export type DisplayEvidenceType = 'document' | 'screenshot' | 'log' | 'audit-report' | 'certificate'
|
||||
export type DisplayFormat = 'pdf' | 'image' | 'text' | 'json'
|
||||
export type DisplayStatus = 'valid' | 'expired' | 'pending-review'
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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'
|
||||
}
|
||||
}
|
||||
|
||||
export function getEvidenceStatus(validUntil: Date | null): DisplayStatus {
|
||||
if (!validUntil) return 'pending-review'
|
||||
const now = new Date()
|
||||
if (validUntil < now) return 'expired'
|
||||
return 'valid'
|
||||
}
|
||||
|
||||
export 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',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
export function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 animate-pulse">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-gray-200 rounded-lg" />
|
||||
<div className="flex-1">
|
||||
<div className="h-6 w-3/4 bg-gray-200 rounded mb-2" />
|
||||
<div className="h-4 w-full bg-gray-100 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user