Files
breakpilot-compliance/admin-compliance/app/sdk/loeschfristen/_components/ExportTab.tsx
Sharang Parnerkar 6c883fb12e 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>
2026-04-11 18:51:16 +02:00

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>
)
}