feat(admin-v2): Major SDK/Compliance overhaul and new modules

SDK modules added/enhanced:
- compliance-hub, compliance-scope, consent-management, notfallplan
- audit-report, workflow, source-policy, dsms
- advisory-board documentation section
- TOM dashboard components, TOM generator SDM mapping
- DSFA: mitigation library, risk catalog, threshold analysis, source attribution
- VVT: baseline catalog, profiling engine, types
- Loeschfristen: baseline catalog, compliance engine, export, profiling, types
- Compliance scope: engine, profiling, golden tests, types

Existing SDK pages updated:
- dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality
- SDKSidebar, StepHeader — new navigation items and layout
- SDK layout, context, types — expanded type system

Other admin-v2 changes:
- AI agents page, RAG pipeline DSFA integration
- GridOverlay component updates
- Companion feature (development + education)
- Compliance advisor SOUL definition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-10 00:01:04 +01:00
parent ee0c4b859c
commit 870302a82b
94 changed files with 29706 additions and 1039 deletions

View File

@@ -0,0 +1,15 @@
'use client'
import { TOMGeneratorProvider } from '@/lib/sdk/tom-generator/context'
import { useSDK } from '@/lib/sdk'
export default function TOMLayout({ children }: { children: React.ReactNode }) {
const { state } = useSDK()
const tenantId = state?.tenantId || 'default'
return (
<TOMGeneratorProvider tenantId={tenantId}>
{children}
</TOMGeneratorProvider>
)
}

View File

@@ -1,377 +1,356 @@
'use client'
import React, { useState, useCallback } from 'react'
import React, { useState, useCallback, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { DocumentUploadSection, type UploadedDocument } from '@/components/sdk'
import { useTOMGenerator } from '@/lib/sdk/tom-generator/context'
import { DerivedTOM } from '@/lib/sdk/tom-generator/types'
import { TOMOverviewTab, TOMEditorTab, TOMGapExportTab } from '@/components/sdk/tom-dashboard'
// =============================================================================
// TYPES
// =============================================================================
interface TOM {
id: string
title: string
description: string
category: 'confidentiality' | 'integrity' | 'availability' | 'resilience'
type: 'technical' | 'organizational'
status: 'implemented' | 'partial' | 'planned' | 'not-implemented'
article32Reference: string
lastReview: Date
nextReview: Date
responsible: string
documentation: string | null
type Tab = 'uebersicht' | 'editor' | 'generator' | 'gap-export'
interface TabDefinition {
key: Tab
label: string
}
// =============================================================================
// MOCK DATA
// =============================================================================
const mockTOMs: TOM[] = [
{
id: 'tom-1',
title: 'Zutrittskontrolle',
description: 'Physische Zugangskontrolle zu Serverraeumen und Rechenzentren',
category: 'confidentiality',
type: 'technical',
status: 'implemented',
article32Reference: 'Art. 32 Abs. 1 lit. b',
lastReview: new Date('2024-01-01'),
nextReview: new Date('2024-07-01'),
responsible: 'Facility Management',
documentation: 'TOM-001-Zutrittskontrolle.pdf',
},
{
id: 'tom-2',
title: 'Zugangskontrolle',
description: 'Authentifizierung und Autorisierung fuer IT-Systeme',
category: 'confidentiality',
type: 'technical',
status: 'implemented',
article32Reference: 'Art. 32 Abs. 1 lit. b',
lastReview: new Date('2024-01-15'),
nextReview: new Date('2024-07-15'),
responsible: 'IT Security',
documentation: 'TOM-002-Zugangskontrolle.pdf',
},
{
id: 'tom-3',
title: 'Verschluesselung',
description: 'Verschluesselung von Daten bei Speicherung und Uebertragung',
category: 'confidentiality',
type: 'technical',
status: 'implemented',
article32Reference: 'Art. 32 Abs. 1 lit. a',
lastReview: new Date('2024-01-10'),
nextReview: new Date('2024-07-10'),
responsible: 'IT Security',
documentation: 'TOM-003-Verschluesselung.pdf',
},
{
id: 'tom-4',
title: 'Datensicherung',
description: 'Regelmaessige Backups und Wiederherstellungstests',
category: 'availability',
type: 'technical',
status: 'implemented',
article32Reference: 'Art. 32 Abs. 1 lit. c',
lastReview: new Date('2023-12-01'),
nextReview: new Date('2024-06-01'),
responsible: 'IT Operations',
documentation: 'TOM-004-Backup.pdf',
},
{
id: 'tom-5',
title: 'Datenschutzschulung',
description: 'Regelmaessige Schulungen fuer alle Mitarbeiter',
category: 'confidentiality',
type: 'organizational',
status: 'partial',
article32Reference: 'Art. 32 Abs. 1 lit. b',
lastReview: new Date('2023-11-01'),
nextReview: new Date('2024-02-01'),
responsible: 'HR / Datenschutz',
documentation: null,
},
{
id: 'tom-6',
title: 'Incident Response Plan',
description: 'Prozess zur Behandlung von Sicherheitsvorfaellen',
category: 'resilience',
type: 'organizational',
status: 'planned',
article32Reference: 'Art. 32 Abs. 1 lit. c',
lastReview: new Date('2024-01-20'),
nextReview: new Date('2024-04-20'),
responsible: 'CISO',
documentation: null,
},
{
id: 'tom-7',
title: 'Protokollierung',
description: 'Logging aller sicherheitsrelevanten Ereignisse',
category: 'integrity',
type: 'technical',
status: 'implemented',
article32Reference: 'Art. 32 Abs. 1 lit. b',
lastReview: new Date('2024-01-05'),
nextReview: new Date('2024-07-05'),
responsible: 'IT Security',
documentation: 'TOM-007-Logging.pdf',
},
const TABS: TabDefinition[] = [
{ key: 'uebersicht', label: 'Uebersicht' },
{ key: 'editor', label: 'Detail-Editor' },
{ key: 'generator', label: 'Generator' },
{ key: 'gap-export', label: 'Gap-Analyse & Export' },
]
// =============================================================================
// COMPONENTS
// =============================================================================
function TOMCard({ tom }: { tom: TOM }) {
const categoryColors = {
confidentiality: 'bg-blue-100 text-blue-700',
integrity: 'bg-green-100 text-green-700',
availability: 'bg-purple-100 text-purple-700',
resilience: 'bg-orange-100 text-orange-700',
}
const categoryLabels = {
confidentiality: 'Vertraulichkeit',
integrity: 'Integritaet',
availability: 'Verfuegbarkeit',
resilience: 'Belastbarkeit',
}
const statusColors = {
implemented: 'bg-green-100 text-green-700 border-green-200',
partial: 'bg-yellow-100 text-yellow-700 border-yellow-200',
planned: 'bg-blue-100 text-blue-700 border-blue-200',
'not-implemented': 'bg-red-100 text-red-700 border-red-200',
}
const statusLabels = {
implemented: 'Implementiert',
partial: 'Teilweise',
planned: 'Geplant',
'not-implemented': 'Nicht implementiert',
}
const isReviewDue = tom.nextReview <= new Date()
return (
<div className={`bg-white rounded-xl border-2 p-6 ${
isReviewDue ? 'border-orange-200' :
tom.status === 'implemented' ? 'border-green-200' : 'border-gray-200'
}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 text-xs rounded-full ${categoryColors[tom.category]}`}>
{categoryLabels[tom.category]}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${
tom.type === 'technical' ? 'bg-gray-100 text-gray-700' : 'bg-purple-50 text-purple-700'
}`}>
{tom.type === 'technical' ? 'Technisch' : 'Organisatorisch'}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${statusColors[tom.status]}`}>
{statusLabels[tom.status]}
</span>
</div>
<h3 className="text-lg font-semibold text-gray-900">{tom.title}</h3>
<p className="text-sm text-gray-500 mt-1">{tom.description}</p>
<p className="text-xs text-gray-400 mt-2">Rechtsgrundlage: {tom.article32Reference}</p>
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">Verantwortlich: </span>
<span className="font-medium text-gray-700">{tom.responsible}</span>
</div>
<div className={isReviewDue ? 'text-orange-600' : ''}>
<span className="text-gray-500">Naechste Pruefung: </span>
<span className="font-medium">
{tom.nextReview.toLocaleDateString('de-DE')}
{isReviewDue && ' (faellig)'}
</span>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
{tom.documentation ? (
<span className="text-sm text-green-600 flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Dokumentiert
</span>
) : (
<span className="text-sm text-gray-400">Keine Dokumentation</span>
)}
<div className="flex items-center gap-2">
<button className="px-3 py-1 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors">
Bearbeiten
</button>
<button className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Pruefung starten
</button>
</div>
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// PAGE COMPONENT
// =============================================================================
export default function TOMPage() {
const router = useRouter()
const { state } = useSDK()
const [toms] = useState<TOM[]>(mockTOMs)
const [filter, setFilter] = useState<string>('all')
const sdk = useSDK()
const { state, dispatch, bulkUpdateTOMs, runGapAnalysis } = useTOMGenerator()
// Handle uploaded document - import into SDK state
const handleDocumentProcessed = useCallback((doc: UploadedDocument) => {
console.log('[TOM Page] Document processed:', doc)
// In production: Parse document content and add to state.toms
// ---------------------------------------------------------------------------
// Local state
// ---------------------------------------------------------------------------
const [tab, setTab] = useState<Tab>('uebersicht')
const [selectedTOMId, setSelectedTOMId] = useState<string | null>(null)
// ---------------------------------------------------------------------------
// Computed / memoised values
// ---------------------------------------------------------------------------
const tomCount = useMemo(() => {
if (!state?.derivedTOMs) return 0
return Array.isArray(state.derivedTOMs)
? state.derivedTOMs.length
: Object.keys(state.derivedTOMs).length
}, [state?.derivedTOMs])
const lastModifiedFormatted = useMemo(() => {
if (!state?.metadata?.lastModified) return null
try {
const date = new Date(state.metadata.lastModified)
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return null
}
}, [state?.metadata?.lastModified])
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handleSelectTOM = useCallback((tomId: string) => {
setSelectedTOMId(tomId)
setTab('editor')
}, [])
// Open document in workflow editor
const handleOpenInEditor = useCallback((doc: UploadedDocument) => {
router.push(`/compliance/workflow?documentType=tom&documentId=${doc.id}&mode=change`)
const handleUpdateTOM = useCallback(
(tomId: string, updates: Partial<DerivedTOM>) => {
dispatch({
type: 'UPDATE_DERIVED_TOM',
payload: { id: tomId, data: updates },
})
},
[dispatch],
)
const handleStartGenerator = useCallback(() => {
router.push('/sdk/tom-generator')
}, [router])
const filteredTOMs = filter === 'all'
? toms
: toms.filter(t => t.category === filter || t.type === filter || t.status === filter)
const handleBackToOverview = useCallback(() => {
setSelectedTOMId(null)
setTab('uebersicht')
}, [])
const implementedCount = toms.filter(t => t.status === 'implemented').length
const technicalCount = toms.filter(t => t.type === 'technical').length
const organizationalCount = toms.filter(t => t.type === 'organizational').length
const reviewDueCount = toms.filter(t => t.nextReview <= new Date()).length
const handleRunGapAnalysis = useCallback(() => {
if (typeof runGapAnalysis === 'function') {
runGapAnalysis()
}
}, [runGapAnalysis])
const stepInfo = STEP_EXPLANATIONS['tom']
const handleTabChange = useCallback((newTab: Tab) => {
setTab(newTab)
}, [])
return (
<div className="space-y-6">
{/* Step Header */}
<StepHeader
stepId="tom"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
TOM hinzufuegen
</button>
</StepHeader>
// ---------------------------------------------------------------------------
// Render helpers
// ---------------------------------------------------------------------------
{/* Document Upload Section */}
<DocumentUploadSection
documentType="tom"
onDocumentProcessed={handleDocumentProcessed}
onOpenInEditor={handleOpenInEditor}
/>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Gesamt</div>
<div className="text-3xl font-bold text-gray-900">{toms.length}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Implementiert</div>
<div className="text-3xl font-bold text-green-600">{implementedCount}</div>
</div>
<div className="bg-white rounded-xl border border-blue-200 p-6">
<div className="text-sm text-blue-600">Technisch / Organisatorisch</div>
<div className="text-3xl font-bold text-blue-600">{technicalCount} / {organizationalCount}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Pruefung faellig</div>
<div className="text-3xl font-bold text-orange-600">{reviewDueCount}</div>
</div>
</div>
{/* Article 32 Overview */}
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-xl border border-blue-200 p-6">
<h3 className="font-semibold text-gray-900 mb-4">Art. 32 DSGVO - Schutzziele</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-blue-600">
{toms.filter(t => t.category === 'confidentiality').length}
</div>
<div className="text-sm text-gray-500">Vertraulichkeit</div>
</div>
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-600">
{toms.filter(t => t.category === 'integrity').length}
</div>
<div className="text-sm text-gray-500">Integritaet</div>
</div>
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-purple-600">
{toms.filter(t => t.category === 'availability').length}
</div>
<div className="text-sm text-gray-500">Verfuegbarkeit</div>
</div>
<div className="bg-white rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-orange-600">
{toms.filter(t => t.category === 'resilience').length}
</div>
<div className="text-sm text-gray-500">Belastbarkeit</div>
</div>
</div>
</div>
{/* Filter */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'confidentiality', 'integrity', 'availability', 'resilience', 'technical', 'organizational', 'implemented', 'partial'].map(f => (
const renderTabBar = () => (
<div className="flex flex-wrap gap-2">
{TABS.map((t) => {
const isActive = tab === t.key
return (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
key={t.key}
onClick={() => handleTabChange(t.key)}
className={`
rounded-lg px-4 py-2 text-sm font-medium transition-colors
${
isActive
? 'bg-purple-600 text-white shadow-sm'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}
`}
>
{f === 'all' ? 'Alle' :
f === 'confidentiality' ? 'Vertraulichkeit' :
f === 'integrity' ? 'Integritaet' :
f === 'availability' ? 'Verfuegbarkeit' :
f === 'resilience' ? 'Belastbarkeit' :
f === 'technical' ? 'Technisch' :
f === 'organizational' ? 'Organisatorisch' :
f === 'implemented' ? 'Implementiert' : 'Teilweise'}
{t.label}
</button>
))}
</div>
)
})}
</div>
)
{/* TOM List */}
<div className="space-y-4">
{filteredTOMs.map(tom => (
<TOMCard key={tom.id} tom={tom} />
))}
</div>
// ---------------------------------------------------------------------------
// Tab 1 Uebersicht
// ---------------------------------------------------------------------------
{filteredTOMs.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
const renderUebersicht = () => (
<TOMOverviewTab
state={state}
onSelectTOM={handleSelectTOM}
onStartGenerator={handleStartGenerator}
/>
)
// ---------------------------------------------------------------------------
// Tab 2 Detail-Editor
// ---------------------------------------------------------------------------
const renderEditor = () => (
<TOMEditorTab
state={state}
selectedTOMId={selectedTOMId}
onUpdateTOM={handleUpdateTOM}
onBack={handleBackToOverview}
/>
)
// ---------------------------------------------------------------------------
// Tab 3 Generator
// ---------------------------------------------------------------------------
const renderGenerator = () => (
<div className="space-y-6">
{/* Info card */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start gap-4">
{/* Icon */}
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-purple-100 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine TOMs gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder fuegen Sie neue TOMs hinzu.</p>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
TOM Generator &ndash; 6-Schritte-Assistent
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Der TOM Generator fuehrt Sie in 6 Schritten durch die systematische
Ableitung Ihrer technischen und organisatorischen Massnahmen. Sie
beantworten gezielte Fragen zu Ihrem Unternehmen, Ihrer
IT-Infrastruktur und Ihren Verarbeitungstaetigkeiten. Daraus werden
passende TOMs automatisch abgeleitet und priorisiert.
</p>
<div className="bg-purple-50 rounded-lg p-4 mb-4">
<h4 className="text-sm font-semibold text-purple-800 mb-2">
Die 6 Schritte im Ueberblick:
</h4>
<ol className="list-decimal list-inside text-sm text-purple-700 space-y-1">
<li>Unternehmenskontext erfassen</li>
<li>IT-Infrastruktur beschreiben</li>
<li>Verarbeitungstaetigkeiten zuordnen</li>
<li>Risikobewertung durchfuehren</li>
<li>TOM-Ableitung und Priorisierung</li>
<li>Ergebnis pruefen und uebernehmen</li>
</ol>
</div>
<button
onClick={handleStartGenerator}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-6 py-3 text-sm font-medium transition-colors shadow-sm"
>
TOM Generator starten
</button>
</div>
</div>
</div>
{/* Quick stats only rendered when derivedTOMs exist */}
{tomCount > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">
Aktueller Stand
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* TOM count */}
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-500 mb-1">Abgeleitete TOMs</p>
<p className="text-2xl font-bold text-gray-900">{tomCount}</p>
</div>
{/* Last generated date */}
{lastModifiedFormatted && (
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-500 mb-1">Zuletzt generiert</p>
<p className="text-lg font-semibold text-gray-900">
{lastModifiedFormatted}
</p>
</div>
)}
{/* Status */}
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-500 mb-1">Status</p>
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-green-500" />
<p className="text-sm font-medium text-gray-900">
TOMs vorhanden
</p>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-100">
<p className="text-xs text-gray-400">
Sie koennen den Generator jederzeit erneut ausfuehren, um Ihre
TOMs zu aktualisieren oder zu erweitern.
</p>
</div>
</div>
)}
{/* Empty state when no TOMs exist yet */}
{tomCount === 0 && (
<div className="bg-white rounded-xl border border-dashed border-gray-300 p-8 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</div>
<h4 className="text-base font-medium text-gray-700 mb-1">
Noch keine TOMs vorhanden
</h4>
<p className="text-sm text-gray-500 mb-4">
Starten Sie den Generator, um Ihre ersten technischen und
organisatorischen Massnahmen abzuleiten.
</p>
<button
onClick={handleStartGenerator}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
>
Jetzt starten
</button>
</div>
)}
</div>
)
// ---------------------------------------------------------------------------
// Tab 4 Gap-Analyse & Export
// ---------------------------------------------------------------------------
const renderGapExport = () => (
<TOMGapExportTab
state={state}
onRunGapAnalysis={handleRunGapAnalysis}
/>
)
// ---------------------------------------------------------------------------
// Tab content router
// ---------------------------------------------------------------------------
const renderActiveTab = () => {
switch (tab) {
case 'uebersicht':
return renderUebersicht()
case 'editor':
return renderEditor()
case 'generator':
return renderGenerator()
case 'gap-export':
return renderGapExport()
default:
return renderUebersicht()
}
}
// ---------------------------------------------------------------------------
// Main render
// ---------------------------------------------------------------------------
return (
<div className="space-y-6">
{/* Step header */}
<StepHeader stepId="tom" {...STEP_EXPLANATIONS['tom']} />
{/* Tab bar */}
<div className="bg-white rounded-xl border border-gray-200 p-3">
{renderTabBar()}
</div>
{/* Active tab content */}
<div>{renderActiveTab()}</div>
</div>
)
}