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:
658
admin-v2/components/sdk/einwilligungen/DataPointCatalog.tsx
Normal file
658
admin-v2/components/sdk/einwilligungen/DataPointCatalog.tsx
Normal file
@@ -0,0 +1,658 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DataPointCatalog Component
|
||||
*
|
||||
* Zeigt den vollstaendigen Datenpunktkatalog an.
|
||||
* Ermoeglicht Filterung, Suche und Auswahl von Datenpunkten.
|
||||
* Unterstützt 18 Kategorien (A-R) inkl. Art. 9 DSGVO Warnungen.
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
CheckCircle,
|
||||
Circle,
|
||||
Shield,
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Key,
|
||||
Megaphone,
|
||||
MessageSquare,
|
||||
CreditCard,
|
||||
Users,
|
||||
Bot,
|
||||
Lock,
|
||||
User,
|
||||
Mail,
|
||||
Activity,
|
||||
MapPin,
|
||||
Smartphone,
|
||||
BarChart3,
|
||||
Share2,
|
||||
Heart,
|
||||
Briefcase,
|
||||
FileText,
|
||||
FileCode,
|
||||
Info,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
RiskLevel,
|
||||
LegalBasis,
|
||||
SupportedLanguage,
|
||||
CATEGORY_METADATA,
|
||||
RISK_LEVEL_STYLING,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
ARTICLE_9_WARNING,
|
||||
EMPLOYEE_DATA_WARNING,
|
||||
AI_DATA_WARNING,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
import { searchDataPoints } from '@/lib/sdk/einwilligungen/catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
interface DataPointCatalogProps {
|
||||
dataPoints: DataPoint[]
|
||||
selectedIds: string[]
|
||||
onToggle: (id: string) => void
|
||||
onSelectAll?: () => void
|
||||
onDeselectAll?: () => void
|
||||
language?: SupportedLanguage
|
||||
showFilters?: boolean
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER COMPONENTS
|
||||
// =============================================================================
|
||||
|
||||
const CategoryIcon: React.FC<{ category: DataPointCategory; className?: string }> = ({
|
||||
category,
|
||||
className = 'w-5 h-5',
|
||||
}) => {
|
||||
const icons: Record<DataPointCategory, React.ReactNode> = {
|
||||
// 18 Kategorien (A-R)
|
||||
MASTER_DATA: <User className={className} />,
|
||||
CONTACT_DATA: <Mail className={className} />,
|
||||
AUTHENTICATION: <Key className={className} />,
|
||||
CONSENT: <CheckCircle className={className} />,
|
||||
COMMUNICATION: <MessageSquare className={className} />,
|
||||
PAYMENT: <CreditCard className={className} />,
|
||||
USAGE_DATA: <Activity className={className} />,
|
||||
LOCATION: <MapPin className={className} />,
|
||||
DEVICE_DATA: <Smartphone className={className} />,
|
||||
MARKETING: <Megaphone className={className} />,
|
||||
ANALYTICS: <BarChart3 className={className} />,
|
||||
SOCIAL_MEDIA: <Share2 className={className} />,
|
||||
HEALTH_DATA: <Heart className={className} />,
|
||||
EMPLOYEE_DATA: <Briefcase className={className} />,
|
||||
CONTRACT_DATA: <FileText className={className} />,
|
||||
LOG_DATA: <FileCode className={className} />,
|
||||
AI_DATA: <Bot className={className} />,
|
||||
SECURITY: <Shield className={className} />,
|
||||
}
|
||||
return <>{icons[category] || <Circle className={className} />}</>
|
||||
}
|
||||
|
||||
const RiskBadge: React.FC<{ level: RiskLevel; language: SupportedLanguage }> = ({
|
||||
level,
|
||||
language,
|
||||
}) => {
|
||||
const styling = RISK_LEVEL_STYLING[level]
|
||||
return (
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${styling.bgColor} ${styling.color}`}
|
||||
>
|
||||
{styling.label[language]}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const LegalBasisBadge: React.FC<{ basis: LegalBasis; language: SupportedLanguage }> = ({
|
||||
basis,
|
||||
language,
|
||||
}) => {
|
||||
const info = LEGAL_BASIS_INFO[basis]
|
||||
const colors: Record<LegalBasis, string> = {
|
||||
CONTRACT: 'bg-blue-100 text-blue-700',
|
||||
CONSENT: 'bg-purple-100 text-purple-700',
|
||||
EXPLICIT_CONSENT: 'bg-rose-100 text-rose-700',
|
||||
LEGITIMATE_INTEREST: 'bg-amber-100 text-amber-700',
|
||||
LEGAL_OBLIGATION: 'bg-slate-100 text-slate-700',
|
||||
VITAL_INTERESTS: 'bg-emerald-100 text-emerald-700',
|
||||
PUBLIC_INTEREST: 'bg-cyan-100 text-cyan-700',
|
||||
}
|
||||
return (
|
||||
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${colors[basis] || 'bg-gray-100 text-gray-700'}`}>
|
||||
{info?.name[language] || basis}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Warnung fuer besondere Kategorien (Art. 9 DSGVO, BDSG § 26, AI Act)
|
||||
*/
|
||||
const SpecialCategoryWarning: React.FC<{
|
||||
category: DataPointCategory
|
||||
language: SupportedLanguage
|
||||
onClose?: () => void
|
||||
}> = ({ category, language, onClose }) => {
|
||||
// Bestimme welche Warnung angezeigt werden soll
|
||||
let warning = null
|
||||
let bgColor = ''
|
||||
let borderColor = ''
|
||||
let iconColor = ''
|
||||
|
||||
if (category === 'HEALTH_DATA') {
|
||||
warning = ARTICLE_9_WARNING
|
||||
bgColor = 'bg-rose-50'
|
||||
borderColor = 'border-rose-200'
|
||||
iconColor = 'text-rose-600'
|
||||
} else if (category === 'EMPLOYEE_DATA') {
|
||||
warning = EMPLOYEE_DATA_WARNING
|
||||
bgColor = 'bg-orange-50'
|
||||
borderColor = 'border-orange-200'
|
||||
iconColor = 'text-orange-600'
|
||||
} else if (category === 'AI_DATA') {
|
||||
warning = AI_DATA_WARNING
|
||||
bgColor = 'bg-fuchsia-50'
|
||||
borderColor = 'border-fuchsia-200'
|
||||
iconColor = 'text-fuchsia-600'
|
||||
}
|
||||
|
||||
if (!warning) return null
|
||||
|
||||
return (
|
||||
<div className={`p-4 rounded-lg border ${bgColor} ${borderColor} mb-4`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className={`w-5 h-5 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className={`font-semibold ${iconColor}`}>
|
||||
{warning.title[language]}
|
||||
</h4>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-600 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
{warning.description[language]}
|
||||
</p>
|
||||
<ul className="mt-3 space-y-1">
|
||||
{warning.requirements.map((req, idx) => (
|
||||
<li key={idx} className="text-sm text-slate-700 flex items-start gap-2">
|
||||
<span className={`${iconColor} font-bold`}>•</span>
|
||||
<span>{req[language]}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline-Hinweis fuer Art. 9 Datenpunkte
|
||||
*/
|
||||
const Article9Badge: React.FC<{ language: SupportedLanguage }> = ({ language }) => (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-rose-100 text-rose-700 border border-rose-200">
|
||||
<Heart className="w-3 h-3" />
|
||||
{language === 'de' ? 'Art. 9 DSGVO' : 'Art. 9 GDPR'}
|
||||
</span>
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// MAIN COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function DataPointCatalog({
|
||||
dataPoints,
|
||||
selectedIds,
|
||||
onToggle,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
language = 'de',
|
||||
showFilters = true,
|
||||
readOnly = false,
|
||||
}: DataPointCatalogProps) {
|
||||
// Alle 18 Kategorien in der richtigen Reihenfolge (A-R)
|
||||
const ALL_CATEGORIES: DataPointCategory[] = [
|
||||
'MASTER_DATA', // A
|
||||
'CONTACT_DATA', // B
|
||||
'AUTHENTICATION', // C
|
||||
'CONSENT', // D
|
||||
'COMMUNICATION', // E
|
||||
'PAYMENT', // F
|
||||
'USAGE_DATA', // G
|
||||
'LOCATION', // H
|
||||
'DEVICE_DATA', // I
|
||||
'MARKETING', // J
|
||||
'ANALYTICS', // K
|
||||
'SOCIAL_MEDIA', // L
|
||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
||||
'CONTRACT_DATA', // O
|
||||
'LOG_DATA', // P
|
||||
'AI_DATA', // Q - AI Act
|
||||
'SECURITY', // R
|
||||
]
|
||||
|
||||
// State
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<DataPointCategory>>(
|
||||
new Set(ALL_CATEGORIES)
|
||||
)
|
||||
const [filterCategory, setFilterCategory] = useState<DataPointCategory | 'ALL'>('ALL')
|
||||
const [filterRisk, setFilterRisk] = useState<RiskLevel | 'ALL'>('ALL')
|
||||
const [filterBasis, setFilterBasis] = useState<LegalBasis | 'ALL'>('ALL')
|
||||
const [dismissedWarnings, setDismissedWarnings] = useState<Set<DataPointCategory>>(new Set())
|
||||
|
||||
// Filtered and searched data points
|
||||
const filteredDataPoints = useMemo(() => {
|
||||
let result = dataPoints
|
||||
|
||||
// Search
|
||||
if (searchQuery.trim()) {
|
||||
result = searchDataPoints(result, searchQuery, language)
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
if (filterCategory !== 'ALL') {
|
||||
result = result.filter((dp) => dp.category === filterCategory)
|
||||
}
|
||||
|
||||
// Filter by risk
|
||||
if (filterRisk !== 'ALL') {
|
||||
result = result.filter((dp) => dp.riskLevel === filterRisk)
|
||||
}
|
||||
|
||||
// Filter by legal basis
|
||||
if (filterBasis !== 'ALL') {
|
||||
result = result.filter((dp) => dp.legalBasis === filterBasis)
|
||||
}
|
||||
|
||||
return result
|
||||
}, [dataPoints, searchQuery, filterCategory, filterRisk, filterBasis, language])
|
||||
|
||||
// Group by category (18 Kategorien)
|
||||
const groupedDataPoints = useMemo(() => {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const cat of ALL_CATEGORIES) {
|
||||
grouped.set(cat, [])
|
||||
}
|
||||
for (const dp of filteredDataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}, [filteredDataPoints])
|
||||
|
||||
// Zaehle ausgewaehlte spezielle Kategorien fuer Warnungen
|
||||
const selectedSpecialCategories = useMemo(() => {
|
||||
const special = new Set<DataPointCategory>()
|
||||
for (const id of selectedIds) {
|
||||
const dp = dataPoints.find(d => d.id === id)
|
||||
if (dp) {
|
||||
if (dp.category === 'HEALTH_DATA' || dp.isSpecialCategory) {
|
||||
special.add('HEALTH_DATA')
|
||||
}
|
||||
if (dp.category === 'EMPLOYEE_DATA') {
|
||||
special.add('EMPLOYEE_DATA')
|
||||
}
|
||||
if (dp.category === 'AI_DATA') {
|
||||
special.add('AI_DATA')
|
||||
}
|
||||
}
|
||||
}
|
||||
return special
|
||||
}, [selectedIds, dataPoints])
|
||||
|
||||
// Toggle category expansion
|
||||
const toggleCategory = (category: DataPointCategory) => {
|
||||
setExpandedCategories((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(category)) {
|
||||
next.delete(category)
|
||||
} else {
|
||||
next.add(category)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
// Stats
|
||||
const totalSelected = selectedIds.length
|
||||
const totalDataPoints = dataPoints.length
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header with Stats */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm text-slate-600">
|
||||
<span className="font-semibold text-slate-900">{totalSelected}</span> von{' '}
|
||||
<span className="font-semibold">{totalDataPoints}</span> Datenpunkte ausgewaehlt
|
||||
</div>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onSelectAll}
|
||||
className="text-sm text-indigo-600 hover:text-indigo-700 font-medium"
|
||||
>
|
||||
Alle auswaehlen
|
||||
</button>
|
||||
<span className="text-slate-300">|</span>
|
||||
<button
|
||||
onClick={onDeselectAll}
|
||||
className="text-sm text-slate-600 hover:text-slate-700 font-medium"
|
||||
>
|
||||
Auswahl aufheben
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Art. 9 DSGVO / BDSG § 26 / AI Act Warnungen */}
|
||||
{selectedSpecialCategories.size > 0 && (
|
||||
<div className="space-y-3">
|
||||
{selectedSpecialCategories.has('HEALTH_DATA') && !dismissedWarnings.has('HEALTH_DATA') && (
|
||||
<SpecialCategoryWarning
|
||||
category="HEALTH_DATA"
|
||||
language={language}
|
||||
onClose={() => setDismissedWarnings(prev => new Set([...prev, 'HEALTH_DATA']))}
|
||||
/>
|
||||
)}
|
||||
{selectedSpecialCategories.has('EMPLOYEE_DATA') && !dismissedWarnings.has('EMPLOYEE_DATA') && (
|
||||
<SpecialCategoryWarning
|
||||
category="EMPLOYEE_DATA"
|
||||
language={language}
|
||||
onClose={() => setDismissedWarnings(prev => new Set([...prev, 'EMPLOYEE_DATA']))}
|
||||
/>
|
||||
)}
|
||||
{selectedSpecialCategories.has('AI_DATA') && !dismissedWarnings.has('AI_DATA') && (
|
||||
<SpecialCategoryWarning
|
||||
category="AI_DATA"
|
||||
language={language}
|
||||
onClose={() => setDismissedWarnings(prev => new Set([...prev, 'AI_DATA']))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Filters */}
|
||||
{showFilters && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 min-w-[200px]">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Datenpunkte suchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value as DataPointCategory | 'ALL')}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="ALL">Alle Kategorien</option>
|
||||
{Object.entries(CATEGORY_METADATA).map(([key, meta]) => (
|
||||
<option key={key} value={key}>
|
||||
{meta.name[language]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Risk Filter */}
|
||||
<select
|
||||
value={filterRisk}
|
||||
onChange={(e) => setFilterRisk(e.target.value as RiskLevel | 'ALL')}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="ALL">Alle Risikostufen</option>
|
||||
<option value="LOW">{RISK_LEVEL_STYLING.LOW.label[language]}</option>
|
||||
<option value="MEDIUM">{RISK_LEVEL_STYLING.MEDIUM.label[language]}</option>
|
||||
<option value="HIGH">{RISK_LEVEL_STYLING.HIGH.label[language]}</option>
|
||||
</select>
|
||||
|
||||
{/* Legal Basis Filter */}
|
||||
<select
|
||||
value={filterBasis}
|
||||
onChange={(e) => setFilterBasis(e.target.value as LegalBasis | 'ALL')}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="ALL">Alle Rechtsgrundlagen</option>
|
||||
{Object.entries(LEGAL_BASIS_INFO).map(([key, info]) => (
|
||||
<option key={key} value={key}>
|
||||
{info.name[language]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Points by Category */}
|
||||
<div className="space-y-3">
|
||||
{Array.from(groupedDataPoints.entries()).map(([category, categoryDataPoints]) => {
|
||||
if (categoryDataPoints.length === 0) return null
|
||||
|
||||
const meta = CATEGORY_METADATA[category]
|
||||
const isExpanded = expandedCategories.has(category)
|
||||
const selectedInCategory = categoryDataPoints.filter((dp) =>
|
||||
selectedIds.includes(dp.id)
|
||||
).length
|
||||
|
||||
return (
|
||||
<div
|
||||
key={category}
|
||||
className="border border-slate-200 rounded-xl overflow-hidden bg-white"
|
||||
>
|
||||
{/* Category Header */}
|
||||
<button
|
||||
onClick={() => toggleCategory(category)}
|
||||
className={`w-full flex items-center justify-between px-4 py-3 transition-colors ${
|
||||
category === 'HEALTH_DATA'
|
||||
? 'bg-rose-50 hover:bg-rose-100 border-l-4 border-rose-400'
|
||||
: category === 'EMPLOYEE_DATA'
|
||||
? 'bg-orange-50 hover:bg-orange-100 border-l-4 border-orange-400'
|
||||
: category === 'AI_DATA'
|
||||
? 'bg-fuchsia-50 hover:bg-fuchsia-100 border-l-4 border-fuchsia-400'
|
||||
: 'bg-slate-50 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
category === 'HEALTH_DATA' ? 'bg-rose-100' :
|
||||
category === 'EMPLOYEE_DATA' ? 'bg-orange-100' :
|
||||
category === 'AI_DATA' ? 'bg-fuchsia-100' :
|
||||
`bg-${meta.color}-100`
|
||||
}`}>
|
||||
<CategoryIcon
|
||||
category={category}
|
||||
className={`w-5 h-5 ${
|
||||
category === 'HEALTH_DATA' ? 'text-rose-600' :
|
||||
category === 'EMPLOYEE_DATA' ? 'text-orange-600' :
|
||||
category === 'AI_DATA' ? 'text-fuchsia-600' :
|
||||
`text-${meta.color}-600`
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-slate-900">
|
||||
{meta.code}. {meta.name[language]}
|
||||
</span>
|
||||
{category === 'HEALTH_DATA' && (
|
||||
<span className="text-xs bg-rose-200 text-rose-700 px-1.5 py-0.5 rounded font-medium">
|
||||
Art. 9 DSGVO
|
||||
</span>
|
||||
)}
|
||||
{category === 'EMPLOYEE_DATA' && (
|
||||
<span className="text-xs bg-orange-200 text-orange-700 px-1.5 py-0.5 rounded font-medium">
|
||||
BDSG § 26
|
||||
</span>
|
||||
)}
|
||||
{category === 'AI_DATA' && (
|
||||
<span className="text-xs bg-fuchsia-200 text-fuchsia-700 px-1.5 py-0.5 rounded font-medium">
|
||||
AI Act
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">{meta.description[language]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-slate-500">
|
||||
{selectedInCategory}/{categoryDataPoints.length}
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-5 h-5 text-slate-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-slate-400" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Data Points List */}
|
||||
{isExpanded && (
|
||||
<div className="divide-y divide-slate-100">
|
||||
{categoryDataPoints.map((dp) => {
|
||||
const isSelected = selectedIds.includes(dp.id)
|
||||
return (
|
||||
<div
|
||||
key={dp.id}
|
||||
className={`flex items-start gap-4 p-4 ${
|
||||
readOnly ? '' : 'cursor-pointer hover:bg-slate-50'
|
||||
} transition-colors ${isSelected ? 'bg-indigo-50/50' : ''}`}
|
||||
onClick={() => !readOnly && onToggle(dp.id)}
|
||||
>
|
||||
{/* Checkbox */}
|
||||
{!readOnly && (
|
||||
<div className="flex-shrink-0 pt-0.5">
|
||||
{isSelected ? (
|
||||
<CheckCircle className="w-5 h-5 text-indigo-600" />
|
||||
) : (
|
||||
<Circle className="w-5 h-5 text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-mono text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
||||
{dp.code}
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{dp.name[language]}
|
||||
</span>
|
||||
{dp.isSpecialCategory && (
|
||||
<Article9Badge language={language} />
|
||||
)}
|
||||
{dp.isCustom && (
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
|
||||
{language === 'de' ? 'Benutzerdefiniert' : 'Custom'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
{dp.description[language]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex flex-col items-end gap-1">
|
||||
<RiskBadge level={dp.riskLevel} language={language} />
|
||||
<LegalBasisBadge basis={dp.legalBasis} language={language} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-500">
|
||||
<span>
|
||||
<strong>{language === 'de' ? 'Zweck' : 'Purpose'}:</strong> {dp.purpose[language]}
|
||||
</span>
|
||||
<span>
|
||||
<strong>{language === 'de' ? 'Loeschfrist' : 'Retention'}:</strong>{' '}
|
||||
{RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[language] || dp.retentionPeriod}
|
||||
</span>
|
||||
{dp.cookieCategory && (
|
||||
<span>
|
||||
<strong>Cookie:</strong> {dp.cookieCategory}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Spezielle Warnungen fuer Art. 9 / BDSG / AI Act */}
|
||||
{(dp.requiresExplicitConsent || dp.isSpecialCategory) && (
|
||||
<div className="mt-2 p-2 rounded-md bg-rose-50 border border-rose-200">
|
||||
<div className="flex items-start gap-2 text-xs text-rose-700">
|
||||
<AlertTriangle className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong>
|
||||
{language === 'de'
|
||||
? 'Ausdrueckliche Einwilligung erforderlich'
|
||||
: 'Explicit consent required'}
|
||||
</strong>
|
||||
{dp.legalBasis === 'EXPLICIT_CONSENT' && (
|
||||
<span className="block mt-1 text-rose-600">
|
||||
{language === 'de'
|
||||
? 'Art. 9 Abs. 2 lit. a DSGVO - Separate Einwilligungserklaerung notwendig'
|
||||
: 'Art. 9(2)(a) GDPR - Separate consent declaration required'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Third Party Recipients */}
|
||||
{dp.thirdPartyRecipients.length > 0 && (
|
||||
<div className="mt-2 text-xs text-slate-500">
|
||||
<strong>Drittanbieter:</strong>{' '}
|
||||
{dp.thirdPartyRecipients.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredDataPoints.length === 0 && (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<Search className="w-12 h-12 mx-auto mb-4 text-slate-300" />
|
||||
<p className="font-medium">Keine Datenpunkte gefunden</p>
|
||||
<p className="text-sm mt-1">Versuchen Sie andere Suchbegriffe oder Filter</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataPointCatalog
|
||||
Reference in New Issue
Block a user