Files
breakpilot-compliance/admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx
Sharang Parnerkar ada50f0466 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>
2026-04-17 09:16:21 +02:00

188 lines
7.3 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import {
AIUseCaseModule,
AI_USE_CASE_TYPES,
PRIVACY_BY_DESIGN_CATEGORIES,
PrivacyByDesignCategory,
PrivacyByDesignMeasure,
AIModuleReviewTrigger,
AIModuleReviewTriggerType,
checkArt22Applicability,
} from '@/lib/sdk/dsfa/ai-use-case-types'
import { TABS, REVIEW_TRIGGER_TYPES } from './AIUseCaseEditorConstants'
import {
Tab1System,
Tab2Data,
Tab3Purpose,
Tab4AIAct,
Tab5Risks,
Tab6PrivacyByDesign,
Tab7Review,
} from './AIUseCaseEditorTabs'
interface AIUseCaseModuleEditorProps {
module: AIUseCaseModule
onSave: (module: AIUseCaseModule) => void
onCancel: () => void
isSaving?: boolean
}
export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel, isSaving }: AIUseCaseModuleEditorProps) {
const [activeTab, setActiveTab] = useState(1)
const [module, setModule] = useState<AIUseCaseModule>(initialModule)
const [newCategory, setNewCategory] = useState('')
const [newOutputCategory, setNewOutputCategory] = useState('')
const [newSubject, setNewSubject] = useState('')
const typeInfo = AI_USE_CASE_TYPES[module.use_case_type]
const art22Required = checkArt22Applicability(module)
const update = (updates: Partial<AIUseCaseModule>) => {
setModule(prev => ({ ...prev, ...updates }))
}
const addToList = (field: keyof AIUseCaseModule, value: string, setter: (v: string) => void) => {
if (!value.trim()) return
const current = (module[field] as string[]) || []
update({ [field]: [...current, value.trim()] } as Partial<AIUseCaseModule>)
setter('')
}
const removeFromList = (field: keyof AIUseCaseModule, idx: number) => {
const current = (module[field] as string[]) || []
update({ [field]: current.filter((_, i) => i !== idx) } as Partial<AIUseCaseModule>)
}
const togglePbdMeasure = (category: PrivacyByDesignCategory) => {
const existing = module.privacy_by_design_measures || []
const found = existing.find(m => m.category === category)
if (found) {
update({ privacy_by_design_measures: existing.map(m =>
m.category === category ? { ...m, implemented: !m.implemented } : m
)})
} else {
const newMeasure: PrivacyByDesignMeasure = {
category,
description: PRIVACY_BY_DESIGN_CATEGORIES[category].description,
implemented: true,
}
update({ privacy_by_design_measures: [...existing, newMeasure] })
}
}
const toggleReviewTrigger = (type: AIModuleReviewTriggerType) => {
const existing = module.review_triggers || []
const found = existing.find(t => t.type === type)
if (found) {
update({ review_triggers: existing.filter(t => t.type !== type) })
} else {
const label = REVIEW_TRIGGER_TYPES.find(rt => rt.value === type)?.label || type
const newTrigger: AIModuleReviewTrigger = { type, description: label }
update({ review_triggers: [...existing, newTrigger] })
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-3">
<span className="text-2xl">{typeInfo.icon}</span>
<div>
<h2 className="text-lg font-semibold text-gray-900">{module.name || typeInfo.label}</h2>
<p className="text-xs text-gray-500">{typeInfo.label} KI-Anwendungsfall-Anhang</p>
</div>
</div>
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Tab Bar */}
<div className="flex gap-1 px-4 py-2 border-b border-gray-200 overflow-x-auto flex-shrink-0">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${
activeTab === tab.id
? 'bg-purple-100 text-purple-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<span>{tab.icon}</span>
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{activeTab === 1 && <Tab1System module={module} update={update} typeInfo={typeInfo} />}
{activeTab === 2 && (
<Tab2Data
module={module}
update={update}
newCategory={newCategory}
setNewCategory={setNewCategory}
newOutputCategory={newOutputCategory}
setNewOutputCategory={setNewOutputCategory}
newSubject={newSubject}
setNewSubject={setNewSubject}
addToList={addToList}
removeFromList={removeFromList}
/>
)}
{activeTab === 3 && <Tab3Purpose module={module} update={update} art22Required={art22Required} />}
{activeTab === 4 && <Tab4AIAct module={module} update={update} />}
{activeTab === 5 && <Tab5Risks module={module} update={update} typeInfo={typeInfo} />}
{activeTab === 6 && <Tab6PrivacyByDesign module={module} update={update} togglePbdMeasure={togglePbdMeasure} />}
{activeTab === 7 && <Tab7Review module={module} update={update} toggleReviewTrigger={toggleReviewTrigger} />}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between flex-shrink-0">
<div className="flex gap-2">
{activeTab > 1 && (
<button
onClick={() => setActiveTab(activeTab - 1)}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Zurück
</button>
)}
{activeTab < 7 && (
<button
onClick={() => setActiveTab(activeTab + 1)}
className="px-4 py-2 text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"
>
Weiter
</button>
)}
</div>
<div className="flex gap-2">
<button
onClick={onCancel}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
onClick={() => onSave(module)}
disabled={isSaving || !module.name || !module.model_description}
className="px-6 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors font-medium"
>
{isSaving ? 'Speichert...' : 'Modul speichern'}
</button>
</div>
</div>
</div>
</div>
)
}