refactor(admin): split AIUseCaseModuleEditor, DataPointCatalog, ProjectSelector components

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>
This commit is contained in:
Sharang Parnerkar
2026-04-17 09:15:23 +02:00
parent ad6e6019e9
commit ada50f0466
14 changed files with 1588 additions and 1390 deletions

View File

@@ -0,0 +1,219 @@
'use client'
import React from 'react'
import {
AIUseCaseModule,
AI_USE_CASE_TYPES,
PRIVACY_BY_DESIGN_CATEGORIES,
PrivacyByDesignCategory,
AIModuleReviewTriggerType,
} from '@/lib/sdk/dsfa/ai-use-case-types'
import { REVIEW_TRIGGER_TYPES } from './AIUseCaseEditorConstants'
type UpdateFn = (updates: Partial<AIUseCaseModule>) => void
// =============================================================================
// TAB 5: Risikoanalyse
// =============================================================================
interface Tab5RisksProps {
module: AIUseCaseModule
update: UpdateFn
typeInfo: typeof AI_USE_CASE_TYPES[keyof typeof AI_USE_CASE_TYPES]
}
export function Tab5Risks({ module, update, typeInfo }: Tab5RisksProps) {
return (
<div className="space-y-4">
<p className="text-sm text-gray-500">
Spezifische Risiken für diesen KI-Anwendungsfall. Typische Risiken basierend auf dem gewählten Typ:
</p>
{typeInfo.typical_risks.length > 0 && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="text-xs font-medium text-yellow-800 mb-1">Typische Risiken für {typeInfo.label}:</div>
<ul className="space-y-0.5">
{typeInfo.typical_risks.map((r, i) => (
<li key={i} className="text-xs text-yellow-700 flex items-center gap-1.5">
<span></span> {r}
</li>
))}
</ul>
</div>
)}
<div className="space-y-2">
{(module.risks || []).map((risk, idx) => (
<div key={idx} className="p-3 border border-gray-200 rounded-lg bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-900">{risk.description}</p>
<div className="flex gap-2 mt-1">
<span className="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-600 rounded">W: {risk.likelihood}</span>
<span className="text-xs px-1.5 py-0.5 bg-purple-100 text-purple-600 rounded">S: {risk.impact}</span>
</div>
</div>
<button
onClick={() => update({ risks: module.risks.filter((_, i) => i !== idx) })}
className="text-gray-400 hover:text-red-500 ml-2"
>
×
</button>
</div>
</div>
))}
{(module.risks || []).length === 0 && (
<p className="text-sm text-gray-400 text-center py-4">Noch keine Risiken dokumentiert</p>
)}
</div>
<button
onClick={() => {
const desc = prompt('Risiko-Beschreibung:')
if (desc) {
update({
risks: [...(module.risks || []), {
risk_id: crypto.randomUUID(),
description: desc,
likelihood: 'medium',
impact: 'medium',
mitigation_ids: [],
}]
})
}
}}
className="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-purple-400 hover:text-purple-600 transition-colors"
>
+ Risiko hinzufügen
</button>
</div>
)
}
// =============================================================================
// TAB 6: Maßnahmen & Privacy by Design
// =============================================================================
interface Tab6PrivacyByDesignProps {
module: AIUseCaseModule
update: UpdateFn
togglePbdMeasure: (category: PrivacyByDesignCategory) => void
}
export function Tab6PrivacyByDesign({ module, togglePbdMeasure }: Tab6PrivacyByDesignProps) {
return (
<div className="space-y-4">
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-3">Privacy by Design Maßnahmen</h4>
<div className="grid grid-cols-2 gap-2">
{(Object.entries(PRIVACY_BY_DESIGN_CATEGORIES) as [PrivacyByDesignCategory, typeof PRIVACY_BY_DESIGN_CATEGORIES[PrivacyByDesignCategory]][]).map(([cat, info]) => {
const measure = module.privacy_by_design_measures?.find(m => m.category === cat)
return (
<button
key={cat}
onClick={() => togglePbdMeasure(cat)}
className={`flex items-start gap-2 p-3 rounded-lg border text-left transition-all ${
measure?.implemented
? 'border-green-400 bg-green-50'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<span className="text-lg flex-shrink-0">{info.icon}</span>
<div>
<div className={`text-xs font-medium ${measure?.implemented ? 'text-green-800' : 'text-gray-700'}`}>
{info.label}
</div>
<p className="text-[10px] text-gray-500 mt-0.5">{info.description}</p>
</div>
</button>
)
})}
</div>
</div>
</div>
)
}
// =============================================================================
// TAB 7: Review-Trigger
// =============================================================================
interface Tab7ReviewProps {
module: AIUseCaseModule
update: UpdateFn
toggleReviewTrigger: (type: AIModuleReviewTriggerType) => void
}
export function Tab7Review({ module, update, toggleReviewTrigger }: Tab7ReviewProps) {
return (
<div className="space-y-4">
<p className="text-sm text-gray-500">
Wählen Sie die Ereignisse, die eine erneute Bewertung dieses KI-Anwendungsfalls auslösen sollen.
</p>
<div className="space-y-2">
{REVIEW_TRIGGER_TYPES.map(rt => {
const active = module.review_triggers?.some(t => t.type === rt.value)
const trigger = module.review_triggers?.find(t => t.type === rt.value)
return (
<div key={rt.value} className={`rounded-lg border p-3 transition-all ${active ? 'border-purple-300 bg-purple-50' : 'border-gray-200'}`}>
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={active || false}
onChange={() => toggleReviewTrigger(rt.value)}
className="h-4 w-4 rounded border-gray-300 text-purple-600"
/>
<span className="text-base">{rt.icon}</span>
<span className="text-sm font-medium text-gray-900">{rt.label}</span>
</div>
{active && (
<div className="mt-2 ml-7 space-y-2">
<input
type="text"
value={trigger?.threshold || ''}
onChange={e => {
const updated = (module.review_triggers || []).map(t =>
t.type === rt.value ? { ...t, threshold: e.target.value } : t
)
update({ review_triggers: updated })
}}
placeholder="Schwellwert (z.B. Genauigkeit < 80%)"
className="w-full px-2 py-1 text-xs border border-purple-200 rounded focus:ring-2 focus:ring-purple-400"
/>
<input
type="text"
value={trigger?.monitoring_interval || ''}
onChange={e => {
const updated = (module.review_triggers || []).map(t =>
t.type === rt.value ? { ...t, monitoring_interval: e.target.value } : t
)
update({ review_triggers: updated })
}}
placeholder="Monitoring-Intervall (z.B. wöchentlich)"
className="w-full px-2 py-1 text-xs border border-purple-200 rounded focus:ring-2 focus:ring-purple-400"
/>
</div>
)}
</div>
)
})}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Monitoring-Beschreibung</label>
<textarea
value={module.monitoring_description || ''}
onChange={e => update({ monitoring_description: e.target.value })}
rows={3}
placeholder="Wie wird das KI-System kontinuierlich überwacht? Welche Metriken werden erfasst?"
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Nächstes Review-Datum</label>
<input
type="date"
value={module.next_review_date || ''}
onChange={e => update({ next_review_date: e.target.value })}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
/>
</div>
</div>
)
}