diff --git a/admin-compliance/components/sdk/ProjectSelector/CreateProjectDialog.tsx b/admin-compliance/components/sdk/ProjectSelector/CreateProjectDialog.tsx new file mode 100644 index 0000000..37605dc --- /dev/null +++ b/admin-compliance/components/sdk/ProjectSelector/CreateProjectDialog.tsx @@ -0,0 +1,169 @@ +'use client' + +import React, { useState } from 'react' +import { useSDK } from '@/lib/sdk' +import type { ProjectInfo, CustomerType } from '@/lib/sdk/types' + +/** Map snake_case backend response to camelCase ProjectInfo */ +export function normalizeProject(p: any): ProjectInfo { + return { + id: p.id, + name: p.name, + description: p.description || '', + customerType: p.customerType || p.customer_type || 'new', + status: p.status || 'active', + projectVersion: p.projectVersion ?? p.project_version ?? 1, + completionPercentage: p.completionPercentage ?? p.completion_percentage ?? 0, + createdAt: p.createdAt || p.created_at || '', + updatedAt: p.updatedAt || p.updated_at || '', + } +} + +interface CreateProjectDialogProps { + open: boolean + onClose: () => void + onCreated: (project: ProjectInfo) => void + existingProjects: ProjectInfo[] +} + +export function CreateProjectDialog({ open, onClose, onCreated, existingProjects }: CreateProjectDialogProps) { + const { createProject } = useSDK() + const [name, setName] = useState('') + const [customerType, setCustomerType] = useState('new') + const [copyFromId, setCopyFromId] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState('') + + if (!open) return null + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) { + setError('Projektname ist erforderlich') + return + } + + setIsSubmitting(true) + setError('') + try { + const project = await createProject( + name.trim(), + customerType, + copyFromId || undefined + ) + onCreated(normalizeProject(project)) + setName('') + setCopyFromId('') + onClose() + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler beim Erstellen des Projekts') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
e.stopPropagation()} + > +

Neues Projekt erstellen

+ +
+
+ + setName(e.target.value)} + placeholder="z.B. KI-Produkt X, SaaS API, Tochter GmbH..." + className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 outline-none" + autoFocus + /> +
+ +
+ +
+ + +
+
+ + {existingProjects.length > 0 && ( +
+ + +

+ Firmenprofil wird kopiert und kann dann unabhaengig bearbeitet werden. +

+
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+
+ ) +} diff --git a/admin-compliance/components/sdk/ProjectSelector/ProjectActionDialog.tsx b/admin-compliance/components/sdk/ProjectSelector/ProjectActionDialog.tsx new file mode 100644 index 0000000..2ba35fc --- /dev/null +++ b/admin-compliance/components/sdk/ProjectSelector/ProjectActionDialog.tsx @@ -0,0 +1,140 @@ +'use client' + +import React, { useState } from 'react' +import type { ProjectInfo } from '@/lib/sdk/types' + +type ActionStep = 'choose' | 'confirm-delete' + +interface ProjectActionDialogProps { + project: ProjectInfo + onArchive: () => void + onPermanentDelete: () => void + onCancel: () => void + isProcessing: boolean +} + +export function ProjectActionDialog({ + project, + onArchive, + onPermanentDelete, + onCancel, + isProcessing, +}: ProjectActionDialogProps) { + const [step, setStep] = useState('choose') + + if (step === 'confirm-delete') { + return ( +
+
e.stopPropagation()} + > +
+
+ + + +
+

Endgueltig loeschen

+
+ +
+

+ Sind Sie sicher, dass Sie {project.name} unwiderruflich loeschen moechten? +

+

+ Alle Projektdaten, SDK-States und Dokumente werden permanent geloescht. Diese Aktion kann nicht rueckgaengig gemacht werden. +

+
+ +
+ + +
+
+
+ ) + } + + return ( +
+
e.stopPropagation()} + > +
+
+ + + +
+

Projekt entfernen

+
+ +

+ Was moechten Sie mit dem Projekt {project.name} tun? +

+ +
+ + + +
+ + +
+
+ ) +} diff --git a/admin-compliance/components/sdk/ProjectSelector/ProjectCard.tsx b/admin-compliance/components/sdk/ProjectSelector/ProjectCard.tsx new file mode 100644 index 0000000..28abced --- /dev/null +++ b/admin-compliance/components/sdk/ProjectSelector/ProjectCard.tsx @@ -0,0 +1,128 @@ +'use client' + +import React from 'react' +import type { ProjectInfo } from '@/lib/sdk/types' + +export function formatTimeAgo(dateStr: string): string { + if (!dateStr) return '' + const date = new Date(dateStr) + if (isNaN(date.getTime())) return '' + const now = Date.now() + const diff = now - date.getTime() + const seconds = Math.floor(diff / 1000) + if (seconds < 60) return 'Gerade eben' + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `vor ${minutes} Min` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `vor ${hours} Std` + const days = Math.floor(hours / 24) + return `vor ${days} Tag${days > 1 ? 'en' : ''}` +} + +interface ProjectCardProps { + project: ProjectInfo + onClick: () => void + onDelete?: () => void + onRestore?: () => void +} + +export function ProjectCard({ + project, + onClick, + onDelete, + onRestore, +}: ProjectCardProps) { + const timeAgo = formatTimeAgo(project.updatedAt) + const isArchived = project.status === 'archived' + + return ( +
+ {/* Action buttons */} +
+ {isArchived && onRestore && ( + + )} + {onDelete && ( + + )} +
+ + {/* Card content */} + +
+ ) +} diff --git a/admin-compliance/components/sdk/ProjectSelector/ProjectSelector.tsx b/admin-compliance/components/sdk/ProjectSelector/ProjectSelector.tsx index d7b4ffa..4fda39a 100644 --- a/admin-compliance/components/sdk/ProjectSelector/ProjectSelector.tsx +++ b/admin-compliance/components/sdk/ProjectSelector/ProjectSelector.tsx @@ -3,454 +3,10 @@ import React, { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useSDK } from '@/lib/sdk' -import type { ProjectInfo, CustomerType } from '@/lib/sdk/types' - -// ============================================================================= -// HELPERS -// ============================================================================= - -/** Map snake_case backend response to camelCase ProjectInfo */ -function normalizeProject(p: any): ProjectInfo { - return { - id: p.id, - name: p.name, - description: p.description || '', - customerType: p.customerType || p.customer_type || 'new', - status: p.status || 'active', - projectVersion: p.projectVersion ?? p.project_version ?? 1, - completionPercentage: p.completionPercentage ?? p.completion_percentage ?? 0, - createdAt: p.createdAt || p.created_at || '', - updatedAt: p.updatedAt || p.updated_at || '', - } -} - -function formatTimeAgo(dateStr: string): string { - if (!dateStr) return '' - const date = new Date(dateStr) - if (isNaN(date.getTime())) return '' - const now = Date.now() - const diff = now - date.getTime() - const seconds = Math.floor(diff / 1000) - if (seconds < 60) return 'Gerade eben' - const minutes = Math.floor(seconds / 60) - if (minutes < 60) return `vor ${minutes} Min` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `vor ${hours} Std` - const days = Math.floor(hours / 24) - return `vor ${days} Tag${days > 1 ? 'en' : ''}` -} - -// ============================================================================= -// CREATE PROJECT DIALOG -// ============================================================================= - -interface CreateProjectDialogProps { - open: boolean - onClose: () => void - onCreated: (project: ProjectInfo) => void - existingProjects: ProjectInfo[] -} - -function CreateProjectDialog({ open, onClose, onCreated, existingProjects }: CreateProjectDialogProps) { - const { createProject } = useSDK() - const [name, setName] = useState('') - const [customerType, setCustomerType] = useState('new') - const [copyFromId, setCopyFromId] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - const [error, setError] = useState('') - - if (!open) return null - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!name.trim()) { - setError('Projektname ist erforderlich') - return - } - - setIsSubmitting(true) - setError('') - try { - const project = await createProject( - name.trim(), - customerType, - copyFromId || undefined - ) - onCreated(normalizeProject(project)) - setName('') - setCopyFromId('') - onClose() - } catch (err) { - setError(err instanceof Error ? err.message : 'Fehler beim Erstellen des Projekts') - } finally { - setIsSubmitting(false) - } - } - - return ( -
-
e.stopPropagation()} - > -

Neues Projekt erstellen

- -
- {/* Project Name */} -
- - setName(e.target.value)} - placeholder="z.B. KI-Produkt X, SaaS API, Tochter GmbH..." - className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 outline-none" - autoFocus - /> -
- - {/* Customer Type */} -
- -
- - -
-
- - {/* Copy from existing project */} - {existingProjects.length > 0 && ( -
- - -

- Firmenprofil wird kopiert und kann dann unabhaengig bearbeitet werden. -

-
- )} - - {/* Error */} - {error && ( -
- {error} -
- )} - - {/* Actions */} -
- - -
-
-
-
- ) -} - -// ============================================================================= -// PROJECT ACTION DIALOG (Archive / Permanent Delete) -// ============================================================================= - -type ActionStep = 'choose' | 'confirm-delete' - -function ProjectActionDialog({ - project, - onArchive, - onPermanentDelete, - onCancel, - isProcessing, -}: { - project: ProjectInfo - onArchive: () => void - onPermanentDelete: () => void - onCancel: () => void - isProcessing: boolean -}) { - const [step, setStep] = useState('choose') - - if (step === 'confirm-delete') { - return ( -
-
e.stopPropagation()} - > -
-
- - - -
-

Endgueltig loeschen

-
- -
-

- Sind Sie sicher, dass Sie {project.name} unwiderruflich loeschen moechten? -

-

- Alle Projektdaten, SDK-States und Dokumente werden permanent geloescht. Diese Aktion kann nicht rueckgaengig gemacht werden. -

-
- -
- - -
-
-
- ) - } - - return ( -
-
e.stopPropagation()} - > -
-
- - - -
-

Projekt entfernen

-
- -

- Was moechten Sie mit dem Projekt {project.name} tun? -

- -
- {/* Archive option */} - - - {/* Permanent delete option */} - -
- - -
-
- ) -} - -// ============================================================================= -// PROJECT CARD -// ============================================================================= - -function ProjectCard({ - project, - onClick, - onDelete, - onRestore, -}: { - project: ProjectInfo - onClick: () => void - onDelete?: () => void - onRestore?: () => void -}) { - const timeAgo = formatTimeAgo(project.updatedAt) - const isArchived = project.status === 'archived' - - return ( -
- {/* Action buttons */} -
- {isArchived && onRestore && ( - - )} - {onDelete && ( - - )} -
- - {/* Card content */} - -
- ) -} - -// ============================================================================= -// MAIN COMPONENT -// ============================================================================= +import type { ProjectInfo } from '@/lib/sdk/types' +import { CreateProjectDialog, normalizeProject } from './CreateProjectDialog' +import { ProjectActionDialog } from './ProjectActionDialog' +import { ProjectCard } from './ProjectCard' export function ProjectSelector() { const router = useRouter() @@ -494,7 +50,6 @@ export function ProjectSelector() { setIsProcessing(true) try { await archiveProject(actionTarget.id) - // Move from active to archived setProjects(prev => prev.filter(p => p.id !== actionTarget.id)) setArchivedProjects(prev => [...prev, { ...actionTarget, status: 'archived' as const }]) setActionTarget(null) diff --git a/admin-compliance/components/sdk/dsfa/AIUseCaseEditorConstants.ts b/admin-compliance/components/sdk/dsfa/AIUseCaseEditorConstants.ts new file mode 100644 index 0000000..0c73357 --- /dev/null +++ b/admin-compliance/components/sdk/dsfa/AIUseCaseEditorConstants.ts @@ -0,0 +1,29 @@ +import { AIModuleReviewTriggerType } from '@/lib/sdk/dsfa/ai-use-case-types' + +export const TABS = [ + { id: 1, label: 'System', icon: '🖥️' }, + { id: 2, label: 'Daten', icon: '📊' }, + { id: 3, label: 'Zweck & Art. 22', icon: '⚖️' }, + { id: 4, label: 'KI-Kriterien', icon: '🔍' }, + { id: 5, label: 'Risiken', icon: '⚠️' }, + { id: 6, label: 'Maßnahmen', icon: '🛡️' }, + { id: 7, label: 'Review', icon: '🔄' }, +] + +export const REVIEW_TRIGGER_TYPES: { value: AIModuleReviewTriggerType; label: string; icon: string }[] = [ + { value: 'model_update', label: 'Modell-Update', icon: '🔄' }, + { value: 'data_drift', label: 'Datendrift', icon: '📉' }, + { value: 'accuracy_drop', label: 'Genauigkeitsabfall', icon: '📊' }, + { value: 'new_use_case', label: 'Neuer Anwendungsfall', icon: '🎯' }, + { value: 'regulatory_change', label: 'Regulatorische Änderung', icon: '📜' }, + { value: 'incident', label: 'Sicherheitsvorfall', icon: '🚨' }, + { value: 'periodic', label: 'Regelmäßig (zeitbasiert)', icon: '📅' }, +] + +export const LEGAL_BASES = [ + 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)', + 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)', + 'Art. 6 Abs. 1 lit. c DSGVO (Rechtliche Verpflichtung)', + 'Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse)', + 'Art. 9 Abs. 2 lit. a DSGVO (Ausdrückliche Einwilligung)', +] diff --git a/admin-compliance/components/sdk/dsfa/AIUseCaseEditorTabs.tsx b/admin-compliance/components/sdk/dsfa/AIUseCaseEditorTabs.tsx new file mode 100644 index 0000000..3745fb0 --- /dev/null +++ b/admin-compliance/components/sdk/dsfa/AIUseCaseEditorTabs.tsx @@ -0,0 +1,4 @@ +// Barrel re-export — implementation split into focused files +export { Tab1System, Tab2Data } from './AIUseCaseTabsSystemData' +export { Tab3Purpose, Tab4AIAct } from './AIUseCaseTabsPurposeAct' +export { Tab5Risks, Tab6PrivacyByDesign, Tab7Review } from './AIUseCaseTabsRisksReview' diff --git a/admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx b/admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx index fa843ca..7c2671b 100644 --- a/admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx +++ b/admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx @@ -3,10 +3,7 @@ import React, { useState } from 'react' import { AIUseCaseModule, - AIUseCaseType, - AIActRiskClass, AI_USE_CASE_TYPES, - AI_ACT_RISK_CLASSES, PRIVACY_BY_DESIGN_CATEGORIES, PrivacyByDesignCategory, PrivacyByDesignMeasure, @@ -14,8 +11,16 @@ import { AIModuleReviewTriggerType, checkArt22Applicability, } from '@/lib/sdk/dsfa/ai-use-case-types' -import { Art22AssessmentPanel } from './Art22AssessmentPanel' -import { AIRiskCriteriaChecklist } from './AIRiskCriteriaChecklist' +import { TABS, REVIEW_TRIGGER_TYPES } from './AIUseCaseEditorConstants' +import { + Tab1System, + Tab2Data, + Tab3Purpose, + Tab4AIAct, + Tab5Risks, + Tab6PrivacyByDesign, + Tab7Review, +} from './AIUseCaseEditorTabs' interface AIUseCaseModuleEditorProps { module: AIUseCaseModule @@ -24,34 +29,6 @@ interface AIUseCaseModuleEditorProps { isSaving?: boolean } -const TABS = [ - { id: 1, label: 'System', icon: '🖥️' }, - { id: 2, label: 'Daten', icon: '📊' }, - { id: 3, label: 'Zweck & Art. 22', icon: '⚖️' }, - { id: 4, label: 'KI-Kriterien', icon: '🔍' }, - { id: 5, label: 'Risiken', icon: '⚠️' }, - { id: 6, label: 'Maßnahmen', icon: '🛡️' }, - { id: 7, label: 'Review', icon: '🔄' }, -] - -const REVIEW_TRIGGER_TYPES: { value: AIModuleReviewTriggerType; label: string; icon: string }[] = [ - { value: 'model_update', label: 'Modell-Update', icon: '🔄' }, - { value: 'data_drift', label: 'Datendrift', icon: '📉' }, - { value: 'accuracy_drop', label: 'Genauigkeitsabfall', icon: '📊' }, - { value: 'new_use_case', label: 'Neuer Anwendungsfall', icon: '🎯' }, - { value: 'regulatory_change', label: 'Regulatorische Änderung', icon: '📜' }, - { value: 'incident', label: 'Sicherheitsvorfall', icon: '🚨' }, - { value: 'periodic', label: 'Regelmäßig (zeitbasiert)', icon: '📅' }, -] - -const LEGAL_BASES = [ - 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)', - 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)', - 'Art. 6 Abs. 1 lit. c DSGVO (Rechtliche Verpflichtung)', - 'Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse)', - 'Art. 9 Abs. 2 lit. a DSGVO (Ausdrückliche Einwilligung)', -] - export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel, isSaving }: AIUseCaseModuleEditorProps) { const [activeTab, setActiveTab] = useState(1) const [module, setModule] = useState(initialModule) @@ -146,514 +123,26 @@ export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel, {/* Content */}
- {/* Tab 1: Systembeschreibung */} - {activeTab === 1 && ( -
-
- - update({ name: e.target.value })} - placeholder={`z.B. ${typeInfo.label} für Kundenservice`} - className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - /> -
-
- -