refactor(admin): split loeschfristen + dsb-portal page.tsx into colocated components
Split two oversized page files into _components/ directories following Next.js 15 conventions and the 500-LOC hard cap: - loeschfristen/page.tsx (2322 LOC -> 412 LOC orchestrator + 6 components) - dsb-portal/page.tsx (2068 LOC -> 135 LOC orchestrator + 9 components) All component files stay under 500 lines. Build verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
261
admin-compliance/app/sdk/loeschfristen/_components/ExportTab.tsx
Normal file
261
admin-compliance/app/sdk/loeschfristen/_components/ExportTab.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { LoeschfristPolicy } from '@/lib/sdk/loeschfristen-types'
|
||||
import { ComplianceCheckResult } from '@/lib/sdk/loeschfristen-compliance'
|
||||
import {
|
||||
exportPoliciesAsJSON, exportPoliciesAsCSV,
|
||||
generateComplianceSummary, downloadFile,
|
||||
} from '@/lib/sdk/loeschfristen-export'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ExportTabProps {
|
||||
policies: LoeschfristPolicy[]
|
||||
complianceResult: ComplianceCheckResult | null
|
||||
runCompliance: () => void
|
||||
setEditingId: (id: string | null) => void
|
||||
setTab: (tab: 'uebersicht' | 'editor' | 'generator' | 'export') => void
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function ExportTab({
|
||||
policies,
|
||||
complianceResult,
|
||||
runCompliance,
|
||||
setEditingId,
|
||||
setTab,
|
||||
}: ExportTabProps) {
|
||||
const allLegalHolds = policies.flatMap((p) =>
|
||||
p.legalHolds.map((h) => ({
|
||||
...h,
|
||||
policyId: p.policyId,
|
||||
policyName: p.dataObjectName,
|
||||
})),
|
||||
)
|
||||
const activeLegalHolds = allLegalHolds.filter((h) => h.status === 'ACTIVE')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Compliance Check */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Compliance-Check</h3>
|
||||
<button
|
||||
onClick={runCompliance}
|
||||
disabled={policies.length === 0}
|
||||
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Analyse starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{policies.length === 0 && (
|
||||
<p className="text-sm text-gray-400">
|
||||
Erstellen Sie zuerst Loeschfristen, um eine Compliance-Analyse durchzufuehren.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{complianceResult && (
|
||||
<ComplianceResultView
|
||||
complianceResult={complianceResult}
|
||||
setEditingId={setEditingId}
|
||||
setTab={setTab}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legal Hold Management */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Legal Hold Verwaltung</h3>
|
||||
|
||||
{allLegalHolds.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">Keine Legal Holds vorhanden.</p>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-gray-700">Gesamt:</span>{' '}
|
||||
<span className="text-gray-900">{allLegalHolds.length}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-orange-600">Aktiv:</span>{' '}
|
||||
<span className="text-gray-900">{activeLegalHolds.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border border-gray-200 rounded-lg">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Loeschfrist</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Bezeichnung</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Grund</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Status</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Erstellt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allLegalHolds.map((hold, idx) => (
|
||||
<tr key={idx} className="border-t border-gray-100">
|
||||
<td className="px-3 py-2">
|
||||
<button
|
||||
onClick={() => { setEditingId(hold.policyId); setTab('editor') }}
|
||||
className="text-purple-600 hover:text-purple-800 font-medium text-xs"
|
||||
>
|
||||
{hold.policyName || hold.policyId}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-900">{hold.name || '-'}</td>
|
||||
<td className="px-3 py-2 text-gray-500">{hold.reason || '-'}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${
|
||||
hold.status === 'ACTIVE'
|
||||
? 'bg-orange-100 text-orange-800'
|
||||
: hold.status === 'RELEASED'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{hold.status === 'ACTIVE' ? 'Aktiv' : hold.status === 'RELEASED' ? 'Aufgehoben' : 'Abgelaufen'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-500 text-xs">{hold.createdAt || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Export */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Datenexport</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Exportieren Sie Ihre Loeschfristen und den Compliance-Status in verschiedenen Formaten.
|
||||
</p>
|
||||
|
||||
{policies.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">
|
||||
Erstellen Sie zuerst Loeschfristen, um Exporte zu generieren.
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => downloadFile(exportPoliciesAsJSON(policies), 'loeschfristen-export.json', 'application/json')}
|
||||
className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
|
||||
>
|
||||
JSON Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => downloadFile(exportPoliciesAsCSV(policies), 'loeschfristen-export.csv', 'text/csv;charset=utf-8')}
|
||||
className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
|
||||
>
|
||||
CSV Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => downloadFile(generateComplianceSummary(policies), 'compliance-bericht.md', 'text/markdown')}
|
||||
className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
|
||||
>
|
||||
Compliance-Bericht
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compliance result sub-component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ComplianceResultView({
|
||||
complianceResult,
|
||||
setEditingId,
|
||||
setTab,
|
||||
}: {
|
||||
complianceResult: ComplianceCheckResult
|
||||
setEditingId: (id: string | null) => void
|
||||
setTab: (tab: 'uebersicht' | 'editor' | 'generator' | 'export') => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Score */}
|
||||
<div className="flex items-center gap-4 p-4 rounded-lg bg-gray-50">
|
||||
<div className={`text-4xl font-bold ${
|
||||
complianceResult.score >= 75 ? 'text-green-600'
|
||||
: complianceResult.score >= 50 ? 'text-yellow-600' : 'text-red-600'
|
||||
}`}>
|
||||
{complianceResult.score}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">Compliance-Score</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{complianceResult.score >= 75 ? 'Guter Zustand - wenige Optimierungen noetig'
|
||||
: complianceResult.score >= 50 ? 'Verbesserungsbedarf - wichtige Punkte offen'
|
||||
: 'Kritisch - dringender Handlungsbedarf'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Issues grouped by severity */}
|
||||
{(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const).map((severity) => {
|
||||
const issues = complianceResult.issues.filter((i) => i.severity === severity)
|
||||
if (issues.length === 0) return null
|
||||
|
||||
const severityConfig = {
|
||||
CRITICAL: { label: 'Kritisch', bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-800', badge: 'bg-red-100 text-red-800' },
|
||||
HIGH: { label: 'Hoch', bg: 'bg-orange-50', border: 'border-orange-200', text: 'text-orange-800', badge: 'bg-orange-100 text-orange-800' },
|
||||
MEDIUM: { label: 'Mittel', bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-800', badge: 'bg-yellow-100 text-yellow-800' },
|
||||
LOW: { label: 'Niedrig', bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-800', badge: 'bg-blue-100 text-blue-800' },
|
||||
}[severity]
|
||||
|
||||
return (
|
||||
<div key={severity}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${severityConfig.badge}`}>
|
||||
{severityConfig.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{issues.length} {issues.length === 1 ? 'Problem' : 'Probleme'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{issues.map((issue, idx) => (
|
||||
<div key={idx} className={`p-3 rounded-lg border ${severityConfig.bg} ${severityConfig.border}`}>
|
||||
<div className={`text-sm font-medium ${severityConfig.text}`}>{issue.title}</div>
|
||||
<p className="text-xs text-gray-600 mt-1">{issue.description}</p>
|
||||
{issue.recommendation && (
|
||||
<p className="text-xs text-gray-500 mt-1 italic">Empfehlung: {issue.recommendation}</p>
|
||||
)}
|
||||
{issue.affectedPolicyId && (
|
||||
<button
|
||||
onClick={() => { setEditingId(issue.affectedPolicyId!); setTab('editor') }}
|
||||
className="text-xs text-purple-600 hover:text-purple-800 font-medium mt-1"
|
||||
>
|
||||
Zur Loeschfrist: {issue.affectedPolicyId}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{complianceResult.issues.length === 0 && (
|
||||
<div className="p-4 rounded-lg bg-green-50 border border-green-200 text-center">
|
||||
<div className="text-green-700 font-medium">Keine Compliance-Probleme gefunden</div>
|
||||
<p className="text-xs text-green-600 mt-1">Alle Loeschfristen entsprechen den Anforderungen.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user