AIUseCaseModuleEditor (698 LOC) → thin orchestrator (187) + constants (29) + barrel tabs (4) + tabs implementation split into SystemData (261), PurposeAct (149), RisksReview (219). DataPointCatalog (658 LOC) → main (291) + helpers (190) + CategoryGroup (124) + Row (108). ProjectSelector (656 LOC) → main (211) + CreateProjectDialog (169) + ProjectActionDialog (140) + ProjectCard (128). All files now under 300 LOC soft target and 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { CheckCircle, Circle, AlertTriangle } from 'lucide-react'
|
|
import {
|
|
DataPoint,
|
|
SupportedLanguage,
|
|
RETENTION_PERIOD_INFO,
|
|
} from '@/lib/sdk/einwilligungen/types'
|
|
import { RiskBadge, LegalBasisBadge, Article9Badge } from './DataPointCatalogHelpers'
|
|
|
|
interface DataPointRowProps {
|
|
dp: DataPoint
|
|
isSelected: boolean
|
|
readOnly: boolean
|
|
language: SupportedLanguage
|
|
onToggle: (id: string) => void
|
|
}
|
|
|
|
export function DataPointRow({ dp, isSelected, readOnly, language, onToggle }: DataPointRowProps) {
|
|
return (
|
|
<div
|
|
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)}
|
|
>
|
|
{!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>
|
|
)}
|
|
<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>
|
|
<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>
|
|
{(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>
|
|
)}
|
|
{dp.thirdPartyRecipients.length > 0 && (
|
|
<div className="mt-2 text-xs text-slate-500">
|
|
<strong>Drittanbieter:</strong>{' '}
|
|
{dp.thirdPartyRecipients.join(', ')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|