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>
351 lines
14 KiB
TypeScript
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
|