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>
262 lines
11 KiB
TypeScript
262 lines
11 KiB
TypeScript
'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>
|
|
)
|
|
}
|