fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
350
admin-v2/components/sdk/einwilligungen/RetentionMatrix.tsx
Normal file
350
admin-v2/components/sdk/einwilligungen/RetentionMatrix.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
'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
|
||||
Reference in New Issue
Block a user