Files
breakpilot-compliance/admin-compliance/components/sdk/einwilligungen/RetentionMatrix.tsx
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

351 lines
14 KiB
TypeScript

'use client'
/**
* RetentionMatrix Component
*
* Visualisiert die Loeschfristen-Matrix nach Kategorien.
*/
import { useState, useMemo } from 'react'
import {
Clock,
Calendar,
Info,
AlertTriangle,
ChevronDown,
ChevronRight,
} from 'lucide-react'
import {
RetentionMatrixEntry,
RetentionPeriod,
DataPointCategory,
SupportedLanguage,
CATEGORY_METADATA,
RETENTION_PERIOD_INFO,
DataPoint,
} from '@/lib/sdk/einwilligungen/types'
// =============================================================================
// TYPES
// =============================================================================
interface RetentionMatrixProps {
matrix: RetentionMatrixEntry[]
dataPoints: DataPoint[]
language?: SupportedLanguage
showDetails?: boolean
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function getRetentionColor(period: RetentionPeriod): string {
const days = RETENTION_PERIOD_INFO[period].days
if (days === null) return 'bg-purple-100 text-purple-700'
if (days <= 30) return 'bg-green-100 text-green-700'
if (days <= 365) return 'bg-blue-100 text-blue-700'
if (days <= 1095) return 'bg-amber-100 text-amber-700'
return 'bg-red-100 text-red-700'
}
function getRetentionBarWidth(period: RetentionPeriod): number {
const days = RETENTION_PERIOD_INFO[period].days
if (days === null) return 100
const maxDays = 3650 // 10 Jahre
return Math.min(100, (days / maxDays) * 100)
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export function RetentionMatrix({
matrix,
dataPoints,
language = 'de',
showDetails = true,
}: RetentionMatrixProps) {
const [expandedCategories, setExpandedCategories] = useState<Set<DataPointCategory>>(new Set())
const toggleCategory = (category: DataPointCategory) => {
setExpandedCategories((prev) => {
const next = new Set(prev)
if (next.has(category)) {
next.delete(category)
} else {
next.add(category)
}
return next
})
}
// Group data points by category
const dataPointsByCategory = useMemo(() => {
const grouped = new Map<DataPointCategory, DataPoint[]>()
for (const dp of dataPoints) {
const existing = grouped.get(dp.category) || []
grouped.set(dp.category, [...existing, dp])
}
return grouped
}, [dataPoints])
// Stats
const stats = useMemo(() => {
const periodCounts: Record<string, number> = {}
for (const dp of dataPoints) {
periodCounts[dp.retentionPeriod] = (periodCounts[dp.retentionPeriod] || 0) + 1
}
return periodCounts
}, [dataPoints])
return (
<div className="space-y-6">
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-green-50 rounded-xl p-4">
<div className="flex items-center gap-2 text-green-600 mb-1">
<Clock className="w-4 h-4" />
<span className="text-sm font-medium">Kurzfristig</span>
</div>
<div className="text-2xl font-bold text-green-700">
{(stats['24_HOURS'] || 0) + (stats['30_DAYS'] || 0)}
</div>
<div className="text-xs text-green-600"> 30 Tage</div>
</div>
<div className="bg-blue-50 rounded-xl p-4">
<div className="flex items-center gap-2 text-blue-600 mb-1">
<Calendar className="w-4 h-4" />
<span className="text-sm font-medium">Mittelfristig</span>
</div>
<div className="text-2xl font-bold text-blue-700">
{(stats['90_DAYS'] || 0) + (stats['12_MONTHS'] || 0)}
</div>
<div className="text-xs text-blue-600">90 Tage - 12 Monate</div>
</div>
<div className="bg-amber-50 rounded-xl p-4">
<div className="flex items-center gap-2 text-amber-600 mb-1">
<Calendar className="w-4 h-4" />
<span className="text-sm font-medium">Langfristig</span>
</div>
<div className="text-2xl font-bold text-amber-700">
{(stats['24_MONTHS'] || 0) + (stats['36_MONTHS'] || 0)}
</div>
<div className="text-xs text-amber-600">2-3 Jahre</div>
</div>
<div className="bg-red-50 rounded-xl p-4">
<div className="flex items-center gap-2 text-red-600 mb-1">
<AlertTriangle className="w-4 h-4" />
<span className="text-sm font-medium">Gesetzlich</span>
</div>
<div className="text-2xl font-bold text-red-700">
{(stats['6_YEARS'] || 0) + (stats['10_YEARS'] || 0)}
</div>
<div className="text-xs text-red-600">6-10 Jahre (AO/HGB)</div>
</div>
</div>
{/* Matrix Table */}
<div className="border border-slate-200 rounded-xl overflow-hidden bg-white">
<table className="w-full">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700">
Kategorie
</th>
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700">
Standard-Loeschfrist
</th>
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700 hidden md:table-cell">
Rechtsgrundlage
</th>
<th className="text-center px-4 py-3 text-sm font-semibold text-slate-700 w-24">
Datenpunkte
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{matrix.map((entry) => {
const meta = CATEGORY_METADATA[entry.category]
const categoryDataPoints = dataPointsByCategory.get(entry.category) || []
const isExpanded = expandedCategories.has(entry.category)
return (
<>
<tr
key={entry.category}
className="hover:bg-slate-50 cursor-pointer transition-colors"
onClick={() => showDetails && toggleCategory(entry.category)}
>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
{showDetails && (
<div className="text-slate-400">
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</div>
)}
<div>
<div className="font-medium text-slate-900">
{meta.code}. {entry.categoryName[language]}
</div>
{entry.exceptions.length > 0 && (
<div className="text-xs text-slate-500">
{entry.exceptions.length} Ausnahme(n)
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="flex flex-col gap-1">
<span
className={`inline-flex px-2.5 py-1 text-xs font-medium rounded-full w-fit ${getRetentionColor(
entry.standardPeriod
)}`}
>
{RETENTION_PERIOD_INFO[entry.standardPeriod].label[language]}
</span>
<div className="h-1.5 bg-slate-100 rounded-full w-full max-w-[200px]">
<div
className={`h-full rounded-full ${
getRetentionColor(entry.standardPeriod).includes('green')
? 'bg-green-400'
: getRetentionColor(entry.standardPeriod).includes('blue')
? 'bg-blue-400'
: getRetentionColor(entry.standardPeriod).includes('amber')
? 'bg-amber-400'
: getRetentionColor(entry.standardPeriod).includes('red')
? 'bg-red-400'
: 'bg-purple-400'
}`}
style={{ width: `${getRetentionBarWidth(entry.standardPeriod)}%` }}
/>
</div>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-600 hidden md:table-cell">
{entry.legalBasis}
</td>
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-slate-100 text-sm font-medium text-slate-700">
{categoryDataPoints.length}
</span>
</td>
</tr>
{/* Expanded Details */}
{showDetails && isExpanded && (
<tr key={`${entry.category}-details`}>
<td colSpan={4} className="bg-slate-50 px-4 py-4">
<div className="space-y-4">
{/* Exceptions */}
{entry.exceptions.length > 0 && (
<div>
<h4 className="text-sm font-medium text-slate-700 mb-2 flex items-center gap-1">
<Info className="w-4 h-4" />
Ausnahmen von der Standardfrist
</h4>
<div className="space-y-2">
{entry.exceptions.map((exc, idx) => (
<div
key={idx}
className="flex items-start gap-3 p-3 bg-white rounded-lg border border-slate-200"
>
<AlertTriangle className="w-4 h-4 text-amber-500 mt-0.5" />
<div>
<div className="text-sm font-medium text-slate-900">
{exc.condition[language]}
</div>
<div className="text-sm text-slate-600">
Loeschfrist:{' '}
<span className="font-medium">
{RETENTION_PERIOD_INFO[exc.period].label[language]}
</span>
</div>
<div className="text-xs text-slate-500 mt-1">
{exc.reason[language]}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Data Points in Category */}
{categoryDataPoints.length > 0 && (
<div>
<h4 className="text-sm font-medium text-slate-700 mb-2">
Datenpunkte in dieser Kategorie
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{categoryDataPoints.map((dp) => (
<div
key={dp.id}
className="flex items-center justify-between p-2 bg-white rounded-lg border border-slate-200"
>
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
{dp.code}
</span>
<span className="text-sm text-slate-700">
{dp.name[language]}
</span>
</div>
<span
className={`text-xs px-2 py-0.5 rounded-full ${getRetentionColor(
dp.retentionPeriod
)}`}
>
{RETENTION_PERIOD_INFO[dp.retentionPeriod].label[language]}
</span>
</div>
))}
</div>
</div>
)}
</div>
</td>
</tr>
)}
</>
)
})}
</tbody>
</table>
</div>
{/* Legend */}
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-600">
<span className="font-medium">Legende:</span>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-green-400" />
30 Tage
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-blue-400" />
90 Tage - 12 Monate
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-amber-400" />
2-3 Jahre
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-400" />
6-10 Jahre
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-purple-400" />
Variabel
</div>
</div>
</div>
)
}
export default RetentionMatrix