refactor(admin): split advisory-board page.tsx into colocated components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
currentStep: number
|
||||
isSubmitting: boolean
|
||||
isEditMode: boolean
|
||||
titleEmpty: boolean
|
||||
onBack: () => void
|
||||
onNext: () => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
export function NavigationButtons({ currentStep, isSubmitting, isEditMode, titleEmpty, onBack, onNext, onSubmit }: Props) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
{currentStep === 1 ? 'Abbrechen' : 'Zurueck'}
|
||||
</button>
|
||||
|
||||
{currentStep < 8 ? (
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={currentStep === 1 && titleEmpty}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitting || titleEmpty}
|
||||
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
Bewerte...
|
||||
</>
|
||||
) : (
|
||||
isEditMode ? 'Speichern & neu bewerten' : 'Assessment starten'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard'
|
||||
|
||||
interface Props {
|
||||
result: unknown
|
||||
onGoToAssessment: (id: string) => void
|
||||
onGoToOverview: () => void
|
||||
}
|
||||
|
||||
export function ResultView({ result, onGoToAssessment, onGoToOverview }: Props) {
|
||||
const r = result as { assessment?: { id: string }; result?: Record<string, unknown> }
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Assessment Ergebnis</h1>
|
||||
<div className="flex gap-2">
|
||||
{r.assessment?.id && (
|
||||
<button
|
||||
onClick={() => onGoToAssessment(r.assessment!.id)}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Zum Assessment
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onGoToOverview}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Zur Uebersicht
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{r.result && (
|
||||
<AssessmentResultCard result={r.result as unknown as Parameters<typeof AssessmentResultCard>[0]['result']} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { AI_USE_CATEGORIES } from '../_data'
|
||||
|
||||
interface Props extends StepProps {
|
||||
profileIndustry: string | string[] | undefined
|
||||
}
|
||||
|
||||
export function Step1Basics({ form, updateForm, profileIndustry }: Props) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Grundlegende Informationen</h2>
|
||||
|
||||
{/* Branche aus Profil (nur Anzeige) */}
|
||||
{profileIndustry && (Array.isArray(profileIndustry) ? profileIndustry.length > 0 : true) && (
|
||||
<div className="bg-gray-50 rounded-lg border border-gray-200 px-4 py-3">
|
||||
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">Branche (aus Unternehmensprofil)</span>
|
||||
<p className="text-sm text-gray-900 mt-0.5">
|
||||
{Array.isArray(profileIndustry) ? profileIndustry.join(', ') : profileIndustry}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Titel des Anwendungsfalls</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={e => updateForm({ title: e.target.value })}
|
||||
placeholder="z.B. Chatbot fuer Kundenservice"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={form.use_case_text}
|
||||
onChange={e => updateForm({ use_case_text: e.target.value })}
|
||||
rows={4}
|
||||
placeholder="Beschreiben Sie den Anwendungsfall..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* KI-Anwendungskategorie als Kacheln */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
In welchem Bereich kommt KI zum Einsatz?
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-3">Waehlen Sie die passende Kategorie fuer Ihren Anwendungsfall.</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{AI_USE_CATEGORIES.map(cat => (
|
||||
<button
|
||||
key={cat.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ category: cat.value })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.category === cat.value
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{cat.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{cat.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{cat.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { DATA_CATEGORY_GROUPS } from '../_data-categories'
|
||||
import { toggleInArray } from '../_data'
|
||||
|
||||
export function Step2DataCategories({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Welche Daten werden verarbeitet?</h2>
|
||||
<p className="text-sm text-gray-500">Waehlen Sie alle Datenkategorien, die in diesem Use Case verarbeitet werden.</p>
|
||||
|
||||
{DATA_CATEGORY_GROUPS.map(group => (
|
||||
<div key={group.group}>
|
||||
<h3 className={`text-sm font-semibold mb-2 ${group.art9 ? 'text-orange-700' : 'text-gray-700'}`}>
|
||||
{group.art9 && '⚠️ '}{group.group}
|
||||
</h3>
|
||||
{group.art9 && (
|
||||
<p className="text-xs text-orange-600 mb-2">Besonders schutzwuerdig — erhoehte Anforderungen an Rechtsgrundlage und TOM</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mb-4">
|
||||
{group.items.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ data_categories: toggleInArray(form.data_categories, item.value) })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.data_categories.includes(item.value)
|
||||
? group.art9
|
||||
? 'border-orange-500 bg-orange-50 ring-1 ring-orange-300'
|
||||
: 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Sonstige Datentypen */}
|
||||
<div className="border border-gray-200 rounded-lg p-4 space-y-3">
|
||||
<div className="font-medium text-gray-900">Sonstige Datentypen</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Falls Ihre Datenkategorie oben nicht aufgefuehrt ist, koennen Sie sie hier ergaenzen.
|
||||
</p>
|
||||
{form.custom_data_types.map((dt, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={dt}
|
||||
onChange={e => {
|
||||
const updated = [...form.custom_data_types]
|
||||
updated[idx] = e.target.value
|
||||
updateForm({ custom_data_types: updated })
|
||||
}}
|
||||
placeholder="Datentyp eingeben..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateForm({ custom_data_types: form.custom_data_types.filter((_, i) => i !== idx) })}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg"
|
||||
title="Entfernen"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => updateForm({ custom_data_types: [...form.custom_data_types, ''] })}
|
||||
className="flex items-center gap-1 text-sm text-purple-600 hover:text-purple-700 font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
||||
Weiteren Datentyp hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{form.data_categories.length > 0 && (
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg px-4 py-3 text-sm text-purple-800">
|
||||
<span className="font-medium">{form.data_categories.length}</span> Datenkategorie{form.data_categories.length !== 1 ? 'n' : ''} ausgewaehlt
|
||||
{form.data_categories.some(c => DATA_CATEGORY_GROUPS.find(g => g.art9)?.items.some(i => i.value === c)) && (
|
||||
<span className="ml-2 text-orange-700 font-medium">— inkl. besonderer Kategorien (Art. 9)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { PURPOSE_TILES } from '../_tiles'
|
||||
import { toggleInArray } from '../_data'
|
||||
|
||||
export function Step3Purposes({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Zweck der Verarbeitung</h2>
|
||||
<p className="text-sm text-gray-500">Waehlen Sie alle zutreffenden Verarbeitungszwecke. Die passende Rechtsgrundlage wird vom SDK automatisch ermittelt.</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{PURPOSE_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ purposes: toggleInArray(form.purposes, item.value) })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.purposes.includes(item.value)
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{form.purposes.includes('profiling') && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 text-sm text-amber-800">
|
||||
<div className="font-medium mb-1">Hinweis: Profiling</div>
|
||||
<p>Profiling unterliegt besonderen Anforderungen nach Art. 22 DSGVO. Betroffene haben das Recht auf Information und Widerspruch.</p>
|
||||
</div>
|
||||
)}
|
||||
{form.purposes.includes('automated_decision') && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-800">
|
||||
<div className="font-medium mb-1">Achtung: Automatisierte Entscheidung</div>
|
||||
<p>Art. 22 DSGVO: Vollautomatisierte Entscheidungen mit rechtlicher Wirkung erfordern besondere Schutzmassnahmen, Informationspflichten und das Recht auf menschliche Ueberpruefung.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { AUTOMATION_TILES } from '../_tiles'
|
||||
|
||||
export function Step4Automation({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Grad der Automatisierung</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Wie stark greift die KI in Entscheidungen ein? Je hoeher der Automatisierungsgrad, desto strenger die regulatorischen Anforderungen.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{AUTOMATION_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ automation: item.value })}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
form.automation === item.value
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-2xl">{item.icon}</span>
|
||||
<span className="text-sm font-semibold text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 ml-11">{item.desc}</p>
|
||||
<p className="text-xs text-gray-400 ml-11 mt-1">Beispiele: {item.examples}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
|
||||
<div className="font-medium mb-1">Warum ist das wichtig?</div>
|
||||
<p>
|
||||
Art. 22 DSGVO regelt automatisierte Einzelentscheidungen. Vollautomatisierte Systeme, die Personen
|
||||
erheblich beeinflussen (z.B. Kreditvergabe, Bewerbungsauswahl), unterliegen strengen Auflagen:
|
||||
Informationspflicht, Recht auf menschliche Ueberpruefung und Anfechtungsmoeglichkeit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { HOSTING_PROVIDER_TILES, HOSTING_REGION_TILES, MODEL_USAGE_TILES } from '../_tiles'
|
||||
import { toggleInArray } from '../_data'
|
||||
|
||||
export function Step5Hosting({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Technische Details</h2>
|
||||
|
||||
{/* Hosting Provider */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">Hosting-Anbieter</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{HOSTING_PROVIDER_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ hosting_provider: item.value })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.hosting_provider === item.value
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hosting Region */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">Hosting-Region</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{HOSTING_REGION_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ hosting_region: item.value })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.hosting_region === item.value
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Usage */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-1">Wie wird das KI-Modell genutzt?</h3>
|
||||
<p className="text-sm text-gray-500 mb-3">Waehlen Sie alle zutreffenden Optionen.</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{MODEL_USAGE_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ model_usage: toggleInArray(form.model_usage, item.value) })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.model_usage.includes(item.value)
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info-Box: Begriffe erklaert */}
|
||||
<details className="bg-amber-50 border border-amber-200 rounded-lg overflow-hidden">
|
||||
<summary className="px-4 py-3 text-sm font-medium text-amber-800 cursor-pointer hover:bg-amber-100">
|
||||
Begriffe erklaert: ML, DL, NLP, LLM — Was bedeutet das?
|
||||
</summary>
|
||||
<div className="px-4 pb-4 space-y-3 text-sm text-amber-900">
|
||||
<div><span className="font-semibold">ML (Machine Learning)</span> — Computer lernt Muster aus Daten. Beispiel: Spam-Filter.</div>
|
||||
<div><span className="font-semibold">DL (Deep Learning)</span> — ML mit neuronalen Netzen. Beispiel: Bilderkennung, Spracherkennung.</div>
|
||||
<div><span className="font-semibold">NLP (Natural Language Processing)</span> — KI versteht Sprache. Beispiel: ChatGPT, DeepL.</div>
|
||||
<div><span className="font-semibold">LLM (Large Language Model)</span> — Grosses Sprachmodell. Beispiel: GPT-4, Claude, Llama.</div>
|
||||
<div><span className="font-semibold">RAG</span> — LLM erhaelt Kontext aus eigener Datenbank. Vorteil: Aktuelle, firmenspezifische Antworten.</div>
|
||||
<div><span className="font-semibold">Fine-Tuning</span> — Bestehendes Modell mit eigenen Daten weitertrainieren. Achtung: Daten werden Teil des Modells.</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { TRANSFER_TARGET_TILES, TRANSFER_MECHANISM_TILES } from '../_tiles'
|
||||
import { toggleInArray } from '../_data'
|
||||
|
||||
export function Step6Transfer({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Internationaler Datentransfer</h2>
|
||||
<p className="text-sm text-gray-500">Wohin werden die Daten uebermittelt? Waehlen Sie alle zutreffenden Ziellaender/-regionen.</p>
|
||||
|
||||
{/* Transfer Targets */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">Datentransfer-Ziele</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{TRANSFER_TARGET_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ transfer_targets: toggleInArray(form.transfer_targets, item.value) })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.transfer_targets.includes(item.value)
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transfer Mechanism — only if not "no_transfer" only */}
|
||||
{form.transfer_targets.length > 0 && !form.transfer_targets.every(t => t === 'no_transfer') && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">Transfer-Mechanismus</h3>
|
||||
<p className="text-sm text-gray-500 mb-3">Welche Schutzgarantie nutzen Sie fuer den Drittlandtransfer?</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{TRANSFER_MECHANISM_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ transfer_mechanism: item.value })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.transfer_mechanism === item.value
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Specific countries text input */}
|
||||
{form.transfer_targets.some(t => !['no_transfer'].includes(t)) && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Konkrete Ziellaender (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.transfer_countries.join(', ')}
|
||||
onChange={e => updateForm({ transfer_countries: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
|
||||
placeholder="z.B. USA, UK, Schweiz, Japan"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Kommagetrennte Laendernamen oder -kuerzel</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { RETENTION_TILES } from '../_tiles'
|
||||
|
||||
export function Step7Retention({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Datenhaltung & Aufbewahrung</h2>
|
||||
<p className="text-sm text-gray-500">Wie lange sollen die Daten gespeichert werden?</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{RETENTION_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ retention_period: item.value })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.retention_period === item.value
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Zweck der Aufbewahrung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={form.retention_purpose}
|
||||
onChange={e => updateForm({ retention_purpose: e.target.value })}
|
||||
rows={2}
|
||||
placeholder="z.B. Vertragliche Pflichten, gesetzliche Aufbewahrungsfristen..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{form.retention_period === 'indefinite' && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 text-sm text-amber-800">
|
||||
<div className="font-medium mb-1">Hinweis: Unbefristete Speicherung</div>
|
||||
<p>Die DSGVO fordert Datenminimierung und Speicherbegrenzung (Art. 5 Abs. 1e). Unbefristete Speicherung muss besonders gut begruendet sein.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { StepProps } from '../_types'
|
||||
import { CONTRACT_TILES } from '../_tiles'
|
||||
import { toggleInArray } from '../_data'
|
||||
|
||||
export function Step8Contracts({ form, updateForm }: StepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Vertraege & Compliance-Dokumentation</h2>
|
||||
<p className="text-sm text-gray-500">Welche Compliance-Dokumente liegen bereits vor? (Mehrfachauswahl moeglich)</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{CONTRACT_TILES.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateForm({ contracts: toggleInArray(form.contracts, item.value) })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
form.contracts.includes(item.value)
|
||||
? item.value === 'none'
|
||||
? 'border-amber-500 bg-amber-50 ring-1 ring-amber-300'
|
||||
: 'border-purple-500 bg-purple-50 ring-1 ring-purple-300'
|
||||
: 'border-gray-200 hover:border-purple-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-tight">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Subprozessoren (optional)</label>
|
||||
<textarea
|
||||
value={form.subprocessors}
|
||||
onChange={e => updateForm({ subprocessors: e.target.value })}
|
||||
rows={2}
|
||||
placeholder="z.B. OpenAI (USA, SCC), Hetzner Cloud (DE)..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { WIZARD_STEPS } from '../_data'
|
||||
|
||||
interface Props {
|
||||
currentStep: number
|
||||
onStepClick: (id: number) => void
|
||||
}
|
||||
|
||||
export function StepIndicator({ currentStep, onStepClick }: Props) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{WIZARD_STEPS.map((step, idx) => (
|
||||
<React.Fragment key={step.id}>
|
||||
<button
|
||||
onClick={() => onStepClick(step.id)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
currentStep === step.id
|
||||
? 'bg-purple-600 text-white'
|
||||
: currentStep > step.id
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<span className="w-6 h-6 rounded-full bg-white/20 flex items-center justify-center text-xs font-bold">
|
||||
{currentStep > step.id ? '✓' : step.id}
|
||||
</span>
|
||||
<span className="hidden md:inline">{step.title}</span>
|
||||
</button>
|
||||
{idx < WIZARD_STEPS.length - 1 && <div className="flex-1 h-px bg-gray-200" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user