diff --git a/admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx b/admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
index f1c5ca5..cc65ef6 100644
--- a/admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
+++ b/admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
@@ -273,6 +273,52 @@ Dein Ziel ist die rechtzeitige Erkennung und Kommunikation relevanter Ereignisse
createdAt: '2024-12-01T00:00:00Z',
updatedAt: '2025-01-12T02:00:00Z'
},
+ 'compliance-advisor': {
+ id: 'compliance-advisor',
+ name: 'Compliance Advisor',
+ description: 'DSGVO/Compliance-Berater fuer SDK-Nutzer',
+ soulFile: 'compliance-advisor.soul.md',
+ soulContent: `# Compliance Advisor Agent
+
+## Identitaet
+Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
+Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
+Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
+offiziellen Quellen und gibst praxisnahe Hinweise.
+
+## Kernprinzipien
+- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
+- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
+- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
+- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
+- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen AUSSER NIBIS-Dokumenten
+
+## Kompetenzbereich
+- DSGVO Art. 1-99 + Erwaegsgruende
+- BDSG (Bundesdatenschutzgesetz)
+- AI Act (EU KI-Verordnung)
+- TTDSG, ePrivacy-Richtlinie
+- DSK-Kurzpapiere (Nr. 1-20)
+- SDM V3.0, BSI-Grundschutz, BSI-TR-03161
+- EDPB Guidelines, Bundes-/Laender-Muss-Listen
+- ISO 27001/27701 (Ueberblick)
+
+## Kommunikationsstil
+- Sachlich, aber verstaendlich
+- Deutsch als Hauptsprache
+- Strukturierte Antworten mit Quellenangabe
+- Praxisbeispiele wo hilfreich`,
+ color: '#6366f1',
+ status: 'running',
+ activeSessions: 0,
+ totalProcessed: 0,
+ avgResponseTime: 0,
+ errorRate: 0,
+ lastRestart: new Date().toISOString(),
+ version: '1.0.0',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ },
'orchestrator': {
id: 'orchestrator',
name: 'Orchestrator',
diff --git a/admin-v2/app/(admin)/ai/agents/page.tsx b/admin-v2/app/(admin)/ai/agents/page.tsx
index 6d88670..de96094 100644
--- a/admin-v2/app/(admin)/ai/agents/page.tsx
+++ b/admin-v2/app/(admin)/ai/agents/page.tsx
@@ -94,6 +94,19 @@ const mockAgents: AgentConfig[] = [
totalProcessed: 8934,
avgResponseTime: 12,
lastActivity: 'just now'
+ },
+ {
+ id: 'compliance-advisor',
+ name: 'Compliance Advisor',
+ description: 'DSGVO/Compliance-Berater fuer SDK-Nutzer',
+ soulFile: 'compliance-advisor.soul.md',
+ color: '#6366f1',
+ icon: 'message',
+ status: 'running',
+ activeSessions: 0,
+ totalProcessed: 0,
+ avgResponseTime: 0,
+ lastActivity: new Date().toISOString()
}
]
diff --git a/admin-v2/app/(admin)/ai/rag-pipeline/dsfa/page.tsx b/admin-v2/app/(admin)/ai/rag-pipeline/dsfa/page.tsx
new file mode 100644
index 0000000..aedbf66
--- /dev/null
+++ b/admin-v2/app/(admin)/ai/rag-pipeline/dsfa/page.tsx
@@ -0,0 +1,674 @@
+'use client'
+
+/**
+ * DSFA Document Manager
+ *
+ * Manages DSFA-related sources and documents for the RAG pipeline.
+ * Features:
+ * - View all registered DSFA sources with license info
+ * - Upload new documents
+ * - Trigger re-indexing
+ * - View corpus statistics
+ */
+
+import { useState, useEffect } from 'react'
+import Link from 'next/link'
+import {
+ ArrowLeft,
+ RefreshCw,
+ Upload,
+ FileText,
+ Database,
+ Scale,
+ ExternalLink,
+ ChevronDown,
+ ChevronUp,
+ Search,
+ Filter,
+ CheckCircle,
+ Clock,
+ AlertCircle,
+ BookOpen
+} from 'lucide-react'
+import {
+ DSFASource,
+ DSFACorpusStats,
+ DSFASourceStats,
+ DSFALicenseCode,
+ DSFA_LICENSE_LABELS,
+ DSFA_DOCUMENT_TYPE_LABELS
+} from '@/lib/sdk/types'
+
+// ============================================================================
+// TYPES
+// ============================================================================
+
+interface APIError {
+ message: string
+ status?: number
+}
+
+// ============================================================================
+// API FUNCTIONS
+// ============================================================================
+
+const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
+
+async function fetchSources(): Promise {
+ try {
+ const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/sources`)
+ if (!response.ok) throw new Error('Failed to fetch sources')
+ return await response.json()
+ } catch {
+ // Return mock data for demo
+ return MOCK_SOURCES
+ }
+}
+
+async function fetchStats(): Promise {
+ try {
+ const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/stats`)
+ if (!response.ok) throw new Error('Failed to fetch stats')
+ return await response.json()
+ } catch {
+ return MOCK_STATS
+ }
+}
+
+async function initializeCorpus(): Promise<{ sources_registered: number }> {
+ const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/init`, {
+ method: 'POST',
+ })
+ if (!response.ok) throw new Error('Failed to initialize corpus')
+ return await response.json()
+}
+
+async function triggerIngestion(sourceCode: string): Promise {
+ const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/sources/${sourceCode}/ingest`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({}),
+ })
+ if (!response.ok) throw new Error('Failed to trigger ingestion')
+}
+
+// ============================================================================
+// MOCK DATA
+// ============================================================================
+
+const MOCK_SOURCES: DSFASource[] = [
+ {
+ id: '1',
+ sourceCode: 'WP248',
+ name: 'WP248 rev.01 - Leitlinien zur DSFA',
+ fullName: 'Leitlinien zur Datenschutz-Folgenabschaetzung',
+ organization: 'Artikel-29-Datenschutzgruppe / EDPB',
+ sourceUrl: 'https://ec.europa.eu/newsroom/article29/items/611236/en',
+ licenseCode: 'EDPB-LICENSE',
+ licenseName: 'EDPB Document License',
+ attributionRequired: true,
+ attributionText: 'Quelle: WP248 rev.01, Artikel-29-Datenschutzgruppe (2017)',
+ documentType: 'guideline',
+ language: 'de',
+ },
+ {
+ id: '2',
+ sourceCode: 'DSK_KP5',
+ name: 'Kurzpapier Nr. 5 - DSFA nach Art. 35 DS-GVO',
+ organization: 'Datenschutzkonferenz (DSK)',
+ sourceUrl: 'https://www.datenschutzkonferenz-online.de/media/kp/dsk_kpnr_5.pdf',
+ licenseCode: 'DL-DE-BY-2.0',
+ licenseName: 'Datenlizenz DE β Namensnennung 2.0',
+ licenseUrl: 'https://www.govdata.de/dl-de/by-2-0',
+ attributionRequired: true,
+ attributionText: 'Quelle: DSK Kurzpapier Nr. 5 (Stand: 2018)',
+ documentType: 'guideline',
+ language: 'de',
+ },
+ {
+ id: '3',
+ sourceCode: 'BFDI_MUSS_PUBLIC',
+ name: 'BfDI DSFA-Liste (oeffentlicher Bereich)',
+ organization: 'BfDI',
+ sourceUrl: 'https://www.bfdi.bund.de',
+ licenseCode: 'DL-DE-ZERO-2.0',
+ licenseName: 'Datenlizenz DE β Zero 2.0',
+ attributionRequired: false,
+ attributionText: 'Quelle: BfDI, Liste gem. Art. 35 Abs. 4 DSGVO',
+ documentType: 'checklist',
+ language: 'de',
+ },
+ {
+ id: '4',
+ sourceCode: 'NI_MUSS_PRIVATE',
+ name: 'LfD NI DSFA-Liste (nicht-oeffentlich)',
+ organization: 'LfD Niedersachsen',
+ sourceUrl: 'https://www.lfd.niedersachsen.de/download/131098',
+ licenseCode: 'DL-DE-BY-2.0',
+ licenseName: 'Datenlizenz DE β Namensnennung 2.0',
+ attributionRequired: true,
+ attributionText: 'Quelle: LfD Niedersachsen, DSFA-Muss-Liste',
+ documentType: 'checklist',
+ language: 'de',
+ },
+]
+
+const MOCK_STATS: DSFACorpusStats = {
+ sources: [
+ {
+ sourceId: '1',
+ sourceCode: 'WP248',
+ name: 'WP248 rev.01',
+ organization: 'EDPB',
+ licenseCode: 'EDPB-LICENSE',
+ documentType: 'guideline',
+ documentCount: 1,
+ chunkCount: 50,
+ lastIndexedAt: '2026-02-09T10:00:00Z',
+ },
+ {
+ sourceId: '2',
+ sourceCode: 'DSK_KP5',
+ name: 'DSK Kurzpapier Nr. 5',
+ organization: 'DSK',
+ licenseCode: 'DL-DE-BY-2.0',
+ documentType: 'guideline',
+ documentCount: 1,
+ chunkCount: 35,
+ lastIndexedAt: '2026-02-09T10:00:00Z',
+ },
+ ],
+ totalSources: 45,
+ totalDocuments: 45,
+ totalChunks: 850,
+ qdrantCollection: 'bp_dsfa_corpus',
+ qdrantPointsCount: 850,
+ qdrantStatus: 'green',
+}
+
+// ============================================================================
+// COMPONENTS
+// ============================================================================
+
+function LicenseBadge({ licenseCode }: { licenseCode: DSFALicenseCode }) {
+ const colorMap: Record = {
+ 'DL-DE-BY-2.0': 'bg-blue-100 text-blue-700 border-blue-200',
+ 'DL-DE-ZERO-2.0': 'bg-gray-100 text-gray-700 border-gray-200',
+ 'CC-BY-4.0': 'bg-green-100 text-green-700 border-green-200',
+ 'EDPB-LICENSE': 'bg-purple-100 text-purple-700 border-purple-200',
+ 'PUBLIC_DOMAIN': 'bg-gray-100 text-gray-600 border-gray-200',
+ 'PROPRIETARY': 'bg-amber-100 text-amber-700 border-amber-200',
+ }
+
+ return (
+
+
+ {DSFA_LICENSE_LABELS[licenseCode] || licenseCode}
+
+ )
+}
+
+function DocumentTypeBadge({ type }: { type?: string }) {
+ if (!type) return null
+
+ const colorMap: Record = {
+ guideline: 'bg-indigo-100 text-indigo-700',
+ checklist: 'bg-emerald-100 text-emerald-700',
+ regulation: 'bg-red-100 text-red-700',
+ template: 'bg-orange-100 text-orange-700',
+ }
+
+ return (
+
+ {DSFA_DOCUMENT_TYPE_LABELS[type as keyof typeof DSFA_DOCUMENT_TYPE_LABELS] || type}
+
+ )
+}
+
+function StatusIndicator({ status }: { status: string }) {
+ const statusConfig: Record = {
+ green: { color: 'text-green-500', icon: , label: 'Aktiv' },
+ yellow: { color: 'text-yellow-500', icon: , label: 'Ausstehend' },
+ red: { color: 'text-red-500', icon: , label: 'Fehler' },
+ }
+
+ const config = statusConfig[status] || statusConfig.yellow
+
+ return (
+
+ {config.icon}
+ {config.label}
+
+ )
+}
+
+function SourceCard({
+ source,
+ stats,
+ onIngest,
+ isIngesting
+}: {
+ source: DSFASource
+ stats?: DSFASourceStats
+ onIngest: () => void
+ isIngesting: boolean
+}) {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ return (
+
+
+
+
+
+
+ {source.sourceCode}
+
+
+
+
+ {source.name}
+
+ {source.organization && (
+
+ {source.organization}
+
+ )}
+
+
setIsExpanded(!isExpanded)}
+ className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
+ >
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {stats && (
+ <>
+
+ {stats.documentCount} Dok.
+
+
+ {stats.chunkCount} Chunks
+
+ >
+ )}
+
+
+ {source.attributionRequired && (
+
+ Attribution: {source.attributionText}
+
+ )}
+
+
+ {isExpanded && (
+
+
+ {source.sourceUrl && (
+ <>
+ Quelle:
+
+
+ Link
+
+
+ >
+ )}
+ {source.licenseUrl && (
+ <>
+ Lizenz-URL:
+
+
+ {source.licenseName}
+
+
+ >
+ )}
+ Sprache:
+ {source.language}
+ {stats?.lastIndexedAt && (
+ <>
+ Zuletzt indexiert:
+ {new Date(stats.lastIndexedAt).toLocaleString('de-DE')}
+ >
+ )}
+
+
+
+
+ {isIngesting ? (
+
+ ) : (
+
+ )}
+ Neu indexieren
+
+
+
+ )}
+
+ )
+}
+
+function StatsOverview({ stats }: { stats: DSFACorpusStats }) {
+ return (
+
+
+
+
+ Corpus-Statistik
+
+
+
+
+
+
+
+ {stats.totalSources}
+
+
Quellen
+
+
+
+ {stats.totalDocuments}
+
+
Dokumente
+
+
+
+ {stats.totalChunks.toLocaleString()}
+
+
Chunks
+
+
+
+ {stats.qdrantPointsCount.toLocaleString()}
+
+
Vektoren
+
+
+
+
+
+ Collection: {' '}
+
+ {stats.qdrantCollection}
+
+
+
+
+ )
+}
+
+// ============================================================================
+// MAIN PAGE
+// ============================================================================
+
+export default function DSFADocumentManagerPage() {
+ const [sources, setSources] = useState([])
+ const [stats, setStats] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [filterType, setFilterType] = useState('all')
+ const [ingestingSource, setIngestingSource] = useState(null)
+ const [isInitializing, setIsInitializing] = useState(false)
+
+ useEffect(() => {
+ async function loadData() {
+ setIsLoading(true)
+ try {
+ const [sourcesData, statsData] = await Promise.all([
+ fetchSources(),
+ fetchStats(),
+ ])
+ setSources(sourcesData)
+ setStats(statsData)
+ setError(null)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to load data')
+ setSources(MOCK_SOURCES)
+ setStats(MOCK_STATS)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+ loadData()
+ }, [])
+
+ const handleInitialize = async () => {
+ setIsInitializing(true)
+ try {
+ await initializeCorpus()
+ // Reload data
+ const [sourcesData, statsData] = await Promise.all([
+ fetchSources(),
+ fetchStats(),
+ ])
+ setSources(sourcesData)
+ setStats(statsData)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to initialize')
+ } finally {
+ setIsInitializing(false)
+ }
+ }
+
+ const handleIngest = async (sourceCode: string) => {
+ setIngestingSource(sourceCode)
+ try {
+ await triggerIngestion(sourceCode)
+ // Reload stats
+ const statsData = await fetchStats()
+ setStats(statsData)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to ingest')
+ } finally {
+ setIngestingSource(null)
+ }
+ }
+
+ // Filter sources
+ const filteredSources = sources.filter(source => {
+ const matchesSearch = searchQuery === '' ||
+ source.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ source.sourceCode.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ source.organization?.toLowerCase().includes(searchQuery.toLowerCase())
+
+ const matchesType = filterType === 'all' || source.documentType === filterType
+
+ return matchesSearch && matchesType
+ })
+
+ // Get stats by source code
+ const getStatsForSource = (sourceCode: string): DSFASourceStats | undefined => {
+ return stats?.sources.find(s => s.sourceCode === sourceCode)
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Zurueck zur RAG-Pipeline
+
+
+
+
+
+
+ DSFA-Quellen Manager
+
+
+ Verwalten Sie DSFA-Guidance Dokumente mit vollstaendiger Lizenzattribution
+
+
+
+
+
+ {isInitializing ? (
+
+ ) : (
+
+ )}
+ Initialisieren
+
+
+
+ Dokument hochladen
+
+
+
+
+
+ {/* Error Banner */}
+ {error && (
+
+
+
+
{error}
+
setError(null)}
+ className="ml-auto text-red-600 hover:text-red-800"
+ >
+ ×
+
+
+
+ )}
+
+ {/* Stats Overview */}
+ {stats &&
}
+
+ {/* Search & Filter */}
+
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800"
+ />
+
+
+
+ setFilterType(e.target.value)}
+ className="pl-9 pr-8 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 appearance-none"
+ >
+ Alle Typen
+ Leitlinien
+ Prueflisten
+ Verordnungen
+
+
+
+
+ {/* Sources List */}
+
+
+
+ Registrierte Quellen ({filteredSources.length})
+
+
+
+ {isLoading ? (
+
+ ) : filteredSources.length === 0 ? (
+
+
+
+ {searchQuery || filterType !== 'all'
+ ? 'Keine Quellen gefunden'
+ : 'Noch keine Quellen registriert'}
+
+ {!searchQuery && filterType === 'all' && (
+
+ Quellen initialisieren
+
+ )}
+
+ ) : (
+
+ {filteredSources.map(source => (
+ handleIngest(source.sourceCode)}
+ isIngesting={ingestingSource === source.sourceCode}
+ />
+ ))}
+
+ )}
+
+
+ {/* Info Box */}
+
+
+ Ueber die Lizenzattribution
+
+
+ Alle DSFA-Quellen werden mit vollstaendiger Lizenzinformation gespeichert.
+ Bei der Nutzung der RAG-Suche werden automatisch die korrekten Attributionen angezeigt.
+
+
+
+
+ Namensnennung
+
+
+
+ Keine Attribution
+
+
+
+ CC Attribution
+
+
+
+
+
+ )
+}
diff --git a/admin-v2/app/(admin)/ai/rag-pipeline/page.tsx b/admin-v2/app/(admin)/ai/rag-pipeline/page.tsx
index 375aa6e..8e39b8f 100644
--- a/admin-v2/app/(admin)/ai/rag-pipeline/page.tsx
+++ b/admin-v2/app/(admin)/ai/rag-pipeline/page.tsx
@@ -128,6 +128,16 @@ const MOCK_DATA_SOURCES: DataSource[] = [
last_updated: '2025-01-10T08:00:00Z',
status: 'active',
},
+ {
+ id: 'dsfa',
+ name: 'DSFA-Guidance',
+ description: 'WP248, DSK Kurzpapiere, Muss-Listen aller Bundeslaender mit Quellenattribution',
+ collection: 'bp_dsfa_corpus',
+ document_count: 45,
+ chunk_count: 850,
+ last_updated: '2026-02-09T10:00:00Z',
+ status: 'active',
+ },
{
id: 'schulordnungen',
name: 'Schulordnungen',
@@ -899,6 +909,21 @@ function DataSourcesTab({ sources }: { sources: DataSource[] }) {
Regelwerk hinzufuegen β
+
diff --git a/admin-v2/app/(admin)/development/companion/page.tsx b/admin-v2/app/(admin)/development/companion/page.tsx
new file mode 100644
index 0000000..8499d6a
--- /dev/null
+++ b/admin-v2/app/(admin)/development/companion/page.tsx
@@ -0,0 +1,39 @@
+'use client'
+
+import { PagePurpose } from '@/components/common/PagePurpose'
+import { getModuleByHref } from '@/lib/navigation'
+import { GraduationCap, Construction } from 'lucide-react'
+
+export default function CompanionPage() {
+ const moduleInfo = getModuleByHref('/development/companion')
+
+ return (
+
+ {moduleInfo && (
+
+ )}
+
+
+
+
Companion Dev
+
+ Lesson-Modus Entwicklung fuer strukturiertes Lernen.
+
+
+
+ In Entwicklung
+
+
+
+ )
+}
diff --git a/admin-v2/app/(admin)/education/companion/page.tsx b/admin-v2/app/(admin)/education/companion/page.tsx
new file mode 100644
index 0000000..448da1a
--- /dev/null
+++ b/admin-v2/app/(admin)/education/companion/page.tsx
@@ -0,0 +1,76 @@
+'use client'
+
+import { Suspense } from 'react'
+import { PagePurpose } from '@/components/common/PagePurpose'
+import { getModuleByHref } from '@/lib/navigation'
+import { CompanionDashboard } from '@/components/companion/CompanionDashboard'
+import { GraduationCap } from 'lucide-react'
+
+function LoadingFallback() {
+ return (
+
+ {/* Header Skeleton */}
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+
+ {/* Phase Timeline Skeleton */}
+
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+
+
+ {/* Stats Skeleton */}
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+ {/* Content Skeleton */}
+
+
+ )
+}
+
+export default function CompanionPage() {
+ const moduleInfo = getModuleByHref('/education/companion')
+
+ return (
+
+ {/* Page Purpose Header */}
+ {moduleInfo && (
+
+ )}
+
+ {/* Main Companion Dashboard */}
+
}>
+
+
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/layout.tsx b/admin-v2/app/(sdk)/layout.tsx
index cfb6406..fb78d27 100644
--- a/admin-v2/app/(sdk)/layout.tsx
+++ b/admin-v2/app/(sdk)/layout.tsx
@@ -1,11 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
-import { useRouter } from 'next/navigation'
+import { useRouter, usePathname } from 'next/navigation'
import { SDKProvider } from '@/lib/sdk'
import { SDKSidebar } from '@/components/sdk/Sidebar/SDKSidebar'
import { CommandBar } from '@/components/sdk/CommandBar'
import { SDKPipelineSidebar } from '@/components/sdk/SDKPipelineSidebar'
+import { ComplianceAdvisorWidget } from '@/components/sdk/ComplianceAdvisorWidget'
import { useSDK } from '@/lib/sdk'
import { getStoredRole } from '@/lib/roles'
@@ -86,6 +87,10 @@ function SDKHeader({ sidebarCollapsed }: { sidebarCollapsed: boolean }) {
function SDKInnerLayout({ children }: { children: React.ReactNode }) {
const { isCommandBarOpen, setCommandBarOpen } = useSDK()
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
+ const pathname = usePathname()
+
+ // Extract current step from pathname (e.g., /sdk/vvt -> vvt)
+ const currentStep = pathname?.split('/').pop() || 'default'
// Load collapsed state from localStorage
useEffect(() => {
@@ -123,6 +128,9 @@ function SDKInnerLayout({ children }: { children: React.ReactNode }) {
{/* Pipeline Sidebar (FAB on mobile/tablet, fixed on desktop xl+) */}
+
+ {/* Compliance Advisor Widget */}
+
)
}
diff --git a/admin-v2/app/(sdk)/sdk/advisory-board/documentation/page.tsx b/admin-v2/app/(sdk)/sdk/advisory-board/documentation/page.tsx
new file mode 100644
index 0000000..a624c61
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/advisory-board/documentation/page.tsx
@@ -0,0 +1,596 @@
+'use client'
+
+/**
+ * UCCA System Documentation Page (SDK Version)
+ *
+ * Displays architecture documentation, auditor information,
+ * and transparency data for the UCCA compliance system.
+ */
+
+import { useState, useEffect } from 'react'
+import Link from 'next/link'
+
+// ============================================================================
+// Types
+// ============================================================================
+
+type DocTab = 'overview' | 'architecture' | 'auditor' | 'rules' | 'legal-corpus'
+
+interface Rule {
+ code: string
+ category: string
+ title: string
+ description: string
+ severity: string
+ gdpr_ref: string
+ rationale?: string
+ risk_add?: number
+}
+
+interface Pattern {
+ id: string
+ title: string
+ description: string
+ benefit?: string
+ effort?: string
+ risk_reduction?: number
+}
+
+interface Control {
+ id: string
+ title: string
+ description: string
+ gdpr_ref?: string
+ effort?: string
+}
+
+// ============================================================================
+// API Configuration
+// ============================================================================
+
+const API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'https://macmini:8090'
+
+// ============================================================================
+// Main Component
+// ============================================================================
+
+export default function DocumentationPage() {
+ const [activeTab, setActiveTab] = useState('overview')
+ const [rules, setRules] = useState([])
+ const [patterns, setPatterns] = useState([])
+ const [controls, setControls] = useState([])
+ const [policyVersion, setPolicyVersion] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true)
+ try {
+ const rulesRes = await fetch(`${API_BASE}/sdk/v1/ucca/rules`, {
+ headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000000' }
+ })
+ if (rulesRes.ok) {
+ const rulesData = await rulesRes.json()
+ setRules(rulesData.rules || [])
+ setPolicyVersion(rulesData.policy_version || '')
+ }
+
+ const patternsRes = await fetch(`${API_BASE}/sdk/v1/ucca/patterns`, {
+ headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000000' }
+ })
+ if (patternsRes.ok) {
+ const patternsData = await patternsRes.json()
+ setPatterns(patternsData.patterns || [])
+ }
+
+ const controlsRes = await fetch(`${API_BASE}/sdk/v1/ucca/controls`, {
+ headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000000' }
+ })
+ if (controlsRes.ok) {
+ const controlsData = await controlsRes.json()
+ setControls(controlsData.controls || [])
+ }
+ } catch (error) {
+ console.error('Failed to fetch documentation data:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchData()
+ }, [])
+
+ // ============================================================================
+ // Tab Content Renderers
+ // ============================================================================
+
+ const renderOverview = () => (
+
+
+
+
Deterministische Regeln
+
{rules.length}
+
+ Alle Entscheidungen basieren auf transparenten, nachvollziehbaren Regeln.
+
+
+
+
Architektur-Patterns
+
{patterns.length}
+
+ Best-Practice-Loesungen fuer datenschutzkonforme KI-Systeme.
+
+
+
+
Compliance-Kontrollen
+
{controls.length}
+
+ Technische und organisatorische Massnahmen.
+
+
+
+
+
+
Was ist UCCA?
+
+
+ UCCA (Use-Case Compliance & Feasibility Advisor) ist ein deterministisches
+ Compliance-Pruefwerkzeug, das Organisationen bei der Bewertung geplanter KI-Anwendungsfaelle
+ hinsichtlich ihrer datenschutzrechtlichen Zulaessigkeit unterstuetzt.
+
+
Kernprinzipien
+
+
+ Determinismus: Alle Entscheidungen basieren auf transparenten Regeln.
+ Die KI trifft KEINE autonomen Entscheidungen.
+
+
+ Transparenz: Alle Regeln, Kontrollen und Patterns sind einsehbar.
+
+
+ Human-in-the-Loop: Kritische Entscheidungen erfordern immer
+ menschliche Pruefung durch DSB oder Legal.
+
+
+ Rechtsgrundlage: Jede Regel referenziert konkrete DSGVO-Artikel.
+
+
+
+
+
+
+
+ Wichtiger Hinweis zur KI-Nutzung
+
+
+ Das System verwendet KI (LLM) ausschliesslich zur Erklaerung bereits
+ getroffener Regelentscheidungen. Die eigentliche Compliance-Bewertung erfolgt
+ rein deterministisch durch die Policy Engine. BLOCK-Entscheidungen
+ koennen NICHT durch KI ueberschrieben werden.
+
+
+
+ )
+
+ const renderArchitecture = () => (
+
+
+
Systemarchitektur
+
+
+
{`
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Frontend (Next.js) β
+β admin-v2:3000/sdk/advisory-board β
+βββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
+ β HTTPS
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β AI Compliance SDK (Go) β
+β Port 8090 β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β Policy Engine β β
+β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
+β β β YAML-basierte Regeln (ucca_policy_v1.yaml) β β β
+β β β ~45 Regeln in 7 Kategorien β β β
+β β β Deterministisch - Kein LLM in Entscheidungslogik β β β
+β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
+β β β β β
+β β βΌ β β
+β β ββββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββ β β
+β β β Controls β β Patterns β β Examples β β β
+β β β Library β β Library β β Library β β β
+β β ββββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββ β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β
+β ββββββββββββββββββββ ββββββββββββββββββββ β
+β β LLM Integration β β Legal RAG ββββββββ β
+β β (nur Explain) β β Client β β β
+β ββββββββββββββββββββ ββββββββββββββββββββ β β
+βββββββββββββββββββββββββββββββ¬βββββββββββββββββββββΌβββββββββββββββββββ
+ β β
+ βΌ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Datenschicht β
+β ββββββββββββββββββββββ ββββββββββββββββββββββ β
+β β PostgreSQL β β Qdrant β β
+β β (Assessments, β β (Legal Corpus, β β
+β β Escalations) β β 2,274 Chunks) β β
+β ββββββββββββββββββββββ ββββββββββββββββββββββ β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ `}
+
+
+
+
+
Datenfluss
+
+ Benutzer beschreibt Use Case im Frontend
+ Policy Engine evaluiert gegen alle Regeln
+ Ergebnis mit Controls + Patterns zurueck
+ Optional: LLM erklaert das Ergebnis
+ Bei Risiko: Automatische Eskalation
+
+
+
+
Sicherheitsmerkmale
+
+ TLS 1.3 Verschluesselung
+ RBAC mit Tenant-Isolation
+ JWT-basierte Authentifizierung
+ Audit-Trail aller Aktionen
+ Keine Rohtext-Speicherung (nur Hash)
+
+
+
+
+
+
+
Eskalations-Workflow
+
+
+
+
+ Level
+ Ausloeser
+ Pruefer
+ SLA
+
+
+
+
+ E0
+ Nur INFO-Regeln, Risiko < 20
+ Automatisch
+ -
+
+
+ E1
+ WARN-Regeln, Risiko 20-40
+ Team-Lead
+ 24h / 72h
+
+
+ E2
+ Art. 9 Daten, DSFA empfohlen, Risiko 40-60
+ DSB
+ 8h / 48h
+
+
+ E3
+ BLOCK-Regeln, Art. 22, Risiko > 60
+ DSB + Legal
+ 4h / 24h
+
+
+
+
+
+
+ )
+
+ const renderAuditorInfo = () => (
+
+
+
+ Dokumentation fuer externe Auditoren
+
+
+ Diese Dokumentation erfuellt die Anforderungen nach Art. 30 DSGVO (Verzeichnis von
+ Verarbeitungstaetigkeiten) und dient als Grundlage fuer Audits nach Art. 32 DSGVO.
+
+
+
+
+
1. Zweck des Systems
+
+ UCCA ist ein Compliance-Pruefwerkzeug zur Bewertung geplanter KI-Anwendungsfaelle
+ hinsichtlich ihrer datenschutzrechtlichen Zulaessigkeit.
+
+
+
+
+
2. Rechtsgrundlage
+
+ Art. 6 Abs. 1 lit. c DSGVO - Erfuellung rechtlicher Verpflichtungen
+ Art. 6 Abs. 1 lit. f DSGVO - Berechtigte Interessen (Compliance-Management)
+
+
+
+
+
3. Verarbeitete Datenkategorien
+
+
+
+
+ Kategorie
+ Speicherung
+ Aufbewahrung
+
+
+
+
+ Use-Case-Beschreibung
+ Nur Hash (SHA-256)
+ 10 Jahre
+
+
+ Bewertungsergebnis
+ Vollstaendig
+ 10 Jahre
+
+
+ Audit-Trail
+ Vollstaendig
+ 10 Jahre
+
+
+ Eskalations-Historie
+ Vollstaendig
+ 10 Jahre
+
+
+
+
+
+
+
+
4. Keine autonomen KI-Entscheidungen
+
+ Das System trifft KEINE automatisierten Einzelentscheidungen im Sinne
+ von Art. 22 DSGVO, da:
+
+
+ Regelauswertung ist keine rechtlich bindende Entscheidung
+ Alle kritischen Faelle werden menschlich geprueft (E1-E3)
+ BLOCK-Entscheidungen erfordern immer menschliche Freigabe
+ Betroffene haben Anfechtungsmoeglichkeit ueber Eskalation
+
+
+
+
+
5. Technische und Organisatorische Massnahmen
+
+
+
Vertraulichkeit
+
+ RBAC mit Tenant-Isolation
+ TLS 1.3 Verschluesselung
+ AES-256 at rest
+
+
+
+
Integritaet
+
+ Unveraenderlicher Audit-Trail
+ Policy-Versionierung
+ Input-Validierung
+
+
+
+
+
+
+
+ )
+
+ const renderRulesTab = () => (
+
+
+
+
Regel-Katalog
+
Policy Version: {policyVersion}
+
+
+ {rules.length} Regeln insgesamt
+
+
+
+ {loading ? (
+
Lade Regeln...
+ ) : (
+
+ {Array.from(new Set(rules.map(r => r.category))).map(category => (
+
+
+
{category}
+
+ {rules.filter(r => r.category === category).length} Regeln
+
+
+
+ {rules.filter(r => r.category === category).map(rule => (
+
+
+
+
+ {rule.code}
+
+ {rule.severity}
+
+
+
{rule.title}
+
{rule.description}
+ {rule.gdpr_ref && (
+
{rule.gdpr_ref}
+ )}
+
+ {rule.risk_add && (
+
+ +{rule.risk_add}
+
+ )}
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ )
+
+ const renderLegalCorpus = () => (
+
+
+
Legal RAG Corpus
+
+ Das System verwendet einen semantischen Suchindex mit 2.274 Chunks aus 19 EU-Regulierungen
+ fuer rechtsgrundlagenbasierte Erklaerungen.
+
+
+
+
+
Indexierte Regulierungen
+
+ DSGVO - Datenschutz-Grundverordnung
+ AI Act - EU KI-Verordnung
+ NIS2 - Cybersicherheits-Richtlinie
+ CRA - Cyber Resilience Act
+ Data Act - Datengesetz
+ DSA/DMA - Digital Services/Markets Act
+ DPF - EU-US Data Privacy Framework
+ BSI-TR-03161 - Digitale Identitaeten
+
+
+
+
RAG-Funktionalitaet
+
+ Hybride Suche (Dense + BM25)
+ Semantisches Chunking
+ Cross-Encoder Reranking
+ Artikel-Referenz-Extraktion
+ Mehrsprachig (DE/EN)
+
+
+
+
+
+
+
Verwendung im System
+
+
+
+ 1
+
+
+
Benutzer fordert Erklaerung an
+
+ Nach der Bewertung kann eine LLM-basierte Erklaerung generiert werden.
+
+
+
+
+
+ 2
+
+
+
Legal RAG Client sucht relevante Artikel
+
+ Basierend auf den ausgeloesten Regeln werden passende Gesetzestexte gefunden.
+
+
+
+
+
+ 3
+
+
+
LLM generiert Erklaerung mit Rechtsgrundlage
+
+ Die Erklaerung referenziert konkrete Artikel aus DSGVO, AI Act etc.
+
+
+
+
+
+
+ )
+
+ // ============================================================================
+ // Tabs Configuration
+ // ============================================================================
+
+ const tabs: { id: DocTab; label: string }[] = [
+ { id: 'overview', label: 'Uebersicht' },
+ { id: 'architecture', label: 'Architektur' },
+ { id: 'auditor', label: 'Fuer Auditoren' },
+ { id: 'rules', label: 'Regel-Katalog' },
+ { id: 'legal-corpus', label: 'Legal RAG' },
+ ]
+
+ // ============================================================================
+ // Main Render
+ // ============================================================================
+
+ return (
+
+
+
+
UCCA System-Dokumentation
+
+ Transparente Dokumentation des UCCA-Systems fuer Entwickler, Auditoren und Datenschutzbeauftragte.
+
+
+
+ Zurueck zum Advisory Board
+
+
+
+ {/* Tab Navigation */}
+
+
+ {tabs.map(tab => (
+ setActiveTab(tab.id)}
+ className={`flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
+ activeTab === tab.id
+ ? 'text-purple-600 border-b-2 border-purple-600 bg-purple-50'
+ : 'text-slate-600 hover:text-slate-800 hover:bg-slate-50'
+ }`}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+ {activeTab === 'overview' && renderOverview()}
+ {activeTab === 'architecture' && renderArchitecture()}
+ {activeTab === 'auditor' && renderAuditorInfo()}
+ {activeTab === 'rules' && renderRulesTab()}
+ {activeTab === 'legal-corpus' && renderLegalCorpus()}
+
+
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/advisory-board/page.tsx b/admin-v2/app/(sdk)/sdk/advisory-board/page.tsx
index c0ffd86..1276a80 100644
--- a/admin-v2/app/(sdk)/sdk/advisory-board/page.tsx
+++ b/admin-v2/app/(sdk)/sdk/advisory-board/page.tsx
@@ -1,6 +1,7 @@
'use client'
import React, { useState } from 'react'
+import Link from 'next/link'
import { useSDK, UseCaseAssessment } from '@/lib/sdk'
// =============================================================================
@@ -601,17 +602,25 @@ export default function AdvisoryBoardPage() {
Erfassen Sie Ihre KI-AnwendungsfΓ€lle und erhalten Sie eine erste Compliance-Bewertung
- {!showWizard && (
- setShowWizard(true)}
- className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
+
+
-
-
-
- Neuer Use Case
-
- )}
+ UCCA-System Dokumentation ansehen
+
+ {!showWizard && (
+
setShowWizard(true)}
+ className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
+ >
+
+
+
+ Neuer Use Case
+
+ )}
+
{/* Wizard or List */}
diff --git a/admin-v2/app/(sdk)/sdk/audit-report/page.tsx b/admin-v2/app/(sdk)/sdk/audit-report/page.tsx
new file mode 100644
index 0000000..906f2d3
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/audit-report/page.tsx
@@ -0,0 +1,343 @@
+'use client'
+
+/**
+ * Audit Report Management Page (SDK Version)
+ *
+ * Create and manage GDPR audit sessions with PDF report generation.
+ */
+
+import { useState, useEffect } from 'react'
+import { useSDK } from '@/lib/sdk'
+import StepHeader from '@/components/sdk/StepHeader/StepHeader'
+
+interface AuditSession {
+ id: string
+ name: string
+ description?: string
+ auditor_name: string
+ auditor_email?: string
+ auditor_organization?: string
+ status: 'draft' | 'in_progress' | 'completed' | 'archived'
+ regulation_ids?: string[]
+ total_items: number
+ completed_items: number
+ compliant_count: number
+ non_compliant_count: number
+ completion_percentage: number
+ created_at: string
+ started_at?: string
+ completed_at?: string
+}
+
+const REGULATIONS = [
+ { code: 'GDPR', name: 'DSGVO / GDPR', description: 'EU-Datenschutzgrundverordnung' },
+ { code: 'BDSG', name: 'BDSG', description: 'Bundesdatenschutzgesetz' },
+ { code: 'TTDSG', name: 'TTDSG', description: 'Telekommunikation-Telemedien-Datenschutz' },
+]
+
+export default function AuditReportPage() {
+ const { state } = useSDK()
+ const [sessions, setSessions] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [activeTab, setActiveTab] = useState<'sessions' | 'new' | 'export'>('sessions')
+
+ const [newSession, setNewSession] = useState({
+ name: '',
+ description: '',
+ auditor_name: '',
+ auditor_email: '',
+ auditor_organization: '',
+ regulation_codes: [] as string[],
+ })
+ const [creating, setCreating] = useState(false)
+ const [generatingPdf, setGeneratingPdf] = useState(null)
+ const [pdfLanguage, setPdfLanguage] = useState<'de' | 'en'>('de')
+ const [statusFilter, setStatusFilter] = useState('all')
+
+ useEffect(() => {
+ fetchSessions()
+ }, [statusFilter])
+
+ const fetchSessions = async () => {
+ try {
+ setLoading(true)
+ const params = statusFilter !== 'all' ? `?status=${statusFilter}` : ''
+ const res = await fetch(`/api/admin/audit/sessions${params}`)
+ if (!res.ok) throw new Error('Fehler beim Laden der Audit-Sessions')
+ const data = await res.json()
+ setSessions(data.sessions || [])
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const createSession = async () => {
+ if (!newSession.name || !newSession.auditor_name) {
+ setError('Name und Auditor-Name sind Pflichtfelder')
+ return
+ }
+ try {
+ setCreating(true)
+ const res = await fetch('/api/admin/audit/sessions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newSession),
+ })
+ if (!res.ok) throw new Error('Fehler beim Erstellen der Session')
+ setNewSession({ name: '', description: '', auditor_name: '', auditor_email: '', auditor_organization: '', regulation_codes: [] })
+ setActiveTab('sessions')
+ fetchSessions()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ const startSession = async (sessionId: string) => {
+ try {
+ const res = await fetch(`/api/admin/audit/sessions/${sessionId}/start`, { method: 'PUT' })
+ if (!res.ok) throw new Error('Fehler beim Starten der Session')
+ fetchSessions()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ }
+ }
+
+ const completeSession = async (sessionId: string) => {
+ try {
+ const res = await fetch(`/api/admin/audit/sessions/${sessionId}/complete`, { method: 'PUT' })
+ if (!res.ok) throw new Error('Fehler beim Abschliessen der Session')
+ fetchSessions()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ }
+ }
+
+ const deleteSession = async (sessionId: string) => {
+ if (!confirm('Session wirklich loeschen?')) return
+ try {
+ const res = await fetch(`/api/admin/audit/sessions/${sessionId}`, { method: 'DELETE' })
+ if (!res.ok) throw new Error('Fehler beim Loeschen der Session')
+ fetchSessions()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ }
+ }
+
+ const downloadPdf = async (sessionId: string) => {
+ try {
+ setGeneratingPdf(sessionId)
+ const res = await fetch(`/api/admin/audit/sessions/${sessionId}/pdf?language=${pdfLanguage}`)
+ if (!res.ok) throw new Error('Fehler bei der PDF-Generierung')
+ const blob = await res.blob()
+ const url = window.URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `audit-report-${sessionId}.pdf`
+ document.body.appendChild(a)
+ a.click()
+ window.URL.revokeObjectURL(url)
+ document.body.removeChild(a)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ } finally {
+ setGeneratingPdf(null)
+ }
+ }
+
+ const getStatusBadge = (status: string) => {
+ const styles: Record = {
+ draft: 'bg-slate-100 text-slate-700',
+ in_progress: 'bg-blue-100 text-blue-700',
+ completed: 'bg-green-100 text-green-700',
+ archived: 'bg-purple-100 text-purple-700',
+ }
+ const labels: Record = {
+ draft: 'Entwurf',
+ in_progress: 'In Bearbeitung',
+ completed: 'Abgeschlossen',
+ archived: 'Archiviert',
+ }
+ return (
+
+ {labels[status] || status}
+
+ )
+ }
+
+ const getComplianceColor = (percentage: number) => {
+ if (percentage >= 80) return 'text-green-600'
+ if (percentage >= 50) return 'text-yellow-600'
+ return 'text-red-600'
+ }
+
+ return (
+
+
+
+ {error && (
+
+ {error}
+ setError(null)} className="text-red-500 hover:text-red-700">×
+
+ )}
+
+ {/* Tabs */}
+
+ {(['sessions', 'new', 'export'] as const).map((tab) => (
+ setActiveTab(tab)}
+ className={`px-4 py-2 rounded-lg font-medium transition-colors ${
+ activeTab === tab ? 'bg-purple-600 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
+ }`}
+ >
+ {tab === 'sessions' ? 'Audit-Sessions' : tab === 'new' ? '+ Neues Audit' : 'Export-Optionen'}
+
+ ))}
+
+
+ {/* Sessions Tab */}
+ {activeTab === 'sessions' && (
+
+
+ Status:
+ setStatusFilter(e.target.value)} className="px-3 py-2 border border-slate-200 rounded-lg text-sm">
+ Alle
+ Entwurf
+ In Bearbeitung
+ Abgeschlossen
+ Archiviert
+
+
+
+ {loading ? (
+
Lade Audit-Sessions...
+ ) : sessions.length === 0 ? (
+
+
Keine Audit-Sessions vorhanden
+
Erstellen Sie ein neues Audit, um mit der DSGVO-Pruefung zu beginnen.
+
setActiveTab('new')} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">Neues Audit erstellen
+
+ ) : (
+
+ {sessions.map((session) => (
+
+
+
+
+
{session.name}
+ {getStatusBadge(session.status)}
+
+ {session.description &&
{session.description}
}
+
+ Auditor: {session.auditor_name}
+ {session.auditor_organization && | {session.auditor_organization} }
+ | Erstellt: {new Date(session.created_at).toLocaleDateString('de-DE')}
+
+
+
+
{session.completion_percentage}%
+
{session.completed_items} / {session.total_items} Punkte
+
+
+
+
= 80 ? 'bg-green-500' : session.completion_percentage >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`} style={{ width: `${session.completion_percentage}%` }} />
+
+
+
{session.compliant_count}
Konform
+
{session.non_compliant_count}
Nicht Konform
+
{session.total_items - session.completed_items}
Ausstehend
+
+
+ {session.status === 'draft' && startSession(session.id)} className="px-3 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700">Audit starten }
+ {session.status === 'in_progress' && completeSession(session.id)} className="px-3 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700">Abschliessen }
+ {(session.status === 'completed' || session.status === 'in_progress') && (
+ downloadPdf(session.id)} disabled={generatingPdf === session.id} className="px-3 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 disabled:opacity-50">
+ {generatingPdf === session.id ? 'Generiere PDF...' : 'PDF-Report'}
+
+ )}
+ {(session.status === 'draft' || session.status === 'archived') && deleteSession(session.id)} className="px-3 py-2 text-red-600 text-sm hover:text-red-700">Loeschen }
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* New Session Tab */}
+ {activeTab === 'new' && (
+
+
Neues Audit erstellen
+
+
+ Audit-Name *
+ setNewSession({ ...newSession, name: e.target.value })} placeholder="z.B. DSGVO Jahresaudit 2026" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
+
+
+ Beschreibung
+
+
+
+
Zu pruefende Regelwerke
+
+ {REGULATIONS.map((reg) => (
+
+ { if (e.target.checked) { setNewSession({ ...newSession, regulation_codes: [...newSession.regulation_codes, reg.code] }) } else { setNewSession({ ...newSession, regulation_codes: newSession.regulation_codes.filter((c) => c !== reg.code) }) } }} className="w-4 h-4 text-purple-600" />
+ {reg.name}
{reg.description}
+
+ ))}
+
+
+
+
+ {creating ? 'Erstelle...' : 'Audit-Session erstellen'}
+
+
+
+
+ )}
+
+ {/* Export Tab */}
+ {activeTab === 'export' && (
+
+
+
PDF-Export Einstellungen
+
+
+
+ )}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/compliance-hub/page.tsx b/admin-v2/app/(sdk)/sdk/compliance-hub/page.tsx
new file mode 100644
index 0000000..e13f5ed
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/compliance-hub/page.tsx
@@ -0,0 +1,521 @@
+'use client'
+
+/**
+ * Compliance Hub Page (SDK Version - Zusatzmodul)
+ *
+ * Central compliance management dashboard with:
+ * - Compliance Score Overview
+ * - Quick Access to all compliance modules (SDK paths)
+ * - Control-Mappings with statistics
+ * - Audit Findings
+ * - Regulations overview
+ */
+
+import { useState, useEffect } from 'react'
+import Link from 'next/link'
+
+// Types
+interface DashboardData {
+ compliance_score: number
+ total_regulations: number
+ total_requirements: number
+ total_controls: number
+ controls_by_status: Record
+ controls_by_domain: Record>
+ total_evidence: number
+ evidence_by_status: Record
+ total_risks: number
+ risks_by_level: Record
+}
+
+interface Regulation {
+ id: string
+ code: string
+ name: string
+ full_name: string
+ regulation_type: string
+ effective_date: string | null
+ description: string
+ requirement_count: number
+}
+
+interface MappingsData {
+ total: number
+ by_regulation: Record
+}
+
+interface FindingsData {
+ major_count: number
+ minor_count: number
+ ofi_count: number
+ total: number
+ open_majors: number
+ open_minors: number
+}
+
+const DOMAIN_LABELS: Record = {
+ gov: 'Governance',
+ priv: 'Datenschutz',
+ iam: 'Identity & Access',
+ crypto: 'Kryptografie',
+ sdlc: 'Secure Dev',
+ ops: 'Operations',
+ ai: 'KI-spezifisch',
+ cra: 'Supply Chain',
+ aud: 'Audit',
+}
+
+export default function ComplianceHubPage() {
+ const [dashboard, setDashboard] = useState(null)
+ const [regulations, setRegulations] = useState([])
+ const [mappings, setMappings] = useState(null)
+ const [findings, setFindings] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [seeding, setSeeding] = useState(false)
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ const loadData = async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ const [dashboardRes, regulationsRes, mappingsRes, findingsRes] = await Promise.all([
+ fetch('/api/admin/compliance/dashboard'),
+ fetch('/api/admin/compliance/regulations'),
+ fetch('/api/admin/compliance/mappings'),
+ fetch('/api/admin/compliance/isms/findings/summary'),
+ ])
+
+ if (dashboardRes.ok) {
+ setDashboard(await dashboardRes.json())
+ }
+ if (regulationsRes.ok) {
+ const data = await regulationsRes.json()
+ setRegulations(data.regulations || [])
+ }
+ if (mappingsRes.ok) {
+ const data = await mappingsRes.json()
+ setMappings(data)
+ }
+ if (findingsRes.ok) {
+ const data = await findingsRes.json()
+ setFindings(data)
+ }
+ } catch (err) {
+ console.error('Failed to load compliance data:', err)
+ setError('Verbindung zum Backend fehlgeschlagen')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const seedDatabase = async () => {
+ setSeeding(true)
+ try {
+ const res = await fetch('/api/admin/compliance/seed', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ force: false }),
+ })
+
+ if (res.ok) {
+ const result = await res.json()
+ alert(`Datenbank erfolgreich initialisiert!\n\nRegulations: ${result.counts?.regulations || 0}\nControls: ${result.counts?.controls || 0}\nRequirements: ${result.counts?.requirements || 0}`)
+ loadData()
+ } else {
+ const error = await res.text()
+ alert(`Fehler beim Seeding: ${error}`)
+ }
+ } catch (err) {
+ console.error('Seeding failed:', err)
+ alert('Fehler beim Initialisieren der Datenbank')
+ } finally {
+ setSeeding(false)
+ }
+ }
+
+ const score = dashboard?.compliance_score || 0
+ const scoreColor = score >= 80 ? 'text-green-600' : score >= 60 ? 'text-yellow-600' : 'text-red-600'
+ const scoreBgColor = score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-yellow-500' : 'bg-red-500'
+
+ return (
+
+ {/* Title Card (Zusatzmodul - no StepHeader) */}
+
+
Compliance Hub
+
+ Zentrale Verwaltung aller Compliance-Anforderungen nach DSGVO, AI Act, BSI TR-03161 und weiteren Regulierungen.
+
+
+
+ {/* Error Banner */}
+ {error && (
+
+
+
+
+
{error}
+
+ Erneut versuchen
+
+
+ )}
+
+ {/* Seed Button if no data */}
+ {!loading && (dashboard?.total_controls || 0) === 0 && (
+
+
+
+
Keine Compliance-Daten vorhanden
+
Initialisieren Sie die Datenbank mit den Seed-Daten.
+
+
+ {seeding ? 'Initialisiere...' : 'Datenbank initialisieren'}
+
+
+
+ )}
+
+ {/* Quick Actions */}
+
+
Schnellzugriff
+
+
+
+
Audit Checkliste
+
{dashboard?.total_requirements || '...'} Anforderungen
+
+
+
+
+
Controls
+
{dashboard?.total_controls || '...'} Massnahmen
+
+
+
+
+
Evidence
+
Nachweise
+
+
+
+
+
Risk Matrix
+
5x5 Risiken
+
+
+
+
+
Service Registry
+
Module
+
+
+
+
+
Audit Report
+
PDF Export
+
+
+
+
+ {loading ? (
+
+ ) : (
+ <>
+ {/* Score and Stats Row */}
+
+
+
Compliance Score
+
+ {score.toFixed(0)}%
+
+
+
+ {dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden
+
+
+
+
+
+
+
Verordnungen
+
{dashboard?.total_regulations || 0}
+
+
+
+
{dashboard?.total_requirements || 0} Anforderungen
+
+
+
+
+
+
Controls
+
{dashboard?.total_controls || 0}
+
+
+
+
{dashboard?.controls_by_status?.pass || 0} bestanden
+
+
+
+
+
+
Nachweise
+
{dashboard?.total_evidence || 0}
+
+
+
+
{dashboard?.evidence_by_status?.valid || 0} aktiv
+
+
+
+
+
+
Risiken
+
{dashboard?.total_risks || 0}
+
+
+
+
+ {(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch
+
+
+
+
+ {/* Control-Mappings & Findings Row */}
+
+
+
+
Control-Mappings
+
+ Alle anzeigen β
+
+
+
+
+
{mappings?.total || 474}
+
Mappings gesamt
+
+
+
Nach Verordnung
+
+ {mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => (
+
+ {reg}: {count}
+
+ ))}
+ {!mappings?.by_regulation && (
+ <>
+ GDPR: 180
+ AI Act: 95
+ BSI: 120
+ CRA: 79
+ >
+ )}
+
+
+
+
+ Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 44} Controls
+ und {dashboard?.total_requirements || 558} Anforderungen aus {dashboard?.total_regulations || 19} Verordnungen.
+
+
+
+
+
+
Audit Findings
+
+ Audit Checkliste β
+
+
+
+
+
+
{findings?.open_majors || 0}
+
offen (blockiert Zertifizierung)
+
+
+
+
{findings?.open_minors || 0}
+
offen (erfordert CAPA)
+
+
+
+
+ Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI)
+
+ {(findings?.open_majors || 0) === 0 ? (
+
+ Zertifizierung moeglich
+
+ ) : (
+
+ Zertifizierung blockiert
+
+ )}
+
+
+
+
+ {/* Domain Chart */}
+
+
Controls nach Domain
+
+ {Object.entries(dashboard?.controls_by_domain || {}).map(([domain, stats]) => {
+ const total = stats.total || 0
+ const pass = stats.pass || 0
+ const partial = stats.partial || 0
+ const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0
+
+ return (
+
+
+
+ {DOMAIN_LABELS[domain] || domain.toUpperCase()}
+
+
+ {pass}/{total} ({passPercent.toFixed(0)}%)
+
+
+
+
+ )
+ })}
+
+
+
+ {/* Regulations Table */}
+
+
+
Verordnungen & Standards ({regulations.length})
+
+ Aktualisieren
+
+
+
+
+
+
+ Code
+ Name
+ Typ
+ Anforderungen
+
+
+
+ {regulations.slice(0, 15).map((reg) => (
+
+
+ {reg.code}
+
+
+ {reg.name}
+
+
+
+ {reg.regulation_type === 'eu_regulation' ? 'EU-VO' :
+ reg.regulation_type === 'eu_directive' ? 'EU-RL' :
+ reg.regulation_type === 'bsi_standard' ? 'BSI' :
+ reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type}
+
+
+
+ {reg.requirement_count}
+
+
+ ))}
+
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/compliance-scope/page.tsx b/admin-v2/app/(sdk)/sdk/compliance-scope/page.tsx
new file mode 100644
index 0000000..0f68b8f
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/compliance-scope/page.tsx
@@ -0,0 +1,385 @@
+'use client'
+
+import React, { useState, useEffect, useCallback, useMemo } from 'react'
+import { useSDK } from '@/lib/sdk'
+import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader/StepHeader'
+import {
+ ScopeOverviewTab,
+ ScopeWizardTab,
+ ScopeDecisionTab,
+ ScopeExportTab
+} from '@/components/sdk/compliance-scope'
+import type {
+ ComplianceScopeState,
+ ScopeProfilingAnswer,
+ ScopeDecision
+} from '@/lib/sdk/compliance-scope-types'
+import {
+ createEmptyScopeState,
+ STORAGE_KEY
+} from '@/lib/sdk/compliance-scope-types'
+import { complianceScopeEngine } from '@/lib/sdk/compliance-scope-engine'
+
+type TabId = 'overview' | 'wizard' | 'decision' | 'export'
+
+const TABS: { id: TabId; label: string; icon: string }[] = [
+ { id: 'overview', label: 'Uebersicht', icon: 'π' },
+ { id: 'wizard', label: 'Scope-Profiling', icon: 'π' },
+ { id: 'decision', label: 'Scope-Entscheidung', icon: 'βοΈ' },
+ { id: 'export', label: 'Export', icon: 'π€' },
+]
+
+export default function ComplianceScopePage() {
+ const { state: sdkState, dispatch } = useSDK()
+
+ // Active tab state
+ const [activeTab, setActiveTab] = useState('overview')
+
+ // Local scope state
+ const [scopeState, setScopeState] = useState(() => {
+ // Try to load from SDK context first
+ if (sdkState.complianceScope) {
+ return sdkState.complianceScope
+ }
+ return createEmptyScopeState()
+ })
+
+ // Loading state
+ const [isLoading, setIsLoading] = useState(true)
+ const [isEvaluating, setIsEvaluating] = useState(false)
+
+ // Load from localStorage on mount
+ useEffect(() => {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored) {
+ const parsed = JSON.parse(stored) as ComplianceScopeState
+ setScopeState(parsed)
+ // Also sync to SDK context
+ dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed })
+ }
+ } catch (error) {
+ console.error('Failed to load compliance scope state from localStorage:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }, [dispatch])
+
+ // Save to localStorage and SDK context whenever state changes
+ useEffect(() => {
+ if (!isLoading) {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(scopeState))
+ dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scopeState })
+ } catch (error) {
+ console.error('Failed to save compliance scope state:', error)
+ }
+ }
+ }, [scopeState, isLoading, dispatch])
+
+ // Handle answers change from wizard
+ const handleAnswersChange = useCallback((answers: Record) => {
+ setScopeState(prev => ({
+ ...prev,
+ answers,
+ lastModified: new Date().toISOString(),
+ }))
+ }, [])
+
+ // Handle evaluate button click
+ const handleEvaluate = useCallback(async () => {
+ setIsEvaluating(true)
+ try {
+ // Run the compliance scope engine
+ const decision = complianceScopeEngine.evaluate(scopeState.answers)
+
+ // Update state with decision
+ setScopeState(prev => ({
+ ...prev,
+ decision,
+ lastModified: new Date().toISOString(),
+ }))
+
+ // Switch to decision tab to show results
+ setActiveTab('decision')
+ } catch (error) {
+ console.error('Failed to evaluate compliance scope:', error)
+ // Optionally show error toast/notification
+ } finally {
+ setIsEvaluating(false)
+ }
+ }, [scopeState.answers])
+
+ // Handle start profiling from overview
+ const handleStartProfiling = useCallback(() => {
+ setActiveTab('wizard')
+ }, [])
+
+ // Handle reset
+ const handleReset = useCallback(() => {
+ const emptyState = createEmptyScopeState()
+ setScopeState(emptyState)
+ setActiveTab('overview')
+ localStorage.removeItem(STORAGE_KEY)
+ }, [])
+
+ // Calculate completion statistics
+ const completionStats = useMemo(() => {
+ const answers = scopeState.answers
+ const totalQuestions = Object.keys(answers).length
+ const answeredQuestions = Object.values(answers).filter(
+ answer => answer.value !== null && answer.value !== undefined
+ ).length
+
+ const completionPercentage = totalQuestions > 0
+ ? Math.round((answeredQuestions / totalQuestions) * 100)
+ : 0
+
+ const isComplete = answeredQuestions === totalQuestions
+
+ return {
+ total: totalQuestions,
+ answered: answeredQuestions,
+ percentage: completionPercentage,
+ isComplete,
+ }
+ }, [scopeState.answers])
+
+ // Auto-enable evaluation when all questions are answered
+ const canEvaluate = useMemo(() => {
+ return completionStats.isComplete
+ }, [completionStats.isComplete])
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {/* Step Header */}
+
+
+ {/* Progress Indicator */}
+ {completionStats.answered > 0 && (
+
+
+
+ Fortschritt: {completionStats.answered} von {completionStats.total} Fragen beantwortet
+
+
+ {completionStats.percentage}%
+
+
+
+
+ )}
+
+ {/* Main Content Card */}
+
+ {/* Tab Navigation */}
+
+
+ {TABS.map(tab => (
+ setActiveTab(tab.id)}
+ className={`
+ flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors
+ ${activeTab === tab.id
+ ? 'border-purple-600 text-purple-700'
+ : 'border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300'
+ }
+ `}
+ >
+ {tab.icon}
+ {tab.label}
+ {tab.id === 'wizard' && completionStats.answered > 0 && (
+
+ {completionStats.percentage}%
+
+ )}
+ {tab.id === 'decision' && scopeState.decision && (
+
+ )}
+
+ ))}
+
+
+
+ {/* Tab Content */}
+
+ {activeTab === 'overview' && (
+ setActiveTab('wizard')}
+ onGoToDecision={() => setActiveTab('decision')}
+ onGoToExport={() => setActiveTab('export')}
+ />
+ )}
+
+ {activeTab === 'wizard' && (
+
+ )}
+
+ {activeTab === 'decision' && (
+ setActiveTab('wizard')}
+ onGoToExport={() => setActiveTab('export')}
+ canEvaluate={canEvaluate}
+ onEvaluate={handleEvaluate}
+ isEvaluating={isEvaluating}
+ />
+ )}
+
+ {activeTab === 'export' && (
+ setActiveTab('decision')}
+ />
+ )}
+
+
+
+ {/* Quick Action Buttons (Fixed at bottom on mobile) */}
+
+
+
+ {completionStats.isComplete ? (
+
+ β
+ Profiling abgeschlossen
+
+ ) : (
+
+ π
+
+ {completionStats.answered === 0
+ ? 'Starten Sie mit dem Profiling'
+ : `Noch ${completionStats.total - completionStats.answered} Fragen offen`
+ }
+
+
+ )}
+
+
+
+
+ {activeTab !== 'wizard' && completionStats.answered > 0 && (
+ setActiveTab('wizard')}
+ className="px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
+ >
+ Zum Fragebogen
+
+ )}
+
+ {canEvaluate && activeTab !== 'decision' && (
+
+ {isEvaluating ? 'Evaluiere...' : 'Scope evaluieren'}
+
+ )}
+
+ {scopeState.decision && activeTab !== 'export' && (
+ setActiveTab('export')}
+ className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
+ >
+ Exportieren
+
+ )}
+
+
+
+ {/* Debug Info (only in development) */}
+ {process.env.NODE_ENV === 'development' && (
+
+
+ Debug Information
+
+
+
+ Active Tab: {activeTab}
+
+
+ Total Answers: {Object.keys(scopeState.answers).length}
+
+
+ Answered: {completionStats.answered} ({completionStats.percentage}%)
+
+
+ Has Decision: {scopeState.decision ? 'Yes' : 'No'}
+
+ {scopeState.decision && (
+ <>
+
+ Level: {scopeState.decision.level}
+
+
+ Score: {scopeState.decision.score}
+
+
+ Hard Triggers: {scopeState.decision.hardTriggers.length}
+
+ >
+ )}
+
+ Last Modified: {scopeState.lastModified || 'Never'}
+
+
+ Can Evaluate: {canEvaluate ? 'Yes' : 'No'}
+
+
+
+ )}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/consent-management/page.tsx b/admin-v2/app/(sdk)/sdk/consent-management/page.tsx
new file mode 100644
index 0000000..8a16978
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/consent-management/page.tsx
@@ -0,0 +1,615 @@
+'use client'
+
+/**
+ * Consent Management Page (SDK Version)
+ *
+ * Admin interface for managing:
+ * - Documents (AGB, Privacy, etc.)
+ * - Document Versions
+ * - Email Templates
+ * - GDPR Processes (Art. 15-21)
+ * - Statistics
+ */
+
+import { useState, useEffect } from 'react'
+import { useSDK } from '@/lib/sdk'
+import StepHeader from '@/components/sdk/StepHeader/StepHeader'
+
+// API Proxy URL (avoids CORS issues)
+const API_BASE = '/api/admin/consent'
+
+type Tab = 'documents' | 'versions' | 'emails' | 'gdpr' | 'stats'
+
+interface Document {
+ id: string
+ type: string
+ name: string
+ description: string
+ mandatory: boolean
+ created_at: string
+ updated_at: string
+}
+
+interface Version {
+ id: string
+ document_id: string
+ version: string
+ language: string
+ title: string
+ content: string
+ status: string
+ created_at: string
+}
+
+export default function ConsentManagementPage() {
+ const { state } = useSDK()
+ const [activeTab, setActiveTab] = useState('documents')
+ const [documents, setDocuments] = useState([])
+ const [versions, setVersions] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [selectedDocument, setSelectedDocument] = useState('')
+
+ // Auth token (in production, get from auth context)
+ const [authToken, setAuthToken] = useState('')
+
+ useEffect(() => {
+ const token = localStorage.getItem('bp_admin_token')
+ if (token) {
+ setAuthToken(token)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (activeTab === 'documents') {
+ loadDocuments()
+ } else if (activeTab === 'versions' && selectedDocument) {
+ loadVersions(selectedDocument)
+ }
+ }, [activeTab, selectedDocument, authToken])
+
+ async function loadDocuments() {
+ setLoading(true)
+ setError(null)
+ try {
+ const res = await fetch(`${API_BASE}/documents`, {
+ headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
+ })
+ if (res.ok) {
+ const data = await res.json()
+ setDocuments(data.documents || [])
+ } else {
+ const errorData = await res.json().catch(() => ({}))
+ setError(errorData.error || 'Fehler beim Laden der Dokumente')
+ }
+ } catch {
+ setError('Verbindungsfehler zum Server')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function loadVersions(docId: string) {
+ setLoading(true)
+ setError(null)
+ try {
+ const res = await fetch(`${API_BASE}/documents/${docId}/versions`, {
+ headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
+ })
+ if (res.ok) {
+ const data = await res.json()
+ setVersions(data.versions || [])
+ } else {
+ const errorData = await res.json().catch(() => ({}))
+ setError(errorData.error || 'Fehler beim Laden der Versionen')
+ }
+ } catch {
+ setError('Verbindungsfehler zum Server')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const tabs: { id: Tab; label: string }[] = [
+ { id: 'documents', label: 'Dokumente' },
+ { id: 'versions', label: 'Versionen' },
+ { id: 'emails', label: 'E-Mail Vorlagen' },
+ { id: 'gdpr', label: 'DSGVO Prozesse' },
+ { id: 'stats', label: 'Statistiken' },
+ ]
+
+ // 16 Lifecycle Email Templates
+ const emailTemplates = [
+ { name: 'Willkommens-E-Mail', key: 'welcome', category: 'onboarding', description: 'Wird bei Registrierung versendet' },
+ { name: 'E-Mail Bestaetigung', key: 'email_verification', category: 'onboarding', description: 'Bestaetigungslink fuer E-Mail-Adresse' },
+ { name: 'Konto aktiviert', key: 'account_activated', category: 'onboarding', description: 'Bestaetigung der Kontoaktivierung' },
+ { name: 'Passwort zuruecksetzen', key: 'password_reset', category: 'security', description: 'Link zum Zuruecksetzen des Passworts' },
+ { name: 'Passwort geaendert', key: 'password_changed', category: 'security', description: 'Bestaetigung der Passwortaenderung' },
+ { name: 'Neue Anmeldung', key: 'new_login', category: 'security', description: 'Benachrichtigung ueber Anmeldung von neuem Geraet' },
+ { name: '2FA aktiviert', key: '2fa_enabled', category: 'security', description: 'Bestaetigung der 2FA-Aktivierung' },
+ { name: 'Neue Dokumentversion', key: 'new_document_version', category: 'consent', description: 'Info ueber neue Dokumentversion zur Zustimmung' },
+ { name: 'Zustimmung erteilt', key: 'consent_given', category: 'consent', description: 'Bestaetigung der erteilten Zustimmung' },
+ { name: 'Zustimmung widerrufen', key: 'consent_withdrawn', category: 'consent', description: 'Bestaetigung des Widerrufs' },
+ { name: 'DSR Anfrage erhalten', key: 'dsr_received', category: 'gdpr', description: 'Bestaetigung des Eingangs einer DSGVO-Anfrage' },
+ { name: 'Datenexport bereit', key: 'export_ready', category: 'gdpr', description: 'Benachrichtigung ueber fertigen Datenexport' },
+ { name: 'Daten geloescht', key: 'data_deleted', category: 'gdpr', description: 'Bestaetigung der Datenloeschung' },
+ { name: 'Daten berichtigt', key: 'data_rectified', category: 'gdpr', description: 'Bestaetigung der Datenberichtigung' },
+ { name: 'Konto deaktiviert', key: 'account_deactivated', category: 'lifecycle', description: 'Konto wurde deaktiviert' },
+ { name: 'Konto geloescht', key: 'account_deleted', category: 'lifecycle', description: 'Bestaetigung der Kontoloeschung' },
+ ]
+
+ // GDPR Article 15-21 Processes
+ const gdprProcesses = [
+ {
+ article: '15',
+ title: 'Auskunftsrecht',
+ description: 'Recht auf Bestaetigung und Auskunft ueber verarbeitete personenbezogene Daten',
+ actions: ['Datenexport generieren', 'Verarbeitungszwecke auflisten', 'Empfaenger auflisten'],
+ sla: '30 Tage',
+ },
+ {
+ article: '16',
+ title: 'Recht auf Berichtigung',
+ description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
+ actions: ['Daten bearbeiten', 'Aenderungshistorie fuehren', 'Benachrichtigung senden'],
+ sla: '30 Tage',
+ },
+ {
+ article: '17',
+ title: 'Recht auf Loeschung ("Vergessenwerden")',
+ description: 'Recht auf Loeschung personenbezogener Daten unter bestimmten Voraussetzungen',
+ actions: ['Loeschantrag pruefen', 'Daten loeschen', 'Aufbewahrungsfristen pruefen', 'Loeschbestaetigung senden'],
+ sla: '30 Tage',
+ },
+ {
+ article: '18',
+ title: 'Recht auf Einschraenkung der Verarbeitung',
+ description: 'Recht auf Markierung von Daten zur eingeschraenkten Verarbeitung',
+ actions: ['Daten markieren', 'Verarbeitung einschraenken', 'Benachrichtigung bei Aufhebung'],
+ sla: '30 Tage',
+ },
+ {
+ article: '19',
+ title: 'Mitteilungspflicht',
+ description: 'Pflicht zur Mitteilung von Berichtigung, Loeschung oder Einschraenkung an Empfaenger',
+ actions: ['Empfaenger ermitteln', 'Mitteilungen versenden', 'Protokollierung'],
+ sla: 'Unverzueglich',
+ },
+ {
+ article: '20',
+ title: 'Recht auf Datenuebertragbarkeit',
+ description: 'Recht auf Erhalt der Daten in strukturiertem, maschinenlesbarem Format',
+ actions: ['JSON/CSV Export', 'API-Schnittstelle', 'Direkte Uebertragung'],
+ sla: '30 Tage',
+ },
+ {
+ article: '21',
+ title: 'Widerspruchsrecht',
+ description: 'Recht auf Widerspruch gegen Verarbeitung aus berechtigtem Interesse oder Direktwerbung',
+ actions: ['Widerspruch erfassen', 'Verarbeitung stoppen', 'Marketing-Opt-out'],
+ sla: 'Unverzueglich',
+ },
+ ]
+
+ const emailCategories = [
+ { key: 'onboarding', label: 'Onboarding', color: 'bg-blue-100 text-blue-700' },
+ { key: 'security', label: 'Sicherheit', color: 'bg-red-100 text-red-700' },
+ { key: 'consent', label: 'Zustimmung', color: 'bg-green-100 text-green-700' },
+ { key: 'gdpr', label: 'DSGVO', color: 'bg-purple-100 text-purple-700' },
+ { key: 'lifecycle', label: 'Lifecycle', color: 'bg-orange-100 text-orange-700' },
+ ]
+
+ return (
+
+
+
+ {/* Token Input */}
+ {!authToken && (
+
+
+ Admin Token
+
+ {
+ setAuthToken(e.target.value)
+ localStorage.setItem('bp_admin_token', e.target.value)
+ }}
+ />
+
+ )}
+
+ {/* Tabs */}
+
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
+ activeTab === tab.id
+ ? 'bg-white text-slate-900 shadow-sm'
+ : 'text-slate-600 hover:text-slate-900'
+ }`}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+ {/* Content */}
+
+ {error && (
+
+ {error}
+ setError(null)}
+ className="ml-4 text-red-500 hover:text-red-700"
+ >
+ X
+
+
+ )}
+
+
+ {/* Documents Tab */}
+ {activeTab === 'documents' && (
+
+
+
Dokumente verwalten
+
+ + Neues Dokument
+
+
+
+ {loading ? (
+
Lade Dokumente...
+ ) : documents.length === 0 ? (
+
+ Keine Dokumente vorhanden
+
+ ) : (
+
+
+
+
+ Typ
+ Name
+ Beschreibung
+ Pflicht
+ Erstellt
+ Aktionen
+
+
+
+ {documents.map((doc) => (
+
+
+
+ {doc.type}
+
+
+ {doc.name}
+ {doc.description}
+
+ {doc.mandatory ? (
+ Ja
+ ) : (
+ Nein
+ )}
+
+
+ {new Date(doc.created_at).toLocaleDateString('de-DE')}
+
+
+ {
+ setSelectedDocument(doc.id)
+ setActiveTab('versions')
+ }}
+ className="text-purple-600 hover:text-purple-700 text-sm font-medium mr-3"
+ >
+ Versionen
+
+
+ Bearbeiten
+
+
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+ {/* Versions Tab */}
+ {activeTab === 'versions' && (
+
+
+
+
Versionen
+ setSelectedDocument(e.target.value)}
+ className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
+ >
+ Dokument auswaehlen...
+ {documents.map((doc) => (
+
+ {doc.name}
+
+ ))}
+
+
+ {selectedDocument && (
+
+ + Neue Version
+
+ )}
+
+
+ {!selectedDocument ? (
+
+ Bitte waehlen Sie ein Dokument aus
+
+ ) : loading ? (
+
Lade Versionen...
+ ) : versions.length === 0 ? (
+
+ Keine Versionen vorhanden
+
+ ) : (
+
+ {versions.map((version) => (
+
+
+
+
+ v{version.version}
+
+ {version.language.toUpperCase()}
+
+
+ {version.status}
+
+
+
{version.title}
+
+ Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')}
+
+
+
+
+ Bearbeiten
+
+ {version.status === 'draft' && (
+
+ Veroeffentlichen
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Emails Tab - 16 Lifecycle Templates */}
+ {activeTab === 'emails' && (
+
+
+
+
E-Mail Vorlagen
+
16 Lifecycle-Vorlagen fuer automatisierte Kommunikation
+
+
+ + Neue Vorlage
+
+
+
+ {/* Category Filter */}
+
+ Filter:
+ {emailCategories.map((cat) => (
+
+ {cat.label}
+
+ ))}
+
+
+ {/* Templates grouped by category */}
+ {emailCategories.map((category) => (
+
+
+
+ {category.label}
+
+
+ {emailTemplates
+ .filter((t) => t.category === category.key)
+ .map((template) => (
+
+
+
+
+
+
{template.name}
+
{template.description}
+
+
+
+ Aktiv
+
+ Bearbeiten
+
+
+ Vorschau
+
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ {/* GDPR Processes Tab - Articles 15-21 */}
+ {activeTab === 'gdpr' && (
+
+
+
+
DSGVO Betroffenenrechte
+
Artikel 15-21 Prozesse und Vorlagen
+
+
+ + DSR Anfrage erstellen
+
+
+
+ {/* Info Banner */}
+
+
+
*
+
+
Data Subject Rights (DSR)
+
+ Hier verwalten Sie alle DSGVO-Anfragen. Jeder Artikel hat definierte Prozesse, SLAs und automatisierte Workflows.
+
+
+
+
+
+ {/* GDPR Process Cards */}
+
+ {gdprProcesses.map((process) => (
+
+
+
+
+ {process.article}
+
+
+
+
{process.title}
+ Aktiv
+
+
{process.description}
+
+ {/* Actions */}
+
+ {process.actions.map((action, idx) => (
+
+ {action}
+
+ ))}
+
+
+ {/* SLA */}
+
+
+ SLA: {process.sla}
+
+ |
+
+ Offene Anfragen: 0
+
+
+
+
+
+
+
+ Anfragen
+
+
+ Vorlage
+
+
+
+
+ ))}
+
+
+ {/* DSR Request Statistics */}
+
+
+ )}
+
+ {/* Stats Tab */}
+ {activeTab === 'stats' && (
+
+
Statistiken
+
+
+
+
0
+
Aktive Zustimmungen
+
+
+
+
0
+
Offene DSR-Anfragen
+
+
+
+
+
Zustimmungsrate nach Dokument
+
+ Noch keine Daten verfuegbar
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/dsfa/[id]/page.tsx b/admin-v2/app/(sdk)/sdk/dsfa/[id]/page.tsx
index e9fcdfb..2c63733 100644
--- a/admin-v2/app/(sdk)/sdk/dsfa/[id]/page.tsx
+++ b/admin-v2/app/(sdk)/sdk/dsfa/[id]/page.tsx
@@ -13,7 +13,27 @@ import {
DSFA_LEGAL_BASES,
DSFA_AFFECTED_RIGHTS,
calculateRiskLevel,
+ SDM_GOALS,
} from '@/lib/sdk/dsfa/types'
+import type { SDMGoal, DSFARiskCategory } from '@/lib/sdk/dsfa/types'
+import {
+ RISK_CATALOG,
+ RISK_CATEGORY_LABELS,
+ COMPONENT_FAMILY_LABELS,
+ getRisksByCategory,
+ getRisksBySDMGoal,
+} from '@/lib/sdk/dsfa/risk-catalog'
+import type { CatalogRisk } from '@/lib/sdk/dsfa/risk-catalog'
+import {
+ MITIGATION_LIBRARY,
+ MITIGATION_TYPE_LABELS,
+ SDM_GOAL_LABELS,
+ EFFECTIVENESS_LABELS,
+ getMitigationsBySDMGoal,
+ getMitigationsByType,
+ getMitigationsForRisk,
+} from '@/lib/sdk/dsfa/mitigation-library'
+import type { CatalogMitigation } from '@/lib/sdk/dsfa/mitigation-library'
import {
getDSFA,
updateDSFASection,
@@ -32,6 +52,8 @@ import {
Art36Warning,
ReviewScheduleSection,
} from '@/components/sdk/dsfa'
+import { SourceAttribution } from '@/components/sdk/dsfa/SourceAttribution'
+import type { DSFALicenseCode, SourceAttributionProps } from '@/lib/sdk/types'
// =============================================================================
// SECTION EDITORS
@@ -483,6 +505,12 @@ function Section3Editor({ dsfa, onUpdate, isSubmitting }: SectionProps) {
)}
+ {/* RAG Search for Risks */}
+
+
{/* Affected Rights */}
Betroffene Rechte & Freiheiten
@@ -648,6 +676,12 @@ function Section4Editor({ dsfa, onUpdate, isSubmitting }: SectionProps) {
'bg-purple-50'
)}
+ {/* RAG Search for Mitigations */}
+
+
{/* TOM References */}
{dsfa.tom_references && dsfa.tom_references.length > 0 && (
@@ -837,6 +871,305 @@ function Section5Editor({ dsfa, onUpdate, isSubmitting }: SectionProps) {
)
}
+// =============================================================================
+// SDM COVERAGE OVERVIEW
+// =============================================================================
+
+function SDMCoverageOverview({ dsfa }: { dsfa: DSFA }) {
+ const goals = Object.keys(SDM_GOALS) as SDMGoal[]
+ const riskCount = dsfa.risks?.length || 0
+ const mitigationCount = dsfa.mitigations?.length || 0
+
+ // Count catalog risks and mitigations per SDM goal by matching descriptions
+ const goalCoverage = goals.map(goal => {
+ const catalogRisks = RISK_CATALOG.filter(r => r.sdmGoal === goal)
+ const catalogMitigations = MITIGATION_LIBRARY.filter(m => m.sdmGoals.includes(goal))
+
+ // Check if any DSFA risk descriptions contain catalog risk titles for this goal
+ const matchedRisks = catalogRisks.filter(cr =>
+ dsfa.risks?.some(r => r.description?.includes(cr.title))
+ ).length
+
+ // Check if any DSFA mitigation descriptions contain catalog mitigation titles for this goal
+ const matchedMitigations = catalogMitigations.filter(cm =>
+ dsfa.mitigations?.some(m => m.description?.includes(cm.title))
+ ).length
+
+ const totalCatalogRisks = catalogRisks.length
+ const totalCatalogMitigations = catalogMitigations.length
+
+ // Coverage: simple heuristic based on whether there are mitigations for risks in this area
+ const hasRisks = matchedRisks > 0 || dsfa.risks?.some(r => {
+ const cat = r.category
+ if (goal === 'vertraulichkeit' && cat === 'confidentiality') return true
+ if (goal === 'integritaet' && cat === 'integrity') return true
+ if (goal === 'verfuegbarkeit' && cat === 'availability') return true
+ if (goal === 'nichtverkettung' && cat === 'rights_freedoms') return true
+ return false
+ })
+
+ const coverage = matchedMitigations > 0 ? 'covered' :
+ hasRisks ? 'gaps' : 'no_data'
+
+ return {
+ goal,
+ info: SDM_GOALS[goal],
+ matchedRisks,
+ matchedMitigations,
+ totalCatalogRisks,
+ totalCatalogMitigations,
+ coverage,
+ }
+ })
+
+ return (
+
+
SDM-Abdeckung (Gewaehrleistungsziele)
+
Uebersicht ueber die Abdeckung der 7 Gewaehrleistungsziele des Standard-Datenschutzmodells.
+
+
+ {goalCoverage.map(({ goal, info, matchedRisks, matchedMitigations, coverage }) => (
+
+
+ {coverage === 'covered' ? '\u2713' : coverage === 'gaps' ? '!' : '\u2013'}
+
+
{info.name}
+
+ {matchedRisks}R / {matchedMitigations}M
+
+
+ ))}
+
+
+
+ Abgedeckt
+ Luecken
+ Keine Daten
+ R = Risiken, M = Massnahmen
+
+
+ )
+}
+
+// =============================================================================
+// RAG SEARCH PANEL
+// =============================================================================
+
+const RAG_API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
+
+interface RAGSearchResult {
+ chunk_id: string
+ content: string
+ score: number
+ source_code: string
+ source_name: string
+ attribution_text: string
+ license_code: string
+ license_name: string
+ license_url?: string
+ source_url?: string
+ document_type?: string
+ category?: string
+ section_title?: string
+}
+
+interface RAGSearchResponse {
+ query: string
+ results: RAGSearchResult[]
+ total_results: number
+ licenses_used: string[]
+ attribution_notice: string
+}
+
+function RAGSearchPanel({
+ context,
+ categories,
+ onInsertText,
+}: {
+ context: string
+ categories?: string[]
+ onInsertText?: (text: string) => void
+}) {
+ const [isOpen, setIsOpen] = useState(false)
+ const [query, setQuery] = useState('')
+ const [isSearching, setIsSearching] = useState(false)
+ const [results, setResults] = useState
(null)
+ const [error, setError] = useState(null)
+ const [copiedId, setCopiedId] = useState(null)
+
+ const buildQuery = () => {
+ if (query.trim()) return query.trim()
+ // Auto-generate query from context
+ return context.substring(0, 200)
+ }
+
+ const handleSearch = async () => {
+ const searchQuery = buildQuery()
+ if (!searchQuery || searchQuery.length < 3) return
+
+ setIsSearching(true)
+ setError(null)
+
+ try {
+ const params = new URLSearchParams({ query: searchQuery, limit: '5' })
+ if (categories?.length) {
+ categories.forEach(c => params.append('categories', c))
+ }
+
+ const response = await fetch(`${RAG_API_BASE}/api/v1/dsfa-rag/search?${params}`)
+ if (!response.ok) throw new Error(`Suche fehlgeschlagen (${response.status})`)
+ const data: RAGSearchResponse = await response.json()
+ setResults(data)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Suche fehlgeschlagen')
+ setResults(null)
+ } finally {
+ setIsSearching(false)
+ }
+ }
+
+ const handleInsert = (text: string, chunkId: string) => {
+ if (onInsertText) {
+ onInsertText(text)
+ } else {
+ navigator.clipboard.writeText(text)
+ }
+ setCopiedId(chunkId)
+ setTimeout(() => setCopiedId(null), 2000)
+ }
+
+ const sourcesForAttribution: SourceAttributionProps['sources'] = (results?.results || []).map(r => ({
+ sourceCode: r.source_code,
+ sourceName: r.source_name,
+ attributionText: r.attribution_text,
+ licenseCode: r.license_code as DSFALicenseCode,
+ sourceUrl: r.source_url,
+ score: r.score,
+ }))
+
+ if (!isOpen) {
+ return (
+ setIsOpen(true)}
+ className="flex items-center gap-2 px-4 py-2 text-sm bg-indigo-50 text-indigo-700 rounded-lg border border-indigo-200 hover:bg-indigo-100 transition-colors"
+ >
+
+
+
+ Empfehlung suchen (RAG)
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+ DSFA-Wissenssuche (RAG)
+
+
{ setIsOpen(false); setResults(null); setError(null) }}
+ className="text-indigo-400 hover:text-indigo-600"
+ >
+
+
+
+
+
+
+ {/* Search Input */}
+
+ setQuery(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleSearch()}
+ placeholder={`Suchbegriff (oder leer fuer automatische Kontextsuche)...`}
+ className="flex-1 px-3 py-2 text-sm border border-indigo-300 rounded-lg bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+ />
+
+ {isSearching ? 'Suche...' : 'Suchen'}
+
+
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Results */}
+ {results && results.results.length > 0 && (
+
+
{results.total_results} Ergebnis(se) gefunden
+
+ {results.results.map(r => (
+
+
+
+ {r.section_title && (
+
{r.section_title}
+ )}
+
+ {r.content.length > 400 ? r.content.substring(0, 400) + '...' : r.content}
+
+
+
+ {r.source_code} ({(r.score * 100).toFixed(0)}%)
+
+ {r.category && (
+ {r.category}
+ )}
+
+
+
handleInsert(r.content, r.chunk_id)}
+ className={`flex-shrink-0 px-3 py-1.5 text-xs rounded-lg transition-colors ${
+ copiedId === r.chunk_id
+ ? 'bg-green-100 text-green-700'
+ : 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200'
+ }`}
+ title="In Beschreibung uebernehmen"
+ >
+ {copiedId === r.chunk_id ? 'Kopiert!' : 'Uebernehmen'}
+
+
+
+ ))}
+
+ {/* Source Attribution */}
+
+
+ )}
+
+ {results && results.results.length === 0 && (
+
+ Keine Ergebnisse gefunden. Versuchen Sie einen anderen Suchbegriff.
+
+ )}
+
+ )
+}
+
// =============================================================================
// MODALS
// =============================================================================
@@ -852,14 +1185,29 @@ function AddRiskModal({
onClose: () => void
onAdd: (data: { category: string; description: string }) => void
}) {
+ const [mode, setMode] = useState<'catalog' | 'manual'>('catalog')
const [category, setCategory] = useState('confidentiality')
const [description, setDescription] = useState('')
+ const [catalogFilter, setCatalogFilter] = useState('all')
+ const [sdmFilter, setSdmFilter] = useState('all')
const { level } = calculateRiskLevel(likelihood, impact)
+ const filteredCatalog = RISK_CATALOG.filter(r => {
+ if (catalogFilter !== 'all' && r.category !== catalogFilter) return false
+ if (sdmFilter !== 'all' && r.sdmGoal !== sdmFilter) return false
+ return true
+ })
+
+ function selectCatalogRisk(risk: CatalogRisk) {
+ setCategory(risk.category)
+ setDescription(`${risk.title}\n\n${risk.description}`)
+ setMode('manual')
+ }
+
return (
-
+
Risiko hinzufuegen
@@ -872,33 +1220,107 @@ function AddRiskModal({
-
-
- Kategorie
- setCategory(e.target.value)}
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
- >
- Vertraulichkeit
- Integritaet
- Verfuegbarkeit
- Rechte & Freiheiten
-
-
-
-
- Beschreibung
-
+ {/* Tab Toggle */}
+
+ setMode('catalog')}
+ className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
+ mode === 'catalog' ? 'border-purple-500 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'
+ }`}
+ >
+ Aus Katalog waehlen ({RISK_CATALOG.length})
+
+ setMode('manual')}
+ className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
+ mode === 'manual' ? 'border-purple-500 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'
+ }`}
+ >
+ Manuell eingeben
+
+ {mode === 'catalog' ? (
+
+ {/* Filters */}
+
+ setCatalogFilter(e.target.value as DSFARiskCategory | 'all')}
+ className="text-sm border rounded px-2 py-1"
+ >
+ Alle Kategorien
+ {Object.entries(RISK_CATEGORY_LABELS).map(([key, label]) => (
+ {label}
+ ))}
+
+ setSdmFilter(e.target.value as SDMGoal | 'all')}
+ className="text-sm border rounded px-2 py-1"
+ >
+ Alle SDM-Ziele
+ {Object.entries(SDM_GOAL_LABELS).map(([key, label]) => (
+ {label}
+ ))}
+
+
+
+ {/* Catalog List */}
+
+ {filteredCatalog.map(risk => (
+
selectCatalogRisk(risk)}
+ className="w-full text-left p-3 rounded-lg border hover:border-purple-300 hover:bg-purple-50 transition-colors"
+ >
+
+ {risk.id}
+
+ {RISK_CATEGORY_LABELS[risk.category]}
+
+
+ {SDM_GOAL_LABELS[risk.sdmGoal]}
+
+
+ {risk.title}
+ {risk.description}
+
+ ))}
+
+ {filteredCatalog.length === 0 && (
+
Keine Risiken fuer die gewaehlten Filter.
+ )}
+
+ ) : (
+
+
+ Kategorie
+ setCategory(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
+ >
+ Vertraulichkeit
+ Integritaet
+ Verfuegbarkeit
+ Rechte & Freiheiten
+
+
+
+
+ Beschreibung
+
+
+ )}
+
onAdd({ category, description })}
- disabled={!description.trim()}
+ disabled={!description.trim() || mode === 'catalog'}
className="flex-1 py-2 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50"
>
Hinzufuegen
@@ -928,68 +1350,170 @@ function AddMitigationModal({
onClose: () => void
onAdd: (data: { risk_id: string; description: string; type: string; responsible_party: string }) => void
}) {
+ const [mode, setMode] = useState<'library' | 'manual'>('library')
const [riskId, setRiskId] = useState(risks[0]?.id || '')
const [type, setType] = useState('technical')
const [description, setDescription] = useState('')
const [responsibleParty, setResponsibleParty] = useState('')
+ const [typeFilter, setTypeFilter] = useState<'all' | 'technical' | 'organizational' | 'legal'>('all')
+ const [sdmFilter, setSdmFilter] = useState('all')
+
+ const filteredLibrary = MITIGATION_LIBRARY.filter(m => {
+ if (typeFilter !== 'all' && m.type !== typeFilter) return false
+ if (sdmFilter !== 'all' && !m.sdmGoals.includes(sdmFilter)) return false
+ return true
+ })
+
+ function selectCatalogMitigation(m: CatalogMitigation) {
+ setType(m.type)
+ setDescription(`${m.title}\n\n${m.description}\n\nRechtsgrundlage: ${m.legalBasis}`)
+ setMode('manual')
+ }
return (
-
+
Massnahme hinzufuegen
-
-
- Zugehoeriges Risiko
- setRiskId(e.target.value)}
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
- >
- {risks.map(risk => (
-
- {risk.description.substring(0, 50)}...
-
- ))}
-
-
-
-
- Typ
- setType(e.target.value)}
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
- >
- Technisch
- Organisatorisch
- Rechtlich
-
-
-
-
- Beschreibung
-
-
-
- Verantwortlich
- setResponsibleParty(e.target.value)}
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
- placeholder="Name oder Rolle..."
- />
-
+ {/* Tab Toggle */}
+
+ setMode('library')}
+ className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
+ mode === 'library' ? 'border-purple-500 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'
+ }`}
+ >
+ Aus Bibliothek waehlen ({MITIGATION_LIBRARY.length})
+
+ setMode('manual')}
+ className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
+ mode === 'manual' ? 'border-purple-500 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'
+ }`}
+ >
+ Manuell eingeben
+
+ {mode === 'library' ? (
+
+ {/* Filters */}
+
+ setTypeFilter(e.target.value as typeof typeFilter)}
+ className="text-sm border rounded px-2 py-1"
+ >
+ Alle Typen
+ {Object.entries(MITIGATION_TYPE_LABELS).map(([key, label]) => (
+ {label}
+ ))}
+
+ setSdmFilter(e.target.value as SDMGoal | 'all')}
+ className="text-sm border rounded px-2 py-1"
+ >
+ Alle SDM-Ziele
+ {Object.entries(SDM_GOAL_LABELS).map(([key, label]) => (
+ {label}
+ ))}
+
+
+
+ {/* Library List */}
+
+ {filteredLibrary.map(m => (
+
selectCatalogMitigation(m)}
+ className="w-full text-left p-3 rounded-lg border hover:border-purple-300 hover:bg-purple-50 transition-colors"
+ >
+
+ {m.id}
+
+ {MITIGATION_TYPE_LABELS[m.type]}
+
+ {m.sdmGoals.map(g => (
+
+ {SDM_GOAL_LABELS[g]}
+
+ ))}
+
+ {EFFECTIVENESS_LABELS[m.effectiveness]}
+
+
+ {m.title}
+ {m.description}
+
+ ))}
+
+ {filteredLibrary.length === 0 && (
+
Keine Massnahmen fuer die gewaehlten Filter.
+ )}
+
+ ) : (
+
+
+ Zugehoeriges Risiko
+ setRiskId(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
+ >
+ {risks.map(risk => (
+
+ {risk.description.substring(0, 50)}...
+
+ ))}
+
+
+
+
+ Typ
+ setType(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
+ >
+ Technisch
+ Organisatorisch
+ Rechtlich
+
+
+
+
+ Beschreibung
+
+
+
+ Verantwortlich
+ setResponsibleParty(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
+ placeholder="Name oder Rolle..."
+ />
+
+
+ )}
+
onAdd({ risk_id: riskId, description, type, responsible_party: responsibleParty })}
- disabled={!description.trim()}
+ disabled={!description.trim() || mode === 'library'}
className="flex-1 py-2 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50"
>
Hinzufuegen
@@ -1264,6 +1788,11 @@ export default function DSFAEditorPage() {
/>
)}
+ {/* SDM Coverage Overview (shown in Section 3 and 4) */}
+ {(activeSection === 3 || activeSection === 4) && (dsfa.risks?.length > 0 || dsfa.mitigations?.length > 0) && (
+
+ )}
+
{/* Section 5: Stakeholder Consultation (NEW) */}
{activeSection === 5 && (
{
+ switch (status) {
+ case 'active':
+ case 'complete':
+ return Aktiv
+ case 'in_progress':
+ return In Arbeit
+ case 'pending':
+ case 'inactive':
+ return Ausstehend
+ default:
+ return null
+ }
+ }
+
+ const calculateScore = () => {
+ let complete = 0
+ let total = 0
+ modules.forEach((m) => {
+ m.items.forEach((item) => {
+ total++
+ if (item.status === 'complete') complete++
+ })
+ })
+ return Math.round((complete / total) * 100)
+ }
+
+ const complianceScore = calculateScore()
+
+ return (
+
+ {/* Title Card (Zusatzmodul - no StepHeader) */}
+
+
Datenschutz-Management-System (DSMS)
+
+ Zentrale Uebersicht aller Datenschutz-Massnahmen und deren Status. Verfolgen Sie den Compliance-Fortschritt und identifizieren Sie offene Aufgaben.
+
+
+
+ {/* Compliance Score */}
+
+
+
+
DSGVO-Compliance Score
+
Gesamtfortschritt der Datenschutz-Massnahmen
+
+
+
= 80 ? 'text-green-600' : complianceScore >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
+ {complianceScore}%
+
+
Compliance
+
+
+
+
= 80 ? 'bg-green-500' : complianceScore >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
+ style={{ width: `${complianceScore}%` }}
+ />
+
+
+
+ {/* Quick Actions */}
+
+
+
+
+
+
DSR bearbeiten
+
Anfragen verwalten
+
+
+
+
+
+
+
+
+
Consents
+
Einwilligungen pruefen
+
+
+
+
+
+
+
+
+
Einwilligungen
+
User Consents pruefen
+
+
+
+
+
+
+
+
+
Loeschfristen
+
Pruefen & durchfuehren
+
+
+
+
+
+ {/* Audit Report Quick Action */}
+
+
+
+
+
+
Audit Report erstellen
+
PDF-Berichte fuer Auditoren und Aufsichtsbehoerden generieren
+
+
+
+
+
+
+
+
+ {/* Compliance Modules */}
+
Compliance-Module
+
+ {modules.map((module) => (
+
+
+
+
{module.title}
+
{module.description}
+
+ {getStatusBadge(module.status)}
+
+
+
+ {module.items.map((item, idx) => (
+
+
+ {item.status === 'complete' ? (
+
+
+
+ ) : item.status === 'in_progress' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {item.name}
+
+
+ {item.lastUpdated && (
+ {item.lastUpdated}
+ )}
+
+ ))}
+
+ {module.href && (
+
+ Verwalten
+
+ )}
+
+
+ ))}
+
+
+ {/* GDPR Rights Overview */}
+
+
DSGVO Betroffenenrechte (Art. 12-22)
+
+
+
Art. 15
+
Auskunftsrecht
+
+
+
Art. 16
+
Recht auf Berichtigung
+
+
+
Art. 17
+
Recht auf Loeschung
+
+
+
Art. 18
+
Recht auf Einschraenkung
+
+
+
Art. 19
+
Mitteilungspflicht
+
+
+
Art. 20
+
Datenuebertragbarkeit
+
+
+
Art. 21
+
Widerspruchsrecht
+
+
+
Art. 22
+
Automatisierte Entscheidungen
+
+
+
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/loeschfristen/page.tsx b/admin-v2/app/(sdk)/sdk/loeschfristen/page.tsx
index fa1a415..dc10ccf 100644
--- a/admin-v2/app/(sdk)/sdk/loeschfristen/page.tsx
+++ b/admin-v2/app/(sdk)/sdk/loeschfristen/page.tsx
@@ -1,317 +1,2165 @@
'use client'
-import React, { useState, useCallback } from 'react'
+import React, { useState, useEffect, 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 {
+ LoeschfristPolicy, LegalHold, StorageLocation,
+ RETENTION_DRIVER_META, RetentionDriverType, DeletionMethodType,
+ DELETION_METHOD_LABELS, STATUS_LABELS, STATUS_COLORS,
+ TRIGGER_LABELS, TRIGGER_COLORS, REVIEW_INTERVAL_LABELS,
+ STORAGE_LOCATION_LABELS, StorageLocationType, PolicyStatus,
+ ReviewInterval, DeletionTriggerLevel, RetentionUnit, LegalHoldStatus,
+ createEmptyPolicy, createEmptyLegalHold, createEmptyStorageLocation,
+ formatRetentionDuration, isPolicyOverdue, getActiveLegalHolds,
+ getEffectiveDeletionTrigger, LOESCHFRISTEN_STORAGE_KEY,
+ generatePolicyId,
+} from '@/lib/sdk/loeschfristen-types'
+import { BASELINE_TEMPLATES, templateToPolicy, getTemplateById, getAllTemplateTags } from '@/lib/sdk/loeschfristen-baseline-catalog'
+import {
+ PROFILING_STEPS, ProfilingAnswer, ProfilingStep,
+ isStepComplete, getProfilingProgress, generatePoliciesFromProfile,
+} from '@/lib/sdk/loeschfristen-profiling'
+import {
+ runComplianceCheck, ComplianceCheckResult, ComplianceIssue,
+} from '@/lib/sdk/loeschfristen-compliance'
+import {
+ exportPoliciesAsJSON, exportPoliciesAsCSV,
+ generateComplianceSummary, downloadFile,
+} from '@/lib/sdk/loeschfristen-export'
-// =============================================================================
-// TYPES
-// =============================================================================
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
-interface RetentionPolicy {
- id: string
- dataCategory: string
- description: string
- retentionPeriod: string
- legalBasis: string
- startEvent: string
- deletionMethod: string
- status: 'active' | 'review-needed' | 'expired'
- lastReview: Date
- nextReview: Date
- recordCount: number | null
-}
+type Tab = 'uebersicht' | 'editor' | 'generator' | 'export'
-// =============================================================================
-// MOCK DATA
-// =============================================================================
+const STORAGE_KEY = 'bp_loeschfristen'
-const mockPolicies: RetentionPolicy[] = [
- {
- id: 'ret-1',
- dataCategory: 'Personalakten',
- description: 'Beschaeftigtendaten und Gehaltsabrechnungen',
- retentionPeriod: '10 Jahre',
- legalBasis: 'Steuerrecht, Sozialversicherungsrecht',
- startEvent: 'Ende des Beschaeftigungsverhaeltnisses',
- deletionMethod: 'Automatische Loeschung nach Ablauf',
- status: 'active',
- lastReview: new Date('2024-01-01'),
- nextReview: new Date('2025-01-01'),
- recordCount: 245,
- },
- {
- id: 'ret-2',
- dataCategory: 'Buchhaltungsbelege',
- description: 'Rechnungen, Kontoauszuege, Buchungsbelege',
- retentionPeriod: '10 Jahre',
- legalBasis: 'HGB, AO',
- startEvent: 'Ende des Kalenderjahres',
- deletionMethod: 'Manuelle Pruefung und Vernichtung',
- status: 'active',
- lastReview: new Date('2024-01-15'),
- nextReview: new Date('2025-01-15'),
- recordCount: 12450,
- },
- {
- id: 'ret-3',
- dataCategory: 'Bewerbungsunterlagen',
- description: 'Lebenslaeufe, Anschreiben, Zeugnisse',
- retentionPeriod: '6 Monate',
- legalBasis: 'Berechtigtes Interesse (AGG-Frist)',
- startEvent: 'Absage oder Stellenbesetzung',
- deletionMethod: 'Automatische Loeschung',
- status: 'active',
- lastReview: new Date('2024-01-10'),
- nextReview: new Date('2024-07-10'),
- recordCount: 89,
- },
- {
- id: 'ret-4',
- dataCategory: 'Marketing-Einwilligungen',
- description: 'Newsletter-Abonnements und Werbeeinwilligungen',
- retentionPeriod: 'Bis Widerruf',
- legalBasis: 'Einwilligung Art. 6 Abs. 1 lit. a DSGVO',
- startEvent: 'Widerruf der Einwilligung',
- deletionMethod: 'Sofortige Loeschung bei Widerruf',
- status: 'active',
- lastReview: new Date('2023-12-01'),
- nextReview: new Date('2024-06-01'),
- recordCount: 5623,
- },
- {
- id: 'ret-5',
- dataCategory: 'Webserver-Logs',
- description: 'IP-Adressen, Zugriffszeiten, User-Agents',
- retentionPeriod: '7 Tage',
- legalBasis: 'Berechtigtes Interesse (IT-Sicherheit)',
- startEvent: 'Zeitpunkt des Zugriffs',
- deletionMethod: 'Automatische Rotation',
- status: 'active',
- lastReview: new Date('2024-01-20'),
- nextReview: new Date('2024-04-20'),
- recordCount: null,
- },
- {
- id: 'ret-6',
- dataCategory: 'Kundenstammdaten',
- description: 'Name, Adresse, Kontaktdaten von Kunden',
- retentionPeriod: '3 Jahre nach letzter Interaktion',
- legalBasis: 'Vertragserfuellung, Berechtigtes Interesse',
- startEvent: 'Letzte Kundeninteraktion',
- deletionMethod: 'Pruefung und manuelle Loeschung',
- status: 'review-needed',
- lastReview: new Date('2023-06-01'),
- nextReview: new Date('2024-01-01'),
- recordCount: 8920,
- },
-]
+// ---------------------------------------------------------------------------
+// Helper: TagInput
+// ---------------------------------------------------------------------------
-// =============================================================================
-// COMPONENTS
-// =============================================================================
+function TagInput({
+ value,
+ onChange,
+ placeholder,
+}: {
+ value: string[]
+ onChange: (v: string[]) => void
+ placeholder?: string
+}) {
+ const [input, setInput] = useState('')
-function PolicyCard({ policy }: { policy: RetentionPolicy }) {
- const statusColors = {
- active: 'bg-green-100 text-green-700 border-green-200',
- 'review-needed': 'bg-yellow-100 text-yellow-700 border-yellow-200',
- expired: 'bg-red-100 text-red-700 border-red-200',
+ const handleKeyDown = (e: React.KeyboardEvent
) => {
+ if (e.key === 'Enter' || e.key === ',') {
+ e.preventDefault()
+ const trimmed = input.trim().replace(/,+$/, '').trim()
+ if (trimmed && !value.includes(trimmed)) {
+ onChange([...value, trimmed])
+ }
+ setInput('')
+ }
}
- const statusLabels = {
- active: 'Aktiv',
- 'review-needed': 'Pruefung erforderlich',
- expired: 'Abgelaufen',
+ const remove = (idx: number) => {
+ onChange(value.filter((_, i) => i !== idx))
}
- const isReviewDue = policy.nextReview <= new Date()
-
return (
-
-
-
-
-
- {statusLabels[policy.status]}
-
-
- {policy.retentionPeriod}
-
-
-
{policy.dataCategory}
-
{policy.description}
-
-
-
-
-
- Rechtsgrundlage:
- {policy.legalBasis}
-
-
- Startereignis:
- {policy.startEvent}
-
-
- Loeschmethode:
- {policy.deletionMethod}
-
-
- Datensaetze:
- {policy.recordCount ? policy.recordCount.toLocaleString('de-DE') : 'N/A'}
-
-
-
-
-
- Naechste Pruefung: {policy.nextReview.toLocaleDateString('de-DE')}
- {isReviewDue && ' (faellig)'}
-
-
-
- Bearbeiten
-
-
- Loeschvorgang starten
-
-
+
+
+ {value.map((tag, idx) => (
+
+ {tag}
+ remove(idx)}
+ className="text-purple-600 hover:text-purple-900"
+ >
+ x
+
+
+ ))}
+
setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder ?? 'Eingabe + Enter'}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
+ />
)
}
-// =============================================================================
-// MAIN PAGE
-// =============================================================================
+// ---------------------------------------------------------------------------
+// Main Page
+// ---------------------------------------------------------------------------
export default function LoeschfristenPage() {
const router = useRouter()
- const { state } = useSDK()
- const [policies] = useState
(mockPolicies)
- const [filter, setFilter] = useState('all')
+ const sdk = useSDK()
- // Handle uploaded document
- const handleDocumentProcessed = useCallback((doc: UploadedDocument) => {
- console.log('[Loeschfristen Page] Document processed:', doc)
+ // ---- Core state ----
+ const [tab, setTab] = useState('uebersicht')
+ const [policies, setPolicies] = useState([])
+ const [loaded, setLoaded] = useState(false)
+ const [editingId, setEditingId] = useState(null)
+ const [filter, setFilter] = useState('all')
+ const [searchQuery, setSearchQuery] = useState('')
+ const [driverFilter, setDriverFilter] = useState('all')
+
+ // ---- Generator state ----
+ const [profilingStep, setProfilingStep] = useState(0)
+ const [profilingAnswers, setProfilingAnswers] = useState([])
+ const [generatedPolicies, setGeneratedPolicies] = useState([])
+ const [selectedGenerated, setSelectedGenerated] = useState>(new Set())
+
+ // ---- Compliance state ----
+ const [complianceResult, setComplianceResult] = useState(null)
+
+ // ---- Legal Hold management ----
+ const [managingLegalHolds, setManagingLegalHolds] = useState(false)
+
+ // ---- VVT data ----
+ const [vvtActivities, setVvtActivities] = useState([])
+
+ // --------------------------------------------------------------------------
+ // Persistence
+ // --------------------------------------------------------------------------
+
+ useEffect(() => {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored) {
+ try {
+ const parsed = JSON.parse(stored) as LoeschfristPolicy[]
+ setPolicies(parsed)
+ } catch (e) {
+ console.error('Failed to parse stored policies:', e)
+ }
+ }
+ setLoaded(true)
}, [])
- // Open document in workflow editor
- const handleOpenInEditor = useCallback((doc: UploadedDocument) => {
- router.push(`/compliance/workflow?documentType=loeschfristen&documentId=${doc.id}&mode=change`)
- }, [router])
+ useEffect(() => {
+ if (loaded && policies.length > 0) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(policies))
+ } else if (loaded && policies.length === 0) {
+ localStorage.removeItem(STORAGE_KEY)
+ }
+ }, [policies, loaded])
- const filteredPolicies = filter === 'all'
- ? policies
- : policies.filter(p => p.status === filter)
+ // Load VVT activities from localStorage
+ useEffect(() => {
+ try {
+ const raw = localStorage.getItem('bp_vvt')
+ if (raw) {
+ const parsed = JSON.parse(raw)
+ if (Array.isArray(parsed)) setVvtActivities(parsed)
+ }
+ } catch {
+ // ignore
+ }
+ }, [tab, editingId])
- const activeCount = policies.filter(p => p.status === 'active').length
- const reviewNeededCount = policies.filter(p => p.status === 'review-needed' || p.nextReview <= new Date()).length
- const totalRecords = policies.reduce((sum, p) => sum + (p.recordCount || 0), 0)
+ // --------------------------------------------------------------------------
+ // Derived
+ // --------------------------------------------------------------------------
- const stepInfo = STEP_EXPLANATIONS['loeschfristen']
+ const editingPolicy = useMemo(
+ () => policies.find((p) => p.policyId === editingId) ?? null,
+ [policies, editingId],
+ )
- return (
-
- {/* Step Header */}
-
{
+ let result = [...policies]
+ if (searchQuery.trim()) {
+ const q = searchQuery.toLowerCase()
+ result = result.filter(
+ (p) =>
+ p.dataObjectName.toLowerCase().includes(q) ||
+ p.policyId.toLowerCase().includes(q) ||
+ p.description.toLowerCase().includes(q),
+ )
+ }
+ if (filter === 'active') result = result.filter((p) => p.status === 'ACTIVE')
+ else if (filter === 'draft') result = result.filter((p) => p.status === 'DRAFT')
+ else if (filter === 'review')
+ result = result.filter((p) => isPolicyOverdue(p))
+ if (driverFilter !== 'all')
+ result = result.filter((p) => p.retentionDriver === driverFilter)
+ return result
+ }, [policies, searchQuery, filter, driverFilter])
+
+ const stats = useMemo(() => {
+ const total = policies.length
+ const active = policies.filter((p) => p.status === 'ACTIVE').length
+ const draft = policies.filter((p) => p.status === 'DRAFT').length
+ const overdue = policies.filter((p) => isPolicyOverdue(p)).length
+ const legalHolds = policies.reduce(
+ (acc, p) => acc + getActiveLegalHolds(p).length,
+ 0,
+ )
+ return { total, active, draft, overdue, legalHolds }
+ }, [policies])
+
+ // --------------------------------------------------------------------------
+ // Handlers
+ // --------------------------------------------------------------------------
+
+ const updatePolicy = useCallback(
+ (id: string, updater: (p: LoeschfristPolicy) => LoeschfristPolicy) => {
+ setPolicies((prev) =>
+ prev.map((p) => (p.policyId === id ? updater(p) : p)),
+ )
+ },
+ [],
+ )
+
+ const createNewPolicy = useCallback(() => {
+ const newP = createEmptyPolicy()
+ setPolicies((prev) => [...prev, newP])
+ setEditingId(newP.policyId)
+ setTab('editor')
+ }, [])
+
+ const deletePolicy = useCallback(
+ (id: string) => {
+ setPolicies((prev) => prev.filter((p) => p.policyId !== id))
+ if (editingId === id) setEditingId(null)
+ },
+ [editingId],
+ )
+
+ const addLegalHold = useCallback(
+ (policyId: string) => {
+ updatePolicy(policyId, (p) => ({
+ ...p,
+ legalHolds: [...p.legalHolds, createEmptyLegalHold()],
+ }))
+ },
+ [updatePolicy],
+ )
+
+ const removeLegalHold = useCallback(
+ (policyId: string, idx: number) => {
+ updatePolicy(policyId, (p) => ({
+ ...p,
+ legalHolds: p.legalHolds.filter((_, i) => i !== idx),
+ }))
+ },
+ [updatePolicy],
+ )
+
+ const addStorageLocation = useCallback(
+ (policyId: string) => {
+ updatePolicy(policyId, (p) => ({
+ ...p,
+ storageLocations: [...p.storageLocations, createEmptyStorageLocation()],
+ }))
+ },
+ [updatePolicy],
+ )
+
+ const removeStorageLocation = useCallback(
+ (policyId: string, idx: number) => {
+ updatePolicy(policyId, (p) => ({
+ ...p,
+ storageLocations: p.storageLocations.filter((_, i) => i !== idx),
+ }))
+ },
+ [updatePolicy],
+ )
+
+ const handleProfilingAnswer = useCallback(
+ (stepIndex: number, questionId: string, value: any) => {
+ setProfilingAnswers((prev) => {
+ const existing = prev.findIndex(
+ (a) => a.stepIndex === stepIndex && a.questionId === questionId,
+ )
+ const answer: ProfilingAnswer = { stepIndex, questionId, value }
+ if (existing >= 0) {
+ const copy = [...prev]
+ copy[existing] = answer
+ return copy
+ }
+ return [...prev, answer]
+ })
+ },
+ [],
+ )
+
+ const handleGenerate = useCallback(() => {
+ const generated = generatePoliciesFromProfile(profilingAnswers)
+ setGeneratedPolicies(generated)
+ setSelectedGenerated(new Set(generated.map((p) => p.policyId)))
+ }, [profilingAnswers])
+
+ const adoptGeneratedPolicies = useCallback(
+ (onlySelected: boolean) => {
+ const toAdopt = onlySelected
+ ? generatedPolicies.filter((p) => selectedGenerated.has(p.policyId))
+ : generatedPolicies
+ setPolicies((prev) => [...prev, ...toAdopt])
+ setGeneratedPolicies([])
+ setSelectedGenerated(new Set())
+ setProfilingStep(0)
+ setProfilingAnswers([])
+ setTab('uebersicht')
+ },
+ [generatedPolicies, selectedGenerated],
+ )
+
+ const runCompliance = useCallback(() => {
+ const result = runComplianceCheck(policies)
+ setComplianceResult(result)
+ }, [policies])
+
+ // --------------------------------------------------------------------------
+ // Tab definitions
+ // --------------------------------------------------------------------------
+
+ const TAB_CONFIG: { key: Tab; label: string }[] = [
+ { key: 'uebersicht', label: 'Uebersicht' },
+ { key: 'editor', label: 'Editor' },
+ { key: 'generator', label: 'Generator' },
+ { key: 'export', label: 'Export & Compliance' },
+ ]
+
+ // --------------------------------------------------------------------------
+ // Render helpers
+ // --------------------------------------------------------------------------
+
+ const renderStatusBadge = (status: PolicyStatus) => {
+ const colors = STATUS_COLORS[status] ?? 'bg-gray-100 text-gray-800'
+ const label = STATUS_LABELS[status] ?? status
+ return (
+
-
-
-
-
- Loeschfrist hinzufuegen
-
-
+ {label}
+
+ )
+ }
- {/* Document Upload Section */}
-
+ const renderTriggerBadge = (trigger: DeletionTriggerLevel) => {
+ const colors = TRIGGER_COLORS[trigger] ?? 'bg-gray-100 text-gray-800'
+ const label = TRIGGER_LABELS[trigger] ?? trigger
+ return (
+
+ {label}
+
+ )
+ }
- {/* Stats */}
-
-
-
Datenkategorien
-
{policies.length}
-
-
-
Aktive Regeln
-
{activeCount}
-
-
-
Pruefung erforderlich
-
{reviewNeededCount}
-
-
-
Betroffene Datensaetze
-
{totalRecords.toLocaleString('de-DE')}
-
-
+ // ==========================================================================
+ // TAB 1: Uebersicht
+ // ==========================================================================
- {/* Info Box */}
-
-
-
-
-
-
-
Hinweis zur Datenspeicherung
-
- Nach Art. 5 Abs. 1 lit. e DSGVO duerfen personenbezogene Daten nur so lange gespeichert werden,
- wie es fuer die Zwecke, fuer die sie verarbeitet werden, erforderlich ist (Speicherbegrenzung).
-
-
-
-
-
- {/* Filter */}
-
-
Filter:
- {['all', 'active', 'review-needed'].map(f => (
-
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'
- }`}
+ const renderUebersicht = () => (
+
+ {/* Stats bar */}
+
+ {[
+ { label: 'Gesamt', value: stats.total, color: 'text-gray-900' },
+ { label: 'Aktiv', value: stats.active, color: 'text-green-600' },
+ { label: 'Entwurf', value: stats.draft, color: 'text-yellow-600' },
+ {
+ label: 'Pruefung faellig',
+ value: stats.overdue,
+ color: 'text-red-600',
+ },
+ {
+ label: 'Legal Holds aktiv',
+ value: stats.legalHolds,
+ color: 'text-orange-600',
+ },
+ ].map((s) => (
+
- {f === 'all' ? 'Alle' :
- f === 'active' ? 'Aktiv' : 'Pruefung erforderlich'}
-
- ))}
-
-
- {/* Policies List */}
-
- {filteredPolicies.map(policy => (
-
- ))}
-
-
- {filteredPolicies.length === 0 && (
-
-
-
-
-
+
{s.value}
+
{s.label}
-
Keine Loeschfristen gefunden
-
Passen Sie den Filter an oder fuegen Sie neue Loeschfristen hinzu.
+ ))}
+
+
+ {/* Search & filters */}
+
+
setSearchQuery(e.target.value)}
+ placeholder="Suche nach Name, ID oder Beschreibung..."
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+
+ Status:
+ {[
+ { key: 'all', label: 'Alle' },
+ { key: 'active', label: 'Aktiv' },
+ { key: 'draft', label: 'Entwurf' },
+ { key: 'review', label: 'Pruefung noetig' },
+ ].map((f) => (
+ setFilter(f.key)}
+ className={`px-3 py-1 rounded-lg text-sm font-medium transition ${
+ filter === f.key
+ ? 'bg-purple-600 text-white'
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
+ }`}
+ >
+ {f.label}
+
+ ))}
+
+ Aufbewahrungstreiber:
+
+ setDriverFilter(e.target.value)}
+ className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Alle
+ {Object.entries(RETENTION_DRIVER_META).map(([key, meta]) => (
+
+ {meta.label}
+
+ ))}
+
+
+
+
+ {/* Policy cards or empty state */}
+ {filteredPolicies.length === 0 && policies.length === 0 ? (
+
+
📋
+
+ Noch keine Loeschfristen angelegt
+
+
+ Starten Sie den Generator, um auf Basis Ihres Unternehmensprofils
+ automatisch passende Loeschfristen zu erstellen, oder legen Sie
+ manuell eine neue Loeschfrist an.
+
+
+ setTab('generator')}
+ className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Generator starten
+
+
+ Neue Loeschfrist
+
+
+
+ ) : filteredPolicies.length === 0 ? (
+
+
+ Keine Loeschfristen entsprechen den aktuellen Filtern.
+
+
+ ) : (
+
+ {filteredPolicies.map((p) => {
+ const trigger = getEffectiveDeletionTrigger(p)
+ const activeHolds = getActiveLegalHolds(p)
+ const overdue = isPolicyOverdue(p)
+ return (
+
+ {activeHolds.length > 0 && (
+
+ ⚠
+
+ )}
+
+ {p.policyId}
+
+
+ {p.dataObjectName || 'Ohne Bezeichnung'}
+
+
+ {renderTriggerBadge(trigger)}
+
+ {formatRetentionDuration(p)}
+
+ {renderStatusBadge(p.status)}
+ {overdue && (
+
+ Pruefung faellig
+
+ )}
+
+ {p.description && (
+
+ {p.description}
+
+ )}
+
{
+ setEditingId(p.policyId)
+ setTab('editor')
+ }}
+ className="text-sm text-purple-600 hover:text-purple-800 font-medium"
+ >
+ Bearbeiten →
+
+
+ )
+ })}
+
+ )}
+
+ {/* Floating action button */}
+ {policies.length > 0 && (
+
+
+ + Neue Loeschfrist
+
)}
)
+
+ // ==========================================================================
+ // TAB 2: Editor
+ // ==========================================================================
+
+ const renderEditorNoSelection = () => (
+
+
+ Loeschfrist zum Bearbeiten waehlen
+
+ {policies.length === 0 ? (
+
+ Noch keine Loeschfristen vorhanden.{' '}
+
+ Neue Loeschfrist anlegen
+
+
+ ) : (
+
+ {policies.map((p) => (
+
setEditingId(p.policyId)}
+ className="w-full text-left px-4 py-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition flex items-center justify-between"
+ >
+
+
+ {p.policyId}
+
+
+ {p.dataObjectName || 'Ohne Bezeichnung'}
+
+
+ {renderStatusBadge(p.status)}
+
+ ))}
+
+ + Neue Loeschfrist anlegen
+
+
+ )}
+
+ )
+
+ const renderEditorForm = (policy: LoeschfristPolicy) => {
+ const pid = policy.policyId
+
+ const set =
(
+ key: K,
+ val: LoeschfristPolicy[K],
+ ) => {
+ updatePolicy(pid, (p) => ({ ...p, [key]: val }))
+ }
+
+ const updateLegalHold = (
+ idx: number,
+ updater: (h: LegalHold) => LegalHold,
+ ) => {
+ updatePolicy(pid, (p) => ({
+ ...p,
+ legalHolds: p.legalHolds.map((h, i) => (i === idx ? updater(h) : h)),
+ }))
+ }
+
+ const updateStorageLocation = (
+ idx: number,
+ updater: (s: StorageLocation) => StorageLocation,
+ ) => {
+ updatePolicy(pid, (p) => ({
+ ...p,
+ storageLocations: p.storageLocations.map((s, i) =>
+ i === idx ? updater(s) : s,
+ ),
+ }))
+ }
+
+ return (
+
+ {/* Header with back button */}
+
+
+ setEditingId(null)}
+ className="text-gray-400 hover:text-gray-600 transition"
+ >
+ ← Zurueck
+
+
+ {policy.dataObjectName || 'Neue Loeschfrist'}
+
+
+ {policy.policyId}
+
+
+ {renderStatusBadge(policy.status)}
+
+
+ {/* Sektion 1: Datenobjekt */}
+
+
+ 1. Datenobjekt
+
+
+
+
+ Name des Datenobjekts *
+
+ set('dataObjectName', e.target.value)}
+ placeholder="z.B. Bewerbungsunterlagen"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+
+
+
+
+ Beschreibung
+
+
+
+
+
+ Betroffene Personengruppen
+
+ set('affectedGroups', v)}
+ placeholder="z.B. Bewerber, Mitarbeiter... (Enter zum Hinzufuegen)"
+ />
+
+
+
+
+ Datenkategorien
+
+ set('dataCategories', v)}
+ placeholder="z.B. Stammdaten, Kontaktdaten... (Enter zum Hinzufuegen)"
+ />
+
+
+
+
+ Primaerer Verarbeitungszweck
+
+
+
+
+ {/* Sektion 2: 3-stufige Loeschlogik */}
+
+
+ 2. 3-stufige Loeschlogik
+
+
+
+
+ Loeschausloeser (Trigger-Stufe)
+
+
+ {(
+ ['PURPOSE_END', 'RETENTION_DRIVER', 'LEGAL_HOLD'] as DeletionTriggerLevel[]
+ ).map((trigger) => (
+
+ set('deletionTrigger', trigger)}
+ className="mt-0.5 text-purple-600 focus:ring-purple-500"
+ />
+
+
+ {renderTriggerBadge(trigger)}
+
+
+ {trigger === 'PURPOSE_END' &&
+ 'Loeschung nach Wegfall des Verarbeitungszwecks'}
+ {trigger === 'RETENTION_DRIVER' &&
+ 'Loeschung nach Ablauf gesetzlicher oder vertraglicher Aufbewahrungsfrist'}
+ {trigger === 'LEGAL_HOLD' &&
+ 'Loeschung durch aktiven Legal Hold blockiert'}
+
+
+
+ ))}
+
+
+
+ {/* Retention driver selection */}
+ {policy.deletionTrigger === 'RETENTION_DRIVER' && (
+
+
+ Aufbewahrungstreiber
+
+ {
+ const driver = e.target.value as RetentionDriverType
+ const meta =
+ RETENTION_DRIVER_META[driver]
+ set('retentionDriver', driver)
+ if (meta) {
+ set('retentionDuration', meta.defaultDuration)
+ set('retentionUnit', meta.defaultUnit as RetentionUnit)
+ set('retentionDescription', meta.description)
+ }
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Bitte waehlen...
+ {Object.entries(RETENTION_DRIVER_META).map(([key, meta]) => (
+
+ {meta.label}
+
+ ))}
+
+
+ )}
+
+
+
+
+ Aufbewahrungsdauer
+
+
+ set('retentionDuration', parseInt(e.target.value) || 0)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+
+
+
+ Einheit
+
+
+ set('retentionUnit', e.target.value as RetentionUnit)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Tage
+ Monate
+ Jahre
+
+
+
+
+
+
+ Beschreibung der Aufbewahrungspflicht
+
+ set('retentionDescription', e.target.value)}
+ placeholder="z.B. Handelsrechtliche Aufbewahrungspflicht gem. HGB"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+
+
+
+
+ Startereignis (Fristbeginn)
+
+ set('startEvent', e.target.value)}
+ placeholder="z.B. Ende des Geschaeftsjahres, Vertragsende..."
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+
+
+ {/* Legal Holds */}
+
+
+
+ Legal Holds
+
+
+
+ set('hasActiveLegalHold', e.target.checked)
+ }
+ className="text-purple-600 focus:ring-purple-500 rounded"
+ />
+ Aktiver Legal Hold
+
+
+
+ {policy.legalHolds.length > 0 && (
+
+
+
+
+
+ Bezeichnung
+
+
+ Grund
+
+
+ Status
+
+
+ Erstellt am
+
+
+ Aktion
+
+
+
+
+ {policy.legalHolds.map((hold, idx) => (
+
+
+
+ updateLegalHold(idx, (h) => ({
+ ...h,
+ name: e.target.value,
+ }))
+ }
+ placeholder="Bezeichnung"
+ className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+
+ updateLegalHold(idx, (h) => ({
+ ...h,
+ reason: e.target.value,
+ }))
+ }
+ placeholder="Grund"
+ className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+
+ updateLegalHold(idx, (h) => ({
+ ...h,
+ status: e.target.value as LegalHoldStatus,
+ }))
+ }
+ className="px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ >
+ Aktiv
+ Aufgehoben
+ Abgelaufen
+
+
+
+
+ updateLegalHold(idx, (h) => ({
+ ...h,
+ createdAt: e.target.value,
+ }))
+ }
+ className="px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+ removeLegalHold(pid, idx)}
+ className="text-red-500 hover:text-red-700 text-sm font-medium"
+ >
+ Entfernen
+
+
+
+ ))}
+
+
+
+ )}
+
+
addLegalHold(pid)}
+ className="text-sm text-purple-600 hover:text-purple-800 font-medium"
+ >
+ + Legal Hold hinzufuegen
+
+
+
+
+ {/* Sektion 3: Speicherorte & Loeschmethode */}
+
+
+ 3. Speicherorte & Loeschmethode
+
+
+ {policy.storageLocations.length > 0 && (
+
+
+
+
+
+ Name
+
+
+ Typ
+
+
+ Backup
+
+
+ Anbieter
+
+
+ Loeschfaehig
+
+
+ Aktion
+
+
+
+
+ {policy.storageLocations.map((loc, idx) => (
+
+
+
+ updateStorageLocation(idx, (s) => ({
+ ...s,
+ name: e.target.value,
+ }))
+ }
+ placeholder="Name"
+ className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+
+ updateStorageLocation(idx, (s) => ({
+ ...s,
+ type: e.target.value as StorageLocationType,
+ }))
+ }
+ className="px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ >
+ {Object.entries(STORAGE_LOCATION_LABELS).map(
+ ([key, label]) => (
+
+ {label}
+
+ ),
+ )}
+
+
+
+
+ updateStorageLocation(idx, (s) => ({
+ ...s,
+ isBackup: e.target.checked,
+ }))
+ }
+ className="text-purple-600 focus:ring-purple-500 rounded"
+ />
+
+
+
+ updateStorageLocation(idx, (s) => ({
+ ...s,
+ provider: e.target.value,
+ }))
+ }
+ placeholder="Anbieter"
+ className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+
+ updateStorageLocation(idx, (s) => ({
+ ...s,
+ deletionCapable: e.target.checked,
+ }))
+ }
+ className="text-purple-600 focus:ring-purple-500 rounded"
+ />
+
+
+ removeStorageLocation(pid, idx)}
+ className="text-red-500 hover:text-red-700 text-sm font-medium"
+ >
+ Entfernen
+
+
+
+ ))}
+
+
+
+ )}
+
+
addStorageLocation(pid)}
+ className="text-sm text-purple-600 hover:text-purple-800 font-medium"
+ >
+ + Speicherort hinzufuegen
+
+
+
+
+
+ Loeschmethode
+
+
+ set('deletionMethod', e.target.value as DeletionMethodType)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ {Object.entries(DELETION_METHOD_LABELS).map(([key, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ Details zur Loeschmethode
+
+
+
+
+
+ {/* Sektion 4: Verantwortlichkeit */}
+
+
+ 4. Verantwortlichkeit
+
+
+
+
+
+
+ Freigabeprozess
+
+
+
+
+ {/* Sektion 5: VVT-Verknuepfung */}
+
+
+ 5. VVT-Verknuepfung
+
+
+ {vvtActivities.length > 0 ? (
+
+
+ Verknuepfen Sie diese Loeschfrist mit einer
+ Verarbeitungstaetigkeit aus Ihrem VVT.
+
+
+ {policy.linkedVvtIds && policy.linkedVvtIds.length > 0 && (
+
+
+ Verknuepfte Taetigkeiten:
+
+
+ {policy.linkedVvtIds.map((vvtId: string) => {
+ const activity = vvtActivities.find(
+ (a: any) => a.id === vvtId,
+ )
+ return (
+
+ {activity?.name || vvtId}
+
+ updatePolicy(pid, (p) => ({
+ ...p,
+ linkedVvtIds: (
+ p.linkedVvtIds || []
+ ).filter((id: string) => id !== vvtId),
+ }))
+ }
+ className="text-blue-600 hover:text-blue-900"
+ >
+ x
+
+
+ )
+ })}
+
+
+ )}
+
{
+ const val = e.target.value
+ if (
+ val &&
+ !(policy.linkedVvtIds || []).includes(val)
+ ) {
+ updatePolicy(pid, (p) => ({
+ ...p,
+ linkedVvtIds: [...(p.linkedVvtIds || []), val],
+ }))
+ }
+ e.target.value = ''
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+
+ Verarbeitungstaetigkeit verknuepfen...
+
+ {vvtActivities
+ .filter(
+ (a: any) =>
+ !(policy.linkedVvtIds || []).includes(a.id),
+ )
+ .map((a: any) => (
+
+ {a.name || a.id}
+
+ ))}
+
+
+
+ ) : (
+
+ Kein VVT gefunden. Erstellen Sie zuerst ein
+ Verarbeitungsverzeichnis, um hier Verknuepfungen herstellen zu
+ koennen.
+
+ )}
+
+
+ {/* Sektion 6: Review-Einstellungen */}
+
+
+ 6. Review-Einstellungen
+
+
+
+
+
+ Status
+
+
+ set('status', e.target.value as PolicyStatus)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ {Object.entries(STATUS_LABELS).map(([key, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ Pruefintervall
+
+
+ set('reviewInterval', e.target.value as ReviewInterval)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ {Object.entries(REVIEW_INTERVAL_LABELS).map(
+ ([key, label]) => (
+
+ {label}
+
+ ),
+ )}
+
+
+
+
+
+
+
+
+ Tags
+
+ set('tags', v)}
+ placeholder="Tags hinzufuegen (Enter zum Bestaetigen)"
+ />
+
+
+
+ {/* Action buttons */}
+
+
{
+ if (
+ confirm(
+ 'Moechten Sie diese Loeschfrist wirklich loeschen?',
+ )
+ ) {
+ deletePolicy(pid)
+ setTab('uebersicht')
+ }
+ }}
+ className="text-red-600 hover:text-red-800 font-medium text-sm"
+ >
+ Loeschfrist loeschen
+
+
+ {
+ setEditingId(null)
+ setTab('uebersicht')
+ }}
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Zurueck zur Uebersicht
+
+ {
+ setEditingId(null)
+ setTab('uebersicht')
+ }}
+ className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Speichern & Schliessen
+
+
+
+
+ )
+ }
+
+ const renderEditor = () => {
+ if (!editingId || !editingPolicy) {
+ return renderEditorNoSelection()
+ }
+ return renderEditorForm(editingPolicy)
+ }
+
+ // ==========================================================================
+ // TAB 3: Generator
+ // ==========================================================================
+
+ const renderGenerator = () => {
+ const totalSteps = PROFILING_STEPS.length
+ const progress = getProfilingProgress(profilingAnswers)
+ const allComplete = PROFILING_STEPS.every((step, idx) =>
+ isStepComplete(step, profilingAnswers.filter((a) => a.stepIndex === idx)),
+ )
+
+ // If we have generated policies, show the preview
+ if (generatedPolicies.length > 0) {
+ return (
+
+
+
+ Generierte Loeschfristen
+
+
+ Auf Basis Ihres Profils wurden {generatedPolicies.length}{' '}
+ Loeschfristen generiert. Waehlen Sie die relevanten aus und
+ uebernehmen Sie sie.
+
+
+
+
+ setSelectedGenerated(
+ new Set(generatedPolicies.map((p) => p.policyId)),
+ )
+ }
+ className="text-sm text-purple-600 hover:text-purple-800 font-medium"
+ >
+ Alle auswaehlen
+
+ setSelectedGenerated(new Set())}
+ className="text-sm text-gray-500 hover:text-gray-700 font-medium"
+ >
+ Alle abwaehlen
+
+
+
+
+ {generatedPolicies.map((gp) => {
+ const selected = selectedGenerated.has(gp.policyId)
+ return (
+
+ {
+ const next = new Set(selectedGenerated)
+ if (e.target.checked) next.add(gp.policyId)
+ else next.delete(gp.policyId)
+ setSelectedGenerated(next)
+ }}
+ className="mt-1 text-purple-600 focus:ring-purple-500 rounded"
+ />
+
+
+
+ {gp.dataObjectName}
+
+
+ {gp.policyId}
+
+
+
+ {gp.description}
+
+
+ {renderTriggerBadge(
+ getEffectiveDeletionTrigger(gp),
+ )}
+
+ {formatRetentionDuration(gp)}
+
+ {gp.retentionDriver && (
+
+ {RETENTION_DRIVER_META[gp.retentionDriver]
+ ?.label || gp.retentionDriver}
+
+ )}
+
+
+
+ )
+ })}
+
+
+
+
+
{
+ setGeneratedPolicies([])
+ setSelectedGenerated(new Set())
+ }}
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Zurueck zum Profiling
+
+
+ adoptGeneratedPolicies(false)}
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Alle uebernehmen ({generatedPolicies.length})
+
+ adoptGeneratedPolicies(true)}
+ disabled={selectedGenerated.size === 0}
+ className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Ausgewaehlte uebernehmen ({selectedGenerated.size})
+
+
+
+
+ )
+ }
+
+ // Profiling wizard
+ const currentStep: ProfilingStep | undefined = PROFILING_STEPS[profilingStep]
+
+ return (
+
+ {/* Progress bar */}
+
+
+
+ Profiling-Assistent
+
+
+ Schritt {profilingStep + 1} von {totalSteps}
+
+
+
+
+ {PROFILING_STEPS.map((step, idx) => (
+ setProfilingStep(idx)}
+ className={`text-xs font-medium transition ${
+ idx === profilingStep
+ ? 'text-purple-600'
+ : idx < profilingStep
+ ? 'text-green-600'
+ : 'text-gray-400'
+ }`}
+ >
+ {step.title}
+
+ ))}
+
+
+
+ {/* Current step questions */}
+ {currentStep && (
+
+
+
+ {currentStep.title}
+
+ {currentStep.description && (
+
+ {currentStep.description}
+
+ )}
+
+
+ {currentStep.questions.map((question) => {
+ const currentAnswer = profilingAnswers.find(
+ (a) =>
+ a.stepIndex === profilingStep &&
+ a.questionId === question.id,
+ )
+
+ return (
+
+
+ {question.label}
+ {question.helpText && (
+
+ {question.helpText}
+
+ )}
+
+
+ {/* Boolean */}
+ {question.type === 'boolean' && (
+
+ {[
+ { val: true, label: 'Ja' },
+ { val: false, label: 'Nein' },
+ ].map((opt) => (
+
+ handleProfilingAnswer(
+ profilingStep,
+ question.id,
+ opt.val,
+ )
+ }
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
+ currentAnswer?.value === opt.val
+ ? 'bg-purple-600 text-white'
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
+ }`}
+ >
+ {opt.label}
+
+ ))}
+
+ )}
+
+ {/* Single select */}
+ {question.type === 'single' && question.options && (
+
+ {question.options.map((opt) => (
+
+
+ handleProfilingAnswer(
+ profilingStep,
+ question.id,
+ opt.value,
+ )
+ }
+ className="text-purple-600 focus:ring-purple-500"
+ />
+
+
+ {opt.label}
+
+ {opt.description && (
+
+ {opt.description}
+
+ )}
+
+
+ ))}
+
+ )}
+
+ {/* Multi select */}
+ {question.type === 'multi' && question.options && (
+
+ {question.options.map((opt) => {
+ const selectedValues: string[] =
+ currentAnswer?.value || []
+ const isSelected = selectedValues.includes(opt.value)
+ return (
+
+ {
+ let next: string[]
+ if (e.target.checked) {
+ next = [...selectedValues, opt.value]
+ } else {
+ next = selectedValues.filter(
+ (v) => v !== opt.value,
+ )
+ }
+ handleProfilingAnswer(
+ profilingStep,
+ question.id,
+ next,
+ )
+ }}
+ className="text-purple-600 focus:ring-purple-500 rounded"
+ />
+
+
+ {opt.label}
+
+ {opt.description && (
+
+ {opt.description}
+
+ )}
+
+
+ )
+ })}
+
+ )}
+
+ {/* Number input */}
+ {question.type === 'number' && (
+
+ handleProfilingAnswer(
+ profilingStep,
+ question.id,
+ e.target.value ? parseInt(e.target.value) : '',
+ )
+ }
+ min={0}
+ placeholder="Bitte Zahl eingeben"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+ )}
+
+ )
+ })}
+
+ )}
+
+ {/* Navigation */}
+
+ setProfilingStep((s) => Math.max(0, s - 1))}
+ disabled={profilingStep === 0}
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Zurueck
+
+
+ {profilingStep < totalSteps - 1 ? (
+
+ setProfilingStep((s) => Math.min(totalSteps - 1, s + 1))
+ }
+ className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Weiter
+
+ ) : (
+
+ Loeschfristen generieren
+
+ )}
+
+
+ )
+ }
+
+ // ==========================================================================
+ // TAB 4: Export & Compliance
+ // ==========================================================================
+
+ const renderExport = () => {
+ const allLegalHolds = policies.flatMap((p) =>
+ p.legalHolds.map((h) => ({
+ ...h,
+ policyId: p.policyId,
+ policyName: p.dataObjectName,
+ })),
+ )
+ const activeLegalHolds = allLegalHolds.filter(
+ (h) => h.status === 'ACTIVE',
+ )
+
+ return (
+
+ {/* Compliance Check */}
+
+
+
+ Compliance-Check
+
+
+ Analyse starten
+
+
+
+ {policies.length === 0 && (
+
+ Erstellen Sie zuerst Loeschfristen, um eine Compliance-Analyse
+ durchzufuehren.
+
+ )}
+
+ {complianceResult && (
+
+ {/* Score */}
+
+
= 75
+ ? 'text-green-600'
+ : complianceResult.score >= 50
+ ? 'text-yellow-600'
+ : 'text-red-600'
+ }`}
+ >
+ {complianceResult.score}
+
+
+
+ Compliance-Score
+
+
+ {complianceResult.score >= 75
+ ? 'Guter Zustand - wenige Optimierungen noetig'
+ : complianceResult.score >= 50
+ ? 'Verbesserungsbedarf - wichtige Punkte offen'
+ : 'Kritisch - dringender Handlungsbedarf'}
+
+
+
+
+ {/* Issues grouped by severity */}
+ {(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const).map(
+ (severity) => {
+ const issues = complianceResult.issues.filter(
+ (i) => i.severity === severity,
+ )
+ if (issues.length === 0) return null
+
+ const severityConfig = {
+ CRITICAL: {
+ label: 'Kritisch',
+ bg: 'bg-red-50',
+ border: 'border-red-200',
+ text: 'text-red-800',
+ badge: 'bg-red-100 text-red-800',
+ },
+ HIGH: {
+ label: 'Hoch',
+ bg: 'bg-orange-50',
+ border: 'border-orange-200',
+ text: 'text-orange-800',
+ badge: 'bg-orange-100 text-orange-800',
+ },
+ MEDIUM: {
+ label: 'Mittel',
+ bg: 'bg-yellow-50',
+ border: 'border-yellow-200',
+ text: 'text-yellow-800',
+ badge: 'bg-yellow-100 text-yellow-800',
+ },
+ LOW: {
+ label: 'Niedrig',
+ bg: 'bg-blue-50',
+ border: 'border-blue-200',
+ text: 'text-blue-800',
+ badge: 'bg-blue-100 text-blue-800',
+ },
+ }[severity]
+
+ return (
+
+
+
+ {severityConfig.label}
+
+
+ {issues.length}{' '}
+ {issues.length === 1 ? 'Problem' : 'Probleme'}
+
+
+
+ {issues.map((issue, idx) => (
+
+
+ {issue.title}
+
+
+ {issue.description}
+
+ {issue.recommendation && (
+
+ Empfehlung: {issue.recommendation}
+
+ )}
+ {issue.affectedPolicyId && (
+
{
+ setEditingId(issue.affectedPolicyId!)
+ setTab('editor')
+ }}
+ className="text-xs text-purple-600 hover:text-purple-800 font-medium mt-1"
+ >
+ Zur Loeschfrist: {issue.affectedPolicyId}
+
+ )}
+
+ ))}
+
+
+ )
+ },
+ )}
+
+ {complianceResult.issues.length === 0 && (
+
+
+ Keine Compliance-Probleme gefunden
+
+
+ Alle Loeschfristen entsprechen den Anforderungen.
+
+
+ )}
+
+ )}
+
+
+ {/* Legal Hold Management */}
+
+
+ Legal Hold Verwaltung
+
+
+ {allLegalHolds.length === 0 ? (
+
+ Keine Legal Holds vorhanden.
+
+ ) : (
+
+
+
+ Gesamt: {' '}
+
+ {allLegalHolds.length}
+
+
+
+ Aktiv: {' '}
+
+ {activeLegalHolds.length}
+
+
+
+
+
+
+
+
+
+ Loeschfrist
+
+
+ Bezeichnung
+
+
+ Grund
+
+
+ Status
+
+
+ Erstellt
+
+
+
+
+ {allLegalHolds.map((hold, idx) => (
+
+
+ {
+ setEditingId(hold.policyId)
+ setTab('editor')
+ }}
+ className="text-purple-600 hover:text-purple-800 font-medium text-xs"
+ >
+ {hold.policyName || hold.policyId}
+
+
+
+ {hold.name || '-'}
+
+
+ {hold.reason || '-'}
+
+
+
+ {hold.status === 'ACTIVE'
+ ? 'Aktiv'
+ : hold.status === 'RELEASED'
+ ? 'Aufgehoben'
+ : 'Abgelaufen'}
+
+
+
+ {hold.createdAt || '-'}
+
+
+ ))}
+
+
+
+
+ )}
+
+
+ {/* Export */}
+
+
+ Datenexport
+
+
+ Exportieren Sie Ihre Loeschfristen und den Compliance-Status in
+ verschiedenen Formaten.
+
+
+ {policies.length === 0 ? (
+
+ Erstellen Sie zuerst Loeschfristen, um Exporte zu generieren.
+
+ ) : (
+
+
+ downloadFile(
+ exportPoliciesAsJSON(policies),
+ 'loeschfristen-export.json',
+ 'application/json',
+ )
+ }
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
+ >
+ JSON Export
+
+
+ downloadFile(
+ exportPoliciesAsCSV(policies),
+ 'loeschfristen-export.csv',
+ 'text/csv;charset=utf-8',
+ )
+ }
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
+ >
+ CSV Export
+
+
+ downloadFile(
+ generateComplianceSummary(policies),
+ 'compliance-bericht.md',
+ 'text/markdown',
+ )
+ }
+ className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
+ >
+ Compliance-Bericht
+
+
+ )}
+
+
+ )
+ }
+
+ // ==========================================================================
+ // Main render
+ // ==========================================================================
+
+ if (!loaded) {
+ return (
+
+ Lade Loeschfristen...
+
+ )
+ }
+
+ return (
+
+ {/* Step Header */}
+
+
+ {/* Tab bar */}
+
+ {TAB_CONFIG.map((t) => (
+ setTab(t.key)}
+ className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition ${
+ tab === t.key
+ ? 'bg-purple-600 text-white shadow-sm'
+ : 'bg-transparent text-gray-600 hover:bg-gray-200'
+ }`}
+ >
+ {t.label}
+
+ ))}
+
+
+ {/* Tab content */}
+ {tab === 'uebersicht' && renderUebersicht()}
+ {tab === 'editor' && renderEditor()}
+ {tab === 'generator' && renderGenerator()}
+ {tab === 'export' && renderExport()}
+
+ )
}
diff --git a/admin-v2/app/(sdk)/sdk/notfallplan/page.tsx b/admin-v2/app/(sdk)/sdk/notfallplan/page.tsx
new file mode 100644
index 0000000..3c07313
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/notfallplan/page.tsx
@@ -0,0 +1,1147 @@
+'use client'
+
+/**
+ * Notfallplan & Breach Response (Art. 33/34 DSGVO)
+ *
+ * 4 Tabs:
+ * 1. Notfallplan-Konfiguration (Meldewege, Zustaendigkeiten, Eskalation)
+ * 2. Incident-Register (Art. 33 Abs. 5)
+ * 3. Melde-Templates (Art. 33 + Art. 34)
+ * 4. Uebungen & Tests
+ */
+
+import { useState, useEffect, useCallback } from 'react'
+import { useSDK } from '@/lib/sdk'
+import StepHeader from '@/components/sdk/StepHeader/StepHeader'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+type Tab = 'config' | 'incidents' | 'templates' | 'exercises'
+
+type IncidentStatus = 'detected' | 'classified' | 'assessed' | 'reported' | 'not_reportable' | 'closed'
+type IncidentSeverity = 'low' | 'medium' | 'high' | 'critical'
+
+interface NotfallplanConfig {
+ meldewege: MeldeStep[]
+ zustaendigkeiten: Zustaendigkeit[]
+ aufsichtsbehoerde: {
+ name: string
+ state: string
+ email: string
+ phone: string
+ url: string
+ }
+ eskalationsstufen: Eskalationsstufe[]
+ sofortmassnahmen: string[]
+}
+
+interface MeldeStep {
+ id: string
+ order: number
+ role: string
+ name: string
+ action: string
+ maxHours: number
+}
+
+interface Zustaendigkeit {
+ id: string
+ role: string
+ name: string
+ email: string
+ phone: string
+}
+
+interface Eskalationsstufe {
+ id: string
+ level: number
+ label: string
+ triggerCondition: string
+ actions: string[]
+}
+
+interface Incident {
+ id: string
+ title: string
+ description: string
+ detectedAt: string
+ detectedBy: string
+ status: IncidentStatus
+ severity: IncidentSeverity
+ affectedDataCategories: string[]
+ estimatedAffectedPersons: number
+ measures: string[]
+ art34Required: boolean
+ art34Justification: string
+ reportedToAuthorityAt?: string
+ notifiedAffectedAt?: string
+ closedAt?: string
+ closedBy?: string
+ lessonsLearned?: string
+}
+
+interface MeldeTemplate {
+ id: string
+ type: 'art33' | 'art34'
+ title: string
+ content: string
+}
+
+interface Exercise {
+ id: string
+ title: string
+ type: 'tabletop' | 'simulation' | 'full_drill'
+ scenario: string
+ scheduledDate: string
+ completedDate?: string
+ participants: string[]
+ lessonsLearned: string
+ nextExerciseDate?: string
+}
+
+// =============================================================================
+// INITIAL DATA
+// =============================================================================
+
+const DEFAULT_CONFIG: NotfallplanConfig = {
+ meldewege: [
+ { id: '1', order: 1, role: 'Entdecker', name: '', action: 'Incident melden an IT-Sicherheit', maxHours: 0 },
+ { id: '2', order: 2, role: 'IT-Sicherheit', name: '', action: 'Erstbewertung und Klassifizierung', maxHours: 4 },
+ { id: '3', order: 3, role: 'Datenschutzbeauftragter', name: '', action: 'Meldepflicht pruefen (Art. 33)', maxHours: 12 },
+ { id: '4', order: 4, role: 'Geschaeftsfuehrung', name: '', action: 'Freigabe der Meldung', maxHours: 24 },
+ { id: '5', order: 5, role: 'DSB', name: '', action: 'Meldung an Aufsichtsbehoerde', maxHours: 72 },
+ ],
+ zustaendigkeiten: [
+ { id: '1', role: 'Incident Owner', name: '', email: '', phone: '' },
+ { id: '2', role: 'Datenschutzbeauftragter', name: '', email: '', phone: '' },
+ { id: '3', role: 'IT-Sicherheit', name: '', email: '', phone: '' },
+ { id: '4', role: 'Recht / Legal', name: '', email: '', phone: '' },
+ { id: '5', role: 'Kommunikation / PR', name: '', email: '', phone: '' },
+ ],
+ aufsichtsbehoerde: {
+ name: '',
+ state: '',
+ email: '',
+ phone: '',
+ url: '',
+ },
+ eskalationsstufen: [
+ { id: '1', level: 1, label: 'Standard', triggerCondition: 'Einzelfall, keine sensiblen Daten', actions: ['IT-Sicherheit informieren', 'DSB benachrichtigen'] },
+ { id: '2', level: 2, label: 'Erhoehte Prioritaet', triggerCondition: 'Sensible Daten oder > 100 Betroffene', actions: ['Geschaeftsfuehrung informieren', 'Notfallteam einberufen'] },
+ { id: '3', level: 3, label: 'Kritisch', triggerCondition: 'Besondere Kategorien Art. 9 oder > 1000 Betroffene', actions: ['Sofortige Krisensitzung', 'Art. 34 Benachrichtigung vorbereiten', 'PR/Kommunikation einbinden'] },
+ ],
+ sofortmassnahmen: [
+ 'Betroffene Systeme isolieren / vom Netz nehmen',
+ 'Zugriffsrechte pruefen und ggf. sperren',
+ 'Beweise sichern (Logs, Screenshots)',
+ 'Zeitleiste der Ereignisse dokumentieren',
+ 'Erstbewertung: Art der Daten, Anzahl Betroffene, Schaden',
+ 'DSB unverzueglich informieren',
+ ],
+}
+
+const DEFAULT_TEMPLATES: MeldeTemplate[] = [
+ {
+ id: 'tpl-art33',
+ type: 'art33',
+ title: 'Meldung an Aufsichtsbehoerde (Art. 33 DSGVO)',
+ content: `Meldung einer Verletzung des Schutzes personenbezogener Daten
+gemaess Art. 33 DSGVO
+
+1. Beschreibung der Verletzung:
+[Art der Verletzung, betroffene Kategorien personenbezogener Daten]
+
+2. Kategorien und ungefaehre Zahl der betroffenen Personen:
+[Anzahl und Kategorien der Betroffenen]
+
+3. Kategorien und ungefaehre Zahl der betroffenen Datensaetze:
+[Datensatz-Kategorien und Anzahl]
+
+4. Name und Kontaktdaten des Datenschutzbeauftragten:
+[Name, E-Mail, Telefon]
+
+5. Beschreibung der wahrscheinlichen Folgen:
+[Moegliche Auswirkungen auf die Betroffenen]
+
+6. Beschreibung der ergriffenen/vorgeschlagenen Massnahmen:
+[Sofortmassnahmen und geplante Abhilfemassnahmen]
+
+7. Zeitpunkt der Feststellung:
+[Datum, Uhrzeit]
+
+8. Begruendung bei verspaeteter Meldung (falls > 72h):
+[Begruendung]`,
+ },
+ {
+ id: 'tpl-art34',
+ type: 'art34',
+ title: 'Benachrichtigung Betroffene (Art. 34 DSGVO)',
+ content: `Benachrichtigung ueber eine Verletzung des Schutzes
+Ihrer personenbezogenen Daten
+
+Sehr geehrte/r [Name/Betroffene],
+
+wir informieren Sie gemaess Art. 34 DSGVO ueber eine Verletzung
+des Schutzes personenbezogener Daten, die Ihre Daten betrifft.
+
+Was ist passiert?
+[Beschreibung in klarer, einfacher Sprache]
+
+Welche Daten sind betroffen?
+[Betroffene Datenkategorien]
+
+Welche Folgen sind moeglich?
+[Moegliche Auswirkungen]
+
+Was haben wir unternommen?
+[Ergriffene Massnahmen]
+
+Was koennen Sie tun?
+[Empfehlungen fuer Betroffene, z.B. Passwort aendern]
+
+Kontakt:
+Unser Datenschutzbeauftragter steht Ihnen fuer Rueckfragen
+zur Verfuegung:
+[Name, E-Mail, Telefon]
+
+Mit freundlichen Gruessen
+[Verantwortlicher]`,
+ },
+]
+
+const INCIDENT_STATUS_LABELS: Record = {
+ detected: 'Erkannt',
+ classified: 'Klassifiziert',
+ assessed: 'Bewertet',
+ reported: 'Gemeldet',
+ not_reportable: 'Nicht meldepflichtig',
+ closed: 'Abgeschlossen',
+}
+
+const INCIDENT_STATUS_COLORS: Record = {
+ detected: 'bg-red-100 text-red-800',
+ classified: 'bg-yellow-100 text-yellow-800',
+ assessed: 'bg-blue-100 text-blue-800',
+ reported: 'bg-green-100 text-green-800',
+ not_reportable: 'bg-gray-100 text-gray-800',
+ closed: 'bg-gray-100 text-gray-600',
+}
+
+const SEVERITY_LABELS: Record = {
+ low: 'Niedrig',
+ medium: 'Mittel',
+ high: 'Hoch',
+ critical: 'Kritisch',
+}
+
+const SEVERITY_COLORS: Record = {
+ low: 'bg-green-100 text-green-800',
+ medium: 'bg-yellow-100 text-yellow-800',
+ high: 'bg-orange-100 text-orange-800',
+ critical: 'bg-red-100 text-red-800',
+}
+
+// =============================================================================
+// LOCAL STORAGE HELPERS
+// =============================================================================
+
+const STORAGE_KEY = 'bp_notfallplan'
+
+function loadFromStorage(): {
+ config: NotfallplanConfig
+ incidents: Incident[]
+ templates: MeldeTemplate[]
+ exercises: Exercise[]
+} {
+ if (typeof window === 'undefined') {
+ return { config: DEFAULT_CONFIG, incidents: [], templates: DEFAULT_TEMPLATES, exercises: [] }
+ }
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored) {
+ return JSON.parse(stored)
+ }
+ } catch {
+ // ignore
+ }
+ return { config: DEFAULT_CONFIG, incidents: [], templates: DEFAULT_TEMPLATES, exercises: [] }
+}
+
+function saveToStorage(data: {
+ config: NotfallplanConfig
+ incidents: Incident[]
+ templates: MeldeTemplate[]
+ exercises: Exercise[]
+}) {
+ if (typeof window === 'undefined') return
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
+}
+
+// =============================================================================
+// COMPONENT: 72h Countdown
+// =============================================================================
+
+function CountdownTimer({ detectedAt }: { detectedAt: string }) {
+ const [remaining, setRemaining] = useState('')
+ const [overdue, setOverdue] = useState(false)
+
+ useEffect(() => {
+ function update() {
+ const detected = new Date(detectedAt).getTime()
+ const deadline = detected + 72 * 60 * 60 * 1000
+ const now = Date.now()
+ const diff = deadline - now
+
+ if (diff <= 0) {
+ const overdueMs = Math.abs(diff)
+ const hours = Math.floor(overdueMs / (1000 * 60 * 60))
+ const mins = Math.floor((overdueMs % (1000 * 60 * 60)) / (1000 * 60))
+ setRemaining(`${hours}h ${mins}min ueberfaellig`)
+ setOverdue(true)
+ } else {
+ const hours = Math.floor(diff / (1000 * 60 * 60))
+ const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+ setRemaining(`${hours}h ${mins}min verbleibend`)
+ setOverdue(false)
+ }
+ }
+ update()
+ const interval = setInterval(update, 60000)
+ return () => clearInterval(interval)
+ }, [detectedAt])
+
+ return (
+
+ 72h-Frist: {remaining}
+
+ )
+}
+
+// =============================================================================
+// MAIN PAGE
+// =============================================================================
+
+export default function NotfallplanPage() {
+ const { state } = useSDK()
+ const [activeTab, setActiveTab] = useState('config')
+ const [config, setConfig] = useState(DEFAULT_CONFIG)
+ const [incidents, setIncidents] = useState([])
+ const [templates, setTemplates] = useState(DEFAULT_TEMPLATES)
+ const [exercises, setExercises] = useState([])
+ const [showAddIncident, setShowAddIncident] = useState(false)
+ const [showAddExercise, setShowAddExercise] = useState(false)
+ const [saved, setSaved] = useState(false)
+
+ useEffect(() => {
+ const data = loadFromStorage()
+ setConfig(data.config)
+ setIncidents(data.incidents)
+ setTemplates(data.templates)
+ setExercises(data.exercises)
+ }, [])
+
+ const handleSave = useCallback(() => {
+ saveToStorage({ config, incidents, templates, exercises })
+ setSaved(true)
+ setTimeout(() => setSaved(false), 2000)
+ }, [config, incidents, templates, exercises])
+
+ const tabs: { id: Tab; label: string; count?: number }[] = [
+ { id: 'config', label: 'Notfallplan' },
+ { id: 'incidents', label: 'Incident-Register', count: incidents.filter(i => i.status !== 'closed').length || undefined },
+ { id: 'templates', label: 'Melde-Templates' },
+ { id: 'exercises', label: 'Uebungen & Tests', count: exercises.filter(e => !e.completedDate).length || undefined },
+ ]
+
+ return (
+
+
+
+ {/* Tab Navigation */}
+
+
+ {tabs.map(tab => (
+ setActiveTab(tab.id)}
+ className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm ${
+ activeTab === tab.id
+ ? 'border-blue-500 text-blue-600'
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+ }`}
+ >
+ {tab.label}
+ {tab.count && (
+
+ {tab.count}
+
+ )}
+
+ ))}
+
+
+
+ {/* Save Button */}
+
+
+ {saved ? 'Gespeichert!' : 'Speichern'}
+
+
+
+ {/* Tab Content */}
+ {activeTab === 'config' && (
+
+ )}
+ {activeTab === 'incidents' && (
+
+ )}
+ {activeTab === 'templates' && (
+
+ )}
+ {activeTab === 'exercises' && (
+
+ )}
+
+ )
+}
+
+// =============================================================================
+// TAB 1: Notfallplan-Konfiguration
+// =============================================================================
+
+function ConfigTab({
+ config,
+ setConfig,
+}: {
+ config: NotfallplanConfig
+ setConfig: React.Dispatch>
+}) {
+ return (
+
+ {/* Meldewege */}
+
+ Meldewege (intern β Aufsichtsbehoerde)
+
+ Definieren Sie die interne Eskalationskette bei einer Datenpanne.
+
+
+ {config.meldewege.map((step, idx) => (
+
+ ))}
+
+
+
+ {/* Zustaendigkeiten */}
+
+
+ {/* Aufsichtsbehoerde */}
+
+ Zustaendige Aufsichtsbehoerde
+
+
+ Name
+ setConfig(prev => ({
+ ...prev,
+ aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, name: e.target.value },
+ }))}
+ placeholder="z.B. LfD Niedersachsen"
+ className="w-full text-sm border rounded px-3 py-2"
+ />
+
+
+ Bundesland
+ setConfig(prev => ({
+ ...prev,
+ aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, state: e.target.value },
+ }))}
+ placeholder="z.B. Niedersachsen"
+ className="w-full text-sm border rounded px-3 py-2"
+ />
+
+
+ E-Mail
+ setConfig(prev => ({
+ ...prev,
+ aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, email: e.target.value },
+ }))}
+ placeholder="poststelle@lfd.niedersachsen.de"
+ className="w-full text-sm border rounded px-3 py-2"
+ />
+
+
+ Telefon
+ setConfig(prev => ({
+ ...prev,
+ aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, phone: e.target.value },
+ }))}
+ placeholder="+49..."
+ className="w-full text-sm border rounded px-3 py-2"
+ />
+
+
+
+
+ {/* Eskalationsstufen */}
+
+ Eskalationsstufen
+
+ {config.eskalationsstufen.map((stufe) => (
+
+
+
+ Stufe {stufe.level}
+
+ {stufe.label}
+
+
Ausloeser: {stufe.triggerCondition}
+
+ {stufe.actions.map((action, i) => (
+ {action}
+ ))}
+
+
+ ))}
+
+
+
+ {/* Sofortmassnahmen-Checkliste */}
+
+ Sofortmassnahmen-Checkliste
+
+ Diese Massnahmen sind sofort bei Entdeckung einer Datenpanne durchzufuehren.
+
+
+ {config.sofortmassnahmen.map((m, idx) => (
+
+
+ {m}
+
+ ))}
+
+
+
+ )
+}
+
+// =============================================================================
+// TAB 2: Incident-Register
+// =============================================================================
+
+function IncidentsTab({
+ incidents,
+ setIncidents,
+ showAdd,
+ setShowAdd,
+}: {
+ incidents: Incident[]
+ setIncidents: React.Dispatch>
+ showAdd: boolean
+ setShowAdd: (v: boolean) => void
+}) {
+ const [newIncident, setNewIncident] = useState>({
+ title: '',
+ description: '',
+ severity: 'medium',
+ affectedDataCategories: [],
+ estimatedAffectedPersons: 0,
+ measures: [],
+ art34Required: false,
+ art34Justification: '',
+ })
+
+ function addIncident() {
+ if (!newIncident.title) return
+ const incident: Incident = {
+ id: `INC-${Date.now()}`,
+ title: newIncident.title || '',
+ description: newIncident.description || '',
+ detectedAt: new Date().toISOString(),
+ detectedBy: 'Admin',
+ status: 'detected',
+ severity: newIncident.severity as IncidentSeverity || 'medium',
+ affectedDataCategories: newIncident.affectedDataCategories || [],
+ estimatedAffectedPersons: newIncident.estimatedAffectedPersons || 0,
+ measures: newIncident.measures || [],
+ art34Required: newIncident.art34Required || false,
+ art34Justification: newIncident.art34Justification || '',
+ }
+ setIncidents(prev => [incident, ...prev])
+ setShowAdd(false)
+ setNewIncident({
+ title: '', description: '', severity: 'medium',
+ affectedDataCategories: [], estimatedAffectedPersons: 0,
+ measures: [], art34Required: false, art34Justification: '',
+ })
+ }
+
+ function updateStatus(id: string, status: IncidentStatus) {
+ setIncidents(prev => prev.map(inc =>
+ inc.id === id
+ ? {
+ ...inc,
+ status,
+ ...(status === 'reported' ? { reportedToAuthorityAt: new Date().toISOString() } : {}),
+ ...(status === 'closed' ? { closedAt: new Date().toISOString(), closedBy: 'Admin' } : {}),
+ }
+ : inc
+ ))
+ }
+
+ return (
+
+
+
+
Incident-Register (Art. 33 Abs. 5)
+
Alle Datenpannen dokumentieren β auch nicht-meldepflichtige.
+
+
setShowAdd(true)}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700"
+ >
+ + Datenpanne melden
+
+
+
+ {/* Add Incident Form */}
+ {showAdd && (
+
+
Neue Datenpanne erfassen
+
+
+ Titel
+ setNewIncident(prev => ({ ...prev, title: e.target.value }))}
+ placeholder="Kurzbeschreibung der Datenpanne"
+ className="w-full border rounded px-3 py-2 text-sm"
+ />
+
+
+ Beschreibung
+
+
+ Schweregrad
+ setNewIncident(prev => ({ ...prev, severity: e.target.value as IncidentSeverity }))}
+ className="w-full border rounded px-3 py-2 text-sm"
+ >
+ Niedrig
+ Mittel
+ Hoch
+ Kritisch
+
+
+
+ Geschaetzte Betroffene
+ setNewIncident(prev => ({ ...prev, estimatedAffectedPersons: parseInt(e.target.value) || 0 }))}
+ className="w-full border rounded px-3 py-2 text-sm"
+ />
+
+
+ setNewIncident(prev => ({ ...prev, art34Required: e.target.checked }))}
+ className="rounded"
+ />
+ Hohes Risiko fuer Betroffene (Art. 34 Benachrichtigungspflicht)
+
+
+
+ setShowAdd(false)}
+ className="px-4 py-2 border rounded-lg text-sm"
+ >
+ Abbrechen
+
+
+ Datenpanne erfassen
+
+
+
+ )}
+
+ {/* Incidents List */}
+ {incidents.length === 0 ? (
+
+
Keine Datenpannen erfasst
+
Dokumentieren Sie hier alle Datenpannen β auch solche, die nicht meldepflichtig sind (Art. 33 Abs. 5).
+
+ ) : (
+
+ {incidents.map(incident => (
+
+
+
+
+ {incident.id}
+
+ {INCIDENT_STATUS_LABELS[incident.status]}
+
+
+ {SEVERITY_LABELS[incident.severity]}
+
+ {incident.art34Required && (
+
+ Art. 34
+
+ )}
+
+
{incident.title}
+
+ {incident.status !== 'closed' && incident.status !== 'reported' && incident.status !== 'not_reportable' && (
+
+ )}
+
+
{incident.description}
+
+ Entdeckt: {new Date(incident.detectedAt).toLocaleString('de-DE')}
+ Betroffene: ~{incident.estimatedAffectedPersons}
+ {incident.reportedToAuthorityAt && (
+ Gemeldet: {new Date(incident.reportedToAuthorityAt).toLocaleString('de-DE')}
+ )}
+
+ {incident.status !== 'closed' && (
+
+ {incident.status === 'detected' && (
+ updateStatus(incident.id, 'classified')} className="text-xs px-3 py-1 bg-yellow-100 text-yellow-800 rounded hover:bg-yellow-200">
+ Klassifizieren
+
+ )}
+ {incident.status === 'classified' && (
+ updateStatus(incident.id, 'assessed')} className="text-xs px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200">
+ Bewerten
+
+ )}
+ {incident.status === 'assessed' && (
+ <>
+ updateStatus(incident.id, 'reported')} className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded hover:bg-green-200">
+ Als gemeldet markieren
+
+ updateStatus(incident.id, 'not_reportable')} className="text-xs px-3 py-1 bg-gray-100 text-gray-800 rounded hover:bg-gray-200">
+ Nicht meldepflichtig
+
+ >
+ )}
+ {(incident.status === 'reported' || incident.status === 'not_reportable') && (
+ updateStatus(incident.id, 'closed')} className="text-xs px-3 py-1 bg-gray-100 text-gray-600 rounded hover:bg-gray-200">
+ Abschliessen
+
+ )}
+
+ )}
+
+ ))}
+
+ )}
+
+ )
+}
+
+// =============================================================================
+// TAB 3: Melde-Templates
+// =============================================================================
+
+function TemplatesTab({
+ templates,
+ setTemplates,
+}: {
+ templates: MeldeTemplate[]
+ setTemplates: React.Dispatch>
+}) {
+ return (
+
+
+
Melde-Templates
+
+ Vorlagen fuer Meldungen an die Aufsichtsbehoerde (Art. 33) und Benachrichtigung Betroffener (Art. 34).
+
+
+
+ {templates.map(template => (
+
+
+
+ {template.type === 'art33' ? 'Art. 33' : 'Art. 34'}
+
+
{template.title}
+
+
+ ))}
+
+ )
+}
+
+// =============================================================================
+// TAB 4: Uebungen & Tests
+// =============================================================================
+
+function ExercisesTab({
+ exercises,
+ setExercises,
+ showAdd,
+ setShowAdd,
+}: {
+ exercises: Exercise[]
+ setExercises: React.Dispatch>
+ showAdd: boolean
+ setShowAdd: (v: boolean) => void
+}) {
+ const [newExercise, setNewExercise] = useState>({
+ title: '',
+ type: 'tabletop',
+ scenario: '',
+ scheduledDate: '',
+ participants: [],
+ lessonsLearned: '',
+ })
+
+ const SCENARIO_PRESETS = [
+ { label: 'Ransomware-Angriff', scenario: 'Ein Ransomware-Angriff verschluesselt saemtliche Produktivdaten. Backups sind vorhanden, aber der letzte Restore-Test liegt 6 Monate zurueck.' },
+ { label: 'Datenabfluss durch Mitarbeiter', scenario: 'Ein ausscheidender Mitarbeiter hat vor seinem letzten Arbeitstag umfangreiche Kundendaten auf einen privaten USB-Stick kopiert.' },
+ { label: 'Cloud-Provider-Ausfall', scenario: 'Ihr primaerer Cloud-Provider meldet einen groesseren Ausfall. Die Wiederherstellungszeit ist unbekannt. Betroffene koennen keine DSGVO-Rechte ausueben.' },
+ { label: 'Phishing-Angriff', scenario: 'Mehrere Mitarbeiter haben auf einen Phishing-Link geklickt. Es besteht Verdacht, dass Anmeldedaten kompromittiert wurden und auf Personaldaten zugegriffen wurde.' },
+ ]
+
+ function addExercise() {
+ if (!newExercise.title || !newExercise.scheduledDate) return
+ const exercise: Exercise = {
+ id: `EX-${Date.now()}`,
+ title: newExercise.title || '',
+ type: (newExercise.type as Exercise['type']) || 'tabletop',
+ scenario: newExercise.scenario || '',
+ scheduledDate: newExercise.scheduledDate || '',
+ participants: newExercise.participants || [],
+ lessonsLearned: '',
+ }
+ setExercises(prev => [...prev, exercise])
+ setShowAdd(false)
+ setNewExercise({ title: '', type: 'tabletop', scenario: '', scheduledDate: '', participants: [], lessonsLearned: '' })
+ }
+
+ function completeExercise(id: string, lessonsLearned: string) {
+ setExercises(prev => prev.map(ex =>
+ ex.id === id
+ ? { ...ex, completedDate: new Date().toISOString(), lessonsLearned }
+ : ex
+ ))
+ }
+
+ return (
+
+
+
+
Notfalluebungen & Tests
+
Planen und dokumentieren Sie regelmaessige Notfalluebungen.
+
+
setShowAdd(true)}
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
+ >
+ + Uebung planen
+
+
+
+ {/* Add Exercise Form */}
+ {showAdd && (
+
+
Neue Uebung planen
+
+
+ Titel
+ setNewExercise(prev => ({ ...prev, title: e.target.value }))}
+ placeholder="z.B. Tabletop: Ransomware-Angriff"
+ className="w-full border rounded px-3 py-2 text-sm"
+ />
+
+
+ Typ
+ setNewExercise(prev => ({ ...prev, type: e.target.value as Exercise['type'] }))}
+ className="w-full border rounded px-3 py-2 text-sm"
+ >
+ Tabletop-Uebung
+ Simulation
+ Vollstaendige Uebung
+
+
+
+ Geplantes Datum
+ setNewExercise(prev => ({ ...prev, scheduledDate: e.target.value }))}
+ className="w-full border rounded px-3 py-2 text-sm"
+ />
+
+
+
+ Szenario
+ oder Vorlage waehlen:
+
+
+ {SCENARIO_PRESETS.map(preset => (
+ setNewExercise(prev => ({
+ ...prev,
+ scenario: preset.scenario,
+ title: prev.title || `Tabletop: ${preset.label}`,
+ }))}
+ className="text-xs px-3 py-1 bg-gray-100 rounded-full hover:bg-gray-200"
+ >
+ {preset.label}
+
+ ))}
+
+
+
+
+ setShowAdd(false)} className="px-4 py-2 border rounded-lg text-sm">
+ Abbrechen
+
+
+ Uebung planen
+
+
+
+ )}
+
+ {/* Exercises List */}
+ {exercises.length === 0 ? (
+
+
Keine Uebungen geplant
+
Regelmaessige Notfalluebungen sind essentiell fuer ein funktionierendes Datenpannen-Management.
+
+ ) : (
+
+ {exercises.map(exercise => (
+
+
+
+ {exercise.completedDate ? 'Abgeschlossen' : 'Geplant'}
+
+
+ {exercise.type === 'tabletop' ? 'Tabletop' : exercise.type === 'simulation' ? 'Simulation' : 'Vollstaendige Uebung'}
+
+
+
{exercise.title}
+
{exercise.scenario}
+
+ Geplant: {new Date(exercise.scheduledDate).toLocaleDateString('de-DE')}
+ {exercise.completedDate && (
+ Durchgefuehrt: {new Date(exercise.completedDate).toLocaleDateString('de-DE')}
+ )}
+
+ {exercise.lessonsLearned && (
+
+
Lessons Learned:
+
{exercise.lessonsLearned}
+
+ )}
+ {!exercise.completedDate && (
+
+ {
+ const lessons = prompt('Lessons Learned aus der Uebung:')
+ if (lessons !== null) {
+ completeExercise(exercise.id, lessons)
+ }
+ }}
+ className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded hover:bg-green-200"
+ >
+ Als durchgefuehrt markieren
+
+
+ )}
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/source-policy/page.tsx b/admin-v2/app/(sdk)/sdk/source-policy/page.tsx
new file mode 100644
index 0000000..7c740f5
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/source-policy/page.tsx
@@ -0,0 +1,202 @@
+'use client'
+
+/**
+ * Source Policy Management Page (SDK Version)
+ *
+ * Whitelist-based data source management for edu-search-service.
+ * For auditors: Full audit trail for all changes.
+ */
+
+import { useState, useEffect } from 'react'
+import { useSDK } from '@/lib/sdk'
+import StepHeader from '@/components/sdk/StepHeader/StepHeader'
+import { SourcesTab } from '@/app/(admin)/compliance/source-policy/components/SourcesTab'
+import { OperationsMatrixTab } from '@/app/(admin)/compliance/source-policy/components/OperationsMatrixTab'
+import { PIIRulesTab } from '@/app/(admin)/compliance/source-policy/components/PIIRulesTab'
+import { AuditTab } from '@/app/(admin)/compliance/source-policy/components/AuditTab'
+
+// API base URL for edu-search-service
+const getApiBase = () => {
+ if (typeof window === 'undefined') return 'http://localhost:8088'
+ const hostname = window.location.hostname
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
+ return 'http://localhost:8088'
+ }
+ return `https://${hostname}:8089`
+}
+
+interface PolicyStats {
+ active_policies: number
+ allowed_sources: number
+ pii_rules: number
+ blocked_today: number
+ blocked_total: number
+}
+
+type TabId = 'dashboard' | 'sources' | 'operations' | 'pii' | 'audit'
+
+export default function SourcePolicyPage() {
+ const { state } = useSDK()
+ const [activeTab, setActiveTab] = useState('dashboard')
+ const [stats, setStats] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [apiBase, setApiBase] = useState(null)
+
+ useEffect(() => {
+ const base = getApiBase()
+ setApiBase(base)
+ }, [])
+
+ useEffect(() => {
+ if (apiBase !== null) {
+ fetchStats()
+ }
+ }, [apiBase])
+
+ const fetchStats = async () => {
+ try {
+ setLoading(true)
+ const res = await fetch(`${apiBase}/v1/admin/policy-stats`)
+
+ if (!res.ok) {
+ throw new Error('Fehler beim Laden der Statistiken')
+ }
+
+ const data = await res.json()
+ setStats(data)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
+ setStats({
+ active_policies: 0,
+ allowed_sources: 0,
+ pii_rules: 0,
+ blocked_today: 0,
+ blocked_total: 0,
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const tabs: { id: TabId; name: string; icon: JSX.Element }[] = [
+ {
+ id: 'dashboard',
+ name: 'Dashboard',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'sources',
+ name: 'Quellen',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'operations',
+ name: 'Operations',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'pii',
+ name: 'PII-Regeln',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'audit',
+ name: 'Audit',
+ icon: (
+
+
+
+ ),
+ },
+ ]
+
+ return (
+
+
+
+ {/* Error Display */}
+ {error && (
+
+ {error}
+ setError(null)} className="text-red-500 hover:text-red-700">
+ ×
+
+
+ )}
+
+ {/* Stats Cards */}
+ {stats && (
+
+
+
{stats.active_policies}
+
Aktive Policies
+
+
+
{stats.allowed_sources}
+
Zugelassene Quellen
+
+
+
{stats.blocked_today}
+
Blockiert (heute)
+
+
+
{stats.pii_rules}
+
PII-Regeln
+
+
+ )}
+
+ {/* Tabs */}
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={`px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 ${
+ activeTab === tab.id
+ ? 'bg-purple-600 text-white'
+ : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
+ }`}
+ >
+ {tab.icon}
+ {tab.name}
+
+ ))}
+
+
+ {/* Tab Content */}
+ {apiBase === null ? (
+
Initialisiere...
+ ) : (
+ <>
+ {activeTab === 'dashboard' && (
+
+ {loading ? 'Lade Dashboard...' : 'Dashboard-Ansicht - Wechseln Sie zu einem Tab fuer Details.'}
+
+ )}
+ {activeTab === 'sources' &&
}
+ {activeTab === 'operations' &&
}
+ {activeTab === 'pii' &&
}
+ {activeTab === 'audit' &&
}
+ >
+ )}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/tom-generator/scope/page.tsx b/admin-v2/app/(sdk)/sdk/tom-generator/scope/page.tsx
index 329ec96..b6cb684 100644
--- a/admin-v2/app/(sdk)/sdk/tom-generator/scope/page.tsx
+++ b/admin-v2/app/(sdk)/sdk/tom-generator/scope/page.tsx
@@ -90,6 +90,21 @@ export default function ScopePage() {
+ {/* Scope Prefill Hint */}
+
+
+
+
+
+
+
+ Tipp: Wenn Sie bereits die Scope-Analyse ausgefuellt haben, werden relevante Felder
+ (Branche, Groesse, Hosting, Verschluesselung) automatisch vorausgefuellt. Anpassungen sind jederzeit moeglich.
+
+
+
+
+
{/* Step Content */}
diff --git a/admin-v2/app/(sdk)/sdk/tom/layout.tsx b/admin-v2/app/(sdk)/sdk/tom/layout.tsx
new file mode 100644
index 0000000..1aa5eb8
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/tom/layout.tsx
@@ -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 (
+
+ {children}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/tom/page.tsx b/admin-v2/app/(sdk)/sdk/tom/page.tsx
index 97c6148..7eb401a 100644
--- a/admin-v2/app/(sdk)/sdk/tom/page.tsx
+++ b/admin-v2/app/(sdk)/sdk/tom/page.tsx
@@ -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 (
-
-
-
-
-
- {categoryLabels[tom.category]}
-
-
- {tom.type === 'technical' ? 'Technisch' : 'Organisatorisch'}
-
-
- {statusLabels[tom.status]}
-
-
-
{tom.title}
-
{tom.description}
-
Rechtsgrundlage: {tom.article32Reference}
-
-
-
-
-
- Verantwortlich:
- {tom.responsible}
-
-
- Naechste Pruefung:
-
- {tom.nextReview.toLocaleDateString('de-DE')}
- {isReviewDue && ' (faellig)'}
-
-
-
-
-
- {tom.documentation ? (
-
-
-
-
- Dokumentiert
-
- ) : (
-
Keine Dokumentation
- )}
-
-
- Bearbeiten
-
-
- Pruefung starten
-
-
-
-
- )
-}
-
-// =============================================================================
-// MAIN PAGE
+// PAGE COMPONENT
// =============================================================================
export default function TOMPage() {
const router = useRouter()
- const { state } = useSDK()
- const [toms] = useState
(mockTOMs)
- const [filter, setFilter] = useState('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('uebersicht')
+ const [selectedTOMId, setSelectedTOMId] = useState(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) => {
+ 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 (
-
- {/* Step Header */}
-
-
-
-
-
- TOM hinzufuegen
-
-
+ // ---------------------------------------------------------------------------
+ // Render helpers
+ // ---------------------------------------------------------------------------
- {/* Document Upload Section */}
-
-
- {/* Stats */}
-
-
-
Gesamt
-
{toms.length}
-
-
-
Implementiert
-
{implementedCount}
-
-
-
Technisch / Organisatorisch
-
{technicalCount} / {organizationalCount}
-
-
-
Pruefung faellig
-
{reviewDueCount}
-
-
-
- {/* Article 32 Overview */}
-
-
Art. 32 DSGVO - Schutzziele
-
-
-
- {toms.filter(t => t.category === 'confidentiality').length}
-
-
Vertraulichkeit
-
-
-
- {toms.filter(t => t.category === 'integrity').length}
-
-
Integritaet
-
-
-
- {toms.filter(t => t.category === 'availability').length}
-
-
Verfuegbarkeit
-
-
-
- {toms.filter(t => t.category === 'resilience').length}
-
-
Belastbarkeit
-
-
-
-
- {/* Filter */}
-
-
Filter:
- {['all', 'confidentiality', 'integrity', 'availability', 'resilience', 'technical', 'organizational', 'implemented', 'partial'].map(f => (
+ const renderTabBar = () => (
+
+ {TABS.map((t) => {
+ const isActive = tab === t.key
+ return (
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}
- ))}
-
+ )
+ })}
+
+ )
- {/* TOM List */}
-
- {filteredTOMs.map(tom => (
-
- ))}
-
+ // ---------------------------------------------------------------------------
+ // Tab 1 β Uebersicht
+ // ---------------------------------------------------------------------------
- {filteredTOMs.length === 0 && (
-
-
-
-
+ const renderUebersicht = () => (
+
+ )
+
+ // ---------------------------------------------------------------------------
+ // Tab 2 β Detail-Editor
+ // ---------------------------------------------------------------------------
+
+ const renderEditor = () => (
+
+ )
+
+ // ---------------------------------------------------------------------------
+ // Tab 3 β Generator
+ // ---------------------------------------------------------------------------
+
+ const renderGenerator = () => (
+
+ {/* Info card */}
+
+
+ {/* Icon */}
+
-
Keine TOMs gefunden
-
Passen Sie den Filter an oder fuegen Sie neue TOMs hinzu.
+
+
+
+ TOM Generator – 6-Schritte-Assistent
+
+
+ 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.
+
+
+
+
+ Die 6 Schritte im Ueberblick:
+
+
+ Unternehmenskontext erfassen
+ IT-Infrastruktur beschreiben
+ Verarbeitungstaetigkeiten zuordnen
+ Risikobewertung durchfuehren
+ TOM-Ableitung und Priorisierung
+ Ergebnis pruefen und uebernehmen
+
+
+
+
+ TOM Generator starten
+
+
+
+
+
+ {/* Quick stats β only rendered when derivedTOMs exist */}
+ {tomCount > 0 && (
+
+
+ Aktueller Stand
+
+
+
+ {/* TOM count */}
+
+
Abgeleitete TOMs
+
{tomCount}
+
+
+ {/* Last generated date */}
+ {lastModifiedFormatted && (
+
+
Zuletzt generiert
+
+ {lastModifiedFormatted}
+
+
+ )}
+
+ {/* Status */}
+
+
+
+
+
+ Sie koennen den Generator jederzeit erneut ausfuehren, um Ihre
+ TOMs zu aktualisieren oder zu erweitern.
+
+
+
+ )}
+
+ {/* Empty state when no TOMs exist yet */}
+ {tomCount === 0 && (
+
+
+
+ Noch keine TOMs vorhanden
+
+
+ Starten Sie den Generator, um Ihre ersten technischen und
+ organisatorischen Massnahmen abzuleiten.
+
+
+ Jetzt starten
+
)}
)
+
+ // ---------------------------------------------------------------------------
+ // Tab 4 β Gap-Analyse & Export
+ // ---------------------------------------------------------------------------
+
+ const renderGapExport = () => (
+
+ )
+
+ // ---------------------------------------------------------------------------
+ // 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 (
+
+ {/* Step header */}
+
+
+ {/* Tab bar */}
+
+ {renderTabBar()}
+
+
+ {/* Active tab content */}
+
{renderActiveTab()}
+
+ )
}
diff --git a/admin-v2/app/(sdk)/sdk/vvt/page.tsx b/admin-v2/app/(sdk)/sdk/vvt/page.tsx
index 41b7b1b..3673c1a 100644
--- a/admin-v2/app/(sdk)/sdk/vvt/page.tsx
+++ b/admin-v2/app/(sdk)/sdk/vvt/page.tsx
@@ -1,210 +1,71 @@
'use client'
-import React, { useState, useCallback } from 'react'
-import { useRouter } from 'next/navigation'
+/**
+ * VVT β Verarbeitungsverzeichnis (Art. 30 DSGVO)
+ *
+ * 4 Tabs:
+ * 1. Verzeichnis (Uebersicht aller Verarbeitungstaetigkeiten)
+ * 2. Verarbeitung bearbeiten (Detail-Editor)
+ * 3. Generator (Profiling-Fragebogen)
+ * 4. Export & Compliance
+ */
+
+import { useState, useEffect, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
-import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
-import { DocumentUploadSection, type UploadedDocument } from '@/components/sdk'
+import StepHeader, { STEP_EXPLANATIONS } from '@/components/sdk/StepHeader/StepHeader'
+import {
+ DATA_SUBJECT_CATEGORY_META,
+ PERSONAL_DATA_CATEGORY_META,
+ LEGAL_BASIS_META,
+ TRANSFER_MECHANISM_META,
+ ART9_CATEGORIES,
+ BUSINESS_FUNCTION_LABELS,
+ STATUS_LABELS,
+ STATUS_COLORS,
+ PROTECTION_LEVEL_LABELS,
+ DEPLOYMENT_LABELS,
+ REVIEW_INTERVAL_LABELS,
+ createEmptyActivity,
+ createDefaultOrgHeader,
+ generateVVTId,
+ isSpecialCategory,
+} from '@/lib/sdk/vvt-types'
+import type { VVTActivity, VVTOrganizationHeader, BusinessFunction } from '@/lib/sdk/vvt-types'
+import {
+ PROFILING_STEPS,
+ PROFILING_QUESTIONS,
+ getQuestionsForStep,
+ getStepProgress,
+ getTotalProgress,
+ generateActivities,
+} from '@/lib/sdk/vvt-profiling'
+import type { ProfilingAnswers } from '@/lib/sdk/vvt-profiling'
// =============================================================================
-// TYPES
+// CONSTANTS
// =============================================================================
-interface ProcessingActivity {
- id: string
- name: string
- description: string
- purpose: string
- legalBasis: string
- dataCategories: string[]
- dataSubjects: string[]
- recipients: string[]
- thirdCountryTransfers: boolean
- retentionPeriod: string
- toms: string[]
- status: 'active' | 'draft' | 'archived'
- lastUpdated: Date
- responsible: string
+type Tab = 'verzeichnis' | 'editor' | 'generator' | 'export'
+
+const STORAGE_KEY = 'bp_vvt'
+
+interface VVTData {
+ activities: VVTActivity[]
+ orgHeader: VVTOrganizationHeader
+ profilingAnswers: ProfilingAnswers
}
-// =============================================================================
-// MOCK DATA
-// =============================================================================
+function loadData(): VVTData {
+ if (typeof window === 'undefined') return { activities: [], orgHeader: createDefaultOrgHeader(), profilingAnswers: {} }
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored) return JSON.parse(stored)
+ } catch { /* ignore */ }
+ return { activities: [], orgHeader: createDefaultOrgHeader(), profilingAnswers: {} }
+}
-const mockActivities: ProcessingActivity[] = [
- {
- id: 'vvt-1',
- name: 'Personalverwaltung',
- description: 'Verarbeitung von Mitarbeiterdaten fuer HR-Zwecke',
- purpose: 'Durchfuehrung des Beschaeftigungsverhaeltnisses',
- legalBasis: 'Art. 6 Abs. 1 lit. b, Art. 88 DSGVO i.V.m. BDSG',
- dataCategories: ['Stammdaten', 'Kontaktdaten', 'Gehaltsdaten', 'Bankverbindung'],
- dataSubjects: ['Mitarbeiter', 'Bewerber'],
- recipients: ['Lohnbuero', 'Finanzamt', 'Sozialversicherungstraeger'],
- thirdCountryTransfers: false,
- retentionPeriod: '10 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
- toms: ['Zugriffskontrolle', 'Verschluesselung', 'Protokollierung'],
- status: 'active',
- lastUpdated: new Date('2024-01-15'),
- responsible: 'HR-Abteilung',
- },
- {
- id: 'vvt-2',
- name: 'Kundenverwaltung (CRM)',
- description: 'Verwaltung von Kundenbeziehungen und Vertriebsaktivitaeten',
- purpose: 'Vertragserfuellung und Kundenbetreuung',
- legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO',
- dataCategories: ['Kontaktdaten', 'Kaufhistorie', 'Kommunikationsverlauf'],
- dataSubjects: ['Kunden', 'Interessenten'],
- recipients: ['Vertrieb', 'Kundenservice'],
- thirdCountryTransfers: true,
- retentionPeriod: '3 Jahre nach letzter Interaktion',
- toms: ['Zugriffskontrolle', 'Backups', 'Verschluesselung'],
- status: 'active',
- lastUpdated: new Date('2024-01-10'),
- responsible: 'Vertriebsleitung',
- },
- {
- id: 'vvt-3',
- name: 'Newsletter-Marketing',
- description: 'Versand von Marketing-E-Mails und Newslettern',
- purpose: 'Direktmarketing und Kundenbindung',
- legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)',
- dataCategories: ['E-Mail-Adresse', 'Name', 'Interaktionsdaten'],
- dataSubjects: ['Newsletter-Abonnenten'],
- recipients: ['Marketing-Abteilung', 'E-Mail-Dienstleister'],
- thirdCountryTransfers: true,
- retentionPeriod: 'Bis zum Widerruf der Einwilligung',
- toms: ['Double-Opt-In', 'Abmeldelink', 'Protokollierung'],
- status: 'active',
- lastUpdated: new Date('2024-01-20'),
- responsible: 'Marketing',
- },
- {
- id: 'vvt-4',
- name: 'KI-gestuetztes Recruiting',
- description: 'Automatisierte Vorauswahl von Bewerbungen',
- purpose: 'Effiziente Bewerberauswahl',
- legalBasis: 'Art. 6 Abs. 1 lit. b, Art. 22 Abs. 2 lit. a DSGVO',
- dataCategories: ['Bewerbungsunterlagen', 'Qualifikationen', 'Berufserfahrung'],
- dataSubjects: ['Bewerber'],
- recipients: ['HR-Abteilung', 'Fachabteilungen'],
- thirdCountryTransfers: false,
- retentionPeriod: '6 Monate nach Absage',
- toms: ['Menschliche Aufsicht', 'Erklaerbarkeit', 'Bias-Pruefung'],
- status: 'draft',
- lastUpdated: new Date('2024-01-22'),
- responsible: 'HR-Abteilung',
- },
- {
- id: 'vvt-5',
- name: 'Website-Analyse',
- description: 'Analyse des Nutzerverhaltens auf der Website',
- purpose: 'Verbesserung der Website und Nutzererfahrung',
- legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung via Cookie-Banner)',
- dataCategories: ['IP-Adresse', 'Seitenaufrufe', 'Verweildauer', 'Geraetedaten'],
- dataSubjects: ['Website-Besucher'],
- recipients: ['Marketing', 'Webentwicklung'],
- thirdCountryTransfers: true,
- retentionPeriod: '14 Monate',
- toms: ['IP-Anonymisierung', 'Cookie-Einwilligung', 'Opt-Out'],
- status: 'active',
- lastUpdated: new Date('2024-01-05'),
- responsible: 'Webmaster',
- },
-]
-
-// =============================================================================
-// COMPONENTS
-// =============================================================================
-
-function ActivityCard({ activity }: { activity: ProcessingActivity }) {
- const statusColors = {
- active: 'bg-green-100 text-green-700',
- draft: 'bg-yellow-100 text-yellow-700',
- archived: 'bg-gray-100 text-gray-500',
- }
-
- const statusLabels = {
- active: 'Aktiv',
- draft: 'Entwurf',
- archived: 'Archiviert',
- }
-
- return (
-
-
-
-
-
- {statusLabels[activity.status]}
-
- {activity.thirdCountryTransfers && (
-
- Drittlandtransfer
-
- )}
-
-
{activity.name}
-
{activity.description}
-
-
-
-
-
- Zweck:
- {activity.purpose}
-
-
- Rechtsgrundlage:
- {activity.legalBasis}
-
-
- Aufbewahrung:
- {activity.retentionPeriod}
-
-
-
-
-
Datenkategorien:
-
- {activity.dataCategories.map(cat => (
-
- {cat}
-
- ))}
-
-
-
-
-
Betroffene:
-
- {activity.dataSubjects.map(subj => (
-
- {subj}
-
- ))}
-
-
-
-
-
- Verantwortlich: {activity.responsible} | Aktualisiert: {activity.lastUpdated.toLocaleDateString('de-DE')}
-
-
-
- Bearbeiten
-
-
- Exportieren
-
-
-
-
- )
+function saveData(data: VVTData) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
}
// =============================================================================
@@ -212,89 +73,221 @@ function ActivityCard({ activity }: { activity: ProcessingActivity }) {
// =============================================================================
export default function VVTPage() {
- const router = useRouter()
const { state } = useSDK()
- const [activities] = useState(mockActivities)
- const [filter, setFilter] = useState('all')
+ const [tab, setTab] = useState('verzeichnis')
+ const [activities, setActivities] = useState([])
+ const [orgHeader, setOrgHeader] = useState(createDefaultOrgHeader())
+ const [profilingAnswers, setProfilingAnswers] = useState({})
+ const [editingId, setEditingId] = useState(null)
+ const [filter, setFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
+ const [sortBy, setSortBy] = useState<'name' | 'date' | 'status'>('name')
+ const [generatorStep, setGeneratorStep] = useState(1)
+ const [generatorPreview, setGeneratorPreview] = useState(null)
- // Handle uploaded document
- const handleDocumentProcessed = useCallback((doc: UploadedDocument) => {
- console.log('[VVT Page] Document processed:', doc)
+ // Load from localStorage
+ useEffect(() => {
+ const data = loadData()
+ setActivities(data.activities)
+ setOrgHeader(data.orgHeader)
+ setProfilingAnswers(data.profilingAnswers)
}, [])
- // Open document in workflow editor
- const handleOpenInEditor = useCallback((doc: UploadedDocument) => {
- router.push(`/compliance/workflow?documentType=vvt&documentId=${doc.id}&mode=change`)
- }, [router])
+ // Save to localStorage on change
+ const persist = useCallback((acts: VVTActivity[], org: VVTOrganizationHeader, prof: ProfilingAnswers) => {
+ saveData({ activities: acts, orgHeader: org, profilingAnswers: prof })
+ }, [])
- const filteredActivities = activities.filter(activity => {
- const matchesFilter = filter === 'all' || activity.status === filter
- const matchesSearch = searchQuery === '' ||
- activity.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- activity.purpose.toLowerCase().includes(searchQuery.toLowerCase())
- return matchesFilter && matchesSearch
- })
+ const updateActivities = useCallback((acts: VVTActivity[]) => {
+ setActivities(acts)
+ persist(acts, orgHeader, profilingAnswers)
+ }, [orgHeader, profilingAnswers, persist])
- const activeCount = activities.filter(a => a.status === 'active').length
- const thirdCountryCount = activities.filter(a => a.thirdCountryTransfers).length
- const totalDataCategories = [...new Set(activities.flatMap(a => a.dataCategories))].length
+ const updateOrgHeader = useCallback((org: VVTOrganizationHeader) => {
+ setOrgHeader(org)
+ persist(activities, org, profilingAnswers)
+ }, [activities, profilingAnswers, persist])
+
+ const updateProfilingAnswers = useCallback((prof: ProfilingAnswers) => {
+ setProfilingAnswers(prof)
+ persist(activities, orgHeader, prof)
+ }, [activities, orgHeader, persist])
+
+ // Computed stats
+ const activeCount = activities.filter(a => a.status === 'APPROVED').length
+ const draftCount = activities.filter(a => a.status === 'DRAFT').length
+ const thirdCountryCount = activities.filter(a => a.thirdCountryTransfers.length > 0).length
+ const art9Count = activities.filter(a => a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c))).length
+
+ // Filtered & sorted activities
+ const filteredActivities = activities
+ .filter(a => {
+ const matchesFilter = filter === 'all' || a.status === filter || (filter === 'thirdcountry' && a.thirdCountryTransfers.length > 0) || (filter === 'art9' && a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c)))
+ const matchesSearch = searchQuery === '' ||
+ a.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ a.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ a.vvtId.toLowerCase().includes(searchQuery.toLowerCase())
+ return matchesFilter && matchesSearch
+ })
+ .sort((a, b) => {
+ if (sortBy === 'name') return a.name.localeCompare(b.name)
+ if (sortBy === 'date') return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
+ return a.status.localeCompare(b.status)
+ })
+
+ const editingActivity = editingId ? activities.find(a => a.id === editingId) : null
const stepInfo = STEP_EXPLANATIONS['vvt']
+ // Tab buttons
+ const tabs: { id: Tab; label: string; count?: number }[] = [
+ { id: 'verzeichnis', label: 'Verzeichnis', count: activities.length },
+ { id: 'editor', label: 'Verarbeitung bearbeiten' },
+ { id: 'generator', label: 'Generator' },
+ { id: 'export', label: 'Export & Compliance' },
+ ]
+
return (
- {/* Step Header */}
-
-
- Exportieren
-
-
-
-
-
- Neue Verarbeitung
-
-
-
-
- {/* Document Upload Section */}
-
- {/* Stats */}
-
-
-
Verarbeitungen
-
{activities.length}
-
-
-
Aktiv
-
{activeCount}
-
-
-
Mit Drittlandtransfer
-
{thirdCountryCount}
-
-
-
Datenkategorien
-
{totalDataCategories}
-
+ {/* Tab Navigation */}
+
+ {tabs.map(t => (
+ setTab(t.id)}
+ className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
+ tab === t.id ? 'bg-white text-purple-700 shadow-sm' : 'text-gray-600 hover:text-gray-900'
+ }`}
+ >
+ {t.label}
+ {t.count !== undefined && (
+
+ {t.count}
+
+ )}
+
+ ))}
- {/* Search and Filter */}
-
-
+ {/* Tab Content */}
+ {tab === 'verzeichnis' && (
+ { setEditingId(id); setTab('editor') }}
+ onNew={() => {
+ const vvtId = generateVVTId(activities.map(a => a.vvtId))
+ const newAct = createEmptyActivity(vvtId)
+ updateActivities([...activities, newAct])
+ setEditingId(newAct.id)
+ setTab('editor')
+ }}
+ onDelete={(id) => updateActivities(activities.filter(a => a.id !== id))}
+ />
+ )}
+
+ {tab === 'editor' && (
+ {
+ const idx = activities.findIndex(a => a.id === updated.id)
+ if (idx >= 0) {
+ const copy = [...activities]
+ copy[idx] = { ...updated, updatedAt: new Date().toISOString() }
+ updateActivities(copy)
+ }
+ }}
+ onBack={() => setTab('verzeichnis')}
+ onSelectActivity={(id) => setEditingId(id)}
+ />
+ )}
+
+ {tab === 'generator' && (
+ {
+ updateActivities([...activities, ...newActivities])
+ setGeneratorPreview(null)
+ setGeneratorStep(1)
+ setTab('verzeichnis')
+ }}
+ />
+ )}
+
+ {tab === 'export' && (
+
+ )}
+
+ )
+}
+
+// =============================================================================
+// TAB 1: VERZEICHNIS
+// =============================================================================
+
+function TabVerzeichnis({
+ activities, allActivities, activeCount, draftCount, thirdCountryCount, art9Count,
+ filter, setFilter, searchQuery, setSearchQuery, sortBy, setSortBy,
+ onEdit, onNew, onDelete,
+}: {
+ activities: VVTActivity[]
+ allActivities: VVTActivity[]
+ activeCount: number
+ draftCount: number
+ thirdCountryCount: number
+ art9Count: number
+ filter: string
+ setFilter: (f: string) => void
+ searchQuery: string
+ setSearchQuery: (q: string) => void
+ sortBy: string
+ setSortBy: (s: 'name' | 'date' | 'status') => void
+ onEdit: (id: string) => void
+ onNew: () => void
+ onDelete: (id: string) => void
+}) {
+ return (
+
+ {/* Stats */}
+
+
+
+
+
+
+
+
+ {/* Search + Filter + New */}
+
+
@@ -302,37 +295,58 @@ export default function VVTPage() {
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
- placeholder="Verarbeitungen durchsuchen..."
+ placeholder="VVT-ID, Name oder Beschreibung suchen..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
-
- {['all', 'active', 'draft', 'archived'].map(f => (
+
+ {[
+ { key: 'all', label: 'Alle' },
+ { key: 'DRAFT', label: 'Entwurf' },
+ { key: 'REVIEW', label: 'Pruefung' },
+ { key: 'APPROVED', label: 'Genehmigt' },
+ { key: 'thirdcountry', label: 'Drittland' },
+ { key: 'art9', label: 'Art. 9' },
+ ].map(f => (
setFilter(f)}
+ key={f.key}
+ onClick={() => setFilter(f.key)}
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'
+ filter === f.key ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
- {f === 'all' ? 'Alle' :
- f === 'active' ? 'Aktiv' :
- f === 'draft' ? 'Entwurf' : 'Archiviert'}
+ {f.label}
))}
+
setSortBy(e.target.value as 'name' | 'date' | 'status')}
+ className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
+ >
+ Name
+ Datum
+ Status
+
+
+
+
+
+ Neue Verarbeitung
+
- {/* Activities List */}
-
- {filteredActivities.map(activity => (
-
+ {/* Activity Cards */}
+
+ {activities.map(activity => (
+
))}
- {filteredActivities.length === 0 && (
+ {activities.length === 0 && (
@@ -340,9 +354,970 @@ export default function VVTPage() {
Keine Verarbeitungen gefunden
-
Passen Sie die Suche oder den Filter an.
+
+ Erstellen Sie eine neue Verarbeitung manuell oder nutzen Sie den Generator, um automatisch Eintraege aus einem Fragebogen zu erzeugen.
+
)}
)
}
+
+function StatCard({ label, value, color }: { label: string; value: number; color: string }) {
+ const borderColors: Record
= {
+ gray: 'border-gray-200', green: 'border-green-200', yellow: 'border-yellow-200', orange: 'border-orange-200', red: 'border-red-200',
+ }
+ const textColors: Record = {
+ gray: 'text-gray-600', green: 'text-green-600', yellow: 'text-yellow-600', orange: 'text-orange-600', red: 'text-red-600',
+ }
+ return (
+
+ )
+}
+
+function ActivityCard({ activity, onEdit, onDelete }: { activity: VVTActivity; onEdit: (id: string) => void; onDelete: (id: string) => void }) {
+ const hasArt9 = activity.personalDataCategories.some(c => ART9_CATEGORIES.includes(c))
+ const hasThirdCountry = activity.thirdCountryTransfers.length > 0
+
+ return (
+
+
+
+
+ {activity.vvtId}
+
+ {STATUS_LABELS[activity.status] || activity.status}
+
+ {hasArt9 && (
+ Art. 9
+ )}
+ {hasThirdCountry && (
+ Drittland
+ )}
+ {activity.dpiaRequired && (
+ DSFA
+ )}
+
+
{activity.name || '(Ohne Namen)'}
+ {activity.description && (
+
{activity.description}
+ )}
+
+ {BUSINESS_FUNCTION_LABELS[activity.businessFunction]}
+ {activity.responsible || 'Kein Verantwortlicher'}
+ Aktualisiert: {new Date(activity.updatedAt).toLocaleDateString('de-DE')}
+
+
+
+
onEdit(activity.id)}
+ className="px-3 py-1.5 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
+ >
+ Bearbeiten
+
+
{ if (confirm('Verarbeitung loeschen?')) onDelete(activity.id) }}
+ className="px-2 py-1.5 text-sm text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
+ >
+
+
+
+
+
+
+
+ )
+}
+
+// =============================================================================
+// TAB 2: EDITOR
+// =============================================================================
+
+function TabEditor({
+ activity, activities, onSave, onBack, onSelectActivity,
+}: {
+ activity: VVTActivity | null | undefined
+ activities: VVTActivity[]
+ onSave: (updated: VVTActivity) => void
+ onBack: () => void
+ onSelectActivity: (id: string) => void
+}) {
+ const [local, setLocal] = useState(null)
+ const [showAdvanced, setShowAdvanced] = useState(false)
+
+ useEffect(() => {
+ setLocal(activity ? { ...activity } : null)
+ }, [activity])
+
+ if (!local) {
+ return (
+
+
+
Keine Verarbeitung ausgewaehlt
+
Waehlen Sie eine Verarbeitung aus dem Verzeichnis oder erstellen Sie eine neue.
+
+ Zum Verzeichnis
+
+
+ {activities.length > 0 && (
+
+
Verarbeitungen zum Bearbeiten:
+
+ {activities.map(a => (
+ onSelectActivity(a.id)}
+ className="w-full text-left px-3 py-2 rounded-lg hover:bg-purple-50 text-sm flex items-center justify-between"
+ >
+ {a.vvtId} {a.name || '(Ohne Namen)'}
+ {STATUS_LABELS[a.status]}
+
+ ))}
+
+
+ )}
+
+ )
+ }
+
+ const update = (patch: Partial) => setLocal(prev => prev ? { ...prev, ...patch } : prev)
+
+ const handleSave = () => {
+ if (local) onSave(local)
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+ {local.vvtId}
+
{local.name || 'Neue Verarbeitung'}
+
+
+
+ update({ status: e.target.value as VVTActivity['status'] })}
+ className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm"
+ >
+ Entwurf
+ In Pruefung
+ Genehmigt
+ Archiviert
+
+
+ Speichern
+
+
+
+
+ {/* Form */}
+
+ {/* Bezeichnung + Beschreibung */}
+
+
+ update({ name: e.target.value })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ placeholder="z.B. Mitarbeiterverwaltung" />
+
+
+
+
+
+ update({ responsible: e.target.value })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="z.B. HR-Abteilung" />
+
+
+ update({ businessFunction: e.target.value as BusinessFunction })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg">
+ {Object.entries(BUSINESS_FUNCTION_LABELS).map(([k, v]) => (
+ {v}
+ ))}
+
+
+
+
+
+ {/* Zwecke */}
+
+ update({ purposes })}
+ placeholder="Zweck eingeben und Enter druecken"
+ />
+
+
+ {/* Rechtsgrundlagen */}
+
+
+ {local.legalBases.map((lb, i) => (
+
+
{
+ const copy = [...local.legalBases]
+ copy[i] = { ...copy[i], type: e.target.value }
+ update({ legalBases: copy })
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
+ >
+ -- Rechtsgrundlage waehlen --
+ {Object.entries(LEGAL_BASIS_META).map(([k, v]) => (
+ {v.label.de} ({v.article})
+ ))}
+
+
{
+ const copy = [...local.legalBases]
+ copy[i] = { ...copy[i], reference: e.target.value }
+ update({ legalBases: copy })
+ }}
+ className="w-48 px-3 py-2 border border-gray-300 rounded-lg text-sm"
+ placeholder="Referenz"
+ />
+
update({ legalBases: local.legalBases.filter((_, j) => j !== i) })}
+ className="p-2 text-gray-400 hover:text-red-500">
+
+
+
+
+
+ ))}
+
update({ legalBases: [...local.legalBases, { type: '', description: '', reference: '' }] })}
+ className="text-sm text-purple-600 hover:text-purple-700">
+ + Rechtsgrundlage hinzufuegen
+
+
+
+
+ {/* Betroffenenkategorien */}
+
+ ({ value: k, label: v.de }))}
+ selected={local.dataSubjectCategories}
+ onChange={(dataSubjectCategories) => update({ dataSubjectCategories })}
+ />
+
+
+ {/* Datenkategorien */}
+
+ ({
+ value: k,
+ label: v.label.de,
+ highlight: v.isSpecial,
+ }))}
+ selected={local.personalDataCategories}
+ onChange={(personalDataCategories) => update({ personalDataCategories })}
+ />
+ {local.personalDataCategories.some(c => ART9_CATEGORIES.includes(c)) && (
+
+ Hinweis: Sie verarbeiten besondere Datenkategorien nach Art. 9 DSGVO. Stellen Sie sicher, dass eine Art.-9-Rechtsgrundlage vorliegt.
+
+ )}
+
+
+ {/* Empfaenger */}
+
+
+ {local.recipientCategories.map((rc, i) => (
+
+
{
+ const copy = [...local.recipientCategories]
+ copy[i] = { ...copy[i], type: e.target.value }
+ update({ recipientCategories: copy })
+ }}
+ className="w-40 px-3 py-2 border border-gray-300 rounded-lg text-sm"
+ >
+ Intern
+ Auftragsverarbeiter
+ Verantwortlicher
+ Behoerde
+ Konzern
+ Sonstige
+
+
{
+ const copy = [...local.recipientCategories]
+ copy[i] = { ...copy[i], name: e.target.value }
+ update({ recipientCategories: copy })
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Name des Empfaengers" />
+
update({ recipientCategories: local.recipientCategories.filter((_, j) => j !== i) })}
+ className="p-2 text-gray-400 hover:text-red-500">
+
+
+
+
+
+ ))}
+
update({ recipientCategories: [...local.recipientCategories, { type: 'INTERNAL', name: '' }] })}
+ className="text-sm text-purple-600 hover:text-purple-700">
+ + Empfaenger hinzufuegen
+
+
+
+
+ {/* Drittlandtransfers */}
+
+
+ {local.thirdCountryTransfers.map((tc, i) => (
+
+ ))}
+
update({ thirdCountryTransfers: [...local.thirdCountryTransfers, { country: '', recipient: '', transferMechanism: '' }] })}
+ className="text-sm text-purple-600 hover:text-purple-700">
+ + Drittlandtransfer hinzufuegen
+
+
+
+
+ {/* Aufbewahrungsfristen */}
+
+
+
+ update({ retentionPeriod: { ...local.retentionPeriod, description: e.target.value } })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="Freitextbeschreibung der Aufbewahrungsfrist" />
+
+
+
+ {/* TOM-Beschreibung */}
+
+
+
+ {/* Advanced (collapsible) */}
+
+
setShowAdvanced(!showAdvanced)}
+ className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
+ >
+
+
+
+ Generator-Felder (Schutzniveau, Systeme, DSFA)
+
+
+ {showAdvanced && (
+
+
+
+ update({ protectionLevel: e.target.value as VVTActivity['protectionLevel'] })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg">
+ {Object.entries(PROTECTION_LEVEL_LABELS).map(([k, v]) => (
+ {v}
+ ))}
+
+
+
+ update({ deploymentModel: e.target.value as VVTActivity['deploymentModel'] })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg">
+ {Object.entries(DEPLOYMENT_LABELS).map(([k, v]) => (
+ {v}
+ ))}
+
+
+
+
+ update({ dpiaRequired: e.target.checked })}
+ className="w-4 h-4 text-purple-600 rounded" />
+ Ja, DSFA nach Art. 35 DSGVO erforderlich
+
+
+
+
+ )}
+
+
+
+ {/* Save button at bottom */}
+
+
+ Zurueck zum Verzeichnis
+
+
+ Speichern
+
+
+
+ )
+}
+
+// =============================================================================
+// TAB 3: GENERATOR
+// =============================================================================
+
+function TabGenerator({
+ step, setStep, answers, setAnswers, preview, setPreview, onAdoptAll,
+}: {
+ step: number
+ setStep: (s: number) => void
+ answers: ProfilingAnswers
+ setAnswers: (a: ProfilingAnswers) => void
+ preview: VVTActivity[] | null
+ setPreview: (p: VVTActivity[] | null) => void
+ onAdoptAll: (activities: VVTActivity[]) => void
+}) {
+ const questions = getQuestionsForStep(step)
+ const totalSteps = PROFILING_STEPS.length
+ const currentStepInfo = PROFILING_STEPS.find(s => s.step === step)
+ const totalProgress = getTotalProgress(answers)
+
+ const handleGenerate = () => {
+ const result = generateActivities(answers)
+ setPreview(result.generatedActivities)
+ }
+
+ // Preview mode
+ if (preview) {
+ return (
+
+
+
Generierte Verarbeitungen
+
+ Basierend auf Ihren Antworten wurden {preview.length} Verarbeitungstaetigkeiten generiert.
+ Sie koennen einzelne Eintraege abwaehlen, bevor Sie diese uebernehmen.
+
+
+ {preview.map((a, i) => (
+
+
{
+ if (!e.target.checked) {
+ setPreview(preview.filter((_, j) => j !== i))
+ }
+ }} />
+
+
+ {a.vvtId}
+ {a.name}
+ {BUSINESS_FUNCTION_LABELS[a.businessFunction]}
+
+
{a.description}
+
+
+ ))}
+
+
+
+ setPreview(null)} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
+ Zurueck zum Fragebogen
+
+ onAdoptAll(preview)} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
+ Alle {preview.length} uebernehmen
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Progress Bar */}
+
+
+ Fortschritt: {totalProgress}%
+ Schritt {step} von {totalSteps}
+
+
+
+ {PROFILING_STEPS.map(s => (
+ setStep(s.step)}
+ className={`flex-1 h-1.5 rounded-full transition-colors ${
+ s.step === step ? 'bg-purple-600' : s.step < step ? 'bg-purple-300' : 'bg-gray-200'
+ }`}
+ />
+ ))}
+
+
+
+ {/* Step Info */}
+
+
{currentStepInfo?.title}
+
{currentStepInfo?.description}
+
+
+ {questions.map(q => (
+
+ ))}
+
+
+
+ {/* Navigation */}
+
+
setStep(Math.max(1, step - 1))}
+ disabled={step === 1}
+ className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Zurueck
+
+
+ {step < totalSteps ? (
+ setStep(step + 1)} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
+ Weiter
+
+ ) : (
+
+ Verarbeitungen generieren
+
+ )}
+
+
+
+ )
+}
+
+// =============================================================================
+// TAB 4: EXPORT & COMPLIANCE
+// =============================================================================
+
+function TabExport({
+ activities, orgHeader, onUpdateOrgHeader,
+}: {
+ activities: VVTActivity[]
+ orgHeader: VVTOrganizationHeader
+ onUpdateOrgHeader: (org: VVTOrganizationHeader) => void
+}) {
+ // Compliance check
+ const issues: { activityId: string; vvtId: string; name: string; issues: string[] }[] = []
+ for (const a of activities) {
+ const actIssues: string[] = []
+ if (!a.name) actIssues.push('Bezeichnung fehlt')
+ if (a.purposes.length === 0) actIssues.push('Zweck(e) fehlen')
+ if (a.legalBases.length === 0) actIssues.push('Rechtsgrundlage fehlt')
+ if (a.dataSubjectCategories.length === 0) actIssues.push('Betroffenenkategorien fehlen')
+ if (a.personalDataCategories.length === 0) actIssues.push('Datenkategorien fehlen')
+ if (!a.retentionPeriod.description) actIssues.push('Aufbewahrungsfrist fehlt')
+ if (!a.tomDescription && a.structuredToms.accessControl.length === 0) actIssues.push('TOM-Beschreibung fehlt')
+
+ // Art. 9 without Art. 9 legal basis
+ const hasArt9Data = a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c))
+ const hasArt9Basis = a.legalBases.some(lb => lb.type.startsWith('ART9_'))
+ if (hasArt9Data && !hasArt9Basis) actIssues.push('Art.-9-Daten ohne Art.-9-Rechtsgrundlage')
+
+ // Third country without mechanism
+ for (const tc of a.thirdCountryTransfers) {
+ if (!tc.transferMechanism) actIssues.push(`Drittland ${tc.country}: Transfer-Mechanismus fehlt`)
+ }
+
+ if (actIssues.length > 0) {
+ issues.push({ activityId: a.id, vvtId: a.vvtId, name: a.name || '(Ohne Namen)', issues: actIssues })
+ }
+ }
+
+ const compliantCount = activities.length - issues.length
+ const compliancePercent = activities.length > 0 ? Math.round((compliantCount / activities.length) * 100) : 0
+
+ const handleExportJSON = () => {
+ const data = {
+ version: '1.0',
+ exportDate: new Date().toISOString(),
+ organization: orgHeader,
+ activities: activities,
+ }
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `vvt-export-${new Date().toISOString().split('T')[0]}.json`
+ a.click()
+ URL.revokeObjectURL(url)
+ }
+
+ const handleExportCSV = () => {
+ const headers = ['VVT-ID', 'Name', 'Beschreibung', 'Zwecke', 'Rechtsgrundlagen', 'Betroffene', 'Datenkategorien', 'Empfaenger', 'Drittlandtransfers', 'Aufbewahrungsfrist', 'TOM', 'Status', 'Verantwortlich']
+ const rows = activities.map(a => [
+ a.vvtId,
+ a.name,
+ a.description,
+ a.purposes.join('; '),
+ a.legalBases.map(lb => `${lb.type}${lb.reference ? ' (' + lb.reference + ')' : ''}`).join('; '),
+ a.dataSubjectCategories.map(c => DATA_SUBJECT_CATEGORY_META[c as keyof typeof DATA_SUBJECT_CATEGORY_META]?.de || c).join('; '),
+ a.personalDataCategories.map(c => PERSONAL_DATA_CATEGORY_META[c as keyof typeof PERSONAL_DATA_CATEGORY_META]?.label?.de || c).join('; '),
+ a.recipientCategories.map(r => `${r.name} (${r.type})`).join('; '),
+ a.thirdCountryTransfers.map(t => `${t.country}: ${t.recipient}`).join('; '),
+ a.retentionPeriod.description,
+ a.tomDescription,
+ STATUS_LABELS[a.status],
+ a.responsible,
+ ])
+
+ const csvContent = [headers, ...rows].map(row =>
+ row.map(cell => `"${String(cell || '').replace(/"/g, '""')}"`).join(',')
+ ).join('\n')
+
+ const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `vvt-export-${new Date().toISOString().split('T')[0]}.csv`
+ a.click()
+ URL.revokeObjectURL(url)
+ }
+
+ return (
+
+ {/* Compliance Overview */}
+
+
Compliance-Check
+
+
+
= 70 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'
+ }`}>
+ {compliancePercent}%
+
+
+
{compliantCount} von {activities.length} vollstaendig
+
{issues.length} Eintraege mit Maengeln
+
+
+
+
+ {issues.length > 0 && (
+
+ {issues.map(issue => (
+
+
+ {issue.vvtId}
+ {issue.name}
+
+
+ {issue.issues.map((iss, i) => (
+
+
+
+
+ {iss}
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ {issues.length === 0 && activities.length > 0 && (
+
+ Alle Verarbeitungen enthalten die erforderlichen Pflichtangaben nach Art. 30 DSGVO.
+
+ )}
+
+
+ {/* Organisation Header */}
+
+
+ {/* Export */}
+
+
Export
+
+
+ JSON exportieren
+
+
+ CSV (Excel) exportieren
+
+
+
+ Der Export enthaelt alle {activities.length} Verarbeitungstaetigkeiten inkl. Organisations-Metadaten.
+
+
+
+ {/* Stats */}
+
+
Statistik
+
+
+
{activities.length}
+
Verarbeitungen
+
+
+
{[...new Set(activities.map(a => a.businessFunction))].length}
+
Geschaeftsbereiche
+
+
+
{[...new Set(activities.flatMap(a => a.dataSubjectCategories))].length}
+
Betroffenenkategorien
+
+
+
{[...new Set(activities.flatMap(a => a.personalDataCategories))].length}
+
Datenkategorien
+
+
+
+
+ )
+}
+
+// =============================================================================
+// SHARED FORM COMPONENTS
+// =============================================================================
+
+function FormSection({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+
{title}
+ {children}
+
+ )
+}
+
+function FormField({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+ {label}
+ {children}
+
+ )
+}
+
+function MultiTextInput({ values, onChange, placeholder }: { values: string[]; onChange: (v: string[]) => void; placeholder?: string }) {
+ const [input, setInput] = useState('')
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && input.trim()) {
+ e.preventDefault()
+ onChange([...values, input.trim()])
+ setInput('')
+ }
+ }
+
+ return (
+
+
+ {values.map((v, i) => (
+
+ {v}
+ onChange(values.filter((_, j) => j !== i))} className="text-purple-400 hover:text-purple-600">
+
+
+
+
+
+ ))}
+
+
setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ placeholder={placeholder}
+ />
+
+ )
+}
+
+function CheckboxGrid({ options, selected, onChange }: {
+ options: { value: string; label: string; highlight?: boolean }[]
+ selected: string[]
+ onChange: (v: string[]) => void
+}) {
+ const toggle = (value: string) => {
+ if (selected.includes(value)) {
+ onChange(selected.filter(v => v !== value))
+ } else {
+ onChange([...selected, value])
+ }
+ }
+
+ return (
+
+ {options.map(opt => (
+
+ toggle(opt.value)}
+ className="w-3.5 h-3.5 text-purple-600 rounded"
+ />
+ {opt.label}
+ {opt.highlight && Art.9 }
+
+ ))}
+
+ )
+}
diff --git a/admin-v2/app/(sdk)/sdk/workflow/page.tsx b/admin-v2/app/(sdk)/sdk/workflow/page.tsx
new file mode 100644
index 0000000..8badb64
--- /dev/null
+++ b/admin-v2/app/(sdk)/sdk/workflow/page.tsx
@@ -0,0 +1,1070 @@
+'use client'
+
+/**
+ * Document Workflow Page (SDK Version)
+ *
+ * Split-view editor for legal documents with synchronized scrolling:
+ * - Left: Current published version (read-only)
+ * - Right: Draft/new version (editable)
+ * - Approval workflow: Draft -> Review -> Approved -> Published
+ * - DOCX upload with Word conversion
+ * - Rich text editor with formatting toolbar
+ */
+
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { useSDK } from '@/lib/sdk'
+import StepHeader from '@/components/sdk/StepHeader/StepHeader'
+
+// Types
+interface Document {
+ id: string
+ type: string
+ name: string
+ description: string
+ mandatory: boolean
+ created_at: string
+ updated_at: string
+}
+
+interface Version {
+ id: string
+ document_id: string
+ version: string
+ language: string
+ title: string
+ content: string
+ summary?: string
+ status: 'draft' | 'review' | 'approved' | 'published' | 'archived' | 'rejected'
+ created_at: string
+ updated_at?: string
+ created_by?: string
+ approved_by?: string
+ approved_at?: string
+ rejection_reason?: string
+}
+
+interface ApprovalHistoryItem {
+ action: string
+ approver: string
+ comment: string
+ created_at: string
+}
+
+const STATUS_LABELS: Record = {
+ draft: { label: 'Entwurf', color: 'bg-yellow-100 text-yellow-700' },
+ review: { label: 'In Pruefung', color: 'bg-blue-100 text-blue-700' },
+ approved: { label: 'Freigegeben', color: 'bg-green-100 text-green-700' },
+ published: { label: 'Veroeffentlicht', color: 'bg-emerald-100 text-emerald-700' },
+ archived: { label: 'Archiviert', color: 'bg-slate-100 text-slate-700' },
+ rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-700' },
+}
+
+const DOCUMENT_TYPES: Record = {
+ terms: 'AGB',
+ privacy: 'Datenschutzerklaerung',
+ cookies: 'Cookie-Richtlinie',
+ community_guidelines: 'Community-Richtlinien',
+ imprint: 'Impressum',
+}
+
+export default function WorkflowPage() {
+ const { state } = useSDK()
+ const [documents, setDocuments] = useState([])
+ const [versions, setVersions] = useState([])
+ const [selectedDocument, setSelectedDocument] = useState(null)
+ const [currentVersion, setCurrentVersion] = useState(null)
+ const [draftVersion, setDraftVersion] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState(null)
+ const [editedContent, setEditedContent] = useState('')
+ const [editedTitle, setEditedTitle] = useState('')
+ const [editedSummary, setEditedSummary] = useState('')
+ const [showHistory, setShowHistory] = useState(false)
+ const [approvalHistory, setApprovalHistory] = useState([])
+ const [approvalComment, setApprovalComment] = useState('')
+ const [showApprovalModal, setShowApprovalModal] = useState<'approve' | 'reject' | null>(null)
+ const [showCompareView, setShowCompareView] = useState(false)
+ const [uploading, setUploading] = useState(false)
+
+ // Refs for synchronized scrolling
+ const leftPanelRef = useRef(null)
+ const rightPanelRef = useRef(null)
+ const editorRef = useRef(null)
+ const fileInputRef = useRef(null)
+ const isScrolling = useRef(false)
+
+ useEffect(() => {
+ loadDocuments()
+ }, [])
+
+ useEffect(() => {
+ if (selectedDocument) {
+ loadVersions(selectedDocument.id)
+ }
+ }, [selectedDocument])
+
+ // Synchronized scrolling setup
+ const setupSyncScroll = useCallback(() => {
+ const leftPanel = leftPanelRef.current
+ const rightPanel = rightPanelRef.current
+
+ if (!leftPanel || !rightPanel) return
+
+ const handleLeftScroll = () => {
+ if (isScrolling.current) return
+ isScrolling.current = true
+
+ const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight || 1)
+ const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight
+ rightPanel.scrollTop = leftScrollPercent * rightMaxScroll
+
+ setTimeout(() => { isScrolling.current = false }, 10)
+ }
+
+ const handleRightScroll = () => {
+ if (isScrolling.current) return
+ isScrolling.current = true
+
+ const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight || 1)
+ const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight
+ leftPanel.scrollTop = rightScrollPercent * leftMaxScroll
+
+ setTimeout(() => { isScrolling.current = false }, 10)
+ }
+
+ leftPanel.addEventListener('scroll', handleLeftScroll)
+ rightPanel.addEventListener('scroll', handleRightScroll)
+
+ return () => {
+ leftPanel.removeEventListener('scroll', handleLeftScroll)
+ rightPanel.removeEventListener('scroll', handleRightScroll)
+ }
+ }, [])
+
+ useEffect(() => {
+ const cleanup = setupSyncScroll()
+ return cleanup
+ }, [setupSyncScroll, currentVersion, draftVersion])
+
+ const loadDocuments = async () => {
+ setLoading(true)
+ try {
+ const res = await fetch('/api/admin/consent/documents')
+ if (res.ok) {
+ const data = await res.json()
+ setDocuments(data.documents || [])
+ if (data.documents?.length > 0 && !selectedDocument) {
+ setSelectedDocument(data.documents[0])
+ }
+ }
+ } catch {
+ setError('Fehler beim Laden der Dokumente')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const loadVersions = async (docId: string) => {
+ try {
+ const res = await fetch(`/api/admin/consent/documents/${docId}/versions`)
+ if (res.ok) {
+ const data = await res.json()
+ const versionList = data.versions || []
+ setVersions(versionList)
+
+ const published = versionList.find((v: Version) => v.status === 'published')
+ setCurrentVersion(published || null)
+
+ const draft = versionList.find((v: Version) =>
+ v.status === 'draft' || v.status === 'review' || v.status === 'approved'
+ )
+ if (draft) {
+ setDraftVersion(draft)
+ setEditedContent(draft.content)
+ setEditedTitle(draft.title)
+ setEditedSummary(draft.summary || '')
+ } else {
+ setDraftVersion(null)
+ setEditedContent(published?.content || '')
+ setEditedTitle(published?.title || '')
+ setEditedSummary(published?.summary || '')
+ }
+ }
+ } catch {
+ setError('Fehler beim Laden der Versionen')
+ }
+ }
+
+ // Rich text editor functions
+ const formatDoc = (cmd: string, value: string | null = null) => {
+ if (editorRef.current) {
+ editorRef.current.focus()
+ document.execCommand(cmd, false, value || undefined)
+ updateEditorContent()
+ }
+ }
+
+ const formatBlock = (tag: string) => {
+ if (editorRef.current) {
+ editorRef.current.focus()
+ document.execCommand('formatBlock', false, `<${tag}>`)
+ updateEditorContent()
+ }
+ }
+
+ const insertLink = () => {
+ const url = prompt('Link-URL eingeben:', 'https://')
+ if (url && editorRef.current) {
+ editorRef.current.focus()
+ document.execCommand('createLink', false, url)
+ updateEditorContent()
+ }
+ }
+
+ const updateEditorContent = () => {
+ if (editorRef.current) {
+ setEditedContent(editorRef.current.innerHTML)
+ }
+ }
+
+ // Word document upload
+ const handleWordUpload = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ setUploading(true)
+
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ const response = await fetch('/api/admin/consent/versions/upload-word', {
+ method: 'POST',
+ body: formData
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ if (editorRef.current) {
+ editorRef.current.innerHTML = data.html || 'Konvertierung fehlgeschlagen
'
+ setEditedContent(editorRef.current.innerHTML)
+ }
+ } else {
+ const errorData = await response.json()
+ alert('Fehler beim Importieren: ' + (errorData.detail || 'Unbekannter Fehler'))
+ }
+ } catch (e) {
+ alert('Fehler beim Hochladen: ' + (e instanceof Error ? e.message : 'Unbekannter Fehler'))
+ } finally {
+ setUploading(false)
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ }
+ }
+
+ // Clean Word HTML on paste
+ const handlePaste = (e: React.ClipboardEvent) => {
+ const clipboardData = e.clipboardData
+ const html = clipboardData.getData('text/html')
+
+ if (html) {
+ e.preventDefault()
+ const cleanHtml = cleanWordHtml(html)
+ document.execCommand('insertHTML', false, cleanHtml)
+ updateEditorContent()
+ }
+ }
+
+ const cleanWordHtml = (html: string): string => {
+ let cleaned = html
+ cleaned = cleaned.replace(/\s*mso-[^:]+:[^;]+;?/gi, '')
+ cleaned = cleaned.replace(/\s*style="[^"]*"/gi, '')
+ cleaned = cleaned.replace(/\s*class="[^"]*"/gi, '')
+ cleaned = cleaned.replace(/<\/o:p>/gi, '')
+ cleaned = cleaned.replace(/<\/?o:[^>]*>/gi, '')
+ cleaned = cleaned.replace(/<\/?w:[^>]*>/gi, '')
+ cleaned = cleaned.replace(/<\/?m:[^>]*>/gi, '')
+ cleaned = cleaned.replace(/]*>\s*<\/span>/gi, '')
+ return cleaned
+ }
+
+ const createNewDraft = async () => {
+ if (!selectedDocument) return
+ setSaving(true)
+ try {
+ const nextVersion = getNextVersionNumber()
+ const res = await fetch('/api/admin/consent/versions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ document_id: selectedDocument.id,
+ version: nextVersion,
+ language: 'de',
+ title: editedTitle || currentVersion?.title || selectedDocument.name,
+ content: editedContent || currentVersion?.content || '',
+ summary: editedSummary || currentVersion?.summary || '',
+ }),
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Erstellen des Entwurfs')
+ }
+ } catch {
+ setError('Fehler beim Erstellen des Entwurfs')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const saveDraft = async () => {
+ if (!draftVersion || draftVersion.status !== 'draft') return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ title: editedTitle,
+ content: editedContent,
+ summary: editedSummary,
+ }),
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Speichern')
+ }
+ } catch {
+ setError('Fehler beim Speichern')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const submitForReview = async () => {
+ if (!draftVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/submit-review`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Einreichen')
+ }
+ } catch {
+ setError('Fehler beim Einreichen zur Pruefung')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const approveVersion = async () => {
+ if (!draftVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/approve`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ comment: approvalComment }),
+ })
+
+ if (res.ok) {
+ setShowApprovalModal(null)
+ setApprovalComment('')
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler bei der Freigabe')
+ }
+ } catch {
+ setError('Fehler bei der Freigabe')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const rejectVersion = async () => {
+ if (!draftVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/reject`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ reason: approvalComment }),
+ })
+
+ if (res.ok) {
+ setShowApprovalModal(null)
+ setApprovalComment('')
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler bei der Ablehnung')
+ }
+ } catch {
+ setError('Fehler bei der Ablehnung')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const publishVersion = async () => {
+ if (!draftVersion || draftVersion.status !== 'approved') return
+ if (!confirm('Version wirklich veroeffentlichen? Die aktuelle Version wird archiviert.')) return
+
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/publish`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Veroeffentlichen')
+ }
+ } catch {
+ setError('Fehler beim Veroeffentlichen')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const getNextVersionNumber = () => {
+ if (versions.length === 0) return '1.0'
+ const latest = versions[0]
+ const parts = latest.version.split('.')
+ const major = parseInt(parts[0]) || 1
+ const minor = parseInt(parts[1]) || 0
+ return `${major}.${minor + 1}`
+ }
+
+ const loadApprovalHistory = async (versionId: string) => {
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${versionId}/approval-history`)
+ if (res.ok) {
+ const data = await res.json()
+ setApprovalHistory(data.approval_history || [])
+ }
+ } catch {
+ console.error('Failed to load approval history')
+ }
+ }
+
+ const isEditable = draftVersion?.status === 'draft' || !draftVersion
+
+ return (
+
+
+
+ {/* Error Banner */}
+ {error && (
+
+
{error}
+
setError(null)} className="text-red-500 hover:text-red-700">
+
+
+
+
+
+ )}
+
+ {/* Document Selector */}
+
+
+
+ Dokument:
+ {
+ const doc = documents.find(d => d.id === e.target.value)
+ setSelectedDocument(doc || null)
+ }}
+ className="px-3 py-2 border border-slate-300 rounded-lg text-sm min-w-[250px]"
+ >
+ Dokument auswaehlen...
+ {documents.map((doc) => (
+
+ {DOCUMENT_TYPES[doc.type] || doc.type} - {doc.name}
+
+ ))}
+
+
+
+
+ {currentVersion && (
+
+ Aktuelle Version: v{currentVersion.version}
+
+ )}
+ {draftVersion && (
+
+ {STATUS_LABELS[draftVersion.status].label}: v{draftVersion.version}
+
+ )}
+
+ setShowCompareView(true)}
+ className="px-3 py-2 text-sm text-purple-600 hover:text-purple-800 border border-purple-300 rounded-lg hover:bg-purple-50"
+ title="Vollbild-Vergleich"
+ >
+ Vollbild
+
+
+ {
+ setShowHistory(!showHistory)
+ if (draftVersion && !showHistory) {
+ loadApprovalHistory(draftVersion.id)
+ }
+ }}
+ className="px-3 py-2 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:bg-slate-50"
+ >
+ Historie
+
+
+
+
+
+ {loading ? (
+
+ ) : !selectedDocument ? (
+
+ Bitte waehlen Sie ein Dokument aus
+
+ ) : (
+ <>
+ {/* Workflow Status Bar */}
+
+
+
+ {['draft', 'review', 'approved', 'published'].map((status, idx) => (
+
+ {idx > 0 &&
}
+
+
{idx + 1}
+
+ {status === 'draft' ? 'Entwurf' :
+ status === 'review' ? 'Pruefung' :
+ status === 'approved' ? 'Freigegeben' : 'Veroeffentlicht'}
+
+
+
+ ))}
+
+
+ {/* Action Buttons */}
+
+ {!draftVersion && (
+
+ Neue Version erstellen
+
+ )}
+
+ {draftVersion?.status === 'draft' && (
+ <>
+
+ {saving ? 'Speichern...' : 'Speichern'}
+
+
+ Zur Pruefung einreichen
+
+ >
+ )}
+
+ {draftVersion?.status === 'review' && (
+ <>
+
setShowApprovalModal('reject')}
+ disabled={saving}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 text-sm font-medium"
+ >
+ Ablehnen
+
+
setShowApprovalModal('approve')}
+ disabled={saving}
+ className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 text-sm font-medium"
+ >
+ Freigeben
+
+ >
+ )}
+
+ {draftVersion?.status === 'approved' && (
+
+ Jetzt veroeffentlichen
+
+ )}
+
+ {draftVersion?.status === 'rejected' && (
+
+ Abgelehnt: {draftVersion.rejection_reason}
+
+ Neu bearbeiten
+
+
+ )}
+
+
+
+
+ {/* Rich Text Toolbar - only shown when editable */}
+ {isEditable && (
+
+
+ {/* Formatting */}
+
+ formatDoc('bold')} className="p-2 hover:bg-slate-100 rounded" title="Fett">
+ B
+
+ formatDoc('italic')} className="p-2 hover:bg-slate-100 rounded" title="Kursiv">
+ I
+
+ formatDoc('underline')} className="p-2 hover:bg-slate-100 rounded" title="Unterstrichen">
+ U
+
+
+
+ {/* Headings */}
+
+ formatBlock('h1')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Ueberschrift 1">
+ H1
+
+ formatBlock('h2')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Ueberschrift 2">
+ H2
+
+ formatBlock('h3')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Ueberschrift 3">
+ H3
+
+ formatBlock('p')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Absatz">
+ P
+
+
+
+ {/* Lists */}
+
+
formatDoc('insertUnorderedList')} className="p-2 hover:bg-slate-100 rounded" title="Aufzaehlung">
+
+
+
+
+
formatDoc('insertOrderedList')} className="p-2 hover:bg-slate-100 rounded" title="Nummerierung">
+
+
+
+
+
+
+ {/* Links */}
+
+
+ {/* Word Upload */}
+
+
+
fileInputRef.current?.click()}
+ disabled={uploading}
+ className="px-3 py-2 bg-blue-50 text-blue-700 hover:bg-blue-100 rounded text-sm flex items-center gap-1"
+ title="Word-Dokument importieren"
+ >
+
+
+
+ {uploading ? 'Importiere...' : 'Word importieren'}
+
+
+
+
+ )}
+
+ {/* Split View Editor - Synchronized Scrolling */}
+
+ {/* Left: Current Published Version */}
+
+
+
+
Veroeffentlichte Version
+ {currentVersion && (
+
v{currentVersion.version}
+ )}
+
+
+ Nur Lesen
+
+
+
+ {currentVersion ? (
+ <>
+
+
+ >
+ ) : (
+
+ Keine veroeffentlichte Version vorhanden
+
+ )}
+
+
+
+ {/* Right: Draft/Edit Version */}
+
+
+
+
+ {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
+
+ {draftVersion && (
+
+ v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
+
+ )}
+
+ {isEditable && (
+
+ Bearbeitbar
+
+ )}
+
+
+
setEditedTitle(e.target.value)}
+ disabled={!isEditable}
+ placeholder="Titel der Version..."
+ className={`w-full px-3 py-2 mb-4 border rounded-lg ${
+ isEditable ? 'border-slate-300 bg-white' : 'border-slate-200 bg-slate-50 text-slate-700'
+ }`}
+ />
+
+ {isEditable ? (
+
+ ) : (
+
+ )}
+
+ {/* Character count */}
+
+ {(editorRef.current?.textContent || editedContent.replace(/<[^>]*>/g, '')).length} Zeichen
+
+
+
+
+
+ {/* History Panel */}
+ {showHistory && (
+
+
Genehmigungsverlauf
+ {approvalHistory.length > 0 ? (
+
+ {approvalHistory.map((item, idx) => (
+
+ {item.action}
+ {item.approver || 'System'}
+ {item.comment && (
+ "{item.comment}"
+ )}
+
+ {new Date(item.created_at).toLocaleString('de-DE')}
+
+
+ ))}
+
+ ) : (
+
Keine Genehmigungshistorie vorhanden.
+ )}
+
+
Alle Versionen
+
+ {versions.map((v) => (
+
+
+
+ v{v.version}
+
+ {STATUS_LABELS[v.status].label}
+
+ {v.title}
+
+
+ {new Date(v.created_at).toLocaleDateString('de-DE')}
+
+
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+ {/* Full Screen Compare View */}
+ {showCompareView && (
+
+ {/* Header */}
+
+
+
Versionsvergleich
+
+ {currentVersion ? `v${currentVersion.version}` : 'Keine Version'}
+ vs
+ {draftVersion ? `v${draftVersion.version}` : 'Neue Version'}
+
+
+
setShowCompareView(false)}
+ className="px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-600"
+ >
+ Schliessen
+
+
+
+ {/* Compare Panels */}
+
+ {/* Left: Published */}
+
+
+ Veroeffentlichte Version
+ {currentVersion && (
+ v{currentVersion.version}
+ )}
+
+
+ {currentVersion ? (
+
+ ) : (
+
Keine veroeffentlichte Version
+ )}
+
+
+
+ {/* Right: Draft */}
+
+
+
+ {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
+
+ {draftVersion && (
+
+ v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
+
+ )}
+
+
+
+
+
+ {/* Footer with Actions */}
+
+ {draftVersion?.status === 'draft' && (
+ <>
+ { setShowCompareView(false); saveDraft() }}
+ className="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-500"
+ >
+ Speichern
+
+ { setShowCompareView(false); submitForReview() }}
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500"
+ >
+ Zur Pruefung einreichen
+
+ >
+ )}
+ {draftVersion?.status === 'review' && (
+ <>
+ { setShowCompareView(false); setShowApprovalModal('reject') }}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-500"
+ >
+ Ablehnen
+
+ { setShowCompareView(false); setShowApprovalModal('approve') }}
+ className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-500"
+ >
+ Freigeben
+
+ >
+ )}
+ {draftVersion?.status === 'approved' && (
+ { setShowCompareView(false); publishVersion() }}
+ className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-500"
+ >
+ Veroeffentlichen
+
+ )}
+
+
+ )}
+
+ {/* Approval Modal */}
+ {showApprovalModal && (
+
+
+
+ {showApprovalModal === 'approve' ? 'Version freigeben' : 'Version ablehnen'}
+
+
+
+ )}
+
+ )
+}
diff --git a/admin-v2/app/api/admin/companion/feedback/route.ts b/admin-v2/app/api/admin/companion/feedback/route.ts
new file mode 100644
index 0000000..64faa36
--- /dev/null
+++ b/admin-v2/app/api/admin/companion/feedback/route.ts
@@ -0,0 +1,129 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+/**
+ * POST /api/admin/companion/feedback
+ * Submit feedback (bug report, feature request, general feedback)
+ * Proxy to backend /api/feedback
+ */
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json()
+
+ // Validate required fields
+ if (!body.type || !body.title || !body.description) {
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Missing required fields: type, title, description',
+ },
+ { status: 400 }
+ )
+ }
+
+ // Validate feedback type
+ const validTypes = ['bug', 'feature', 'feedback']
+ if (!validTypes.includes(body.type)) {
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid feedback type. Must be: bug, feature, or feedback',
+ },
+ { status: 400 }
+ )
+ }
+
+ // TODO: Replace with actual backend call
+ // const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+ // const response = await fetch(`${backendUrl}/api/feedback`, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // // Add auth headers
+ // },
+ // body: JSON.stringify({
+ // type: body.type,
+ // title: body.title,
+ // description: body.description,
+ // screenshot: body.screenshot,
+ // sessionId: body.sessionId,
+ // metadata: {
+ // ...body.metadata,
+ // source: 'companion',
+ // timestamp: new Date().toISOString(),
+ // userAgent: request.headers.get('user-agent'),
+ // },
+ // }),
+ // })
+ //
+ // if (!response.ok) {
+ // throw new Error(`Backend responded with ${response.status}`)
+ // }
+ //
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response - just acknowledge the submission
+ const feedbackId = `fb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+
+ console.log('Feedback received:', {
+ id: feedbackId,
+ type: body.type,
+ title: body.title,
+ description: body.description.substring(0, 100) + '...',
+ hasScreenshot: !!body.screenshot,
+ sessionId: body.sessionId,
+ })
+
+ return NextResponse.json({
+ success: true,
+ message: 'Feedback submitted successfully',
+ data: {
+ feedbackId,
+ submittedAt: new Date().toISOString(),
+ },
+ })
+ } catch (error) {
+ console.error('Submit feedback error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * GET /api/admin/companion/feedback
+ * Get feedback history (admin only)
+ */
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const type = searchParams.get('type')
+ const limit = parseInt(searchParams.get('limit') || '10')
+
+ // TODO: Replace with actual backend call
+
+ // Mock response - empty list for now
+ return NextResponse.json({
+ success: true,
+ data: {
+ feedback: [],
+ total: 0,
+ page: 1,
+ limit,
+ },
+ })
+ } catch (error) {
+ console.error('Get feedback error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/admin-v2/app/api/admin/companion/lesson/route.ts b/admin-v2/app/api/admin/companion/lesson/route.ts
new file mode 100644
index 0000000..4124bcd
--- /dev/null
+++ b/admin-v2/app/api/admin/companion/lesson/route.ts
@@ -0,0 +1,194 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+/**
+ * POST /api/admin/companion/lesson
+ * Start a new lesson session
+ * Proxy to backend /api/classroom/sessions
+ */
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json()
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+
+ // TODO: Replace with actual backend call
+ // const response = await fetch(`${backendUrl}/api/classroom/sessions`, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // },
+ // body: JSON.stringify(body),
+ // })
+ //
+ // if (!response.ok) {
+ // throw new Error(`Backend responded with ${response.status}`)
+ // }
+ //
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response - create a new session
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+
+ const mockSession = {
+ success: true,
+ data: {
+ sessionId,
+ classId: body.classId,
+ className: body.className || body.classId,
+ subject: body.subject,
+ topic: body.topic,
+ startTime: new Date().toISOString(),
+ phases: [
+ { phase: 'einstieg', duration: 8, status: 'active', actualTime: 0 },
+ { phase: 'erarbeitung', duration: 20, status: 'planned', actualTime: 0 },
+ { phase: 'sicherung', duration: 10, status: 'planned', actualTime: 0 },
+ { phase: 'transfer', duration: 7, status: 'planned', actualTime: 0 },
+ { phase: 'reflexion', duration: 5, status: 'planned', actualTime: 0 },
+ ],
+ totalPlannedDuration: 50,
+ currentPhaseIndex: 0,
+ elapsedTime: 0,
+ isPaused: false,
+ pauseDuration: 0,
+ overtimeMinutes: 0,
+ status: 'in_progress',
+ homeworkList: [],
+ materials: [],
+ },
+ }
+
+ return NextResponse.json(mockSession)
+ } catch (error) {
+ console.error('Start lesson error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * GET /api/admin/companion/lesson
+ * Get current lesson session or list of recent sessions
+ */
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const sessionId = searchParams.get('sessionId')
+
+ // TODO: Replace with actual backend call
+ // const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+ // const url = sessionId
+ // ? `${backendUrl}/api/classroom/sessions/${sessionId}`
+ // : `${backendUrl}/api/classroom/sessions`
+ //
+ // const response = await fetch(url)
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response
+ if (sessionId) {
+ return NextResponse.json({
+ success: true,
+ data: null, // No active session stored on server in mock
+ })
+ }
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ sessions: [], // Empty list for now
+ },
+ })
+ } catch (error) {
+ console.error('Get lesson error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * PATCH /api/admin/companion/lesson
+ * Update lesson session (timer state, phase changes, etc.)
+ */
+export async function PATCH(request: NextRequest) {
+ try {
+ const body = await request.json()
+ const { sessionId, ...updates } = body
+
+ if (!sessionId) {
+ return NextResponse.json(
+ { success: false, error: 'Session ID required' },
+ { status: 400 }
+ )
+ }
+
+ // TODO: Replace with actual backend call
+ // const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+ // const response = await fetch(`${backendUrl}/api/classroom/sessions/${sessionId}`, {
+ // method: 'PATCH',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify(updates),
+ // })
+ //
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response - just acknowledge the update
+ return NextResponse.json({
+ success: true,
+ message: 'Session updated',
+ })
+ } catch (error) {
+ console.error('Update lesson error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * DELETE /api/admin/companion/lesson
+ * End/delete a lesson session
+ */
+export async function DELETE(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const sessionId = searchParams.get('sessionId')
+
+ if (!sessionId) {
+ return NextResponse.json(
+ { success: false, error: 'Session ID required' },
+ { status: 400 }
+ )
+ }
+
+ // TODO: Replace with actual backend call
+
+ return NextResponse.json({
+ success: true,
+ message: 'Session ended',
+ })
+ } catch (error) {
+ console.error('End lesson error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/admin-v2/app/api/admin/companion/route.ts b/admin-v2/app/api/admin/companion/route.ts
new file mode 100644
index 0000000..b33c6f1
--- /dev/null
+++ b/admin-v2/app/api/admin/companion/route.ts
@@ -0,0 +1,102 @@
+import { NextResponse } from 'next/server'
+
+/**
+ * GET /api/admin/companion
+ * Proxy to backend /api/state/dashboard for companion dashboard data
+ */
+export async function GET() {
+ try {
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+
+ // TODO: Replace with actual backend call when endpoint is available
+ // const response = await fetch(`${backendUrl}/api/state/dashboard`, {
+ // method: 'GET',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // },
+ // })
+ //
+ // if (!response.ok) {
+ // throw new Error(`Backend responded with ${response.status}`)
+ // }
+ //
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response for development
+ const mockData = {
+ success: true,
+ data: {
+ context: {
+ currentPhase: 'erarbeitung',
+ phaseDisplayName: 'Erarbeitung',
+ },
+ stats: {
+ classesCount: 4,
+ studentsCount: 96,
+ learningUnitsCreated: 23,
+ gradesEntered: 156,
+ },
+ phases: [
+ { id: 'einstieg', shortName: 'E', displayName: 'Einstieg', duration: 8, status: 'completed', color: '#4A90E2' },
+ { id: 'erarbeitung', shortName: 'A', displayName: 'Erarbeitung', duration: 20, status: 'active', color: '#F5A623' },
+ { id: 'sicherung', shortName: 'S', displayName: 'Sicherung', duration: 10, status: 'planned', color: '#7ED321' },
+ { id: 'transfer', shortName: 'T', displayName: 'Transfer', duration: 7, status: 'planned', color: '#9013FE' },
+ { id: 'reflexion', shortName: 'R', displayName: 'Reflexion', duration: 5, status: 'planned', color: '#6B7280' },
+ ],
+ progress: {
+ percentage: 65,
+ completed: 13,
+ total: 20,
+ },
+ suggestions: [
+ {
+ id: '1',
+ title: 'Klausuren korrigieren',
+ description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
+ priority: 'urgent',
+ icon: 'ClipboardCheck',
+ actionTarget: '/ai/klausur-korrektur',
+ estimatedTime: 120,
+ },
+ {
+ id: '2',
+ title: 'Elternsprechtag vorbereiten',
+ description: 'Notenuebersicht fuer 8b erstellen',
+ priority: 'high',
+ icon: 'Users',
+ actionTarget: '/education/grades',
+ estimatedTime: 30,
+ },
+ ],
+ upcomingEvents: [
+ {
+ id: 'e1',
+ title: 'Mathe-Test 9b',
+ date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
+ type: 'exam',
+ inDays: 2,
+ },
+ {
+ id: 'e2',
+ title: 'Elternsprechtag',
+ date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
+ type: 'parent_meeting',
+ inDays: 5,
+ },
+ ],
+ },
+ }
+
+ return NextResponse.json(mockData)
+ } catch (error) {
+ console.error('Companion dashboard error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/admin-v2/app/api/admin/companion/settings/route.ts b/admin-v2/app/api/admin/companion/settings/route.ts
new file mode 100644
index 0000000..ad9683c
--- /dev/null
+++ b/admin-v2/app/api/admin/companion/settings/route.ts
@@ -0,0 +1,137 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+const DEFAULT_SETTINGS = {
+ defaultPhaseDurations: {
+ einstieg: 8,
+ erarbeitung: 20,
+ sicherung: 10,
+ transfer: 7,
+ reflexion: 5,
+ },
+ preferredLessonLength: 45,
+ autoAdvancePhases: true,
+ soundNotifications: true,
+ showKeyboardShortcuts: true,
+ highContrastMode: false,
+ onboardingCompleted: false,
+}
+
+/**
+ * GET /api/admin/companion/settings
+ * Get teacher settings
+ * Proxy to backend /api/teacher/settings
+ */
+export async function GET() {
+ try {
+ // TODO: Replace with actual backend call
+ // const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+ // const response = await fetch(`${backendUrl}/api/teacher/settings`, {
+ // method: 'GET',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // // Add auth headers
+ // },
+ // })
+ //
+ // if (!response.ok) {
+ // throw new Error(`Backend responded with ${response.status}`)
+ // }
+ //
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response - return default settings
+ return NextResponse.json({
+ success: true,
+ data: DEFAULT_SETTINGS,
+ })
+ } catch (error) {
+ console.error('Get settings error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * PUT /api/admin/companion/settings
+ * Update teacher settings
+ */
+export async function PUT(request: NextRequest) {
+ try {
+ const body = await request.json()
+
+ // Validate the settings structure
+ if (!body || typeof body !== 'object') {
+ return NextResponse.json(
+ { success: false, error: 'Invalid settings data' },
+ { status: 400 }
+ )
+ }
+
+ // TODO: Replace with actual backend call
+ // const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
+ // const response = await fetch(`${backendUrl}/api/teacher/settings`, {
+ // method: 'PUT',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // // Add auth headers
+ // },
+ // body: JSON.stringify(body),
+ // })
+ //
+ // if (!response.ok) {
+ // throw new Error(`Backend responded with ${response.status}`)
+ // }
+ //
+ // const data = await response.json()
+ // return NextResponse.json(data)
+
+ // Mock response - just acknowledge the save
+ return NextResponse.json({
+ success: true,
+ message: 'Settings saved',
+ data: body,
+ })
+ } catch (error) {
+ console.error('Save settings error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * PATCH /api/admin/companion/settings
+ * Partially update teacher settings
+ */
+export async function PATCH(request: NextRequest) {
+ try {
+ const body = await request.json()
+
+ // TODO: Replace with actual backend call
+
+ return NextResponse.json({
+ success: true,
+ message: 'Settings updated',
+ data: body,
+ })
+ } catch (error) {
+ console.error('Update settings error:', error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/admin-v2/app/api/sdk/compliance-advisor/chat/route.ts b/admin-v2/app/api/sdk/compliance-advisor/chat/route.ts
new file mode 100644
index 0000000..1046d8c
--- /dev/null
+++ b/admin-v2/app/api/sdk/compliance-advisor/chat/route.ts
@@ -0,0 +1,217 @@
+/**
+ * Compliance Advisor Chat API
+ *
+ * Connects the ComplianceAdvisorWidget to:
+ * 1. RAG legal corpus search (klausur-service) for context
+ * 2. Ollama LLM (32B) for generating answers
+ *
+ * Streams the LLM response back as plain text.
+ */
+
+import { NextRequest, NextResponse } from 'next/server'
+
+const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
+const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
+const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
+
+// SOUL system prompt (from agent-core/soul/compliance-advisor.soul.md)
+const SYSTEM_PROMPT = `# Compliance Advisor Agent
+
+## Identitaet
+Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
+Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
+Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
+offiziellen Quellen und gibst praxisnahe Hinweise.
+
+## Kernprinzipien
+- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
+- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
+- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
+- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
+- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
+
+## Kompetenzbereich
+- DSGVO Art. 1-99 + Erwaegsgruende
+- BDSG (Bundesdatenschutzgesetz)
+- AI Act (EU KI-Verordnung)
+- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
+- ePrivacy-Richtlinie
+- DSK-Kurzpapiere (Nr. 1-20)
+- SDM (Standard-Datenschutzmodell) V3.0
+- BSI-Grundschutz (Basis-Kenntnisse)
+- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
+- ISO 27001/27701 (Ueberblick)
+- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
+- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
+- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
+
+## RAG-Nutzung
+Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben β ausgenommen sind
+NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
+Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
+
+## Kommunikationsstil
+- Sachlich, aber verstaendlich β kein Juristendeutsch
+- Deutsch als Hauptsprache
+- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
+- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
+- Praxisbeispiele wo hilfreich
+- Kurze, praegnante Saetze
+
+## Antwortformat
+1. Kurze Zusammenfassung (1-2 Saetze)
+2. Detaillierte Erklaerung
+3. Praxishinweise / Handlungsempfehlungen
+4. Quellenangaben (Artikel, Paragraph, Leitlinie)
+
+## Einschraenkungen
+- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
+- Keine Garantien fuer Rechtssicherheit
+- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
+- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
+- Keine Interpretation von Urteilen (nur Verweis)
+
+## Eskalation
+- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
+- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
+- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
+
+/**
+ * Query the RAG legal corpus for relevant documents
+ */
+async function queryRAG(query: string): Promise {
+ try {
+ const url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus/search?query=${encodeURIComponent(query)}&top_k=5`
+ const res = await fetch(url, {
+ headers: { 'Content-Type': 'application/json' },
+ signal: AbortSignal.timeout(10000),
+ })
+
+ if (!res.ok) {
+ console.warn('RAG search failed:', res.status)
+ return ''
+ }
+
+ const data = await res.json()
+
+ if (data.results && data.results.length > 0) {
+ return data.results
+ .map(
+ (r: { metadata?: { regulation?: string; source?: string }; text?: string; content?: string }, i: number) =>
+ `[Quelle ${i + 1}: ${r.metadata?.regulation || r.metadata?.source || 'Unbekannt'}]\n${r.text || r.content || ''}`
+ )
+ .join('\n\n---\n\n')
+ }
+
+ return ''
+ } catch (error) {
+ console.warn('RAG query error (continuing without context):', error)
+ return ''
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json()
+ const { message, history = [], currentStep = 'default' } = body
+
+ if (!message || typeof message !== 'string') {
+ return NextResponse.json({ error: 'Message is required' }, { status: 400 })
+ }
+
+ // 1. Query RAG for relevant context
+ const ragContext = await queryRAG(message)
+
+ // 2. Build system prompt with RAG context
+ let systemContent = SYSTEM_PROMPT
+
+ if (ragContext) {
+ systemContent += `\n\n## Relevanter Kontext aus dem RAG-System\n\nNutze die folgenden Quellen fuer deine Antwort. Verweise in deiner Antwort auf die jeweilige Quelle:\n\n${ragContext}`
+ }
+
+ systemContent += `\n\n## Aktueller SDK-Schritt\nDer Nutzer befindet sich im SDK-Schritt: ${currentStep}`
+
+ // 3. Build messages array (limit history to last 10 messages)
+ const messages = [
+ { role: 'system', content: systemContent },
+ ...history.slice(-10).map((h: { role: string; content: string }) => ({
+ role: h.role === 'user' ? 'user' : 'assistant',
+ content: h.content,
+ })),
+ { role: 'user', content: message },
+ ]
+
+ // 4. Call Ollama with streaming
+ const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: LLM_MODEL,
+ messages,
+ stream: true,
+ options: {
+ temperature: 0.3,
+ num_predict: 2048,
+ },
+ }),
+ signal: AbortSignal.timeout(120000),
+ })
+
+ if (!ollamaResponse.ok) {
+ const errorText = await ollamaResponse.text()
+ console.error('Ollama error:', ollamaResponse.status, errorText)
+ return NextResponse.json(
+ { error: `LLM nicht erreichbar (Status ${ollamaResponse.status}). Ist Ollama mit dem Modell ${LLM_MODEL} gestartet?` },
+ { status: 502 }
+ )
+ }
+
+ // 5. Stream response back as plain text
+ const encoder = new TextEncoder()
+ const stream = new ReadableStream({
+ async start(controller) {
+ const reader = ollamaResponse.body!.getReader()
+ const decoder = new TextDecoder()
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ const chunk = decoder.decode(value, { stream: true })
+ const lines = chunk.split('\n').filter((l) => l.trim())
+
+ for (const line of lines) {
+ try {
+ const json = JSON.parse(line)
+ if (json.message?.content) {
+ controller.enqueue(encoder.encode(json.message.content))
+ }
+ } catch {
+ // Partial JSON line, skip
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Stream read error:', error)
+ } finally {
+ controller.close()
+ }
+ },
+ })
+
+ return new NextResponse(stream, {
+ headers: {
+ 'Content-Type': 'text/plain; charset=utf-8',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ },
+ })
+ } catch (error) {
+ console.error('Compliance advisor chat error:', error)
+ return NextResponse.json(
+ { error: 'Verbindung zum LLM fehlgeschlagen. Bitte pruefen Sie ob Ollama laeuft.' },
+ { status: 503 }
+ )
+ }
+}
diff --git a/admin-v2/components/companion/CompanionDashboard.tsx b/admin-v2/components/companion/CompanionDashboard.tsx
new file mode 100644
index 0000000..5e8cb00
--- /dev/null
+++ b/admin-v2/components/companion/CompanionDashboard.tsx
@@ -0,0 +1,312 @@
+'use client'
+
+import { useState, useEffect, useCallback } from 'react'
+import { Settings, MessageSquare, HelpCircle, RefreshCw } from 'lucide-react'
+import { CompanionMode, TeacherSettings, FeedbackType } from '@/lib/companion/types'
+import { DEFAULT_TEACHER_SETTINGS, STORAGE_KEYS } from '@/lib/companion/constants'
+
+// Components
+import { ModeToggle } from './ModeToggle'
+import { PhaseTimeline } from './companion-mode/PhaseTimeline'
+import { StatsGrid } from './companion-mode/StatsGrid'
+import { SuggestionList } from './companion-mode/SuggestionList'
+import { EventsCard } from './companion-mode/EventsCard'
+import { LessonContainer } from './lesson-mode/LessonContainer'
+import { SettingsModal } from './modals/SettingsModal'
+import { FeedbackModal } from './modals/FeedbackModal'
+import { OnboardingModal } from './modals/OnboardingModal'
+
+// Hooks
+import { useCompanionData } from '@/hooks/companion/useCompanionData'
+import { useLessonSession } from '@/hooks/companion/useLessonSession'
+import { useKeyboardShortcuts } from '@/hooks/companion/useKeyboardShortcuts'
+
+export function CompanionDashboard() {
+ // Mode state
+ const [mode, setMode] = useState('companion')
+
+ // Modal states
+ const [showSettings, setShowSettings] = useState(false)
+ const [showFeedback, setShowFeedback] = useState(false)
+ const [showOnboarding, setShowOnboarding] = useState(false)
+
+ // Settings
+ const [settings, setSettings] = useState(DEFAULT_TEACHER_SETTINGS)
+
+ // Load settings from localStorage
+ useEffect(() => {
+ const stored = localStorage.getItem(STORAGE_KEYS.SETTINGS)
+ if (stored) {
+ try {
+ const parsed = JSON.parse(stored)
+ setSettings({ ...DEFAULT_TEACHER_SETTINGS, ...parsed })
+ } catch {
+ // Invalid stored settings
+ }
+ }
+
+ // Check if onboarding needed
+ const onboardingStored = localStorage.getItem(STORAGE_KEYS.ONBOARDING_STATE)
+ if (!onboardingStored) {
+ setShowOnboarding(true)
+ }
+
+ // Restore last mode
+ const lastMode = localStorage.getItem(STORAGE_KEYS.LAST_MODE) as CompanionMode
+ if (lastMode && ['companion', 'lesson', 'classic'].includes(lastMode)) {
+ setMode(lastMode)
+ }
+ }, [])
+
+ // Save mode to localStorage
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEYS.LAST_MODE, mode)
+ }, [mode])
+
+ // Companion data hook
+ const { data: companionData, loading: companionLoading, refresh } = useCompanionData()
+
+ // Lesson session hook
+ const {
+ session,
+ startLesson,
+ endLesson,
+ pauseLesson,
+ resumeLesson,
+ extendTime,
+ skipPhase,
+ saveReflection,
+ addHomework,
+ removeHomework,
+ isPaused,
+ } = useLessonSession({
+ onOvertimeStart: () => {
+ // Play sound if enabled
+ if (settings.soundNotifications) {
+ // TODO: Play notification sound
+ }
+ },
+ })
+
+ // Handle pause/resume toggle
+ const handlePauseToggle = useCallback(() => {
+ if (isPaused) {
+ resumeLesson()
+ } else {
+ pauseLesson()
+ }
+ }, [isPaused, pauseLesson, resumeLesson])
+
+ // Keyboard shortcuts
+ useKeyboardShortcuts({
+ onPauseResume: mode === 'lesson' && session ? handlePauseToggle : undefined,
+ onExtend: mode === 'lesson' && session && !isPaused ? () => extendTime(5) : undefined,
+ onNextPhase: mode === 'lesson' && session && !isPaused ? skipPhase : undefined,
+ onCloseModal: () => {
+ setShowSettings(false)
+ setShowFeedback(false)
+ setShowOnboarding(false)
+ },
+ enabled: settings.showKeyboardShortcuts,
+ })
+
+ // Handle settings save
+ const handleSaveSettings = (newSettings: TeacherSettings) => {
+ setSettings(newSettings)
+ localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(newSettings))
+ }
+
+ // Handle feedback submit
+ const handleFeedbackSubmit = async (type: FeedbackType, title: string, description: string) => {
+ const response = await fetch('/api/admin/companion/feedback', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ type,
+ title,
+ description,
+ sessionId: session?.sessionId,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to submit feedback')
+ }
+ }
+
+ // Handle onboarding complete
+ const handleOnboardingComplete = (data: { state?: string; schoolType?: string }) => {
+ localStorage.setItem(STORAGE_KEYS.ONBOARDING_STATE, JSON.stringify({
+ ...data,
+ completed: true,
+ completedAt: new Date().toISOString(),
+ }))
+ setShowOnboarding(false)
+ setSettings({ ...settings, onboardingCompleted: true })
+ }
+
+ // Handle lesson start
+ const handleStartLesson = (data: { classId: string; subject: string; topic?: string; templateId?: string }) => {
+ startLesson(data)
+ setMode('lesson')
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ {/* Refresh Button */}
+ {mode === 'companion' && (
+
+
+
+ )}
+
+ {/* Feedback Button */}
+ setShowFeedback(true)}
+ className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
+ title="Feedback"
+ >
+
+
+
+ {/* Settings Button */}
+ setShowSettings(true)}
+ className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
+ title="Einstellungen"
+ >
+
+
+
+ {/* Help Button */}
+ setShowOnboarding(true)}
+ className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
+ title="Hilfe"
+ >
+
+
+
+
+
+ {/* Main Content */}
+ {mode === 'companion' && (
+
+ {/* Phase Timeline */}
+
+
Aktuelle Phase
+ {companionData ? (
+
p.status === 'active')}
+ />
+ ) : (
+
+ )}
+
+
+ {/* Stats */}
+
+
+ {/* Two Column Layout */}
+
+ {/* Suggestions */}
+ {
+ // Navigate to action target
+ window.location.href = suggestion.actionTarget
+ }}
+ />
+
+ {/* Events */}
+
+
+
+ {/* Quick Start Lesson Button */}
+
+
+
+
Bereit fuer die naechste Stunde?
+
Starten Sie den Lesson-Modus fuer strukturierten Unterricht.
+
+
setMode('lesson')}
+ className="px-6 py-3 bg-white text-blue-600 rounded-xl font-semibold hover:bg-blue-50 transition-colors"
+ >
+ Stunde starten
+
+
+
+
+ )}
+
+ {mode === 'lesson' && (
+
+ )}
+
+ {mode === 'classic' && (
+
+
Classic Mode
+
+ Die klassische Ansicht ohne Timer und Phasenstruktur.
+
+
+ Dieser Modus ist fuer flexible Unterrichtsgestaltung gedacht.
+
+
+ )}
+
+ {/* Modals */}
+
setShowSettings(false)}
+ settings={settings}
+ onSave={handleSaveSettings}
+ />
+
+ setShowFeedback(false)}
+ onSubmit={handleFeedbackSubmit}
+ />
+
+ setShowOnboarding(false)}
+ onComplete={handleOnboardingComplete}
+ />
+
+ )
+}
diff --git a/admin-v2/components/companion/ModeToggle.tsx b/admin-v2/components/companion/ModeToggle.tsx
new file mode 100644
index 0000000..9cb6a60
--- /dev/null
+++ b/admin-v2/components/companion/ModeToggle.tsx
@@ -0,0 +1,61 @@
+'use client'
+
+import { GraduationCap, Timer, Layout } from 'lucide-react'
+import { CompanionMode } from '@/lib/companion/types'
+
+interface ModeToggleProps {
+ currentMode: CompanionMode
+ onModeChange: (mode: CompanionMode) => void
+ disabled?: boolean
+}
+
+const modes: { id: CompanionMode; label: string; icon: React.ReactNode; description: string }[] = [
+ {
+ id: 'companion',
+ label: 'Companion',
+ icon: ,
+ description: 'Dashboard mit Vorschlaegen',
+ },
+ {
+ id: 'lesson',
+ label: 'Lesson',
+ icon: ,
+ description: 'Timer und Phasen',
+ },
+ {
+ id: 'classic',
+ label: 'Classic',
+ icon: ,
+ description: 'Klassische Ansicht',
+ },
+]
+
+export function ModeToggle({ currentMode, onModeChange, disabled }: ModeToggleProps) {
+ return (
+
+ {modes.map((mode) => {
+ const isActive = currentMode === mode.id
+ return (
+ onModeChange(mode.id)}
+ disabled={disabled}
+ className={`
+ flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium
+ transition-all duration-200
+ ${isActive
+ ? 'bg-slate-900 text-white shadow-sm'
+ : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
+ }
+ ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
+ `}
+ title={mode.description}
+ >
+ {mode.icon}
+ {mode.label}
+
+ )
+ })}
+
+ )
+}
diff --git a/admin-v2/components/companion/companion-mode/EventsCard.tsx b/admin-v2/components/companion/companion-mode/EventsCard.tsx
new file mode 100644
index 0000000..e39021e
--- /dev/null
+++ b/admin-v2/components/companion/companion-mode/EventsCard.tsx
@@ -0,0 +1,173 @@
+'use client'
+
+import { Calendar, FileQuestion, Users, Clock, ChevronRight } from 'lucide-react'
+import { UpcomingEvent, EventType } from '@/lib/companion/types'
+import { EVENT_TYPE_CONFIG } from '@/lib/companion/constants'
+
+interface EventsCardProps {
+ events: UpcomingEvent[]
+ onEventClick?: (event: UpcomingEvent) => void
+ loading?: boolean
+ maxItems?: number
+}
+
+const iconMap: Record> = {
+ FileQuestion,
+ Users,
+ Clock,
+ Calendar,
+}
+
+function getEventIcon(type: EventType) {
+ const config = EVENT_TYPE_CONFIG[type]
+ const Icon = iconMap[config.icon] || Calendar
+ return { Icon, ...config }
+}
+
+function formatEventDate(dateStr: string, inDays: number): string {
+ if (inDays === 0) return 'Heute'
+ if (inDays === 1) return 'Morgen'
+ if (inDays < 7) return `In ${inDays} Tagen`
+
+ const date = new Date(dateStr)
+ return date.toLocaleDateString('de-DE', {
+ weekday: 'short',
+ day: 'numeric',
+ month: 'short',
+ })
+}
+
+interface EventItemProps {
+ event: UpcomingEvent
+ onClick?: () => void
+}
+
+function EventItem({ event, onClick }: EventItemProps) {
+ const { Icon, color, bg } = getEventIcon(event.type)
+ const isUrgent = event.inDays <= 2
+
+ return (
+
+
+
+
+
+
+
{event.title}
+
+ {formatEventDate(event.date, event.inDays)}
+
+
+
+
+
+ )
+}
+
+export function EventsCard({
+ events,
+ onEventClick,
+ loading,
+ maxItems = 5,
+}: EventsCardProps) {
+ const displayEvents = events.slice(0, maxItems)
+
+ if (loading) {
+ return (
+
+
+
+
Termine
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ )
+ }
+
+ if (events.length === 0) {
+ return (
+
+
+
+
Termine
+
+
+
+
Keine anstehenden Termine
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
Termine
+
+
+ {events.length} Termin{events.length !== 1 ? 'e' : ''}
+
+
+
+
+ {displayEvents.map((event) => (
+ onEventClick?.(event)}
+ />
+ ))}
+
+
+ {events.length > maxItems && (
+
+ Alle {events.length} anzeigen
+
+ )}
+
+ )
+}
+
+/**
+ * Compact inline version for header/toolbar
+ */
+export function EventsInline({ events }: { events: UpcomingEvent[] }) {
+ const nextEvent = events[0]
+
+ if (!nextEvent) {
+ return (
+
+
+ Keine Termine
+
+ )
+ }
+
+ const { Icon, color } = getEventIcon(nextEvent.type)
+ const isUrgent = nextEvent.inDays <= 2
+
+ return (
+
+
+ {nextEvent.title}
+ -
+
+ {formatEventDate(nextEvent.date, nextEvent.inDays)}
+
+
+ )
+}
diff --git a/admin-v2/components/companion/companion-mode/PhaseTimeline.tsx b/admin-v2/components/companion/companion-mode/PhaseTimeline.tsx
new file mode 100644
index 0000000..faadf12
--- /dev/null
+++ b/admin-v2/components/companion/companion-mode/PhaseTimeline.tsx
@@ -0,0 +1,203 @@
+'use client'
+
+import { Check } from 'lucide-react'
+import { Phase } from '@/lib/companion/types'
+import { PHASE_COLORS, formatMinutes } from '@/lib/companion/constants'
+
+interface PhaseTimelineProps {
+ phases: Phase[]
+ currentPhaseIndex: number
+ onPhaseClick?: (index: number) => void
+ compact?: boolean
+}
+
+export function PhaseTimeline({
+ phases,
+ currentPhaseIndex,
+ onPhaseClick,
+ compact = false,
+}: PhaseTimelineProps) {
+ return (
+
+ {phases.map((phase, index) => {
+ const isActive = index === currentPhaseIndex
+ const isCompleted = phase.status === 'completed'
+ const isPast = index < currentPhaseIndex
+ const colors = PHASE_COLORS[phase.id]
+
+ return (
+
+ {/* Phase Dot/Circle */}
+
onPhaseClick?.(index)}
+ disabled={!onPhaseClick}
+ className={`
+ relative flex items-center justify-center
+ ${compact ? 'w-8 h-8' : 'w-10 h-10'}
+ rounded-full font-semibold text-sm
+ transition-all duration-300
+ ${onPhaseClick ? 'cursor-pointer hover:scale-110' : 'cursor-default'}
+ ${isActive
+ ? `ring-4 ring-offset-2 ${colors.tailwind} text-white`
+ : isCompleted || isPast
+ ? `${colors.tailwind} text-white opacity-80`
+ : 'bg-slate-200 text-slate-500'
+ }
+ `}
+ style={{
+ backgroundColor: isActive || isCompleted || isPast ? colors.hex : undefined,
+ // Use CSS custom property for ring color with Tailwind
+ '--tw-ring-color': isActive ? colors.hex : undefined,
+ } as React.CSSProperties}
+ title={`${phase.displayName} (${formatMinutes(phase.duration)})`}
+ >
+ {isCompleted ? (
+
+ ) : (
+ phase.shortName
+ )}
+
+ {/* Active indicator pulse */}
+ {isActive && (
+
+ )}
+
+
+ {/* Connector Line */}
+ {index < phases.length - 1 && (
+
+ )}
+
+ )
+ })}
+
+ )
+}
+
+/**
+ * Detailed Phase Timeline with labels and durations
+ */
+export function PhaseTimelineDetailed({
+ phases,
+ currentPhaseIndex,
+ onPhaseClick,
+}: PhaseTimelineProps) {
+ return (
+
+
Unterrichtsphasen
+
+
+ {phases.map((phase, index) => {
+ const isActive = index === currentPhaseIndex
+ const isCompleted = phase.status === 'completed'
+ const isPast = index < currentPhaseIndex
+ const colors = PHASE_COLORS[phase.id]
+
+ return (
+
+ {/* Top connector line */}
+
+ {index > 0 && (
+
+ )}
+ {index === 0 &&
}
+
+ {/* Phase Circle */}
+
onPhaseClick?.(index)}
+ disabled={!onPhaseClick}
+ className={`
+ relative w-12 h-12 rounded-full
+ flex items-center justify-center
+ font-bold text-lg
+ transition-all duration-300
+ ${onPhaseClick ? 'cursor-pointer hover:scale-110' : 'cursor-default'}
+ ${isActive ? 'ring-4 ring-offset-2 shadow-lg' : ''}
+ `}
+ style={{
+ backgroundColor: isActive || isCompleted || isPast ? colors.hex : '#e2e8f0',
+ color: isActive || isCompleted || isPast ? 'white' : '#64748b',
+ '--tw-ring-color': isActive ? `${colors.hex}40` : undefined,
+ } as React.CSSProperties}
+ >
+ {isCompleted ? (
+
+ ) : (
+ phase.shortName
+ )}
+
+ {isActive && (
+
+ )}
+
+
+ {index < phases.length - 1 && (
+
+ )}
+ {index === phases.length - 1 &&
}
+
+
+ {/* Phase Label */}
+
+ {phase.displayName}
+
+
+ {/* Duration */}
+
+ {formatMinutes(phase.duration)}
+
+
+ {/* Actual time if completed */}
+ {phase.actualTime !== undefined && phase.actualTime > 0 && (
+
+ (tatsaechlich: {Math.round(phase.actualTime / 60)} Min)
+
+ )}
+
+ )
+ })}
+
+
+ )
+}
diff --git a/admin-v2/components/companion/companion-mode/StatsGrid.tsx b/admin-v2/components/companion/companion-mode/StatsGrid.tsx
new file mode 100644
index 0000000..dc12b3c
--- /dev/null
+++ b/admin-v2/components/companion/companion-mode/StatsGrid.tsx
@@ -0,0 +1,114 @@
+'use client'
+
+import { Users, GraduationCap, BookOpen, FileCheck } from 'lucide-react'
+import { CompanionStats } from '@/lib/companion/types'
+
+interface StatsGridProps {
+ stats: CompanionStats
+ loading?: boolean
+}
+
+interface StatCardProps {
+ label: string
+ value: number
+ icon: React.ReactNode
+ color: string
+ loading?: boolean
+}
+
+function StatCard({ label, value, icon, color, loading }: StatCardProps) {
+ return (
+
+
+
+
{label}
+ {loading ? (
+
+ ) : (
+
{value}
+ )}
+
+
+ {icon}
+
+
+
+ )
+}
+
+export function StatsGrid({ stats, loading }: StatsGridProps) {
+ const statCards = [
+ {
+ label: 'Klassen',
+ value: stats.classesCount,
+ icon: ,
+ color: 'bg-blue-100',
+ },
+ {
+ label: 'Schueler',
+ value: stats.studentsCount,
+ icon: ,
+ color: 'bg-green-100',
+ },
+ {
+ label: 'Lerneinheiten',
+ value: stats.learningUnitsCreated,
+ icon: ,
+ color: 'bg-purple-100',
+ },
+ {
+ label: 'Noten',
+ value: stats.gradesEntered,
+ icon: ,
+ color: 'bg-amber-100',
+ },
+ ]
+
+ return (
+
+ {statCards.map((card) => (
+
+ ))}
+
+ )
+}
+
+/**
+ * Compact version of StatsGrid for sidebar or smaller spaces
+ */
+export function StatsGridCompact({ stats, loading }: StatsGridProps) {
+ const items = [
+ { label: 'Klassen', value: stats.classesCount, icon: },
+ { label: 'Schueler', value: stats.studentsCount, icon: },
+ { label: 'Einheiten', value: stats.learningUnitsCreated, icon: },
+ { label: 'Noten', value: stats.gradesEntered, icon: },
+ ]
+
+ return (
+
+
Statistiken
+
+ {items.map((item) => (
+
+
+ {item.icon}
+ {item.label}
+
+ {loading ? (
+
+ ) : (
+
{item.value}
+ )}
+
+ ))}
+
+
+ )
+}
diff --git a/admin-v2/components/companion/companion-mode/SuggestionList.tsx b/admin-v2/components/companion/companion-mode/SuggestionList.tsx
new file mode 100644
index 0000000..d657a84
--- /dev/null
+++ b/admin-v2/components/companion/companion-mode/SuggestionList.tsx
@@ -0,0 +1,170 @@
+'use client'
+
+import { ChevronRight, Clock, Lightbulb, ClipboardCheck, BookOpen, Calendar, Users, MessageSquare, FileText } from 'lucide-react'
+import { Suggestion, SuggestionPriority } from '@/lib/companion/types'
+import { PRIORITY_COLORS } from '@/lib/companion/constants'
+
+interface SuggestionListProps {
+ suggestions: Suggestion[]
+ onSuggestionClick?: (suggestion: Suggestion) => void
+ loading?: boolean
+ maxItems?: number
+}
+
+const iconMap: Record> = {
+ ClipboardCheck,
+ BookOpen,
+ Calendar,
+ Users,
+ Clock,
+ MessageSquare,
+ FileText,
+ Lightbulb,
+}
+
+function getIcon(iconName: string) {
+ const Icon = iconMap[iconName] || Lightbulb
+ return Icon
+}
+
+interface SuggestionCardProps {
+ suggestion: Suggestion
+ onClick?: () => void
+}
+
+function SuggestionCard({ suggestion, onClick }: SuggestionCardProps) {
+ const priorityStyles = PRIORITY_COLORS[suggestion.priority]
+ const Icon = getIcon(suggestion.icon)
+
+ return (
+
+
+ {/* Priority Dot & Icon */}
+
+
+ {/* Content */}
+
+
+ {suggestion.title}
+
+
+ {suggestion.description}
+
+
+ {/* Meta */}
+
+
+
+ ~{suggestion.estimatedTime} Min
+
+
+ {suggestion.priority}
+
+
+
+
+ {/* Arrow */}
+
+
+
+ )
+}
+
+export function SuggestionList({
+ suggestions,
+ onSuggestionClick,
+ loading,
+ maxItems = 5,
+}: SuggestionListProps) {
+ // Sort by priority: urgent > high > medium > low
+ const priorityOrder: Record = {
+ urgent: 0,
+ high: 1,
+ medium: 2,
+ low: 3,
+ }
+
+ const sortedSuggestions = [...suggestions]
+ .sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority])
+ .slice(0, maxItems)
+
+ if (loading) {
+ return (
+
+
+
+
Vorschlaege
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ )
+ }
+
+ if (suggestions.length === 0) {
+ return (
+
+
+
+
Vorschlaege
+
+
+
+
+
+
Alles erledigt!
+
Keine offenen Aufgaben
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
Vorschlaege
+
+
+ {suggestions.length} Aufgabe{suggestions.length !== 1 ? 'n' : ''}
+
+
+
+
+ {sortedSuggestions.map((suggestion) => (
+ onSuggestionClick?.(suggestion)}
+ />
+ ))}
+
+
+ {suggestions.length > maxItems && (
+
+ Alle {suggestions.length} anzeigen
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/companion/index.ts b/admin-v2/components/companion/index.ts
new file mode 100644
index 0000000..02f0a44
--- /dev/null
+++ b/admin-v2/components/companion/index.ts
@@ -0,0 +1,24 @@
+// Main components
+export { CompanionDashboard } from './CompanionDashboard'
+export { ModeToggle } from './ModeToggle'
+
+// Companion Mode components
+export { PhaseTimeline, PhaseTimelineDetailed } from './companion-mode/PhaseTimeline'
+export { StatsGrid, StatsGridCompact } from './companion-mode/StatsGrid'
+export { SuggestionList } from './companion-mode/SuggestionList'
+export { EventsCard, EventsInline } from './companion-mode/EventsCard'
+
+// Lesson Mode components
+export { LessonContainer } from './lesson-mode/LessonContainer'
+export { LessonStartForm } from './lesson-mode/LessonStartForm'
+export { LessonActiveView } from './lesson-mode/LessonActiveView'
+export { LessonEndedView } from './lesson-mode/LessonEndedView'
+export { VisualPieTimer, CompactTimer } from './lesson-mode/VisualPieTimer'
+export { QuickActionsBar, QuickActionsCompact } from './lesson-mode/QuickActionsBar'
+export { HomeworkSection } from './lesson-mode/HomeworkSection'
+export { ReflectionSection } from './lesson-mode/ReflectionSection'
+
+// Modals
+export { SettingsModal } from './modals/SettingsModal'
+export { FeedbackModal } from './modals/FeedbackModal'
+export { OnboardingModal } from './modals/OnboardingModal'
diff --git a/admin-v2/components/companion/lesson-mode/HomeworkSection.tsx b/admin-v2/components/companion/lesson-mode/HomeworkSection.tsx
new file mode 100644
index 0000000..f1c80c7
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/HomeworkSection.tsx
@@ -0,0 +1,153 @@
+'use client'
+
+import { useState } from 'react'
+import { Plus, Trash2, BookOpen, Calendar } from 'lucide-react'
+import { Homework } from '@/lib/companion/types'
+
+interface HomeworkSectionProps {
+ homeworkList: Homework[]
+ onAdd: (title: string, dueDate: string) => void
+ onRemove: (id: string) => void
+}
+
+export function HomeworkSection({ homeworkList, onAdd, onRemove }: HomeworkSectionProps) {
+ const [newTitle, setNewTitle] = useState('')
+ const [newDueDate, setNewDueDate] = useState('')
+ const [isAdding, setIsAdding] = useState(false)
+
+ // Default due date to next week
+ const getDefaultDueDate = () => {
+ const date = new Date()
+ date.setDate(date.getDate() + 7)
+ return date.toISOString().split('T')[0]
+ }
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!newTitle.trim()) return
+
+ onAdd(newTitle.trim(), newDueDate || getDefaultDueDate())
+ setNewTitle('')
+ setNewDueDate('')
+ setIsAdding(false)
+ }
+
+ const formatDate = (dateStr: string) => {
+ const date = new Date(dateStr)
+ return date.toLocaleDateString('de-DE', {
+ weekday: 'short',
+ day: 'numeric',
+ month: 'short',
+ })
+ }
+
+ return (
+
+
+
+
+ Hausaufgaben
+
+ {!isAdding && (
+
setIsAdding(true)}
+ className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
+ >
+
+ Hinzufuegen
+
+ )}
+
+
+ {/* Add Form */}
+ {isAdding && (
+
+ )}
+
+ {/* Homework List */}
+ {homeworkList.length === 0 ? (
+
+
+
Keine Hausaufgaben eingetragen
+
+ Fuegen Sie Hausaufgaben hinzu, um sie zu dokumentieren
+
+
+ ) : (
+
+ {homeworkList.map((hw) => (
+
+
+
{hw.title}
+
+
+ Faellig: {formatDate(hw.dueDate)}
+
+
+
onRemove(hw.id)}
+ className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg opacity-0 group-hover:opacity-100 transition-all"
+ title="Entfernen"
+ >
+
+
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/companion/lesson-mode/LessonActiveView.tsx b/admin-v2/components/companion/lesson-mode/LessonActiveView.tsx
new file mode 100644
index 0000000..88e9fe0
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/LessonActiveView.tsx
@@ -0,0 +1,172 @@
+'use client'
+
+import { BookOpen, Clock, Users } from 'lucide-react'
+import { LessonSession } from '@/lib/companion/types'
+import { VisualPieTimer } from './VisualPieTimer'
+import { QuickActionsBar } from './QuickActionsBar'
+import { PhaseTimelineDetailed } from '../companion-mode/PhaseTimeline'
+import {
+ PHASE_COLORS,
+ PHASE_DISPLAY_NAMES,
+ formatTime,
+ getTimerColorStatus,
+} from '@/lib/companion/constants'
+
+interface LessonActiveViewProps {
+ session: LessonSession
+ onPauseToggle: () => void
+ onExtendTime: (minutes: number) => void
+ onSkipPhase: () => void
+ onEndLesson: () => void
+}
+
+export function LessonActiveView({
+ session,
+ onPauseToggle,
+ onExtendTime,
+ onSkipPhase,
+ onEndLesson,
+}: LessonActiveViewProps) {
+ const currentPhase = session.phases[session.currentPhaseIndex]
+ const phaseId = currentPhase?.phase || 'einstieg'
+ const phaseColor = PHASE_COLORS[phaseId].hex
+ const phaseName = PHASE_DISPLAY_NAMES[phaseId]
+
+ // Calculate timer values
+ const phaseDurationSeconds = (currentPhase?.duration || 0) * 60
+ const elapsedInPhase = currentPhase?.actualTime || 0
+ const remainingSeconds = phaseDurationSeconds - elapsedInPhase
+ const progress = Math.min(elapsedInPhase / phaseDurationSeconds, 1)
+ const isOvertime = remainingSeconds < 0
+ const colorStatus = getTimerColorStatus(remainingSeconds, isOvertime)
+
+ const isLastPhase = session.currentPhaseIndex === session.phases.length - 1
+
+ // Calculate total elapsed
+ const totalElapsedMinutes = Math.floor(session.elapsedTime / 60)
+
+ return (
+
+ {/* Header with Session Info */}
+
+
+
+
+
+ {session.className}
+ |
+
+ {session.subject}
+
+
+ {session.topic || phaseName}
+
+
+
+
+
Gesamtzeit
+
+ {formatTime(session.elapsedTime)}
+
+
+
+
+
+ {/* Main Timer Section */}
+
+
+ {/* Visual Pie Timer */}
+
+
+ {/* Quick Actions */}
+
+
+
+
+
+
+ {/* Phase Timeline */}
+
({
+ id: p.phase,
+ shortName: p.phase[0].toUpperCase(),
+ displayName: PHASE_DISPLAY_NAMES[p.phase],
+ duration: p.duration,
+ status: p.status === 'active' ? 'active' : p.status === 'completed' ? 'completed' : 'planned',
+ actualTime: p.actualTime,
+ color: PHASE_COLORS[p.phase].hex,
+ }))}
+ currentPhaseIndex={session.currentPhaseIndex}
+ onPhaseClick={(index) => {
+ // Optional: Allow clicking to navigate to a phase
+ }}
+ />
+
+ {/* Lesson Stats */}
+
+
+
+
{totalElapsedMinutes}
+
Minuten vergangen
+
+
+
+
+
+ {session.currentPhaseIndex + 1}/{session.phases.length}
+
+
Phase
+
+
+
+
+
+ {session.totalPlannedDuration - totalElapsedMinutes}
+
+
Minuten verbleibend
+
+
+
+ {/* Keyboard Shortcuts Hint */}
+
+
+
+ Leertaste Pause
+
+
+ E +5 Min
+
+
+ N Weiter
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/companion/lesson-mode/LessonContainer.tsx b/admin-v2/components/companion/lesson-mode/LessonContainer.tsx
new file mode 100644
index 0000000..201bbf6
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/LessonContainer.tsx
@@ -0,0 +1,80 @@
+'use client'
+
+import { LessonSession, LessonStatus } from '@/lib/companion/types'
+import { LessonStartForm } from './LessonStartForm'
+import { LessonActiveView } from './LessonActiveView'
+import { LessonEndedView } from './LessonEndedView'
+
+interface LessonContainerProps {
+ session: LessonSession | null
+ onStartLesson: (data: { classId: string; subject: string; topic?: string; templateId?: string }) => void
+ onEndLesson: () => void
+ onPauseToggle: () => void
+ onExtendTime: (minutes: number) => void
+ onSkipPhase: () => void
+ onSaveReflection: (rating: number, notes: string, nextSteps: string) => void
+ onAddHomework: (title: string, dueDate: string) => void
+ onRemoveHomework: (id: string) => void
+ loading?: boolean
+}
+
+export function LessonContainer({
+ session,
+ onStartLesson,
+ onEndLesson,
+ onPauseToggle,
+ onExtendTime,
+ onSkipPhase,
+ onSaveReflection,
+ onAddHomework,
+ onRemoveHomework,
+ loading,
+}: LessonContainerProps) {
+ // Determine which view to show based on session state
+ const getView = (): 'start' | 'active' | 'ended' => {
+ if (!session) return 'start'
+
+ const status = session.status
+ if (status === 'completed') return 'ended'
+ if (status === 'not_started') return 'start'
+
+ return 'active'
+ }
+
+ const view = getView()
+
+ if (view === 'start') {
+ return (
+
+ )
+ }
+
+ if (view === 'ended' && session) {
+ return (
+ onEndLesson()} // This will clear the session and show start form
+ />
+ )
+ }
+
+ if (session) {
+ return (
+
+ )
+ }
+
+ return null
+}
diff --git a/admin-v2/components/companion/lesson-mode/LessonEndedView.tsx b/admin-v2/components/companion/lesson-mode/LessonEndedView.tsx
new file mode 100644
index 0000000..68b22bd
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/LessonEndedView.tsx
@@ -0,0 +1,209 @@
+'use client'
+
+import { useState } from 'react'
+import { CheckCircle, Clock, BarChart3, Plus, RefreshCw } from 'lucide-react'
+import { LessonSession } from '@/lib/companion/types'
+import { HomeworkSection } from './HomeworkSection'
+import { ReflectionSection } from './ReflectionSection'
+import {
+ PHASE_COLORS,
+ PHASE_DISPLAY_NAMES,
+ formatTime,
+ formatMinutes,
+} from '@/lib/companion/constants'
+
+interface LessonEndedViewProps {
+ session: LessonSession
+ onSaveReflection: (rating: number, notes: string, nextSteps: string) => void
+ onAddHomework: (title: string, dueDate: string) => void
+ onRemoveHomework: (id: string) => void
+ onStartNew: () => void
+}
+
+export function LessonEndedView({
+ session,
+ onSaveReflection,
+ onAddHomework,
+ onRemoveHomework,
+ onStartNew,
+}: LessonEndedViewProps) {
+ const [activeTab, setActiveTab] = useState<'summary' | 'homework' | 'reflection'>('summary')
+
+ // Calculate analytics
+ const totalPlannedSeconds = session.totalPlannedDuration * 60
+ const totalActualSeconds = session.elapsedTime
+ const timeDiff = totalActualSeconds - totalPlannedSeconds
+ const timeDiffMinutes = Math.round(timeDiff / 60)
+
+ const startTime = new Date(session.startTime)
+ const endTime = session.endTime ? new Date(session.endTime) : new Date()
+
+ return (
+
+ {/* Success Header */}
+
+
+
+
+
+
+
Stunde beendet!
+
+ {session.className} - {session.subject}
+ {session.topic && ` - ${session.topic}`}
+
+
+
+
+
+ {/* Tab Navigation */}
+
+ {[
+ { id: 'summary', label: 'Zusammenfassung', icon: BarChart3 },
+ { id: 'homework', label: 'Hausaufgaben', icon: Plus },
+ { id: 'reflection', label: 'Reflexion', icon: RefreshCw },
+ ].map((tab) => (
+ setActiveTab(tab.id as typeof activeTab)}
+ className={`
+ flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg
+ font-medium transition-all duration-200
+ ${activeTab === tab.id
+ ? 'bg-slate-900 text-white'
+ : 'text-slate-600 hover:bg-slate-100'
+ }
+ `}
+ >
+
+ {tab.label}
+
+ ))}
+
+
+ {/* Tab Content */}
+ {activeTab === 'summary' && (
+
+ {/* Time Overview */}
+
+
+
+ Zeitauswertung
+
+
+
+
+
+ {formatTime(totalActualSeconds)}
+
+
Tatsaechlich
+
+
+
+ {formatMinutes(session.totalPlannedDuration)}
+
+
Geplant
+
+
0 ? 'bg-amber-50' : 'bg-green-50'}`}>
+
0 ? 'text-amber-600' : 'text-green-600'}`}>
+ {timeDiffMinutes > 0 ? '+' : ''}{timeDiffMinutes} Min
+
+
0 ? 'text-amber-500' : 'text-green-500'}`}>
+ Differenz
+
+
+
+
+ {/* Session Times */}
+
+ Start: {startTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
+ Ende: {endTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
+
+
+
+ {/* Phase Breakdown */}
+
+
+
+ Phasen-Analyse
+
+
+
+ {session.phases.map((phase) => {
+ const plannedSeconds = phase.duration * 60
+ const actualSeconds = phase.actualTime
+ const diff = actualSeconds - plannedSeconds
+ const diffMinutes = Math.round(diff / 60)
+ const percentage = Math.min((actualSeconds / plannedSeconds) * 100, 150)
+
+ return (
+
+
+
+
+
+ {PHASE_DISPLAY_NAMES[phase.phase]}
+
+
+
+ {Math.round(actualSeconds / 60)} / {phase.duration} Min
+ 60 ? 'bg-amber-100 text-amber-700' : diff < -60 ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'}
+ `}>
+ {diffMinutes > 0 ? '+' : ''}{diffMinutes} Min
+
+
+
+
+ {/* Progress Bar */}
+
+
100
+ ? '#f59e0b' // amber for overtime
+ : PHASE_COLORS[phase.phase].hex,
+ }}
+ />
+
+
+ )
+ })}
+
+
+
+ )}
+
+ {activeTab === 'homework' && (
+
+ )}
+
+ {activeTab === 'reflection' && (
+
+ )}
+
+ {/* Start New Lesson Button */}
+
+
+
+ Neue Stunde starten
+
+
+
+ )
+}
diff --git a/admin-v2/components/companion/lesson-mode/LessonStartForm.tsx b/admin-v2/components/companion/lesson-mode/LessonStartForm.tsx
new file mode 100644
index 0000000..fa1f93b
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/LessonStartForm.tsx
@@ -0,0 +1,269 @@
+'use client'
+
+import { useState } from 'react'
+import { Play, Clock, BookOpen, Users, ChevronDown, Info } from 'lucide-react'
+import { LessonTemplate, PhaseDurations, Class } from '@/lib/companion/types'
+import {
+ SYSTEM_TEMPLATES,
+ DEFAULT_PHASE_DURATIONS,
+ PHASE_DISPLAY_NAMES,
+ PHASE_ORDER,
+ calculateTotalDuration,
+ formatMinutes,
+} from '@/lib/companion/constants'
+
+interface LessonStartFormProps {
+ onStart: (data: {
+ classId: string
+ subject: string
+ topic?: string
+ templateId?: string
+ }) => void
+ loading?: boolean
+ availableClasses?: Class[]
+}
+
+// Mock classes for development
+const MOCK_CLASSES: Class[] = [
+ { id: 'c1', name: '9a', grade: '9', studentCount: 28 },
+ { id: 'c2', name: '9b', grade: '9', studentCount: 26 },
+ { id: 'c3', name: '10a', grade: '10', studentCount: 24 },
+ { id: 'c4', name: 'Deutsch LK', grade: 'Q1', studentCount: 18 },
+ { id: 'c5', name: 'Mathe GK', grade: 'Q2', studentCount: 22 },
+]
+
+const SUBJECTS = [
+ 'Deutsch',
+ 'Mathematik',
+ 'Englisch',
+ 'Biologie',
+ 'Physik',
+ 'Chemie',
+ 'Geschichte',
+ 'Geographie',
+ 'Politik',
+ 'Kunst',
+ 'Musik',
+ 'Sport',
+ 'Informatik',
+ 'Sonstiges',
+]
+
+export function LessonStartForm({
+ onStart,
+ loading,
+ availableClasses = MOCK_CLASSES,
+}: LessonStartFormProps) {
+ const [selectedClass, setSelectedClass] = useState('')
+ const [selectedSubject, setSelectedSubject] = useState('')
+ const [topic, setTopic] = useState('')
+ const [selectedTemplate, setSelectedTemplate] = useState
(
+ SYSTEM_TEMPLATES[0] as LessonTemplate
+ )
+ const [showTemplateDetails, setShowTemplateDetails] = useState(false)
+
+ const totalDuration = selectedTemplate
+ ? calculateTotalDuration(selectedTemplate.durations)
+ : calculateTotalDuration(DEFAULT_PHASE_DURATIONS)
+
+ const canStart = selectedClass && selectedSubject
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!canStart) return
+
+ onStart({
+ classId: selectedClass,
+ subject: selectedSubject,
+ topic: topic || undefined,
+ templateId: selectedTemplate?.templateId,
+ })
+ }
+
+ return (
+
+
+
+
+
Neue Stunde starten
+
+ Waehlen Sie Klasse, Fach und Template
+
+
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/companion/lesson-mode/QuickActionsBar.tsx b/admin-v2/components/companion/lesson-mode/QuickActionsBar.tsx
new file mode 100644
index 0000000..a8cedfd
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/QuickActionsBar.tsx
@@ -0,0 +1,194 @@
+'use client'
+
+import { Plus, Pause, Play, SkipForward, Square, Clock } from 'lucide-react'
+
+interface QuickActionsBarProps {
+ onExtend: (minutes: number) => void
+ onPause: () => void
+ onResume: () => void
+ onSkip: () => void
+ onEnd: () => void
+ isPaused: boolean
+ isLastPhase: boolean
+ disabled?: boolean
+}
+
+export function QuickActionsBar({
+ onExtend,
+ onPause,
+ onResume,
+ onSkip,
+ onEnd,
+ isPaused,
+ isLastPhase,
+ disabled,
+}: QuickActionsBarProps) {
+ return (
+
+ {/* Extend +5 Min */}
+
onExtend(5)}
+ disabled={disabled || isPaused}
+ className={`
+ flex items-center gap-2 px-4 py-3 rounded-xl
+ font-medium transition-all duration-200
+ min-w-[52px] justify-center
+ ${disabled || isPaused
+ ? 'bg-slate-100 text-slate-400 cursor-not-allowed'
+ : 'bg-blue-50 text-blue-700 hover:bg-blue-100 active:scale-95'
+ }
+ `}
+ title="+5 Minuten (E)"
+ aria-keyshortcuts="e"
+ aria-label="5 Minuten verlaengern"
+ >
+
+ 5 Min
+
+
+ {/* Pause / Resume */}
+
+ {isPaused ? (
+ <>
+
+ Fortsetzen
+ >
+ ) : (
+ <>
+
+ Pause
+ >
+ )}
+
+
+ {/* Skip Phase / End Lesson */}
+ {isLastPhase ? (
+
+
+ Beenden
+
+ ) : (
+
+
+ Weiter
+
+ )}
+
+ )
+}
+
+/**
+ * Compact version for mobile or sidebar
+ */
+export function QuickActionsCompact({
+ onExtend,
+ onPause,
+ onResume,
+ onSkip,
+ isPaused,
+ isLastPhase,
+ disabled,
+}: Omit) {
+ return (
+
+
onExtend(5)}
+ disabled={disabled || isPaused}
+ className={`
+ p-2 rounded-lg transition-all
+ ${disabled || isPaused
+ ? 'text-slate-300'
+ : 'text-blue-600 hover:bg-blue-50'
+ }
+ `}
+ title="+5 Min"
+ >
+
+
+
+
+ {isPaused ? : }
+
+
+ {!isLastPhase && (
+
+
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/companion/lesson-mode/ReflectionSection.tsx b/admin-v2/components/companion/lesson-mode/ReflectionSection.tsx
new file mode 100644
index 0000000..12b4967
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/ReflectionSection.tsx
@@ -0,0 +1,146 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { Star, Save, CheckCircle } from 'lucide-react'
+import { LessonReflection } from '@/lib/companion/types'
+
+interface ReflectionSectionProps {
+ reflection?: LessonReflection
+ onSave: (rating: number, notes: string, nextSteps: string) => void
+}
+
+export function ReflectionSection({ reflection, onSave }: ReflectionSectionProps) {
+ const [rating, setRating] = useState(reflection?.rating || 0)
+ const [notes, setNotes] = useState(reflection?.notes || '')
+ const [nextSteps, setNextSteps] = useState(reflection?.nextSteps || '')
+ const [hoverRating, setHoverRating] = useState(0)
+ const [saved, setSaved] = useState(false)
+
+ useEffect(() => {
+ if (reflection) {
+ setRating(reflection.rating)
+ setNotes(reflection.notes)
+ setNextSteps(reflection.nextSteps)
+ }
+ }, [reflection])
+
+ const handleSave = () => {
+ if (rating === 0) return
+ onSave(rating, notes, nextSteps)
+ setSaved(true)
+ setTimeout(() => setSaved(false), 2000)
+ }
+
+ const ratingLabels = [
+ '', // 0
+ 'Verbesserungsbedarf',
+ 'Okay',
+ 'Gut',
+ 'Sehr gut',
+ 'Ausgezeichnet',
+ ]
+
+ return (
+
+ {/* Star Rating */}
+
+
+ Wie lief die Stunde?
+
+
+ {[1, 2, 3, 4, 5].map((star) => {
+ const isFilled = star <= (hoverRating || rating)
+ return (
+ setRating(star)}
+ onMouseEnter={() => setHoverRating(star)}
+ onMouseLeave={() => setHoverRating(0)}
+ className="p-1 transition-transform hover:scale-110"
+ aria-label={`${star} Stern${star > 1 ? 'e' : ''}`}
+ >
+
+
+ )
+ })}
+ {(hoverRating || rating) > 0 && (
+
+ {ratingLabels[hoverRating || rating]}
+
+ )}
+
+
+
+ {/* Notes */}
+
+
+ Notizen zur Stunde
+
+
+
+ {/* Next Steps */}
+
+
+ Naechste Schritte
+
+
+
+ {/* Save Button */}
+
+ {saved ? (
+ <>
+
+ Gespeichert!
+ >
+ ) : (
+ <>
+
+ Reflexion speichern
+ >
+ )}
+
+
+ {/* Previous Reflection Info */}
+ {reflection?.savedAt && (
+
+ Zuletzt gespeichert: {new Date(reflection.savedAt).toLocaleString('de-DE')}
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/companion/lesson-mode/VisualPieTimer.tsx b/admin-v2/components/companion/lesson-mode/VisualPieTimer.tsx
new file mode 100644
index 0000000..e8c27c9
--- /dev/null
+++ b/admin-v2/components/companion/lesson-mode/VisualPieTimer.tsx
@@ -0,0 +1,220 @@
+'use client'
+
+import { Pause, Play } from 'lucide-react'
+import { TimerColorStatus } from '@/lib/companion/types'
+import {
+ PIE_TIMER_RADIUS,
+ PIE_TIMER_CIRCUMFERENCE,
+ PIE_TIMER_STROKE_WIDTH,
+ PIE_TIMER_SIZE,
+ TIMER_COLOR_CLASSES,
+ TIMER_BG_COLORS,
+ formatTime,
+} from '@/lib/companion/constants'
+
+interface VisualPieTimerProps {
+ progress: number // 0-1 (how much time has elapsed)
+ remainingSeconds: number
+ totalSeconds: number
+ colorStatus: TimerColorStatus
+ isPaused: boolean
+ currentPhaseName: string
+ phaseColor: string
+ onTogglePause?: () => void
+ size?: 'sm' | 'md' | 'lg'
+}
+
+const sizeConfig = {
+ sm: { outer: 120, viewBox: 100, radius: 38, stroke: 6, fontSize: 'text-lg' },
+ md: { outer: 180, viewBox: 100, radius: 40, stroke: 7, fontSize: 'text-2xl' },
+ lg: { outer: 240, viewBox: 100, radius: 42, stroke: 8, fontSize: 'text-4xl' },
+}
+
+export function VisualPieTimer({
+ progress,
+ remainingSeconds,
+ totalSeconds,
+ colorStatus,
+ isPaused,
+ currentPhaseName,
+ phaseColor,
+ onTogglePause,
+ size = 'lg',
+}: VisualPieTimerProps) {
+ const config = sizeConfig[size]
+ const circumference = 2 * Math.PI * config.radius
+
+ // Calculate stroke-dashoffset for progress
+ // Progress goes from 0 (full) to 1 (empty), so offset decreases as time passes
+ const strokeDashoffset = circumference * (1 - progress)
+
+ // For overtime, show a pulsing full circle
+ const isOvertime = colorStatus === 'overtime'
+ const displayTime = formatTime(remainingSeconds)
+
+ // Get color classes based on status
+ const colorClasses = TIMER_COLOR_CLASSES[colorStatus]
+ const bgColorClass = TIMER_BG_COLORS[colorStatus]
+
+ return (
+
+ {/* Timer Circle */}
+
+
+ {/* Background circle */}
+
+
+ {/* Progress circle */}
+
+
+
+ {/* Center Content */}
+
+ {/* Time Display */}
+
+ {displayTime}
+
+
+ {/* Phase Name */}
+
+ {currentPhaseName}
+
+
+ {/* Paused Indicator */}
+ {isPaused && (
+
+
+ Pausiert
+
+ )}
+
+ {/* Overtime Badge */}
+ {isOvertime && (
+
+ +{Math.abs(Math.floor(remainingSeconds / 60))} Min
+
+ )}
+
+
+ {/* Pause/Play Button (overlay) */}
+ {onTogglePause && (
+
+ {isPaused ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {/* Status Text */}
+
+ {isOvertime ? (
+
+ Ueberzogen - Zeit fuer die naechste Phase!
+
+ ) : colorStatus === 'critical' ? (
+
+ Weniger als 2 Minuten verbleibend
+
+ ) : colorStatus === 'warning' ? (
+
+ Weniger als 5 Minuten verbleibend
+
+ ) : null}
+
+
+ )
+}
+
+/**
+ * Compact timer for header/toolbar
+ */
+export function CompactTimer({
+ remainingSeconds,
+ colorStatus,
+ isPaused,
+ phaseName,
+ phaseColor,
+}: {
+ remainingSeconds: number
+ colorStatus: TimerColorStatus
+ isPaused: boolean
+ phaseName: string
+ phaseColor: string
+}) {
+ const isOvertime = colorStatus === 'overtime'
+
+ return (
+
+ {/* Phase indicator */}
+
+
+ {/* Phase name */}
+
{phaseName}
+
+ {/* Time */}
+
+ {formatTime(remainingSeconds)}
+
+
+ {/* Paused badge */}
+ {isPaused && (
+
+ Pausiert
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/companion/modals/FeedbackModal.tsx b/admin-v2/components/companion/modals/FeedbackModal.tsx
new file mode 100644
index 0000000..31b72da
--- /dev/null
+++ b/admin-v2/components/companion/modals/FeedbackModal.tsx
@@ -0,0 +1,201 @@
+'use client'
+
+import { useState } from 'react'
+import { X, MessageSquare, Bug, Lightbulb, Send, CheckCircle } from 'lucide-react'
+import { FeedbackType } from '@/lib/companion/types'
+
+interface FeedbackModalProps {
+ isOpen: boolean
+ onClose: () => void
+ onSubmit: (type: FeedbackType, title: string, description: string) => Promise
+}
+
+const feedbackTypes: { id: FeedbackType; label: string; icon: typeof Bug; color: string }[] = [
+ { id: 'bug', label: 'Bug melden', icon: Bug, color: 'text-red-600 bg-red-50' },
+ { id: 'feature', label: 'Feature-Wunsch', icon: Lightbulb, color: 'text-amber-600 bg-amber-50' },
+ { id: 'feedback', label: 'Allgemeines Feedback', icon: MessageSquare, color: 'text-blue-600 bg-blue-50' },
+]
+
+export function FeedbackModal({ isOpen, onClose, onSubmit }: FeedbackModalProps) {
+ const [type, setType] = useState('feedback')
+ const [title, setTitle] = useState('')
+ const [description, setDescription] = useState('')
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+
+ if (!isOpen) return null
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!title.trim() || !description.trim()) return
+
+ setIsSubmitting(true)
+ try {
+ await onSubmit(type, title.trim(), description.trim())
+ setIsSuccess(true)
+ setTimeout(() => {
+ setIsSuccess(false)
+ setTitle('')
+ setDescription('')
+ setType('feedback')
+ onClose()
+ }, 2000)
+ } catch (error) {
+ console.error('Failed to submit feedback:', error)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Header */}
+
+
+
+
+
+
Feedback senden
+
+
+
+
+
+
+ {/* Success State */}
+ {isSuccess ? (
+
+
+
+
+
Vielen Dank!
+
Ihr Feedback wurde erfolgreich gesendet.
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/admin-v2/components/companion/modals/OnboardingModal.tsx b/admin-v2/components/companion/modals/OnboardingModal.tsx
new file mode 100644
index 0000000..a1c7af7
--- /dev/null
+++ b/admin-v2/components/companion/modals/OnboardingModal.tsx
@@ -0,0 +1,280 @@
+'use client'
+
+import { useState } from 'react'
+import { ChevronRight, ChevronLeft, Check, GraduationCap, Settings, Timer } from 'lucide-react'
+
+interface OnboardingModalProps {
+ isOpen: boolean
+ onClose: () => void
+ onComplete: (data: { state?: string; schoolType?: string }) => void
+}
+
+const STATES = [
+ 'Baden-Wuerttemberg',
+ 'Bayern',
+ 'Berlin',
+ 'Brandenburg',
+ 'Bremen',
+ 'Hamburg',
+ 'Hessen',
+ 'Mecklenburg-Vorpommern',
+ 'Niedersachsen',
+ 'Nordrhein-Westfalen',
+ 'Rheinland-Pfalz',
+ 'Saarland',
+ 'Sachsen',
+ 'Sachsen-Anhalt',
+ 'Schleswig-Holstein',
+ 'Thueringen',
+]
+
+const SCHOOL_TYPES = [
+ 'Grundschule',
+ 'Hauptschule',
+ 'Realschule',
+ 'Gymnasium',
+ 'Gesamtschule',
+ 'Berufsschule',
+ 'Foerderschule',
+ 'Andere',
+]
+
+interface Step {
+ id: number
+ title: string
+ description: string
+ icon: typeof GraduationCap
+}
+
+const steps: Step[] = [
+ {
+ id: 1,
+ title: 'Willkommen',
+ description: 'Der Companion hilft Ihnen bei der Unterrichtsplanung und -durchfuehrung.',
+ icon: GraduationCap,
+ },
+ {
+ id: 2,
+ title: 'Ihre Schule',
+ description: 'Waehlen Sie Ihr Bundesland und Ihre Schulform.',
+ icon: Settings,
+ },
+ {
+ id: 3,
+ title: 'Bereit!',
+ description: 'Sie koennen jetzt mit dem Lesson-Modus starten.',
+ icon: Timer,
+ },
+]
+
+export function OnboardingModal({ isOpen, onClose, onComplete }: OnboardingModalProps) {
+ const [currentStep, setCurrentStep] = useState(1)
+ const [selectedState, setSelectedState] = useState('')
+ const [selectedSchoolType, setSelectedSchoolType] = useState('')
+
+ if (!isOpen) return null
+
+ const canProceed = () => {
+ if (currentStep === 2) {
+ return selectedState !== '' && selectedSchoolType !== ''
+ }
+ return true
+ }
+
+ const handleNext = () => {
+ if (currentStep < 3) {
+ setCurrentStep(currentStep + 1)
+ } else {
+ onComplete({
+ state: selectedState,
+ schoolType: selectedSchoolType,
+ })
+ }
+ }
+
+ const handleBack = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1)
+ }
+ }
+
+ const currentStepData = steps[currentStep - 1]
+ const Icon = currentStepData.icon
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Progress Bar */}
+
+
+ {/* Content */}
+
+ {/* Step Indicator */}
+
+ {steps.map((step) => (
+
+ ))}
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Title & Description */}
+
+ {currentStepData.title}
+
+
+ {currentStepData.description}
+
+
+ {/* Step Content */}
+ {currentStep === 1 && (
+
+
+
+ Einstieg β Erarbeitung β Sicherung β Transfer β Reflexion
+
+
+ )}
+
+ {currentStep === 2 && (
+
+ {/* State Selection */}
+
+
+ Bundesland
+
+ setSelectedState(e.target.value)}
+ className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
+ >
+ Bitte waehlen...
+ {STATES.map((state) => (
+
+ {state}
+
+ ))}
+
+
+
+ {/* School Type Selection */}
+
+
+ Schulform
+
+ setSelectedSchoolType(e.target.value)}
+ className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
+ >
+ Bitte waehlen...
+ {SCHOOL_TYPES.map((type) => (
+
+ {type}
+
+ ))}
+
+
+
+ )}
+
+ {currentStep === 3 && (
+
+
+
+
+
+
+ Bundesland: {selectedState || 'Nicht angegeben'}
+
+
+ Schulform: {selectedSchoolType || 'Nicht angegeben'}
+
+
+
+ Sie koennen diese Einstellungen jederzeit aendern.
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+ {currentStep === 1 ? (
+ 'Ueberspringen'
+ ) : (
+ <>
+
+ Zurueck
+ >
+ )}
+
+
+ {currentStep === 3 ? (
+ <>
+
+ Fertig
+ >
+ ) : (
+ <>
+ Weiter
+
+ >
+ )}
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/companion/modals/SettingsModal.tsx b/admin-v2/components/companion/modals/SettingsModal.tsx
new file mode 100644
index 0000000..1dc0249
--- /dev/null
+++ b/admin-v2/components/companion/modals/SettingsModal.tsx
@@ -0,0 +1,248 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { X, Settings, Save, RotateCcw } from 'lucide-react'
+import { TeacherSettings, PhaseDurations } from '@/lib/companion/types'
+import {
+ DEFAULT_TEACHER_SETTINGS,
+ DEFAULT_PHASE_DURATIONS,
+ PHASE_ORDER,
+ PHASE_DISPLAY_NAMES,
+ PHASE_COLORS,
+ calculateTotalDuration,
+} from '@/lib/companion/constants'
+
+interface SettingsModalProps {
+ isOpen: boolean
+ onClose: () => void
+ settings: TeacherSettings
+ onSave: (settings: TeacherSettings) => void
+}
+
+export function SettingsModal({
+ isOpen,
+ onClose,
+ settings,
+ onSave,
+}: SettingsModalProps) {
+ const [localSettings, setLocalSettings] = useState(settings)
+ const [durations, setDurations] = useState(settings.defaultPhaseDurations)
+
+ useEffect(() => {
+ setLocalSettings(settings)
+ setDurations(settings.defaultPhaseDurations)
+ }, [settings])
+
+ if (!isOpen) return null
+
+ const totalDuration = calculateTotalDuration(durations)
+
+ const handleDurationChange = (phase: keyof PhaseDurations, value: number) => {
+ const newDurations = { ...durations, [phase]: Math.max(1, Math.min(60, value)) }
+ setDurations(newDurations)
+ }
+
+ const handleReset = () => {
+ setDurations(DEFAULT_PHASE_DURATIONS)
+ setLocalSettings(DEFAULT_TEACHER_SETTINGS)
+ }
+
+ const handleSave = () => {
+ const newSettings: TeacherSettings = {
+ ...localSettings,
+ defaultPhaseDurations: durations,
+ }
+ onSave(newSettings)
+ onClose()
+ }
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Header */}
+
+
+ {/* Content */}
+
+ {/* Phase Durations */}
+
+
+ Standard-Phasendauern (Minuten)
+
+
+ {PHASE_ORDER.map((phase) => (
+
+
+
+
+ {PHASE_DISPLAY_NAMES[phase]}
+
+
+
handleDurationChange(phase, parseInt(e.target.value) || 1)}
+ className="w-20 px-3 py-2 border border-slate-200 rounded-lg text-center focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ />
+
handleDurationChange(phase, parseInt(e.target.value))}
+ className="flex-1"
+ style={{
+ accentColor: PHASE_COLORS[phase].hex,
+ }}
+ />
+
+ ))}
+
+
+ Gesamtdauer:
+ {totalDuration} Minuten
+
+
+
+ {/* Other Settings */}
+
+
+ Weitere Einstellungen
+
+
+ {/* Auto Advance */}
+
+
+
+ Automatischer Phasenwechsel
+
+
+ Phasen automatisch wechseln wenn Zeit abgelaufen
+
+
+
+ setLocalSettings({ ...localSettings, autoAdvancePhases: e.target.checked })
+ }
+ className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
+ />
+
+
+ {/* Sound Notifications */}
+
+
+
+ Ton-Benachrichtigungen
+
+
+ Signalton bei Phasenende und Warnungen
+
+
+
+ setLocalSettings({ ...localSettings, soundNotifications: e.target.checked })
+ }
+ className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
+ />
+
+
+ {/* Keyboard Shortcuts */}
+
+
+
+ Tastaturkuerzel anzeigen
+
+
+ Hinweise zu Tastaturkuerzeln einblenden
+
+
+
+ setLocalSettings({ ...localSettings, showKeyboardShortcuts: e.target.checked })
+ }
+ className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
+ />
+
+
+ {/* High Contrast */}
+
+
+
+ Hoher Kontrast
+
+
+ Bessere Sichtbarkeit durch erhoehten Kontrast
+
+
+
+ setLocalSettings({ ...localSettings, highContrastMode: e.target.checked })
+ }
+ className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
+ />
+
+
+
+
+ {/* Footer */}
+
+
+
+ Zuruecksetzen
+
+
+
+ Abbrechen
+
+
+
+ Speichern
+
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/ocr/GridOverlay.tsx b/admin-v2/components/ocr/GridOverlay.tsx
index c1f20cc..0014bc9 100644
--- a/admin-v2/components/ocr/GridOverlay.tsx
+++ b/admin-v2/components/ocr/GridOverlay.tsx
@@ -4,11 +4,15 @@
* GridOverlay Component
*
* SVG overlay for displaying detected OCR grid structure on document images.
- * Shows recognized (green), problematic (orange), manual (blue), and empty (transparent) cells.
- * Supports click-to-edit for problematic cells.
+ * Features:
+ * - Cell status visualization (recognized/problematic/manual/empty)
+ * - 1mm grid overlay for A4 pages (210x297mm)
+ * - Text at original bounding-box positions
+ * - Editable text (contentEditable) at original positions
+ * - Click-to-edit for cells
*/
-import { useCallback } from 'react'
+import { useCallback, useState } from 'react'
import { cn } from '@/lib/utils'
export type CellStatus = 'empty' | 'recognized' | 'problematic' | 'manual'
@@ -24,6 +28,10 @@ export interface GridCell {
confidence: number
status: CellStatus
column_type?: 'english' | 'german' | 'example' | 'unknown'
+ x_mm?: number
+ y_mm?: number
+ width_mm?: number
+ height_mm?: number
}
export interface GridData {
@@ -42,57 +50,198 @@ export interface GridData {
total: number
coverage: number
}
+ page_dimensions?: {
+ width_mm: number
+ height_mm: number
+ format: string
+ }
+ source?: string
}
interface GridOverlayProps {
grid: GridData
imageUrl?: string
onCellClick?: (cell: GridCell) => void
+ onCellTextChange?: (cell: GridCell, newText: string) => void
selectedCell?: GridCell | null
showEmpty?: boolean
showLabels?: boolean
- showNumbers?: boolean // Show block numbers in cells
- highlightedBlockNumber?: number | null // Highlight specific block
+ showNumbers?: boolean
+ showTextLabels?: boolean
+ showMmGrid?: boolean
+ showTextAtPosition?: boolean
+ editableText?: boolean
+ highlightedBlockNumber?: number | null
className?: string
}
// Status colors
const STATUS_COLORS = {
recognized: {
- fill: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity
- stroke: '#22c55e', // green-500
+ fill: 'rgba(34, 197, 94, 0.2)',
+ stroke: '#22c55e',
hoverFill: 'rgba(34, 197, 94, 0.3)',
},
problematic: {
- fill: 'rgba(249, 115, 22, 0.3)', // orange-500 with opacity
- stroke: '#f97316', // orange-500
+ fill: 'rgba(249, 115, 22, 0.3)',
+ stroke: '#f97316',
hoverFill: 'rgba(249, 115, 22, 0.4)',
},
manual: {
- fill: 'rgba(59, 130, 246, 0.2)', // blue-500 with opacity
- stroke: '#3b82f6', // blue-500
+ fill: 'rgba(59, 130, 246, 0.2)',
+ stroke: '#3b82f6',
hoverFill: 'rgba(59, 130, 246, 0.3)',
},
empty: {
fill: 'transparent',
- stroke: 'rgba(148, 163, 184, 0.3)', // slate-400 with opacity
+ stroke: 'rgba(148, 163, 184, 0.3)',
hoverFill: 'rgba(148, 163, 184, 0.1)',
},
}
+// A4 dimensions for mm grid
+const A4_WIDTH_MM = 210
+const A4_HEIGHT_MM = 297
+
// Helper to calculate block number (1-indexed, row-by-row)
export function getCellBlockNumber(cell: GridCell, grid: GridData): number {
return cell.row * grid.columns + cell.col + 1
}
+/**
+ * 1mm Grid SVG Lines for A4 format.
+ * Renders inside a viewBox="0 0 100 100" (percentage-based).
+ */
+function MmGridLines() {
+ const lines: React.ReactNode[] = []
+
+ // Vertical lines: 210 lines for 210mm
+ for (let mm = 0; mm <= A4_WIDTH_MM; mm++) {
+ const x = (mm / A4_WIDTH_MM) * 100
+ const isCm = mm % 10 === 0
+ lines.push(
+
+ )
+ }
+
+ // Horizontal lines: 297 lines for 297mm
+ for (let mm = 0; mm <= A4_HEIGHT_MM; mm++) {
+ const y = (mm / A4_HEIGHT_MM) * 100
+ const isCm = mm % 10 === 0
+ lines.push(
+
+ )
+ }
+
+ return {lines}
+}
+
+/**
+ * Positioned text overlay using absolute-positioned HTML divs.
+ * Each cell's text appears at its bounding-box position with matching font size.
+ */
+function PositionedTextLayer({
+ cells,
+ editable,
+ onTextChange,
+}: {
+ cells: GridCell[]
+ editable: boolean
+ onTextChange?: (cell: GridCell, text: string) => void
+}) {
+ const [hoveredCell, setHoveredCell] = useState(null)
+
+ return (
+
+ {cells.map((cell) => {
+ if (cell.status === 'empty' || !cell.text) return null
+
+ const cellKey = `pos-${cell.row}-${cell.col}`
+ const isHovered = hoveredCell === cellKey
+ // Estimate font size from cell height: height_pct maps to roughly pt size
+ // A4 at 100% = 297mm height. Cell height in % * 297mm / 100 = height_mm
+ // Font size ~= height_mm * 2.2 (roughly matching print)
+ const heightMm = cell.height_mm ?? (cell.height / 100 * A4_HEIGHT_MM)
+ const fontSizePt = Math.max(6, Math.min(18, heightMm * 2.2))
+
+ return (
+
setHoveredCell(cellKey)}
+ onMouseLeave={() => setHoveredCell(null)}
+ >
+ {editable ? (
+ {
+ const newText = e.currentTarget.textContent ?? ''
+ if (newText !== cell.text && onTextChange) {
+ onTextChange(cell, newText)
+ }
+ }}
+ >
+ {cell.text}
+
+ ) : (
+ {cell.text}
+ )}
+
+ )
+ })}
+
+ )
+}
+
export function GridOverlay({
grid,
imageUrl,
onCellClick,
+ onCellTextChange,
selectedCell,
showEmpty = false,
showLabels = true,
showNumbers = false,
+ showTextLabels = false,
+ showMmGrid = false,
+ showTextAtPosition = false,
+ editableText = false,
highlightedBlockNumber,
className,
}: GridOverlayProps) {
@@ -125,6 +274,9 @@ export function GridOverlay({
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
+ {/* 1mm Grid */}
+ {showMmGrid && }
+
{/* Column type labels */}
{showLabels && grid.column_types.length > 0 && (
@@ -150,15 +302,14 @@ export function GridOverlay({
)}
- {/* Grid cells */}
- {flatCells.map((cell) => {
+ {/* Grid cells (skip if showing text at position to avoid double rendering) */}
+ {!showTextAtPosition && flatCells.map((cell) => {
const colors = STATUS_COLORS[cell.status]
const isSelected = selectedCell?.row === cell.row && selectedCell?.col === cell.col
const isClickable = cell.status !== 'empty' && onCellClick
const blockNumber = getCellBlockNumber(cell, grid)
const isHighlighted = highlightedBlockNumber === blockNumber
- // Skip empty cells if not showing them
if (!showEmpty && cell.status === 'empty') {
return null
}
@@ -170,7 +321,6 @@ export function GridOverlay({
onClick={() => handleCellClick(cell)}
className={isClickable ? 'cursor-pointer' : ''}
>
- {/* Cell rectangle */}
- {/* Block number badge */}
{showNumbers && cell.status !== 'empty' && (
<>
)}
- {/* Status indicator dot (only when not showing numbers) */}
- {!showNumbers && cell.status !== 'empty' && (
+ {!showNumbers && !showTextLabels && cell.status !== 'empty' && (
)}
- {/* Confidence indicator (for recognized cells) */}
+ {showTextLabels && (cell.status === 'recognized' || cell.status === 'manual') && cell.text && (
+
+ {cell.text.length > 15 ? cell.text.slice(0, 15) + '\u2026' : cell.text}
+
+ )}
+
{cell.status === 'recognized' && cell.confidence < 0.7 && (
)}
- {/* Selection highlight */}
{isSelected && (
{
+ if (cell.status === 'empty') return null
+ return (
+
+ )
+ })}
+
+ {/* Row boundaries */}
{grid.row_boundaries.map((y, idx) => (
))}
+
+ {/* Positioned text HTML overlay (outside SVG for proper text rendering) */}
+ {showTextAtPosition && (
+ c.status !== 'empty' && c.text)}
+ editable={editableText}
+ onTextChange={onCellTextChange}
+ />
+ )}
)
}
/**
* GridStats Component
- *
- * Displays statistics about the grid detection results.
*/
interface GridStatsProps {
stats: GridData['stats']
deskewAngle?: number
+ source?: string
className?: string
}
-export function GridStats({ stats, deskewAngle, className }: GridStatsProps) {
+export function GridStats({ stats, deskewAngle, source, className }: GridStatsProps) {
const coveragePercent = Math.round(stats.coverage * 100)
return (
@@ -326,6 +513,11 @@ export function GridStats({ stats, deskewAngle, className }: GridStatsProps) {
Begradigt: {deskewAngle.toFixed(1)}
)}
+ {source && (
+
+ Quelle: {source === 'tesseract+grid_service' ? 'Tesseract' : 'Vision LLM'}
+
+ )}
)
}
diff --git a/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx b/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx
new file mode 100644
index 0000000..a1586d5
--- /dev/null
+++ b/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx
@@ -0,0 +1,442 @@
+'use client'
+
+import { useState, useEffect, useRef, useCallback } from 'react'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+interface Message {
+ id: string
+ role: 'user' | 'agent'
+ content: string
+ timestamp: Date
+}
+
+interface ComplianceAdvisorWidgetProps {
+ currentStep?: string
+}
+
+// =============================================================================
+// EXAMPLE QUESTIONS BY STEP
+// =============================================================================
+
+const EXAMPLE_QUESTIONS: Record
= {
+ vvt: [
+ 'Was ist ein Verarbeitungsverzeichnis?',
+ 'Welche Informationen muss ich erfassen?',
+ 'Wie dokumentiere ich die Rechtsgrundlage?',
+ ],
+ 'compliance-scope': [
+ 'Was bedeutet L3?',
+ 'Wann brauche ich eine DSFA?',
+ 'Was ist der Unterschied zwischen L2 und L3?',
+ ],
+ tom: [
+ 'Was sind TOM?',
+ 'Welche Massnahmen sind erforderlich?',
+ 'Wie dokumentiere ich Verschluesselung?',
+ ],
+ dsfa: [
+ 'Was ist eine DSFA?',
+ 'Wann ist eine DSFA verpflichtend?',
+ 'Wie bewerte ich Risiken?',
+ ],
+ loeschfristen: [
+ 'Wie definiere ich Loeschfristen?',
+ 'Was ist der Unterschied zwischen Loeschpflicht und Aufbewahrungspflicht?',
+ 'Wann muss ich Daten loeschen?',
+ ],
+ default: [
+ 'Wie starte ich mit dem SDK?',
+ 'Was ist der erste Schritt?',
+ 'Welche Compliance-Anforderungen gelten fuer KI-Systeme?',
+ ],
+}
+
+// =============================================================================
+// COMPONENT
+// =============================================================================
+
+export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) {
+ const [isOpen, setIsOpen] = useState(false)
+ const [messages, setMessages] = useState([])
+ const [inputValue, setInputValue] = useState('')
+ const [isTyping, setIsTyping] = useState(false)
+ const messagesEndRef = useRef(null)
+ const abortControllerRef = useRef(null)
+
+ // Get example questions for current step
+ const exampleQuestions = EXAMPLE_QUESTIONS[currentStep] || EXAMPLE_QUESTIONS.default
+
+ // Auto-scroll to bottom when messages change
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }, [messages])
+
+ // Cleanup abort controller on unmount
+ useEffect(() => {
+ return () => {
+ abortControllerRef.current?.abort()
+ }
+ }, [])
+
+ // Handle send message with real LLM + RAG
+ const handleSendMessage = useCallback(
+ async (content: string) => {
+ if (!content.trim() || isTyping) return
+
+ const userMessage: Message = {
+ id: `msg-${Date.now()}`,
+ role: 'user',
+ content: content.trim(),
+ timestamp: new Date(),
+ }
+
+ setMessages((prev) => [...prev, userMessage])
+ setInputValue('')
+ setIsTyping(true)
+
+ const agentMessageId = `msg-${Date.now()}-agent`
+
+ // Create abort controller for this request
+ abortControllerRef.current = new AbortController()
+
+ try {
+ // Build conversation history for context
+ const history = messages.map((m) => ({
+ role: m.role === 'user' ? 'user' : 'assistant',
+ content: m.content,
+ }))
+
+ const response = await fetch('/api/sdk/compliance-advisor/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ message: content.trim(),
+ history,
+ currentStep,
+ }),
+ signal: abortControllerRef.current.signal,
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
+ throw new Error(errorData.error || `Server-Fehler (${response.status})`)
+ }
+
+ // Add empty agent message for streaming
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: agentMessageId,
+ role: 'agent',
+ content: '',
+ timestamp: new Date(),
+ },
+ ])
+
+ // Read streaming response
+ const reader = response.body!.getReader()
+ const decoder = new TextDecoder()
+ let accumulated = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ accumulated += decoder.decode(value, { stream: true })
+
+ // Update agent message with accumulated content
+ const currentText = accumulated
+ setMessages((prev) =>
+ prev.map((m) => (m.id === agentMessageId ? { ...m, content: currentText } : m))
+ )
+ }
+
+ setIsTyping(false)
+ } catch (error) {
+ if ((error as Error).name === 'AbortError') {
+ // User cancelled, keep partial response
+ setIsTyping(false)
+ return
+ }
+
+ const errorMessage =
+ error instanceof Error ? error.message : 'Verbindung fehlgeschlagen'
+
+ // Add or update agent message with error
+ setMessages((prev) => {
+ const hasAgent = prev.some((m) => m.id === agentMessageId)
+ if (hasAgent) {
+ return prev.map((m) =>
+ m.id === agentMessageId
+ ? { ...m, content: `Fehler: ${errorMessage}` }
+ : m
+ )
+ }
+ return [
+ ...prev,
+ {
+ id: agentMessageId,
+ role: 'agent' as const,
+ content: `Fehler: ${errorMessage}`,
+ timestamp: new Date(),
+ },
+ ]
+ })
+ setIsTyping(false)
+ }
+ },
+ [isTyping, messages, currentStep]
+ )
+
+ // Handle stop generation
+ const handleStopGeneration = useCallback(() => {
+ abortControllerRef.current?.abort()
+ setIsTyping(false)
+ }, [])
+
+ // Handle example question click
+ const handleExampleClick = (question: string) => {
+ handleSendMessage(question)
+ }
+
+ // Handle key press
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ handleSendMessage(inputValue)
+ }
+ }
+
+ if (!isOpen) {
+ return (
+ setIsOpen(true)}
+ className="fixed bottom-6 right-6 w-14 h-14 bg-indigo-600 hover:bg-indigo-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-110 z-50"
+ aria-label="Compliance Advisor oeffnen"
+ >
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Compliance Advisor
+
KI-gestuetzter Assistent
+
+
+
setIsOpen(false)}
+ className="text-white/80 hover:text-white transition-colors"
+ aria-label="Schliessen"
+ >
+
+
+
+
+
+
+ {/* Messages Area */}
+
+ {messages.length === 0 ? (
+
+
+
+ Willkommen beim Compliance Advisor
+
+
+ Stellen Sie Fragen zu DSGVO, KI-Verordnung und mehr.
+
+
+ {/* Example Questions */}
+
+
+ Beispielfragen:
+
+ {exampleQuestions.map((question, idx) => (
+
handleExampleClick(question)}
+ className="w-full text-left px-3 py-2 text-xs bg-white hover:bg-purple-50 border border-gray-200 rounded-lg transition-colors text-gray-700"
+ >
+ {question}
+
+ ))}
+
+
+ ) : (
+ <>
+ {messages.map((message) => (
+
+
+
+ {message.content || (message.role === 'agent' && isTyping ? '' : message.content)}
+
+
+ {message.timestamp.toLocaleTimeString('de-DE', {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+ ))}
+
+ {isTyping && (
+
+ )}
+
+
+ >
+ )}
+
+
+ {/* Input Area */}
+
+
+
setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Frage eingeben..."
+ disabled={isTyping}
+ className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50"
+ />
+ {isTyping ? (
+
+
+
+
+
+ ) : (
+
handleSendMessage(inputValue)}
+ disabled={!inputValue.trim()}
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/admin-v2/components/sdk/Sidebar/SDKSidebar.tsx b/admin-v2/components/sdk/Sidebar/SDKSidebar.tsx
index c4f2a31..5865e52 100644
--- a/admin-v2/components/sdk/Sidebar/SDKSidebar.tsx
+++ b/admin-v2/components/sdk/Sidebar/SDKSidebar.tsx
@@ -489,6 +489,30 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
isActive={pathname === '/sdk/security-backlog'}
collapsed={collapsed}
/>
+
+
+
+ }
+ label="Compliance Hub"
+ isActive={pathname === '/sdk/compliance-hub'}
+ collapsed={collapsed}
+ />
+
+
+
+ }
+ label="DSMS"
+ isActive={pathname === '/sdk/dsms'}
+ collapsed={collapsed}
+ />
diff --git a/admin-v2/components/sdk/StepHeader/StepHeader.tsx b/admin-v2/components/sdk/StepHeader/StepHeader.tsx
index 8f3490b..ef24084 100644
--- a/admin-v2/components/sdk/StepHeader/StepHeader.tsx
+++ b/admin-v2/components/sdk/StepHeader/StepHeader.tsx
@@ -3,7 +3,7 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
-import { useSDK, getStepById, getNextStep, getPreviousStep, SDKStep } from '@/lib/sdk'
+import { useSDK, getStepById, getNextStep, getPreviousStep, SDKStep, SDK_STEPS } from '@/lib/sdk'
// =============================================================================
// TYPES
@@ -133,7 +133,7 @@ export function StepHeader({
// Calculate step progress within phase
const phaseSteps = currentStep ?
- (currentStep.phase === 1 ? 8 : 11) : 0
+ SDK_STEPS.filter(s => s.phase === currentStep.phase).length : 0
const stepNumber = currentStep?.order || 0
return (
@@ -314,6 +314,28 @@ export const STEP_EXPLANATIONS = {
},
],
},
+ 'compliance-scope': {
+ title: 'Compliance Scope',
+ description: 'Umfang und Tiefe Ihrer Compliance-Dokumentation bestimmen',
+ explanation: 'Die Compliance Scope Engine bestimmt deterministisch, welche Dokumente Sie in welcher Tiefe benoetigen. Basierend auf 35 Fragen in 6 Bloecken werden Risiko-, Komplexitaets- und Assurance-Scores berechnet, die in ein 4-Level-Modell (L1 Lean bis L4 Zertifizierungsbereit) muenden.',
+ tips: [
+ {
+ icon: 'lightbulb' as const,
+ title: 'Deterministisch',
+ description: 'Alle Entscheidungen sind nachvollziehbar β keine KI, keine Black Box. Jede Einstufung wird mit Rechtsgrundlage und Audit-Trail begruendet.',
+ },
+ {
+ icon: 'info' as const,
+ title: '4-Level-Modell',
+ description: 'L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Hard Triggers (Art. 9, Minderjaehrige, Zertifizierungsziele) heben das Level automatisch an.',
+ },
+ {
+ icon: 'warning' as const,
+ title: 'Hard Triggers',
+ description: '50 deterministische Regeln pruefen besondere Kategorien (Art. 9), Minderjaehrige, KI-Einsatz, Drittlandtransfers und Zertifizierungsziele.',
+ },
+ ],
+ },
'use-case-assessment': {
title: 'Anwendungsfall-Erfassung',
description: 'Erfassen Sie Ihre KI-AnwendungsfΓ€lle systematisch',
@@ -487,34 +509,44 @@ export const STEP_EXPLANATIONS = {
'tom': {
title: 'Technische und Organisatorische Massnahmen',
description: 'Dokumentieren Sie Ihre TOMs nach Art. 32 DSGVO',
- explanation: 'TOMs sind konkrete Sicherheitsmassnahmen zum Schutz personenbezogener Daten. Sie umfassen Zutrittskontrolle, Zugangskontrolle, Zugriffskontrolle und mehr.',
+ explanation: 'TOMs sind konkrete Sicherheitsmassnahmen zum Schutz personenbezogener Daten. Das Dashboard zeigt den Status aller aus dem TOM Generator abgeleiteten Massnahmen mit SDM-Mapping und Gap-Analyse.',
tips: [
{
- icon: 'info' as const,
- title: 'Kategorien',
- description: 'TOMs werden in technische (z.B. Verschluesselung) und organisatorische (z.B. Schulungen) Massnahmen unterteilt.',
+ icon: 'warning' as const,
+ title: 'Nachweispflicht',
+ description: 'TOMs muessen nachweisbar real sein. Verknuepfen Sie Evidence-Dokumente (Policies, Zertifikate, Screenshots) mit jeder Massnahme, um die Rechenschaftspflicht (Art. 5 Abs. 2 DSGVO) zu erfuellen.',
},
{
- icon: 'success' as const,
- title: 'Nachweis',
- description: 'Dokumentieren Sie fuer jede TOM einen Nachweis der Umsetzung.',
+ icon: 'info' as const,
+ title: 'Generator nutzen',
+ description: 'Der 6-Schritt-Wizard leitet TOMs systematisch aus Ihrem Risikoprofil ab. Starten Sie dort, um eine vollstaendige Baseline zu erhalten.',
+ },
+ {
+ icon: 'info' as const,
+ title: 'SDM-Mapping',
+ description: 'Kontrollen werden den 7 SDM-Gewaehrleistungszielen zugeordnet: Verfuegbarkeit, Integritaet, Vertraulichkeit, Nichtverkettung, Intervenierbarkeit, Transparenz, Datenminimierung.',
},
],
},
'vvt': {
title: 'Verarbeitungsverzeichnis',
- description: 'Erstellen Sie Ihr Verzeichnis nach Art. 30 DSGVO',
- explanation: 'Das Verarbeitungsverzeichnis dokumentiert alle Verarbeitungstaetigkeiten mit personenbezogenen Daten. Es ist fuer die meisten Unternehmen Pflicht.',
+ description: 'Erstellen und verwalten Sie Ihr Verzeichnis nach Art. 30 DSGVO',
+ explanation: 'Das Verarbeitungsverzeichnis (VVT) dokumentiert alle Verarbeitungstaetigkeiten mit personenbezogenen Daten. Der integrierte Generator-Fragebogen befuellt 70-90% der Pflichtfelder automatisch anhand Ihres Unternehmensprofils.',
tips: [
{
icon: 'warning' as const,
- title: 'Vollstaendigkeit',
- description: 'Das VVT muss alle Verarbeitungen enthalten. Fehlende Eintraege koennen bei Audits zu Beanstandungen fuehren.',
+ title: 'Pflicht fuer alle',
+ description: 'Die Ausnahme fuer Unternehmen <250 Mitarbeiter greift nur bei gelegentlicher, risikoarmer Verarbeitung ohne besondere Kategorien (Art. 30 Abs. 5).',
},
{
icon: 'info' as const,
- title: 'Pflichtangaben',
- description: 'Jeder Eintrag muss enthalten: Zweck, Datenkategorien, Empfaenger, Loeschfristen, TOMs.',
+ title: 'Zweck-zuerst',
+ description: 'Definieren Sie Verarbeitungen nach Geschaeftszweck, nicht nach Tool. Ein Tool kann mehrere Verarbeitungen abdecken, eine Verarbeitung kann mehrere Tools nutzen.',
+ },
+ {
+ icon: 'info' as const,
+ title: 'Kein oeffentliches Dokument',
+ description: 'Das VVT ist ein internes Dokument. Es muss der Aufsichtsbehoerde nur auf Verlangen vorgelegt werden (Art. 30 Abs. 4).',
},
],
},
@@ -555,17 +587,22 @@ export const STEP_EXPLANATIONS = {
'loeschfristen': {
title: 'Loeschfristen',
description: 'Definieren Sie Aufbewahrungsrichtlinien fuer Ihre Daten',
- explanation: 'Loeschfristen legen fest, wie lange personenbezogene Daten gespeichert werden duerfen. Nach Ablauf muessen die Daten geloescht oder anonymisiert werden.',
+ explanation: 'Loeschfristen legen fest, wie lange personenbezogene Daten gespeichert werden duerfen. Die 3-Stufen-Logik (Zweckende, Aufbewahrungspflicht, Legal Hold) stellt sicher, dass alle gesetzlichen Anforderungen beruecksichtigt werden.',
tips: [
{
icon: 'warning' as const,
- title: 'Gesetzliche Fristen',
- description: 'Beachten Sie gesetzliche Aufbewahrungspflichten (z.B. Steuerrecht: 10 Jahre, Handelsrecht: 6 Jahre).',
+ title: '3-Stufen-Logik',
+ description: 'Jede Loeschfrist folgt einer 3-Stufen-Logik: 1. Zweckende (Daten werden nach Zweckwegfall geloescht), 2. Aufbewahrungspflicht (gesetzliche Fristen verhindern Loeschung), 3. Legal Hold (laufende Verfahren blockieren Loeschung).',
},
{
- icon: 'lightbulb' as const,
- title: 'Automatisierung',
- description: 'Richten Sie automatische Loeschprozesse ein, um Compliance sicherzustellen.',
+ icon: 'info' as const,
+ title: 'Deutsche Rechtsgrundlagen',
+ description: 'Der Generator kennt die wichtigsten Aufbewahrungstreiber: AO (10 J. Steuer), HGB (10/6 J. Handel), UStG (10 J. Rechnungen), BGB (3 J. Verjaehrung), ArbZG (2 J. Zeiterfassung), AGG (6 Mon. Bewerbungen).',
+ },
+ {
+ icon: 'info' as const,
+ title: 'Backup-Behandlung',
+ description: 'Auch Backups muessen ins Loeschkonzept einbezogen werden. Daten koennen nach primaerer Loeschung noch in Backup-Systemen existieren.',
},
],
},
@@ -659,6 +696,91 @@ export const STEP_EXPLANATIONS = {
},
],
},
+ 'source-policy': {
+ title: 'Source Policy',
+ description: 'Verwalten Sie Ihre Datenquellen-Governance',
+ explanation: 'Die Source Policy definiert, welche externen Datenquellen fuer Ihre Anwendung zugelassen sind. Sie umfasst eine Whitelist, Operationsmatrix (Lookup, RAG, Training, Export), PII-Regeln und ein Audit-Trail.',
+ tips: [
+ {
+ icon: 'warning' as const,
+ title: 'Lizenzierung',
+ description: 'Pruefen Sie die Lizenzen aller Datenquellen (DL-DE-BY, CC-BY, CC0). Nicht-lizenzierte Quellen koennen rechtliche Risiken bergen.',
+ },
+ {
+ icon: 'info' as const,
+ title: 'PII-Regeln',
+ description: 'Definieren Sie klare Regeln fuer den Umgang mit personenbezogenen Daten in externen Quellen.',
+ },
+ ],
+ },
+ 'audit-report': {
+ title: 'Audit Report',
+ description: 'Erstellen und verwalten Sie Audit-Sitzungen',
+ explanation: 'Im Audit Report erstellen Sie formelle Audit-Sitzungen mit Pruefer-Informationen, fuehren die Pruefung durch und generieren PDF-Reports. Jede Sitzung dokumentiert den Compliance-Stand zu einem bestimmten Zeitpunkt.',
+ tips: [
+ {
+ icon: 'lightbulb' as const,
+ title: 'Regelmaessigkeit',
+ description: 'Fuehren Sie mindestens jaehrlich ein formelles Audit durch. Dokumentieren Sie Abweichungen und Massnahmenplaene.',
+ },
+ {
+ icon: 'success' as const,
+ title: 'PDF-Export',
+ description: 'Generieren Sie PDF-Reports in Deutsch oder Englisch fuer externe Pruefer.',
+ },
+ ],
+ },
+ 'workflow': {
+ title: 'Document Workflow',
+ description: 'Verwalten Sie den Freigabe-Prozess Ihrer rechtlichen Dokumente',
+ explanation: 'Der Document Workflow bietet einen Split-View-Editor mit synchronisiertem Scrollen. Dokumente durchlaufen den Status Draft β Review β Approved β Published. Aenderungen werden versioniert und der Freigabeprozess wird protokolliert.',
+ tips: [
+ {
+ icon: 'warning' as const,
+ title: 'Vier-Augen-Prinzip',
+ description: 'Rechtliche Dokumente sollten immer von mindestens einer weiteren Person geprueft werden, bevor sie veroeffentlicht werden.',
+ },
+ {
+ icon: 'info' as const,
+ title: 'Versionierung',
+ description: 'Jede Aenderung wird als neue Version gespeichert. So koennen Sie jederzeit den Stand eines Dokuments nachvollziehen.',
+ },
+ ],
+ },
+ 'consent-management': {
+ title: 'Consent Verwaltung',
+ description: 'Verwalten Sie Consent-Dokumente, Versionen und DSGVO-Prozesse',
+ explanation: 'Die Consent Verwaltung umfasst das Lifecycle-Management Ihrer rechtlichen Dokumente (AGB, Datenschutz, Cookie-Richtlinien), die Verwaltung von E-Mail-Templates (16 Lifecycle-E-Mails) und die Steuerung der DSGVO-Prozesse (Art. 15-21).',
+ tips: [
+ {
+ icon: 'info' as const,
+ title: 'Dokumentversionen',
+ description: 'Jede Aenderung an einem Consent-Dokument erzeugt eine neue Version. Aktive Nutzer muessen bei Aenderungen erneut zustimmen.',
+ },
+ {
+ icon: 'warning' as const,
+ title: 'DSGVO-Fristen',
+ description: 'Betroffenenrechte (Art. 15-21) haben gesetzliche Fristen. Auskunft: 30 Tage, Loeschung: unverzueglich.',
+ },
+ ],
+ },
+ 'notfallplan': {
+ title: 'Notfallplan & Breach Response',
+ description: 'Verwalten Sie Ihr Datenpannen-Management nach Art. 33/34 DSGVO',
+ explanation: 'Der Notfallplan definiert Ihren Prozess bei Datenpannen gemaess Art. 33/34 DSGVO. Er umfasst die 72-Stunden-Meldepflicht an die Aufsichtsbehoerde, die Benachrichtigung betroffener Personen bei hohem Risiko, Incident-Klassifizierung, Eskalationswege und Dokumentationspflichten.',
+ tips: [
+ {
+ icon: 'warning' as const,
+ title: '72-Stunden-Frist',
+ description: 'Art. 33 DSGVO: Meldung an die Aufsichtsbehoerde innerhalb von 72 Stunden nach Bekanntwerden. Verspaetete Meldungen muessen begruendet werden.',
+ },
+ {
+ icon: 'info' as const,
+ title: 'Dokumentationspflicht',
+ description: 'Art. 33 Abs. 5: Alle Datenpannen muessen dokumentiert werden β auch solche, die nicht meldepflichtig sind. Die Dokumentation muss der Aufsichtsbehoerde auf Verlangen vorgelegt werden koennen.',
+ },
+ ],
+ },
}
export default StepHeader
diff --git a/admin-v2/components/sdk/compliance-scope/ScopeDecisionTab.tsx b/admin-v2/components/sdk/compliance-scope/ScopeDecisionTab.tsx
new file mode 100644
index 0000000..30dfe0c
--- /dev/null
+++ b/admin-v2/components/sdk/compliance-scope/ScopeDecisionTab.tsx
@@ -0,0 +1,362 @@
+'use client'
+import React, { useState } from 'react'
+import type { ScopeDecision, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
+import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
+
+interface ScopeDecisionTabProps {
+ decision: ScopeDecision | null
+}
+
+export function ScopeDecisionTab({ decision }: ScopeDecisionTabProps) {
+ const [expandedTrigger, setExpandedTrigger] = useState
(null)
+ const [showAuditTrail, setShowAuditTrail] = useState(false)
+
+ if (!decision) {
+ return (
+
+
+
Keine Entscheidung vorhanden
+
Bitte fΓΌhren Sie zuerst das Scope-Profiling durch.
+
+ )
+ }
+
+ const getScoreColor = (score: number): string => {
+ if (score >= 80) return 'from-red-500 to-red-600'
+ if (score >= 60) return 'from-orange-500 to-orange-600'
+ if (score >= 40) return 'from-yellow-500 to-yellow-600'
+ return 'from-green-500 to-green-600'
+ }
+
+ const getSeverityBadge = (severity: 'low' | 'medium' | 'high' | 'critical') => {
+ const colors = {
+ low: 'bg-gray-100 text-gray-800',
+ medium: 'bg-yellow-100 text-yellow-800',
+ high: 'bg-orange-100 text-orange-800',
+ critical: 'bg-red-100 text-red-800',
+ }
+ const labels = {
+ low: 'Niedrig',
+ medium: 'Mittel',
+ high: 'Hoch',
+ critical: 'Kritisch',
+ }
+ return (
+
+ {labels[severity]}
+
+ )
+ }
+
+ const renderScoreBar = (label: string, score: number | undefined) => {
+ const value = score ?? 0
+ return (
+
+
+ {label}
+ {value}/100
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Level Determination */}
+
+
+
+
+ {decision.level}
+
+
+
+
+ {DEPTH_LEVEL_LABELS[decision.level]}
+
+
{DEPTH_LEVEL_DESCRIPTIONS[decision.level]}
+ {decision.reasoning && (
+
{decision.reasoning}
+ )}
+
+
+
+
+ {/* Score Breakdown */}
+ {decision.scores && (
+
+
Score-Analyse
+
+ {renderScoreBar('Risiko-Score', decision.scores.riskScore)}
+ {renderScoreBar('KomplexitΓ€ts-Score', decision.scores.complexityScore)}
+ {renderScoreBar('Assurance-Score', decision.scores.assuranceScore)}
+
+ {renderScoreBar('Gesamt-Score', decision.scores.compositeScore)}
+
+
+
+ )}
+
+ {/* Hard Triggers */}
+ {decision.hardTriggers && decision.hardTriggers.length > 0 && (
+
+
Hard-Trigger
+
+ {decision.hardTriggers.map((trigger, idx) => (
+
+
setExpandedTrigger(expandedTrigger === idx ? null : idx)}
+ className="w-full px-4 py-3 flex items-center justify-between hover:bg-opacity-80 transition-colors"
+ >
+
+ {trigger.matched && (
+
+
+
+ )}
+
{trigger.label}
+
+
+
+
+
+ {expandedTrigger === idx && (
+
+
{trigger.description}
+ {trigger.legalReference && (
+
+ Rechtsgrundlage: {trigger.legalReference}
+
+ )}
+ {trigger.matchedValue && (
+
+ Erfasster Wert: {trigger.matchedValue}
+
+ )}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Required Documents */}
+ {decision.requiredDocuments && decision.requiredDocuments.length > 0 && (
+
+
Erforderliche Dokumente
+
+
+
+
+ Typ
+ Tiefe
+ Aufwand
+ Status
+ Aktion
+
+
+
+ {decision.requiredDocuments.map((doc, idx) => (
+
+
+
+
+ {DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType}
+
+ {doc.isMandatory && (
+
+ Pflicht
+
+ )}
+
+
+ {doc.depthDescription}
+
+ {doc.effortEstimate ? `${doc.effortEstimate.days} Tage` : '-'}
+
+
+ {doc.triggeredByHardTrigger && (
+
+ Hard-Trigger
+
+ )}
+
+
+ {doc.sdkStepUrl && (
+
+ Zum SDK-Schritt β
+
+ )}
+
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Risk Flags */}
+ {decision.riskFlags && decision.riskFlags.length > 0 && (
+
+
Risiko-Flags
+
+ {decision.riskFlags.map((flag, idx) => (
+
+
+
{flag.title}
+ {getSeverityBadge(flag.severity)}
+
+
{flag.description}
+
+ Empfehlung: {flag.recommendation}
+
+
+ ))}
+
+
+ )}
+
+ {/* Gap Analysis */}
+ {decision.gapAnalysis && decision.gapAnalysis.length > 0 && (
+
+
Gap-Analyse
+
+ {decision.gapAnalysis.map((gap, idx) => (
+
+
+
{gap.title}
+ {getSeverityBadge(gap.severity)}
+
+
{gap.description}
+
+ Empfehlung: {gap.recommendation}
+
+ {gap.relatedDocuments && gap.relatedDocuments.length > 0 && (
+
+ Betroffene Dokumente:
+ {gap.relatedDocuments.map((doc, docIdx) => (
+
+ {DOCUMENT_TYPE_LABELS[doc] || doc}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Next Actions */}
+ {decision.nextActions && decision.nextActions.length > 0 && (
+
+
NΓ€chste Schritte
+
+ {decision.nextActions.map((action, idx) => (
+
+
+ {action.priority}
+
+
+
{action.title}
+
{action.description}
+
+ {action.effortDays && (
+
+ Aufwand: {action.effortDays} Tage
+
+ )}
+ {action.relatedDocuments && action.relatedDocuments.length > 0 && (
+
+ Dokumente: {action.relatedDocuments.length}
+
+ )}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Audit Trail */}
+ {decision.auditTrail && decision.auditTrail.length > 0 && (
+
+
setShowAuditTrail(!showAuditTrail)}
+ className="w-full flex items-center justify-between mb-4"
+ >
+ Audit-Trail
+
+
+
+
+ {showAuditTrail && (
+
+ {decision.auditTrail.map((entry, idx) => (
+
+
{entry.step}
+
{entry.description}
+ {entry.details && entry.details.length > 0 && (
+
+ {entry.details.map((detail, detailIdx) => (
+ β’ {detail}
+ ))}
+
+ )}
+
+ ))}
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/sdk/compliance-scope/ScopeExportTab.tsx b/admin-v2/components/sdk/compliance-scope/ScopeExportTab.tsx
new file mode 100644
index 0000000..7dab44f
--- /dev/null
+++ b/admin-v2/components/sdk/compliance-scope/ScopeExportTab.tsx
@@ -0,0 +1,334 @@
+'use client'
+import React, { useState, useCallback } from 'react'
+import type { ScopeDecision, ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types'
+import { DEPTH_LEVEL_LABELS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
+
+interface ScopeExportTabProps {
+ decision: ScopeDecision | null
+ answers: ScopeProfilingAnswer[]
+}
+
+export function ScopeExportTab({ decision, answers }: ScopeExportTabProps) {
+ const [copiedMarkdown, setCopiedMarkdown] = useState(false)
+
+ const handleDownloadJSON = useCallback(() => {
+ if (!decision) return
+ const dataStr = JSON.stringify(decision, null, 2)
+ const dataBlob = new Blob([dataStr], { type: 'application/json' })
+ const url = URL.createObjectURL(dataBlob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = `compliance-scope-decision-${new Date().toISOString().split('T')[0]}.json`
+ link.click()
+ URL.revokeObjectURL(url)
+ }, [decision])
+
+ const handleDownloadCSV = useCallback(() => {
+ if (!decision || !decision.requiredDocuments) return
+
+ const headers = ['Typ', 'Tiefe', 'Aufwand (Tage)', 'Pflicht', 'Hard-Trigger']
+ const rows = decision.requiredDocuments.map((doc) => [
+ DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType,
+ doc.depthDescription,
+ doc.effortEstimate?.days?.toString() || '0',
+ doc.isMandatory ? 'Ja' : 'Nein',
+ doc.triggeredByHardTrigger ? 'Ja' : 'Nein',
+ ])
+
+ const csvContent = [headers, ...rows].map((row) => row.map((cell) => `"${cell}"`).join(',')).join('\n')
+
+ const dataBlob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
+ const url = URL.createObjectURL(dataBlob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = `compliance-scope-documents-${new Date().toISOString().split('T')[0]}.csv`
+ link.click()
+ URL.revokeObjectURL(url)
+ }, [decision])
+
+ const generateMarkdownSummary = useCallback(() => {
+ if (!decision) return ''
+
+ let markdown = `# Compliance Scope Entscheidung\n\n`
+ markdown += `**Datum:** ${new Date().toLocaleDateString('de-DE')}\n\n`
+ markdown += `## Einstufung\n\n`
+ markdown += `**Level:** ${decision.level} - ${DEPTH_LEVEL_LABELS[decision.level]}\n\n`
+ if (decision.reasoning) {
+ markdown += `**BegrΓΌndung:** ${decision.reasoning}\n\n`
+ }
+
+ if (decision.scores) {
+ markdown += `## Scores\n\n`
+ markdown += `- **Risiko-Score:** ${decision.scores.riskScore}/100\n`
+ markdown += `- **KomplexitΓ€ts-Score:** ${decision.scores.complexityScore}/100\n`
+ markdown += `- **Assurance-Score:** ${decision.scores.assuranceScore}/100\n`
+ markdown += `- **Gesamt-Score:** ${decision.scores.compositeScore}/100\n\n`
+ }
+
+ if (decision.hardTriggers && decision.hardTriggers.length > 0) {
+ const matchedTriggers = decision.hardTriggers.filter((ht) => ht.matched)
+ if (matchedTriggers.length > 0) {
+ markdown += `## Aktive Hard-Trigger\n\n`
+ matchedTriggers.forEach((trigger) => {
+ markdown += `- **${trigger.label}**\n`
+ markdown += ` - ${trigger.description}\n`
+ if (trigger.legalReference) {
+ markdown += ` - Rechtsgrundlage: ${trigger.legalReference}\n`
+ }
+ })
+ markdown += `\n`
+ }
+ }
+
+ if (decision.requiredDocuments && decision.requiredDocuments.length > 0) {
+ markdown += `## Erforderliche Dokumente\n\n`
+ markdown += `| Typ | Tiefe | Aufwand | Pflicht | Hard-Trigger |\n`
+ markdown += `|-----|-------|---------|---------|-------------|\n`
+ decision.requiredDocuments.forEach((doc) => {
+ markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.depthDescription} | ${
+ doc.effortEstimate?.days || 0
+ } Tage | ${doc.isMandatory ? 'Ja' : 'Nein'} | ${doc.triggeredByHardTrigger ? 'Ja' : 'Nein'} |\n`
+ })
+ markdown += `\n`
+ }
+
+ if (decision.riskFlags && decision.riskFlags.length > 0) {
+ markdown += `## Risiko-Flags\n\n`
+ decision.riskFlags.forEach((flag) => {
+ markdown += `### ${flag.title} (${flag.severity})\n\n`
+ markdown += `${flag.description}\n\n`
+ markdown += `**Empfehlung:** ${flag.recommendation}\n\n`
+ })
+ }
+
+ if (decision.nextActions && decision.nextActions.length > 0) {
+ markdown += `## NΓ€chste Schritte\n\n`
+ decision.nextActions.forEach((action) => {
+ markdown += `${action.priority}. **${action.title}**\n`
+ markdown += ` ${action.description}\n`
+ if (action.effortDays) {
+ markdown += ` Aufwand: ${action.effortDays} Tage\n`
+ }
+ markdown += `\n`
+ })
+ }
+
+ return markdown
+ }, [decision])
+
+ const handleCopyMarkdown = useCallback(() => {
+ const markdown = generateMarkdownSummary()
+ navigator.clipboard.writeText(markdown).then(() => {
+ setCopiedMarkdown(true)
+ setTimeout(() => setCopiedMarkdown(false), 2000)
+ })
+ }, [generateMarkdownSummary])
+
+ const handlePrintView = useCallback(() => {
+ if (!decision) return
+
+ const markdown = generateMarkdownSummary()
+ const htmlContent = `
+
+
+
+
+ Compliance Scope Entscheidung
+
+
+
+ ${markdown}
+
+
+ `
+ const printWindow = window.open('', '_blank')
+ if (printWindow) {
+ printWindow.document.write(htmlContent)
+ printWindow.document.close()
+ printWindow.focus()
+ setTimeout(() => printWindow.print(), 250)
+ }
+ }, [decision, generateMarkdownSummary])
+
+ if (!decision) {
+ return (
+
+
+
Keine Daten zum Export
+
Bitte fΓΌhren Sie zuerst das Scope-Profiling durch.
+
+ )
+ }
+
+ return (
+
+ {/* JSON Export */}
+
+
+
+
+
+ Exportieren Sie die vollstΓ€ndige Entscheidung als strukturierte JSON-Datei fΓΌr weitere Verarbeitung oder
+ Archivierung.
+
+
+ JSON herunterladen
+
+
+
+
+
+ {/* CSV Export */}
+
+
+
+
+
+ Exportieren Sie die Liste der erforderlichen Dokumente als CSV-Datei fΓΌr Excel, Google Sheets oder andere
+ Tabellenkalkulationen.
+
+
+ CSV herunterladen
+
+
+
+
+
+ {/* Markdown Summary */}
+
+
+
+
+
+
Markdown-Zusammenfassung
+
+
+ Strukturierte Zusammenfassung im Markdown-Format fΓΌr Dokumentation oder Berichte.
+
+
+
+ {copiedMarkdown ? 'Kopiert!' : 'Kopieren'}
+
+
+
+ {/* Print View */}
+
+
+
+
+
+ Γffnen Sie eine druckfreundliche HTML-Ansicht der Entscheidung in einem neuen Fenster.
+
+
+ Druckansicht ΓΆffnen
+
+
+
+
+
+ {/* Export Info */}
+
+
+
+
+
+
+
Export-Hinweise
+
+ β’ JSON-Exporte enthalten alle Daten und kΓΆnnen wieder importiert werden
+ β’ CSV-Exporte sind ideal fΓΌr Tabellenkalkulation und AufwandsschΓ€tzungen
+ β’ Markdown eignet sich fΓΌr Dokumentation und Berichte
+ β’ Die Druckansicht ist optimiert fΓΌr PDF-Export ΓΌber den Browser
+
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/sdk/compliance-scope/ScopeOverviewTab.tsx b/admin-v2/components/sdk/compliance-scope/ScopeOverviewTab.tsx
new file mode 100644
index 0000000..f7621ac
--- /dev/null
+++ b/admin-v2/components/sdk/compliance-scope/ScopeOverviewTab.tsx
@@ -0,0 +1,267 @@
+'use client'
+import React from 'react'
+import type { ComplianceScopeState, ScopeDecision, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
+import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
+
+interface ScopeOverviewTabProps {
+ scopeState: ComplianceScopeState
+ onStartProfiling: () => void
+ onRefreshDecision: () => void
+}
+
+export function ScopeOverviewTab({ scopeState, onStartProfiling, onRefreshDecision }: ScopeOverviewTabProps) {
+ const { decision, answers } = scopeState
+ const hasAnswers = answers && answers.length > 0
+
+ const getScoreColor = (score: number): string => {
+ if (score >= 80) return 'from-red-500 to-red-600'
+ if (score >= 60) return 'from-orange-500 to-orange-600'
+ if (score >= 40) return 'from-yellow-500 to-yellow-600'
+ return 'from-green-500 to-green-600'
+ }
+
+ const getScoreColorBg = (score: number): string => {
+ if (score >= 80) return 'bg-red-100'
+ if (score >= 60) return 'bg-orange-100'
+ if (score >= 40) return 'bg-yellow-100'
+ return 'bg-green-100'
+ }
+
+ const renderScoreGauge = (label: string, score: number | undefined) => {
+ const value = score ?? 0
+ return (
+
+
+ {label}
+ {value}/100
+
+
+
+ )
+ }
+
+ const renderLevelBadge = () => {
+ if (!decision?.level) {
+ return (
+
+
+ ?
+
+
Noch nicht bewertet
+
+ FΓΌhren Sie das Scope-Profiling durch, um Ihre Compliance-Tiefe zu bestimmen.
+
+
+ )
+ }
+
+ const levelColors = DEPTH_LEVEL_COLORS[decision.level]
+ return (
+
+
+ {decision.level}
+
+
+ {DEPTH_LEVEL_LABELS[decision.level]}
+
+
{DEPTH_LEVEL_DESCRIPTIONS[decision.level]}
+
+ )
+ }
+
+ const renderActiveHardTriggers = () => {
+ if (!decision?.hardTriggers || decision.hardTriggers.length === 0) {
+ return null
+ }
+
+ const activeHardTriggers = decision.hardTriggers.filter((ht) => ht.matched)
+
+ if (activeHardTriggers.length === 0) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
Aktive Hard-Trigger
+
+
+ {activeHardTriggers.map((trigger, idx) => (
+
+
+
+
{trigger.label}
+
{trigger.description}
+ {trigger.legalReference && (
+
+ Rechtsgrundlage: {trigger.legalReference}
+
+ )}
+ {trigger.matchedValue && (
+
+ Erfasster Wert: {trigger.matchedValue}
+
+ )}
+
+
+
+ ))}
+
+
+ )
+ }
+
+ const renderDocumentSummary = () => {
+ if (!decision?.requiredDocuments) {
+ return null
+ }
+
+ const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.isMandatory)
+ const optionalDocs = decision.requiredDocuments.filter((doc) => !doc.isMandatory)
+ const totalEffortDays = decision.requiredDocuments.reduce(
+ (sum, doc) => sum + (doc.effortEstimate?.days ?? 0),
+ 0
+ )
+
+ return (
+
+
Dokumenten-Γbersicht
+
+
+
{mandatoryDocs.length}
+
Pflichtdokumente
+
+
+
{optionalDocs.length}
+
Optional
+
+
+
{totalEffortDays}
+
Tage Aufwand (geschΓ€tzt)
+
+
+
+ )
+ }
+
+ const renderRiskFlagsSummary = () => {
+ if (!decision?.riskFlags || decision.riskFlags.length === 0) {
+ return null
+ }
+
+ const critical = decision.riskFlags.filter((rf) => rf.severity === 'critical').length
+ const high = decision.riskFlags.filter((rf) => rf.severity === 'high').length
+ const medium = decision.riskFlags.filter((rf) => rf.severity === 'medium').length
+
+ return (
+
+
+
+ {critical > 0 && (
+
+
+ Kritisch
+
+ {critical}
+
+ )}
+ {high > 0 && (
+
+
+ Hoch
+
+ {high}
+
+ )}
+ {medium > 0 && (
+
+
+ Mittel
+
+ {medium}
+
+ )}
+
+
+ )
+ }
+
+ return (
+
+ {/* Level Badge */}
+ {renderLevelBadge()}
+
+ {/* Scores Section */}
+ {decision && (
+
+
Score-Γbersicht
+
+ {renderScoreGauge('Risiko-Score', decision.scores?.riskScore)}
+ {renderScoreGauge('KomplexitΓ€ts-Score', decision.scores?.complexityScore)}
+ {renderScoreGauge('Assurance-Score', decision.scores?.assuranceScore)}
+
+ {renderScoreGauge('Gesamt-Score', decision.scores?.compositeScore)}
+
+
+
+ )}
+
+ {/* Active Hard Triggers */}
+ {renderActiveHardTriggers()}
+
+ {/* Document Summary */}
+ {renderDocumentSummary()}
+
+ {/* Risk Flags Summary */}
+ {renderRiskFlagsSummary()}
+
+ {/* CTA Section */}
+
+
+
+
+ {!hasAnswers ? 'Bereit fΓΌr das Scope-Profiling?' : 'Ergebnis aktualisieren'}
+
+
+ {!hasAnswers
+ ? 'Beantworten Sie einige Fragen zu Ihrem Unternehmen und erhalten Sie eine prΓ€zise Compliance-Bewertung.'
+ : 'Haben sich Ihre Unternehmensparameter geΓ€ndert? Aktualisieren Sie Ihre Bewertung.'}
+
+
+
+ {!hasAnswers ? 'Scope-Profiling starten' : 'Ergebnis aktualisieren'}
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/sdk/compliance-scope/ScopeWizardTab.tsx b/admin-v2/components/sdk/compliance-scope/ScopeWizardTab.tsx
new file mode 100644
index 0000000..f34c420
--- /dev/null
+++ b/admin-v2/components/sdk/compliance-scope/ScopeWizardTab.tsx
@@ -0,0 +1,410 @@
+'use client'
+import React, { useState, useCallback } from 'react'
+import type { ScopeProfilingAnswer, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
+import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, getAllQuestions } from '@/lib/sdk/compliance-scope-profiling'
+import type { CompanyProfile } from '@/lib/sdk/types'
+import { prefillFromCompanyProfile } from '@/lib/sdk/compliance-scope-profiling'
+import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_COLORS } from '@/lib/sdk/compliance-scope-types'
+
+interface ScopeWizardTabProps {
+ answers: ScopeProfilingAnswer[]
+ onAnswersChange: (answers: ScopeProfilingAnswer[]) => void
+ onComplete: () => void
+ companyProfile: CompanyProfile | null
+ currentLevel: ComplianceDepthLevel | null
+}
+
+export function ScopeWizardTab({
+ answers,
+ onAnswersChange,
+ onComplete,
+ companyProfile,
+ currentLevel,
+}: ScopeWizardTabProps) {
+ const [currentBlockIndex, setCurrentBlockIndex] = useState(0)
+ const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex]
+ const totalProgress = getTotalProgress(answers)
+
+ const handleAnswerChange = useCallback(
+ (questionId: string, value: any) => {
+ const existingIndex = answers.findIndex((a) => a.questionId === questionId)
+ if (existingIndex >= 0) {
+ const newAnswers = [...answers]
+ newAnswers[existingIndex] = { questionId, value, answeredAt: new Date().toISOString() }
+ onAnswersChange(newAnswers)
+ } else {
+ onAnswersChange([...answers, { questionId, value, answeredAt: new Date().toISOString() }])
+ }
+ },
+ [answers, onAnswersChange]
+ )
+
+ const handlePrefillFromProfile = useCallback(() => {
+ if (!companyProfile) return
+ const prefilledAnswers = prefillFromCompanyProfile(companyProfile, answers)
+ onAnswersChange(prefilledAnswers)
+ }, [companyProfile, answers, onAnswersChange])
+
+ const handleNext = useCallback(() => {
+ if (currentBlockIndex < SCOPE_QUESTION_BLOCKS.length - 1) {
+ setCurrentBlockIndex(currentBlockIndex + 1)
+ } else {
+ onComplete()
+ }
+ }, [currentBlockIndex, onComplete])
+
+ const handleBack = useCallback(() => {
+ if (currentBlockIndex > 0) {
+ setCurrentBlockIndex(currentBlockIndex - 1)
+ }
+ }, [currentBlockIndex])
+
+ const renderQuestion = (question: any) => {
+ const currentValue = getAnswerValue(answers, question.id)
+
+ switch (question.type) {
+ case 'boolean':
+ return (
+
+
+
+ {question.question}
+ {question.required && * }
+
+ {question.helpText && (
+
+
+
+
+
+ )}
+
+
+ handleAnswerChange(question.id, true)}
+ className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
+ currentValue === true
+ ? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
+ : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
+ }`}
+ >
+ Ja
+
+ handleAnswerChange(question.id, false)}
+ className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
+ currentValue === false
+ ? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
+ : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
+ }`}
+ >
+ Nein
+
+
+
+ )
+
+ case 'single':
+ return (
+
+
+ {question.question}
+ {question.required && * }
+ {question.helpText && (
+
+
+
+
+
+ )}
+
+
+ {question.options?.map((option: any) => (
+ handleAnswerChange(question.id, option.value)}
+ className={`w-full text-left py-3 px-4 rounded-lg border-2 transition-all ${
+ currentValue === option.value
+ ? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
+ : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
+ }`}
+ >
+ {option.label}
+
+ ))}
+
+
+ )
+
+ case 'multi':
+ return (
+
+
+ {question.question}
+ {question.required && * }
+ {question.helpText && (
+
+
+
+
+
+ )}
+
+
+ {question.options?.map((option: any) => {
+ const selectedValues = Array.isArray(currentValue) ? currentValue : []
+ const isChecked = selectedValues.includes(option.value)
+ return (
+
+ {
+ const newValues = e.target.checked
+ ? [...selectedValues, option.value]
+ : selectedValues.filter((v) => v !== option.value)
+ handleAnswerChange(question.id, newValues)
+ }}
+ className="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
+ />
+
+ {option.label}
+
+
+ )
+ })}
+
+
+ )
+
+ case 'number':
+ return (
+
+
+ {question.question}
+ {question.required && * }
+ {question.helpText && (
+
+
+
+
+
+ )}
+
+
handleAnswerChange(question.id, parseInt(e.target.value, 10))}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ placeholder="Zahl eingeben"
+ />
+
+ )
+
+ case 'text':
+ return (
+
+
+ {question.question}
+ {question.required && * }
+ {question.helpText && (
+
+
+
+
+
+ )}
+
+
handleAnswerChange(question.id, e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ placeholder="Text eingeben"
+ />
+
+ )
+
+ default:
+ return null
+ }
+ }
+
+ return (
+
+ {/* Left Sidebar - Block Navigation */}
+
+
+
Fortschritt
+
+ {SCOPE_QUESTION_BLOCKS.map((block, idx) => {
+ const progress = getBlockProgress(answers, block.id)
+ const isActive = idx === currentBlockIndex
+ return (
+
setCurrentBlockIndex(idx)}
+ className={`w-full text-left px-3 py-2 rounded-lg transition-all ${
+ isActive
+ ? 'bg-purple-50 border-2 border-purple-500'
+ : 'bg-gray-50 border border-gray-200 hover:border-gray-300'
+ }`}
+ >
+
+
+ {block.title}
+
+
+ {progress}%
+
+
+
+
+ )
+ })}
+
+
+
+
+ {/* Main Content Area */}
+
+ {/* Progress Bar */}
+
+
+
Gesamtfortschritt
+
+ {currentLevel && (
+
+ VorlΓ€ufige Einstufung:
+
+ {currentLevel} - {DEPTH_LEVEL_LABELS[currentLevel]}
+
+
+ )}
+
{totalProgress}%
+
+
+
+
+
+ {/* Current Block */}
+
+
+
+
{currentBlock.title}
+
{currentBlock.description}
+
+ {companyProfile && (
+
+ Aus Unternehmensprofil ΓΌbernehmen
+
+ )}
+
+
+ {/* Questions */}
+
+ {currentBlock.questions.map((question) => (
+
+ {renderQuestion(question)}
+
+ ))}
+
+
+
+ {/* Navigation Buttons */}
+
+
+ ZurΓΌck
+
+
+ Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}
+
+
+ {currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? 'Auswertung starten' : 'Weiter'}
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/sdk/compliance-scope/index.ts b/admin-v2/components/sdk/compliance-scope/index.ts
new file mode 100644
index 0000000..991345f
--- /dev/null
+++ b/admin-v2/components/sdk/compliance-scope/index.ts
@@ -0,0 +1,4 @@
+export { ScopeOverviewTab } from './ScopeOverviewTab'
+export { ScopeWizardTab } from './ScopeWizardTab'
+export { ScopeDecisionTab } from './ScopeDecisionTab'
+export { ScopeExportTab } from './ScopeExportTab'
diff --git a/admin-v2/components/sdk/dsfa/SourceAttribution.tsx b/admin-v2/components/sdk/dsfa/SourceAttribution.tsx
new file mode 100644
index 0000000..8ba4295
--- /dev/null
+++ b/admin-v2/components/sdk/dsfa/SourceAttribution.tsx
@@ -0,0 +1,320 @@
+'use client'
+
+import { useState } from 'react'
+import { BookOpen, ExternalLink, Scale, ChevronDown, ChevronUp, Info } from 'lucide-react'
+import {
+ DSFALicenseCode,
+ DSFA_LICENSE_LABELS,
+ SourceAttributionProps
+} from '@/lib/sdk/types'
+
+/**
+ * Get license badge color based on license type
+ */
+function getLicenseBadgeColor(licenseCode: DSFALicenseCode): string {
+ switch (licenseCode) {
+ case 'DL-DE-BY-2.0':
+ case 'DL-DE-ZERO-2.0':
+ return 'bg-blue-100 text-blue-700 border-blue-200'
+ case 'CC-BY-4.0':
+ return 'bg-green-100 text-green-700 border-green-200'
+ case 'EDPB-LICENSE':
+ return 'bg-purple-100 text-purple-700 border-purple-200'
+ case 'PUBLIC_DOMAIN':
+ return 'bg-gray-100 text-gray-700 border-gray-200'
+ case 'PROPRIETARY':
+ return 'bg-amber-100 text-amber-700 border-amber-200'
+ default:
+ return 'bg-slate-100 text-slate-700 border-slate-200'
+ }
+}
+
+/**
+ * Get license URL based on license code
+ */
+function getLicenseUrl(licenseCode: DSFALicenseCode): string | null {
+ switch (licenseCode) {
+ case 'DL-DE-BY-2.0':
+ return 'https://www.govdata.de/dl-de/by-2-0'
+ case 'DL-DE-ZERO-2.0':
+ return 'https://www.govdata.de/dl-de/zero-2-0'
+ case 'CC-BY-4.0':
+ return 'https://creativecommons.org/licenses/by/4.0/'
+ case 'EDPB-LICENSE':
+ return 'https://edpb.europa.eu/about-edpb/legal-notice_en'
+ default:
+ return null
+ }
+}
+
+/**
+ * License badge component
+ */
+function LicenseBadge({ licenseCode }: { licenseCode: DSFALicenseCode }) {
+ const licenseUrl = getLicenseUrl(licenseCode)
+ const colorClass = getLicenseBadgeColor(licenseCode)
+ const label = DSFA_LICENSE_LABELS[licenseCode] || licenseCode
+
+ if (licenseUrl) {
+ return (
+
+
+ {label}
+
+
+ )
+ }
+
+ return (
+
+
+ {label}
+
+ )
+}
+
+/**
+ * Single source item in the attribution list
+ */
+function SourceItem({
+ source,
+ index,
+ showScore
+}: {
+ source: SourceAttributionProps['sources'][0]
+ index: number
+ showScore: boolean
+}) {
+ return (
+
+
+
+ {index + 1}.
+
+
+
+ {source.sourceUrl ? (
+
+ {source.sourceName}
+
+ ) : (
+
+ {source.sourceName}
+
+ )}
+ {showScore && source.score !== undefined && (
+
+ ({(source.score * 100).toFixed(0)}%)
+
+ )}
+
+
+ {source.attributionText}
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Compact source badge for inline display
+ */
+function CompactSourceBadge({
+ source
+}: {
+ source: SourceAttributionProps['sources'][0]
+}) {
+ return (
+
+
+ {source.sourceCode}
+
+ )
+}
+
+/**
+ * SourceAttribution component - displays source/license information for DSFA RAG results
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function SourceAttribution({
+ sources,
+ compact = false,
+ showScores = false
+}: SourceAttributionProps) {
+ const [isExpanded, setIsExpanded] = useState(!compact)
+
+ if (!sources || sources.length === 0) {
+ return null
+ }
+
+ // Compact mode - just show badges
+ if (compact && !isExpanded) {
+ return (
+
+ setIsExpanded(true)}
+ className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-700"
+ >
+
+ Quellen ({sources.length})
+
+
+ {sources.slice(0, 3).map((source, i) => (
+
+ ))}
+ {sources.length > 3 && (
+ +{sources.length - 3}
+ )}
+
+ )
+ }
+
+ return (
+
+
+
+
+ Quellen & Lizenzen
+
+ {compact && (
+ setIsExpanded(false)}
+ className="text-xs text-slate-400 hover:text-slate-600 flex items-center gap-1"
+ >
+ Einklappen
+
+
+ )}
+
+
+
+ {sources.map((source, i) => (
+
+ ))}
+
+
+ {/* Aggregated license notice */}
+ {sources.length > 1 && (
+
+
+ Hinweis: Die angezeigten Inhalte stammen aus {sources.length} verschiedenen Quellen
+ mit unterschiedlichen Lizenzen. Bitte beachten Sie die jeweiligen Attributionsanforderungen.
+
+
+ )}
+
+ )
+}
+
+/**
+ * Inline source reference for use within text
+ */
+export function InlineSourceRef({
+ sourceCode,
+ sourceName,
+ sourceUrl
+}: {
+ sourceCode: string
+ sourceName: string
+ sourceUrl?: string
+}) {
+ if (sourceUrl) {
+ return (
+
+ [{sourceCode}]
+
+
+ )
+ }
+
+ return (
+
+ [{sourceCode}]
+
+ )
+}
+
+/**
+ * Attribution footer for generated documents
+ */
+export function AttributionFooter({
+ sources,
+ generatedAt
+}: {
+ sources: SourceAttributionProps['sources']
+ generatedAt?: Date
+}) {
+ if (!sources || sources.length === 0) {
+ return null
+ }
+
+ // Group by license
+ const byLicense = sources.reduce((acc, source) => {
+ const key = source.licenseCode
+ if (!acc[key]) acc[key] = []
+ acc[key].push(source)
+ return acc
+ }, {} as Record)
+
+ return (
+
+ )
+}
+
+export default SourceAttribution
diff --git a/admin-v2/components/sdk/dsfa/ThresholdAnalysisSection.tsx b/admin-v2/components/sdk/dsfa/ThresholdAnalysisSection.tsx
index 1551c2c..7027e1d 100644
--- a/admin-v2/components/sdk/dsfa/ThresholdAnalysisSection.tsx
+++ b/admin-v2/components/sdk/dsfa/ThresholdAnalysisSection.tsx
@@ -201,6 +201,62 @@ export function ThresholdAnalysisSection({ dsfa, onUpdate, isSubmitting }: Thres
{wp248Result.reason}
+
+ {/* Annex-Trigger: Empfehlung bei >= 2 WP248 Kriterien */}
+ {wp248Selected.length >= 2 && (
+
+
+
+
+
Annex mit separater Risikobewertung empfohlen
+
+ Bei {wp248Selected.length} erfuellten WP248-Kriterien wird ein Annex mit detaillierter Risikobewertung empfohlen.
+
+
+
Vorgeschlagene Annex-Scopes basierend auf Ihren Kriterien:
+
+ {wp248Selected.includes('scoring_profiling') && (
+ - Annex: Profiling & Scoring β Detailanalyse der Bewertungslogik
+ )}
+ {wp248Selected.includes('automated_decision') && (
+ - Annex: Automatisierte Einzelentscheidung β Art. 22 Pruefung
+ )}
+ {wp248Selected.includes('systematic_monitoring') && (
+ - Annex: Systematische Ueberwachung β Verhaeltnismaessigkeitspruefung
+ )}
+ {wp248Selected.includes('sensitive_data') && (
+ - Annex: Besondere Datenkategorien β Schutzbedarfsanalyse Art. 9
+ )}
+ {wp248Selected.includes('large_scale') && (
+ - Annex: Umfangsanalyse β Quantitative Bewertung der Verarbeitung
+ )}
+ {wp248Selected.includes('matching_combining') && (
+ - Annex: Datenzusammenfuehrung β Zweckbindungspruefung
+ )}
+ {wp248Selected.includes('vulnerable_subjects') && (
+ - Annex: Schutzbeduerftige Betroffene β Verstaerkte Schutzmassnahmen
+ )}
+ {wp248Selected.includes('innovative_technology') && (
+ - Annex: Innovative Technologie β Technikfolgenabschaetzung
+ )}
+ {wp248Selected.includes('preventing_rights') && (
+ - Annex: Rechteausuebung β Barrierefreiheit der Betroffenenrechte
+ )}
+
+
+ {aiTriggersSelected.length > 0 && (
+
+ + KI-Trigger aktiv: Zusaetzlicher Annex fuer KI-Risikobewertung empfohlen (AI Act Konformitaet).
+
+ )}
+
+
+
+ )}
{/* Step 2: Art. 35 Abs. 3 Cases */}
diff --git a/admin-v2/components/sdk/dsfa/index.ts b/admin-v2/components/sdk/dsfa/index.ts
index e93cee2..0550924 100644
--- a/admin-v2/components/sdk/dsfa/index.ts
+++ b/admin-v2/components/sdk/dsfa/index.ts
@@ -11,6 +11,7 @@ export { DSFASidebar } from './DSFASidebar'
export { StakeholderConsultationSection } from './StakeholderConsultationSection'
export { Art36Warning } from './Art36Warning'
export { ReviewScheduleSection } from './ReviewScheduleSection'
+export { SourceAttribution, InlineSourceRef, AttributionFooter } from './SourceAttribution'
// =============================================================================
// DSFA Card Component
diff --git a/admin-v2/components/sdk/tom-dashboard/TOMEditorTab.tsx b/admin-v2/components/sdk/tom-dashboard/TOMEditorTab.tsx
new file mode 100644
index 0000000..aa42e53
--- /dev/null
+++ b/admin-v2/components/sdk/tom-dashboard/TOMEditorTab.tsx
@@ -0,0 +1,376 @@
+'use client'
+
+import { useMemo, useState, useEffect } from 'react'
+import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
+import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
+
+interface TOMEditorTabProps {
+ state: TOMGeneratorState
+ selectedTOMId: string | null
+ onUpdateTOM: (tomId: string, updates: Partial) => void
+ onBack: () => void
+}
+
+const STATUS_OPTIONS: { value: DerivedTOM['implementationStatus']; label: string; className: string }[] = [
+ { value: 'IMPLEMENTED', label: 'Implementiert', className: 'border-green-300 bg-green-50 text-green-700' },
+ { value: 'PARTIAL', label: 'Teilweise implementiert', className: 'border-yellow-300 bg-yellow-50 text-yellow-700' },
+ { value: 'NOT_IMPLEMENTED', label: 'Nicht implementiert', className: 'border-red-300 bg-red-50 text-red-700' },
+]
+
+const TYPE_BADGES: Record = {
+ TECHNICAL: { label: 'Technisch', className: 'bg-blue-100 text-blue-700' },
+ ORGANIZATIONAL: { label: 'Organisatorisch', className: 'bg-indigo-100 text-indigo-700' },
+}
+
+interface VVTActivity {
+ id: string
+ name?: string
+ title?: string
+ structuredToms?: { category?: string }[]
+}
+
+export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOMEditorTabProps) {
+ const tom = useMemo(() => {
+ if (!selectedTOMId) return null
+ return state.derivedTOMs.find(t => t.id === selectedTOMId) || null
+ }, [state.derivedTOMs, selectedTOMId])
+
+ const control = useMemo(() => {
+ if (!tom) return null
+ return getControlById(tom.controlId)
+ }, [tom])
+
+ const [implementationStatus, setImplementationStatus] = useState('NOT_IMPLEMENTED')
+ const [responsiblePerson, setResponsiblePerson] = useState('')
+ const [implementationDate, setImplementationDate] = useState('')
+ const [notes, setNotes] = useState('')
+ const [linkedEvidence, setLinkedEvidence] = useState([])
+ const [selectedEvidenceId, setSelectedEvidenceId] = useState('')
+
+ useEffect(() => {
+ if (tom) {
+ setImplementationStatus(tom.implementationStatus)
+ setResponsiblePerson(tom.responsiblePerson || '')
+ setImplementationDate(tom.implementationDate ? new Date(tom.implementationDate).toISOString().slice(0, 10) : '')
+ setNotes(tom.aiGeneratedDescription || '')
+ setLinkedEvidence(tom.linkedEvidence || [])
+ }
+ }, [tom])
+
+ const vvtActivities = useMemo(() => {
+ if (!control) return []
+ try {
+ const raw = localStorage.getItem('bp_vvt')
+ if (!raw) return []
+ const activities: VVTActivity[] = JSON.parse(raw)
+ return activities.filter(a =>
+ a.structuredToms?.some(t => t.category === control.category)
+ )
+ } catch {
+ return []
+ }
+ }, [control])
+
+ const availableDocuments = useMemo(() => {
+ return (state.documents || []).filter(
+ doc => !linkedEvidence.includes(doc.id)
+ )
+ }, [state.documents, linkedEvidence])
+
+ const linkedDocuments = useMemo(() => {
+ return linkedEvidence
+ .map(id => (state.documents || []).find(d => d.id === id))
+ .filter(Boolean)
+ }, [state.documents, linkedEvidence])
+
+ const evidenceGaps = useMemo(() => {
+ if (!control?.evidenceRequirements) return []
+ return control.evidenceRequirements.map(req => {
+ const hasMatch = (state.documents || []).some(doc =>
+ linkedEvidence.includes(doc.id) &&
+ (doc.filename?.toLowerCase().includes(req.toLowerCase()) ||
+ doc.documentType?.toLowerCase().includes(req.toLowerCase()))
+ )
+ return { requirement: req, fulfilled: hasMatch }
+ })
+ }, [control, state.documents, linkedEvidence])
+
+ const handleSave = () => {
+ if (!tom) return
+ onUpdateTOM(tom.id, {
+ implementationStatus,
+ responsiblePerson: responsiblePerson || null,
+ implementationDate: implementationDate ? new Date(implementationDate) : null,
+ aiGeneratedDescription: notes || null,
+ linkedEvidence,
+ })
+ }
+
+ const handleAddEvidence = () => {
+ if (!selectedEvidenceId) return
+ setLinkedEvidence(prev => [...prev, selectedEvidenceId])
+ setSelectedEvidenceId('')
+ }
+
+ const handleRemoveEvidence = (docId: string) => {
+ setLinkedEvidence(prev => prev.filter(id => id !== docId))
+ }
+
+ if (!selectedTOMId || !tom) {
+ return (
+
+
+
Keine TOM ausgewaehlt
+
Waehlen Sie eine TOM aus der Uebersicht, um sie zu bearbeiten.
+
+ )
+ }
+
+ const typeBadge = TYPE_BADGES[control?.type || 'TECHNICAL'] || TYPE_BADGES.TECHNICAL
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Zurueck zur Uebersicht
+
+
+ Aenderungen speichern
+
+
+
+ {/* TOM Header Card */}
+
+
+ {control?.code || tom.controlId}
+
+ {typeBadge.label}
+
+
+ {control?.category || 'Unbekannt'}
+
+
+
{control?.name?.de || tom.controlId}
+ {control?.description?.de && (
+
{control.description.de}
+ )}
+
+
+ {/* Implementation Status */}
+
+
Implementierungsstatus
+
+ {STATUS_OPTIONS.map(opt => (
+
+ setImplementationStatus(opt.value)}
+ className="sr-only"
+ />
+
+ {implementationStatus === opt.value && (
+
+ )}
+
+ {opt.label}
+
+ ))}
+
+
+
+ {/* Responsible Person */}
+
+
Verantwortliche Person
+
+
+
+ {/* Notes */}
+
+
Anmerkungen
+ setNotes(e.target.value)}
+ rows={4}
+ placeholder="Anmerkungen zur Umsetzung, Besonderheiten, etc."
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-y"
+ />
+
+
+ {/* Evidence Section */}
+
+
Nachweisdokumente
+
+ {linkedDocuments.length > 0 ? (
+
+ {linkedDocuments.map(doc => doc && (
+
+
+
+
+
+
{doc.originalName || doc.filename || doc.id}
+
+
handleRemoveEvidence(doc.id)}
+ className="text-red-500 hover:text-red-700 text-xs font-medium"
+ >
+ Entfernen
+
+
+ ))}
+
+ ) : (
+
Keine Nachweisdokumente verknuepft.
+ )}
+
+ {availableDocuments.length > 0 && (
+
+
+ Dokument hinzufuegen
+ setSelectedEvidenceId(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Dokument auswaehlen...
+ {availableDocuments.map(doc => (
+ {doc.originalName || doc.filename || doc.id}
+ ))}
+
+
+
+ Hinzufuegen
+
+
+ )}
+
+
+ {/* Evidence Gaps */}
+ {evidenceGaps.length > 0 && (
+
+
Nachweis-Anforderungen
+
+ {evidenceGaps.map((gap, idx) => (
+
+
+ {gap.fulfilled ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {gap.requirement}
+
+
+ ))}
+
+
+ )}
+
+ {/* VVT Cross-References */}
+ {vvtActivities.length > 0 && (
+
+
VVT-Querverweise
+
+ {vvtActivities.map(activity => (
+
+
+
+
+
{activity.name || activity.title || activity.id}
+
+ ))}
+
+
+ )}
+
+ {/* Framework Mappings */}
+ {control?.mappings && control.mappings.length > 0 && (
+
+
Framework-Zuordnungen
+
+ {control.mappings.map((mapping, idx) => (
+
+ {mapping.framework}
+ {mapping.reference}
+
+ ))}
+
+
+ )}
+
+ {/* Bottom Save */}
+
+
+ Zurueck zur Uebersicht
+
+
+ Aenderungen speichern
+
+
+
+ )
+}
diff --git a/admin-v2/components/sdk/tom-dashboard/TOMGapExportTab.tsx b/admin-v2/components/sdk/tom-dashboard/TOMGapExportTab.tsx
new file mode 100644
index 0000000..9925a1d
--- /dev/null
+++ b/admin-v2/components/sdk/tom-dashboard/TOMGapExportTab.tsx
@@ -0,0 +1,328 @@
+'use client'
+
+import { useMemo } from 'react'
+import { TOMGeneratorState, GapAnalysisResult, DerivedTOM } from '@/lib/sdk/tom-generator/types'
+import { getControlById, getAllControls } from '@/lib/sdk/tom-generator/controls/loader'
+import {
+ SDM_GOAL_LABELS,
+ SDM_GOAL_DESCRIPTIONS,
+ getSDMCoverageStats,
+ MODULE_LABELS,
+ getModuleCoverageStats,
+ SDMGewaehrleistungsziel,
+ TOMModuleCategory,
+} from '@/lib/sdk/tom-generator/sdm-mapping'
+
+interface TOMGapExportTabProps {
+ state: TOMGeneratorState
+ onRunGapAnalysis: () => void
+}
+
+function getScoreColor(score: number): string {
+ if (score >= 75) return 'text-green-600'
+ if (score >= 50) return 'text-yellow-600'
+ return 'text-red-600'
+}
+
+function getScoreBgColor(score: number): string {
+ if (score >= 75) return 'bg-green-50 border-green-200'
+ if (score >= 50) return 'bg-yellow-50 border-yellow-200'
+ return 'bg-red-50 border-red-200'
+}
+
+function getBarColor(score: number): string {
+ if (score >= 75) return 'bg-green-500'
+ if (score >= 50) return 'bg-yellow-500'
+ return 'bg-red-500'
+}
+
+function downloadJSON(data: unknown, filename: string) {
+ const json = JSON.stringify(data, null, 2)
+ const blob = new Blob([json], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+}
+
+export function TOMGapExportTab({ state, onRunGapAnalysis }: TOMGapExportTabProps) {
+ const gap = state.gapAnalysis as GapAnalysisResult | null | undefined
+
+ const sdmGoals = useMemo(() => {
+ const goals = Object.keys(SDM_GOAL_LABELS) as SDMGewaehrleistungsziel[]
+ const allStats = getSDMCoverageStats(state.derivedTOMs)
+ return goals.map(key => {
+ const stats = allStats[key] || { total: 0, implemented: 0, partial: 0, missing: 0 }
+ const total = stats.total || 1
+ const percent = Math.round((stats.implemented / total) * 100)
+ return {
+ key,
+ label: SDM_GOAL_LABELS[key],
+ description: SDM_GOAL_DESCRIPTIONS[key],
+ stats,
+ percent,
+ }
+ })
+ }, [state.derivedTOMs])
+
+ const modules = useMemo(() => {
+ const moduleKeys = Object.keys(MODULE_LABELS) as TOMModuleCategory[]
+ const allStats = getModuleCoverageStats(state.derivedTOMs)
+ return moduleKeys.map(key => {
+ const stats = allStats[key] || { total: 0, implemented: 0 }
+ const total = stats.total || 1
+ const percent = Math.round((stats.implemented / total) * 100)
+ return {
+ key,
+ label: MODULE_LABELS[key],
+ stats: { ...stats, partial: 0, missing: total - stats.implemented },
+ percent,
+ }
+ })
+ }, [state.derivedTOMs])
+
+ const handleExportTOMs = () => {
+ downloadJSON(state.derivedTOMs, `tom-export-${new Date().toISOString().slice(0, 10)}.json`)
+ }
+
+ const handleExportGap = () => {
+ if (!gap) return
+ downloadJSON(gap, `gap-analyse-${new Date().toISOString().slice(0, 10)}.json`)
+ }
+
+ return (
+
+ {/* Gap Analysis */}
+
+
+
Gap-Analyse
+
+ Analyse ausfuehren
+
+
+
+ {gap ? (
+
+ {/* Score Gauge */}
+
+
+
+ {gap.overallScore}
+
+
von 100 Punkten
+
+
+
+ {/* Missing Controls */}
+ {gap.missingControls && gap.missingControls.length > 0 && (
+
+
+ Fehlende Kontrollen ({gap.missingControls.length})
+
+
+ {gap.missingControls.map((mc, idx) => {
+ const control = getControlById(mc.controlId)
+ return (
+
+ {control?.code || mc.controlId}
+ {control?.name?.de || mc.controlId}
+ {mc.reason && {mc.reason} }
+
+ )
+ })}
+
+
+ )}
+
+ {/* Partial Controls */}
+ {gap.partialControls && gap.partialControls.length > 0 && (
+
+
+ Teilweise implementierte Kontrollen ({gap.partialControls.length})
+
+
+ {gap.partialControls.map((pc, idx) => {
+ const control = getControlById(pc.controlId)
+ return (
+
+ {control?.code || pc.controlId}
+ {control?.name?.de || pc.controlId}
+
+ )
+ })}
+
+
+ )}
+
+ {/* Missing Evidence */}
+ {gap.missingEvidence && gap.missingEvidence.length > 0 && (
+
+
+ Fehlende Nachweise ({gap.missingEvidence.length})
+
+
+ {gap.missingEvidence.map((item, idx) => {
+ const control = getControlById(item.controlId)
+ return (
+
+
+
+
+
+ {control?.name?.de || item.controlId}: {item.requiredEvidence.join(', ')}
+
+
+ )
+ })}
+
+
+ )}
+
+ {/* Recommendations */}
+ {gap.recommendations && gap.recommendations.length > 0 && (
+
+
+ Empfehlungen ({gap.recommendations.length})
+
+
+ {gap.recommendations.map((rec, idx) => (
+
+
+
+
+
+ {typeof rec === 'string' ? rec : (rec as { text?: string; message?: string }).text || (rec as { text?: string; message?: string }).message || JSON.stringify(rec)}
+
+
+ ))}
+
+
+ )}
+
+ ) : (
+
+
+
+
+
Fuehren Sie die Gap-Analyse aus, um Luecken in Ihren TOMs zu identifizieren.
+
+ )}
+
+
+ {/* SDM Gewaehrleistungsziele */}
+
+
SDM Gewaehrleistungsziele
+
+ {sdmGoals.map(goal => (
+
+
+
+ {goal.label}
+ {goal.description && (
+ {goal.description}
+ )}
+
+
+ {goal.stats.implemented}/{goal.stats.total} implementiert
+ {goal.stats.partial > 0 && ` | ${goal.stats.partial} teilweise`}
+ {goal.stats.missing > 0 && ` | ${goal.stats.missing} fehlend`}
+
+
+
+
+ ))}
+
+
+
+ {/* Module Coverage */}
+
+
Modul-Abdeckung
+
+ {modules.map(mod => (
+
+
{mod.label}
+
+
+ {mod.percent}%
+
+
+ ({mod.stats.implemented}/{mod.stats.total})
+
+
+
+ {mod.stats.partial > 0 && (
+
{mod.stats.partial} teilweise
+ )}
+ {mod.stats.missing > 0 && (
+
{mod.stats.missing} fehlend
+ )}
+
+ ))}
+
+
+
+ {/* Export Section */}
+
+
Export
+
+
+
+
+
+ JSON Export
+ Alle TOMs als JSON
+
+
+
+
+
+
+ Gap-Analyse Export
+ Analyseergebnis als JSON
+
+
+
+
+
+
+
Vollstaendiger Export (ZIP)
+
+ Nutzen Sie den TOM Generator fuer den vollstaendigen Export mit DOCX/PDF
+
+
+
+
+
+ )
+}
diff --git a/admin-v2/components/sdk/tom-dashboard/TOMOverviewTab.tsx b/admin-v2/components/sdk/tom-dashboard/TOMOverviewTab.tsx
new file mode 100644
index 0000000..06d0008
--- /dev/null
+++ b/admin-v2/components/sdk/tom-dashboard/TOMOverviewTab.tsx
@@ -0,0 +1,267 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
+import { getControlById, getControlsByCategory, getAllCategories } from '@/lib/sdk/tom-generator/controls/loader'
+import { SDM_GOAL_LABELS, getSDMCoverageStats, SDMGewaehrleistungsziel } from '@/lib/sdk/tom-generator/sdm-mapping'
+
+interface TOMOverviewTabProps {
+ state: TOMGeneratorState
+ onSelectTOM: (tomId: string) => void
+ onStartGenerator: () => void
+}
+
+const STATUS_BADGES: Record = {
+ IMPLEMENTED: { label: 'Implementiert', className: 'bg-green-100 text-green-700' },
+ PARTIAL: { label: 'Teilweise', className: 'bg-yellow-100 text-yellow-700' },
+ NOT_IMPLEMENTED: { label: 'Fehlend', className: 'bg-red-100 text-red-700' },
+}
+
+const TYPE_BADGES: Record = {
+ TECHNICAL: { label: 'Technisch', className: 'bg-blue-100 text-blue-700' },
+ ORGANIZATIONAL: { label: 'Organisatorisch', className: 'bg-indigo-100 text-indigo-700' },
+}
+
+const SCHUTZZIELE: { key: SDMGewaehrleistungsziel; label: string }[] = [
+ { key: 'Vertraulichkeit', label: 'Vertraulichkeit' },
+ { key: 'Integritaet', label: 'Integritaet' },
+ { key: 'Verfuegbarkeit', label: 'Verfuegbarkeit' },
+ { key: 'Nichtverkettung', label: 'Nichtverkettung' },
+]
+
+export function TOMOverviewTab({ state, onSelectTOM, onStartGenerator }: TOMOverviewTabProps) {
+ const [categoryFilter, setCategoryFilter] = useState('ALL')
+ const [typeFilter, setTypeFilter] = useState('ALL')
+ const [statusFilter, setStatusFilter] = useState('ALL')
+ const [applicabilityFilter, setApplicabilityFilter] = useState('ALL')
+
+ const categories = useMemo(() => getAllCategories(), [])
+
+ const stats = useMemo(() => {
+ const toms = state.derivedTOMs
+ return {
+ total: toms.length,
+ implemented: toms.filter(t => t.implementationStatus === 'IMPLEMENTED').length,
+ partial: toms.filter(t => t.implementationStatus === 'PARTIAL').length,
+ missing: toms.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length,
+ }
+ }, [state.derivedTOMs])
+
+ const sdmStats = useMemo(() => {
+ const allStats = getSDMCoverageStats(state.derivedTOMs)
+ return SCHUTZZIELE.map(sz => ({
+ ...sz,
+ stats: allStats[sz.key] || { total: 0, implemented: 0, partial: 0, missing: 0 },
+ }))
+ }, [state.derivedTOMs])
+
+ const filteredTOMs = useMemo(() => {
+ let toms = state.derivedTOMs
+
+ if (categoryFilter !== 'ALL') {
+ const categoryControlIds = getControlsByCategory(categoryFilter).map(c => c.id)
+ toms = toms.filter(t => categoryControlIds.includes(t.controlId))
+ }
+
+ if (typeFilter !== 'ALL') {
+ toms = toms.filter(t => {
+ const ctrl = getControlById(t.controlId)
+ return ctrl?.type === typeFilter
+ })
+ }
+
+ if (statusFilter !== 'ALL') {
+ toms = toms.filter(t => t.implementationStatus === statusFilter)
+ }
+
+ if (applicabilityFilter !== 'ALL') {
+ toms = toms.filter(t => t.applicability === applicabilityFilter)
+ }
+
+ return toms
+ }, [state.derivedTOMs, categoryFilter, typeFilter, statusFilter, applicabilityFilter])
+
+ if (state.derivedTOMs.length === 0) {
+ return (
+
+
+
Keine TOMs vorhanden
+
+ Starten Sie den TOM Generator, um technische und organisatorische Massnahmen basierend auf Ihrem Verarbeitungsverzeichnis abzuleiten.
+
+
+ TOM Generator starten
+
+
+ )
+ }
+
+ return (
+
+ {/* Stats Row */}
+
+
+
{stats.total}
+
Gesamt TOMs
+
+
+
{stats.implemented}
+
Implementiert
+
+
+
{stats.partial}
+
Teilweise
+
+
+
{stats.missing}
+
Fehlend
+
+
+
+ {/* Art. 32 Schutzziele */}
+
+
Art. 32 DSGVO Schutzziele
+
+ {sdmStats.map(sz => {
+ const total = sz.stats.total || 1
+ const implPercent = Math.round((sz.stats.implemented / total) * 100)
+ const partialPercent = Math.round((sz.stats.partial / total) * 100)
+ return (
+
+
{sz.label}
+
+
+ {sz.stats.implemented}/{sz.stats.total} implementiert
+
+
+ )
+ })}
+
+
+
+ {/* Filter Controls */}
+
+
+
+ Kategorie
+ setCategoryFilter(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Alle Kategorien
+ {categories.map(cat => (
+ {cat}
+ ))}
+
+
+
+ Typ
+ setTypeFilter(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Alle
+ Technisch
+ Organisatorisch
+
+
+
+ Status
+ setStatusFilter(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Alle
+ Implementiert
+ Teilweise
+ Fehlend
+
+
+
+ Anwendbarkeit
+ setApplicabilityFilter(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ >
+ Alle
+ Erforderlich
+ Empfohlen
+ Optional
+
+
+
+
+
+ {/* TOM Card Grid */}
+
+ {filteredTOMs.map(tom => {
+ const control = getControlById(tom.controlId)
+ const statusBadge = STATUS_BADGES[tom.implementationStatus] || STATUS_BADGES.NOT_IMPLEMENTED
+ const typeBadge = TYPE_BADGES[control?.type || 'TECHNICAL'] || TYPE_BADGES.TECHNICAL
+ const evidenceCount = tom.linkedEvidence?.length || 0
+
+ return (
+
onSelectTOM(tom.id)}
+ className="bg-white rounded-xl border border-gray-200 p-5 text-left hover:border-purple-300 hover:shadow-md transition-all group"
+ >
+
+
+ {control?.code || tom.controlId}
+
+ {statusBadge.label}
+
+
+ {typeBadge.label}
+
+
+ {evidenceCount > 0 && (
+
+ {evidenceCount} Nachweise
+
+ )}
+
+
+ {control?.name?.de || tom.controlId}
+
+
+ {control?.category || 'Unbekannte Kategorie'}
+
+
+ )
+ })}
+
+
+ {filteredTOMs.length === 0 && state.derivedTOMs.length > 0 && (
+
+
Keine TOMs entsprechen den aktuellen Filterkriterien.
+
+ )}
+
+ )
+}
diff --git a/admin-v2/components/sdk/tom-dashboard/index.ts b/admin-v2/components/sdk/tom-dashboard/index.ts
new file mode 100644
index 0000000..2c8239c
--- /dev/null
+++ b/admin-v2/components/sdk/tom-dashboard/index.ts
@@ -0,0 +1,3 @@
+export { TOMOverviewTab } from './TOMOverviewTab'
+export { TOMEditorTab } from './TOMEditorTab'
+export { TOMGapExportTab } from './TOMGapExportTab'
diff --git a/admin-v2/hooks/companion/index.ts b/admin-v2/hooks/companion/index.ts
new file mode 100644
index 0000000..85edd4c
--- /dev/null
+++ b/admin-v2/hooks/companion/index.ts
@@ -0,0 +1,3 @@
+export { useCompanionData } from './useCompanionData'
+export { useLessonSession } from './useLessonSession'
+export { useKeyboardShortcuts, useKeyboardShortcutHints } from './useKeyboardShortcuts'
diff --git a/admin-v2/hooks/companion/useCompanionData.ts b/admin-v2/hooks/companion/useCompanionData.ts
new file mode 100644
index 0000000..129f119
--- /dev/null
+++ b/admin-v2/hooks/companion/useCompanionData.ts
@@ -0,0 +1,156 @@
+'use client'
+
+import { useState, useEffect, useCallback } from 'react'
+import { CompanionData } from '@/lib/companion/types'
+import { createDefaultPhases } from '@/lib/companion/constants'
+
+interface UseCompanionDataOptions {
+ pollingInterval?: number // ms, default 30000
+ autoRefresh?: boolean
+}
+
+interface UseCompanionDataReturn {
+ data: CompanionData | null
+ loading: boolean
+ error: string | null
+ refresh: () => Promise
+ lastUpdated: Date | null
+}
+
+// Mock data for development - will be replaced with actual API calls
+function getMockData(): CompanionData {
+ return {
+ context: {
+ currentPhase: 'erarbeitung',
+ phaseDisplayName: 'Erarbeitung',
+ },
+ stats: {
+ classesCount: 4,
+ studentsCount: 96,
+ learningUnitsCreated: 23,
+ gradesEntered: 156,
+ },
+ phases: createDefaultPhases(),
+ progress: {
+ percentage: 65,
+ completed: 13,
+ total: 20,
+ },
+ suggestions: [
+ {
+ id: '1',
+ title: 'Klausuren korrigieren',
+ description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
+ priority: 'urgent',
+ icon: 'ClipboardCheck',
+ actionTarget: '/ai/klausur-korrektur',
+ estimatedTime: 120,
+ },
+ {
+ id: '2',
+ title: 'Elternsprechtag vorbereiten',
+ description: 'Notenuebersicht fuer 8b erstellen',
+ priority: 'high',
+ icon: 'Users',
+ actionTarget: '/education/grades',
+ estimatedTime: 30,
+ },
+ {
+ id: '3',
+ title: 'Material hochladen',
+ description: 'Arbeitsblatt fuer naechste Woche bereitstellen',
+ priority: 'medium',
+ icon: 'FileText',
+ actionTarget: '/development/content',
+ estimatedTime: 15,
+ },
+ {
+ id: '4',
+ title: 'Lernstandserhebung planen',
+ description: 'Mathe 7a - Naechster Test in 2 Wochen',
+ priority: 'low',
+ icon: 'Calendar',
+ actionTarget: '/education/planning',
+ estimatedTime: 45,
+ },
+ ],
+ upcomingEvents: [
+ {
+ id: 'e1',
+ title: 'Mathe-Test 9b',
+ date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
+ type: 'exam',
+ inDays: 2,
+ },
+ {
+ id: 'e2',
+ title: 'Elternsprechtag',
+ date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
+ type: 'parent_meeting',
+ inDays: 5,
+ },
+ {
+ id: 'e3',
+ title: 'Notenschluss Q1',
+ date: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
+ type: 'deadline',
+ inDays: 14,
+ },
+ ],
+ }
+}
+
+export function useCompanionData(options: UseCompanionDataOptions = {}): UseCompanionDataReturn {
+ const { pollingInterval = 30000, autoRefresh = true } = options
+
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [lastUpdated, setLastUpdated] = useState(null)
+
+ const fetchData = useCallback(async () => {
+ try {
+ // TODO: Replace with actual API call
+ // const response = await fetch('/api/admin/companion')
+ // if (!response.ok) throw new Error('Failed to fetch companion data')
+ // const result = await response.json()
+ // setData(result.data)
+
+ // For now, use mock data with a small delay to simulate network
+ await new Promise((resolve) => setTimeout(resolve, 300))
+ setData(getMockData())
+ setLastUpdated(new Date())
+ setError(null)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ const refresh = useCallback(async () => {
+ setLoading(true)
+ await fetchData()
+ }, [fetchData])
+
+ // Initial fetch
+ useEffect(() => {
+ fetchData()
+ }, [fetchData])
+
+ // Polling
+ useEffect(() => {
+ if (!autoRefresh || pollingInterval <= 0) return
+
+ const interval = setInterval(fetchData, pollingInterval)
+ return () => clearInterval(interval)
+ }, [autoRefresh, pollingInterval, fetchData])
+
+ return {
+ data,
+ loading,
+ error,
+ refresh,
+ lastUpdated,
+ }
+}
diff --git a/admin-v2/hooks/companion/useKeyboardShortcuts.ts b/admin-v2/hooks/companion/useKeyboardShortcuts.ts
new file mode 100644
index 0000000..0f3666e
--- /dev/null
+++ b/admin-v2/hooks/companion/useKeyboardShortcuts.ts
@@ -0,0 +1,113 @@
+'use client'
+
+import { useEffect, useCallback, useRef } from 'react'
+import { KEYBOARD_SHORTCUTS } from '@/lib/companion/constants'
+
+interface UseKeyboardShortcutsOptions {
+ onPauseResume?: () => void
+ onExtend?: () => void
+ onNextPhase?: () => void
+ onCloseModal?: () => void
+ onShowHelp?: () => void
+ enabled?: boolean
+}
+
+export function useKeyboardShortcuts({
+ onPauseResume,
+ onExtend,
+ onNextPhase,
+ onCloseModal,
+ onShowHelp,
+ enabled = true,
+}: UseKeyboardShortcutsOptions) {
+ // Track if we're in an input field
+ const isInputFocused = useRef(false)
+
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (!enabled) return
+
+ // Don't trigger shortcuts when typing in inputs
+ const target = event.target as HTMLElement
+ const isInput =
+ target.tagName === 'INPUT' ||
+ target.tagName === 'TEXTAREA' ||
+ target.tagName === 'SELECT' ||
+ target.isContentEditable
+
+ if (isInput) {
+ isInputFocused.current = true
+ // Only allow Escape in inputs
+ if (event.key !== 'Escape') return
+ } else {
+ isInputFocused.current = false
+ }
+
+ // Handle shortcuts
+ switch (event.key) {
+ case KEYBOARD_SHORTCUTS.PAUSE_RESUME:
+ if (!isInput) {
+ event.preventDefault()
+ onPauseResume?.()
+ }
+ break
+
+ case KEYBOARD_SHORTCUTS.EXTEND_5MIN:
+ case KEYBOARD_SHORTCUTS.EXTEND_5MIN.toUpperCase():
+ if (!isInput) {
+ event.preventDefault()
+ onExtend?.()
+ }
+ break
+
+ case KEYBOARD_SHORTCUTS.NEXT_PHASE:
+ case KEYBOARD_SHORTCUTS.NEXT_PHASE.toUpperCase():
+ if (!isInput) {
+ event.preventDefault()
+ onNextPhase?.()
+ }
+ break
+
+ case KEYBOARD_SHORTCUTS.CLOSE_MODAL:
+ event.preventDefault()
+ onCloseModal?.()
+ break
+
+ case KEYBOARD_SHORTCUTS.SHOW_HELP:
+ if (!isInput) {
+ event.preventDefault()
+ onShowHelp?.()
+ }
+ break
+ }
+ },
+ [enabled, onPauseResume, onExtend, onNextPhase, onCloseModal, onShowHelp]
+ )
+
+ useEffect(() => {
+ if (!enabled) return
+
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [enabled, handleKeyDown])
+
+ return {
+ isInputFocused: isInputFocused.current,
+ }
+}
+
+/**
+ * Hook to display keyboard shortcut hints
+ */
+export function useKeyboardShortcutHints(show: boolean) {
+ const shortcuts = [
+ { key: 'Leertaste', action: 'Pause/Fortsetzen', code: 'space' },
+ { key: 'E', action: '+5 Minuten', code: 'e' },
+ { key: 'N', action: 'Naechste Phase', code: 'n' },
+ { key: 'Esc', action: 'Modal schliessen', code: 'escape' },
+ ]
+
+ if (!show) return null
+
+ return shortcuts
+}
diff --git a/admin-v2/hooks/companion/useLessonSession.ts b/admin-v2/hooks/companion/useLessonSession.ts
new file mode 100644
index 0000000..a0529cb
--- /dev/null
+++ b/admin-v2/hooks/companion/useLessonSession.ts
@@ -0,0 +1,446 @@
+'use client'
+
+import { useState, useEffect, useCallback, useRef } from 'react'
+import { LessonSession, LessonPhase, TimerState, PhaseDurations } from '@/lib/companion/types'
+import {
+ PHASE_ORDER,
+ PHASE_DISPLAY_NAMES,
+ PHASE_COLORS,
+ DEFAULT_PHASE_DURATIONS,
+ SYSTEM_TEMPLATES,
+ getTimerColorStatus,
+ STORAGE_KEYS,
+} from '@/lib/companion/constants'
+
+interface UseLessonSessionOptions {
+ onPhaseComplete?: (phaseIndex: number) => void
+ onLessonComplete?: (session: LessonSession) => void
+ onOvertimeStart?: () => void
+}
+
+interface UseLessonSessionReturn {
+ session: LessonSession | null
+ timerState: TimerState | null
+ startLesson: (data: {
+ classId: string
+ className?: string
+ subject: string
+ topic?: string
+ templateId?: string
+ }) => void
+ endLesson: () => void
+ pauseLesson: () => void
+ resumeLesson: () => void
+ extendTime: (minutes: number) => void
+ skipPhase: () => void
+ saveReflection: (rating: number, notes: string, nextSteps: string) => void
+ addHomework: (title: string, dueDate: string) => void
+ removeHomework: (id: string) => void
+ isRunning: boolean
+ isPaused: boolean
+}
+
+function createInitialPhases(durations: PhaseDurations): LessonPhase[] {
+ return PHASE_ORDER.map((phaseId) => ({
+ phase: phaseId,
+ duration: durations[phaseId],
+ status: 'planned',
+ actualTime: 0,
+ }))
+}
+
+function generateSessionId(): string {
+ return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+}
+
+export function useLessonSession(
+ options: UseLessonSessionOptions = {}
+): UseLessonSessionReturn {
+ const { onPhaseComplete, onLessonComplete, onOvertimeStart } = options
+
+ const [session, setSession] = useState(null)
+ const [timerState, setTimerState] = useState(null)
+
+ const timerRef = useRef(null)
+ const lastTickRef = useRef(Date.now())
+ const hasTriggeredOvertimeRef = useRef(false)
+
+ // Calculate timer state from session
+ const calculateTimerState = useCallback((sess: LessonSession): TimerState | null => {
+ if (!sess || sess.status === 'completed') return null
+
+ const currentPhase = sess.phases[sess.currentPhaseIndex]
+ if (!currentPhase) return null
+
+ const phaseDurationSeconds = currentPhase.duration * 60
+ const elapsedInPhase = currentPhase.actualTime
+ const remainingSeconds = phaseDurationSeconds - elapsedInPhase
+ const progress = Math.min(elapsedInPhase / phaseDurationSeconds, 1)
+ const isOvertime = remainingSeconds < 0
+
+ return {
+ isRunning: sess.status === 'in_progress' && !sess.isPaused,
+ isPaused: sess.isPaused,
+ elapsedSeconds: elapsedInPhase,
+ remainingSeconds: Math.max(remainingSeconds, -999),
+ totalSeconds: phaseDurationSeconds,
+ progress,
+ colorStatus: getTimerColorStatus(remainingSeconds, isOvertime),
+ currentPhase,
+ }
+ }, [])
+
+ // Timer tick function
+ const tick = useCallback(() => {
+ if (!session || session.isPaused || session.status !== 'in_progress') return
+
+ const now = Date.now()
+ const delta = Math.floor((now - lastTickRef.current) / 1000)
+ lastTickRef.current = now
+
+ if (delta <= 0) return
+
+ setSession((prev) => {
+ if (!prev) return null
+
+ const updatedPhases = [...prev.phases]
+ const currentPhase = updatedPhases[prev.currentPhaseIndex]
+ if (!currentPhase) return prev
+
+ currentPhase.actualTime += delta
+
+ // Check for overtime
+ const phaseDurationSeconds = currentPhase.duration * 60
+ if (
+ currentPhase.actualTime > phaseDurationSeconds &&
+ !hasTriggeredOvertimeRef.current
+ ) {
+ hasTriggeredOvertimeRef.current = true
+ onOvertimeStart?.()
+ }
+
+ // Update total elapsed time
+ const totalElapsed = prev.elapsedTime + delta
+
+ return {
+ ...prev,
+ phases: updatedPhases,
+ elapsedTime: totalElapsed,
+ overtimeMinutes: Math.max(
+ 0,
+ Math.floor((currentPhase.actualTime - phaseDurationSeconds) / 60)
+ ),
+ }
+ })
+ }, [session, onOvertimeStart])
+
+ // Start/stop timer based on session state
+ useEffect(() => {
+ if (session?.status === 'in_progress' && !session.isPaused) {
+ lastTickRef.current = Date.now()
+ timerRef.current = setInterval(tick, 100) // Update every 100ms for smooth animation
+ } else {
+ if (timerRef.current) {
+ clearInterval(timerRef.current)
+ timerRef.current = null
+ }
+ }
+
+ return () => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current)
+ }
+ }
+ }, [session?.status, session?.isPaused, tick])
+
+ // Update timer state when session changes
+ useEffect(() => {
+ if (session) {
+ setTimerState(calculateTimerState(session))
+ } else {
+ setTimerState(null)
+ }
+ }, [session, calculateTimerState])
+
+ // Persist session to localStorage
+ useEffect(() => {
+ if (session) {
+ localStorage.setItem(STORAGE_KEYS.CURRENT_SESSION, JSON.stringify(session))
+ }
+ }, [session])
+
+ // Restore session from localStorage on mount
+ useEffect(() => {
+ const stored = localStorage.getItem(STORAGE_KEYS.CURRENT_SESSION)
+ if (stored) {
+ try {
+ const parsed = JSON.parse(stored) as LessonSession
+ // Only restore if session is not completed and not too old (< 24h)
+ const sessionTime = new Date(parsed.startTime).getTime()
+ const isRecent = Date.now() - sessionTime < 24 * 60 * 60 * 1000
+
+ if (parsed.status !== 'completed' && isRecent) {
+ // Pause the restored session
+ setSession({ ...parsed, isPaused: true })
+ }
+ } catch {
+ // Invalid stored session, ignore
+ }
+ }
+ }, [])
+
+ const startLesson = useCallback(
+ (data: {
+ classId: string
+ className?: string
+ subject: string
+ topic?: string
+ templateId?: string
+ }) => {
+ // Find template durations
+ let durations = DEFAULT_PHASE_DURATIONS
+ if (data.templateId) {
+ const template = SYSTEM_TEMPLATES.find((t) => t.templateId === data.templateId)
+ if (template) {
+ durations = template.durations as PhaseDurations
+ }
+ }
+
+ const phases = createInitialPhases(durations)
+ phases[0].status = 'active'
+ phases[0].startedAt = new Date().toISOString()
+
+ const newSession: LessonSession = {
+ sessionId: generateSessionId(),
+ classId: data.classId,
+ className: data.className || data.classId,
+ subject: data.subject,
+ topic: data.topic,
+ startTime: new Date().toISOString(),
+ phases,
+ totalPlannedDuration: Object.values(durations).reduce((a, b) => a + b, 0),
+ currentPhaseIndex: 0,
+ elapsedTime: 0,
+ isPaused: false,
+ pauseDuration: 0,
+ overtimeMinutes: 0,
+ status: 'in_progress',
+ homeworkList: [],
+ materials: [],
+ }
+
+ hasTriggeredOvertimeRef.current = false
+ setSession(newSession)
+ },
+ []
+ )
+
+ const endLesson = useCallback(() => {
+ if (!session) return
+
+ const completedSession: LessonSession = {
+ ...session,
+ status: 'completed',
+ endTime: new Date().toISOString(),
+ phases: session.phases.map((p, i) => ({
+ ...p,
+ status: i <= session.currentPhaseIndex ? 'completed' : 'skipped',
+ completedAt: i <= session.currentPhaseIndex ? new Date().toISOString() : undefined,
+ })),
+ }
+
+ setSession(completedSession)
+ onLessonComplete?.(completedSession)
+ }, [session, onLessonComplete])
+
+ const pauseLesson = useCallback(() => {
+ if (!session || session.isPaused) return
+
+ setSession((prev) =>
+ prev
+ ? {
+ ...prev,
+ isPaused: true,
+ pausedAt: new Date().toISOString(),
+ status: 'paused',
+ }
+ : null
+ )
+ }, [session])
+
+ const resumeLesson = useCallback(() => {
+ if (!session || !session.isPaused) return
+
+ const pausedAt = session.pausedAt ? new Date(session.pausedAt).getTime() : Date.now()
+ const pauseDelta = Math.floor((Date.now() - pausedAt) / 1000)
+
+ setSession((prev) =>
+ prev
+ ? {
+ ...prev,
+ isPaused: false,
+ pausedAt: undefined,
+ pauseDuration: prev.pauseDuration + pauseDelta,
+ status: 'in_progress',
+ }
+ : null
+ )
+
+ lastTickRef.current = Date.now()
+ }, [session])
+
+ const extendTime = useCallback(
+ (minutes: number) => {
+ if (!session) return
+
+ setSession((prev) => {
+ if (!prev) return null
+
+ const updatedPhases = [...prev.phases]
+ const currentPhase = updatedPhases[prev.currentPhaseIndex]
+ if (!currentPhase) return prev
+
+ currentPhase.duration += minutes
+
+ // Reset overtime trigger if we've added time
+ if (hasTriggeredOvertimeRef.current) {
+ const phaseDurationSeconds = currentPhase.duration * 60
+ if (currentPhase.actualTime < phaseDurationSeconds) {
+ hasTriggeredOvertimeRef.current = false
+ }
+ }
+
+ return {
+ ...prev,
+ phases: updatedPhases,
+ totalPlannedDuration: prev.totalPlannedDuration + minutes,
+ }
+ })
+ },
+ [session]
+ )
+
+ const skipPhase = useCallback(() => {
+ if (!session) return
+
+ const nextPhaseIndex = session.currentPhaseIndex + 1
+
+ // Check if this was the last phase
+ if (nextPhaseIndex >= session.phases.length) {
+ endLesson()
+ return
+ }
+
+ setSession((prev) => {
+ if (!prev) return null
+
+ const updatedPhases = [...prev.phases]
+
+ // Complete current phase
+ updatedPhases[prev.currentPhaseIndex] = {
+ ...updatedPhases[prev.currentPhaseIndex],
+ status: 'completed',
+ completedAt: new Date().toISOString(),
+ }
+
+ // Start next phase
+ updatedPhases[nextPhaseIndex] = {
+ ...updatedPhases[nextPhaseIndex],
+ status: 'active',
+ startedAt: new Date().toISOString(),
+ }
+
+ return {
+ ...prev,
+ phases: updatedPhases,
+ currentPhaseIndex: nextPhaseIndex,
+ overtimeMinutes: 0,
+ }
+ })
+
+ hasTriggeredOvertimeRef.current = false
+ onPhaseComplete?.(session.currentPhaseIndex)
+ }, [session, endLesson, onPhaseComplete])
+
+ const saveReflection = useCallback(
+ (rating: number, notes: string, nextSteps: string) => {
+ if (!session) return
+
+ setSession((prev) =>
+ prev
+ ? {
+ ...prev,
+ reflection: {
+ rating,
+ notes,
+ nextSteps,
+ savedAt: new Date().toISOString(),
+ },
+ }
+ : null
+ )
+ },
+ [session]
+ )
+
+ const addHomework = useCallback(
+ (title: string, dueDate: string) => {
+ if (!session) return
+
+ const newHomework = {
+ id: `hw-${Date.now()}`,
+ title,
+ dueDate,
+ completed: false,
+ }
+
+ setSession((prev) =>
+ prev
+ ? {
+ ...prev,
+ homeworkList: [...prev.homeworkList, newHomework],
+ }
+ : null
+ )
+ },
+ [session]
+ )
+
+ const removeHomework = useCallback(
+ (id: string) => {
+ if (!session) return
+
+ setSession((prev) =>
+ prev
+ ? {
+ ...prev,
+ homeworkList: prev.homeworkList.filter((hw) => hw.id !== id),
+ }
+ : null
+ )
+ },
+ [session]
+ )
+
+ // Clear session (for starting new)
+ const clearSession = useCallback(() => {
+ setSession(null)
+ localStorage.removeItem(STORAGE_KEYS.CURRENT_SESSION)
+ }, [])
+
+ return {
+ session,
+ timerState,
+ startLesson,
+ endLesson: session?.status === 'completed' ? clearSession : endLesson,
+ pauseLesson,
+ resumeLesson,
+ extendTime,
+ skipPhase,
+ saveReflection,
+ addHomework,
+ removeHomework,
+ isRunning: session?.status === 'in_progress' && !session?.isPaused,
+ isPaused: session?.isPaused ?? false,
+ }
+}
diff --git a/admin-v2/lib/companion/constants.ts b/admin-v2/lib/companion/constants.ts
new file mode 100644
index 0000000..a89d50e
--- /dev/null
+++ b/admin-v2/lib/companion/constants.ts
@@ -0,0 +1,364 @@
+/**
+ * Constants for Companion Module
+ * Phase colors, defaults, and configuration
+ */
+
+import { PhaseId, PhaseDurations, Phase, TeacherSettings } from './types'
+
+// ============================================================================
+// Phase Colors (Didactic Color Psychology)
+// ============================================================================
+
+export const PHASE_COLORS: Record = {
+ einstieg: {
+ hex: '#4A90E2',
+ tailwind: 'bg-blue-500',
+ gradient: 'from-blue-500 to-blue-600',
+ },
+ erarbeitung: {
+ hex: '#F5A623',
+ tailwind: 'bg-orange-500',
+ gradient: 'from-orange-500 to-orange-600',
+ },
+ sicherung: {
+ hex: '#7ED321',
+ tailwind: 'bg-green-500',
+ gradient: 'from-green-500 to-green-600',
+ },
+ transfer: {
+ hex: '#9013FE',
+ tailwind: 'bg-purple-600',
+ gradient: 'from-purple-600 to-purple-700',
+ },
+ reflexion: {
+ hex: '#6B7280',
+ tailwind: 'bg-gray-500',
+ gradient: 'from-gray-500 to-gray-600',
+ },
+}
+
+// ============================================================================
+// Phase Definitions
+// ============================================================================
+
+export const PHASE_SHORT_NAMES: Record = {
+ einstieg: 'E',
+ erarbeitung: 'A',
+ sicherung: 'S',
+ transfer: 'T',
+ reflexion: 'R',
+}
+
+export const PHASE_DISPLAY_NAMES: Record = {
+ einstieg: 'Einstieg',
+ erarbeitung: 'Erarbeitung',
+ sicherung: 'Sicherung',
+ transfer: 'Transfer',
+ reflexion: 'Reflexion',
+}
+
+export const PHASE_DESCRIPTIONS: Record = {
+ einstieg: 'Motivation, Kontext setzen, Vorwissen aktivieren',
+ erarbeitung: 'Hauptinhalt, aktives Lernen, neue Konzepte',
+ sicherung: 'Konsolidierung, Zusammenfassung, Uebungen',
+ transfer: 'Anwendung, neue Kontexte, kreative Aufgaben',
+ reflexion: 'Rueckblick, Selbsteinschaetzung, Ausblick',
+}
+
+export const PHASE_ORDER: PhaseId[] = [
+ 'einstieg',
+ 'erarbeitung',
+ 'sicherung',
+ 'transfer',
+ 'reflexion',
+]
+
+// ============================================================================
+// Default Durations (in minutes)
+// ============================================================================
+
+export const DEFAULT_PHASE_DURATIONS: PhaseDurations = {
+ einstieg: 8,
+ erarbeitung: 20,
+ sicherung: 10,
+ transfer: 7,
+ reflexion: 5,
+}
+
+export const DEFAULT_LESSON_LENGTH = 45 // minutes (German standard)
+export const EXTENDED_LESSON_LENGTH = 50 // minutes (with buffer)
+
+// ============================================================================
+// Timer Thresholds (in seconds)
+// ============================================================================
+
+export const TIMER_WARNING_THRESHOLD = 5 * 60 // 5 minutes = warning (yellow)
+export const TIMER_CRITICAL_THRESHOLD = 2 * 60 // 2 minutes = critical (red)
+
+// ============================================================================
+// SVG Pie Timer Constants
+// ============================================================================
+
+export const PIE_TIMER_RADIUS = 42
+export const PIE_TIMER_CIRCUMFERENCE = 2 * Math.PI * PIE_TIMER_RADIUS // ~263.89
+export const PIE_TIMER_STROKE_WIDTH = 8
+export const PIE_TIMER_SIZE = 120 // viewBox size
+
+// ============================================================================
+// Timer Color Classes
+// ============================================================================
+
+export const TIMER_COLOR_CLASSES = {
+ plenty: 'text-green-500 stroke-green-500',
+ warning: 'text-amber-500 stroke-amber-500',
+ critical: 'text-red-500 stroke-red-500',
+ overtime: 'text-red-600 stroke-red-600 animate-pulse',
+}
+
+export const TIMER_BG_COLORS = {
+ plenty: 'bg-green-500/10',
+ warning: 'bg-amber-500/10',
+ critical: 'bg-red-500/10',
+ overtime: 'bg-red-600/20',
+}
+
+// ============================================================================
+// Keyboard Shortcuts
+// ============================================================================
+
+export const KEYBOARD_SHORTCUTS = {
+ PAUSE_RESUME: ' ', // Spacebar
+ EXTEND_5MIN: 'e',
+ NEXT_PHASE: 'n',
+ CLOSE_MODAL: 'Escape',
+ SHOW_HELP: '?',
+} as const
+
+export const KEYBOARD_SHORTCUT_DESCRIPTIONS: Record = {
+ ' ': 'Pause/Fortsetzen',
+ 'e': '+5 Minuten',
+ 'n': 'Naechste Phase',
+ 'Escape': 'Modal schliessen',
+ '?': 'Hilfe anzeigen',
+}
+
+// ============================================================================
+// Default Settings
+// ============================================================================
+
+export const DEFAULT_TEACHER_SETTINGS: TeacherSettings = {
+ defaultPhaseDurations: DEFAULT_PHASE_DURATIONS,
+ preferredLessonLength: DEFAULT_LESSON_LENGTH,
+ autoAdvancePhases: true,
+ soundNotifications: true,
+ showKeyboardShortcuts: true,
+ highContrastMode: false,
+ onboardingCompleted: false,
+}
+
+// ============================================================================
+// System Templates
+// ============================================================================
+
+export const SYSTEM_TEMPLATES = [
+ {
+ templateId: 'standard-45',
+ name: 'Standard (45 Min)',
+ description: 'Klassische Unterrichtsstunde',
+ durations: DEFAULT_PHASE_DURATIONS,
+ isSystemTemplate: true,
+ },
+ {
+ templateId: 'double-90',
+ name: 'Doppelstunde (90 Min)',
+ description: 'Fuer laengere Arbeitsphasen',
+ durations: {
+ einstieg: 10,
+ erarbeitung: 45,
+ sicherung: 15,
+ transfer: 12,
+ reflexion: 8,
+ },
+ isSystemTemplate: true,
+ },
+ {
+ templateId: 'math-focused',
+ name: 'Mathematik-fokussiert',
+ description: 'Lange Erarbeitung und Sicherung',
+ durations: {
+ einstieg: 5,
+ erarbeitung: 25,
+ sicherung: 10,
+ transfer: 5,
+ reflexion: 5,
+ },
+ isSystemTemplate: true,
+ },
+ {
+ templateId: 'language-practice',
+ name: 'Sprachpraxis',
+ description: 'Betont kommunikative Phasen',
+ durations: {
+ einstieg: 10,
+ erarbeitung: 15,
+ sicherung: 8,
+ transfer: 10,
+ reflexion: 7,
+ },
+ isSystemTemplate: true,
+ },
+]
+
+// ============================================================================
+// Suggestion Icons (Lucide icon names)
+// ============================================================================
+
+export const SUGGESTION_ICONS = {
+ grading: 'ClipboardCheck',
+ homework: 'BookOpen',
+ planning: 'Calendar',
+ meeting: 'Users',
+ deadline: 'Clock',
+ material: 'FileText',
+ communication: 'MessageSquare',
+ default: 'Lightbulb',
+}
+
+// ============================================================================
+// Priority Colors
+// ============================================================================
+
+export const PRIORITY_COLORS = {
+ urgent: {
+ bg: 'bg-red-100',
+ text: 'text-red-700',
+ border: 'border-red-200',
+ dot: 'bg-red-500',
+ },
+ high: {
+ bg: 'bg-orange-100',
+ text: 'text-orange-700',
+ border: 'border-orange-200',
+ dot: 'bg-orange-500',
+ },
+ medium: {
+ bg: 'bg-yellow-100',
+ text: 'text-yellow-700',
+ border: 'border-yellow-200',
+ dot: 'bg-yellow-500',
+ },
+ low: {
+ bg: 'bg-slate-100',
+ text: 'text-slate-700',
+ border: 'border-slate-200',
+ dot: 'bg-slate-400',
+ },
+}
+
+// ============================================================================
+// Event Type Icons & Colors
+// ============================================================================
+
+export const EVENT_TYPE_CONFIG = {
+ exam: {
+ icon: 'FileQuestion',
+ color: 'text-red-600',
+ bg: 'bg-red-50',
+ },
+ parent_meeting: {
+ icon: 'Users',
+ color: 'text-blue-600',
+ bg: 'bg-blue-50',
+ },
+ deadline: {
+ icon: 'Clock',
+ color: 'text-amber-600',
+ bg: 'bg-amber-50',
+ },
+ other: {
+ icon: 'Calendar',
+ color: 'text-slate-600',
+ bg: 'bg-slate-50',
+ },
+}
+
+// ============================================================================
+// Storage Keys
+// ============================================================================
+
+export const STORAGE_KEYS = {
+ SETTINGS: 'companion_settings',
+ CURRENT_SESSION: 'companion_current_session',
+ ONBOARDING_STATE: 'companion_onboarding',
+ CUSTOM_TEMPLATES: 'companion_custom_templates',
+ LAST_MODE: 'companion_last_mode',
+}
+
+// ============================================================================
+// API Endpoints (relative to backend)
+// ============================================================================
+
+export const API_ENDPOINTS = {
+ DASHBOARD: '/api/state/dashboard',
+ LESSON_START: '/api/classroom/sessions',
+ LESSON_UPDATE: '/api/classroom/sessions', // + /{id}
+ TEMPLATES: '/api/classroom/templates',
+ SETTINGS: '/api/teacher/settings',
+ FEEDBACK: '/api/feedback',
+}
+
+// ============================================================================
+// Helper Functions
+// ============================================================================
+
+/**
+ * Create default phases array from durations
+ */
+export function createDefaultPhases(durations: PhaseDurations = DEFAULT_PHASE_DURATIONS): Phase[] {
+ return PHASE_ORDER.map((phaseId, index) => ({
+ id: phaseId,
+ shortName: PHASE_SHORT_NAMES[phaseId],
+ displayName: PHASE_DISPLAY_NAMES[phaseId],
+ duration: durations[phaseId],
+ status: index === 0 ? 'active' : 'planned',
+ color: PHASE_COLORS[phaseId].hex,
+ }))
+}
+
+/**
+ * Calculate total duration from phase durations
+ */
+export function calculateTotalDuration(durations: PhaseDurations): number {
+ return Object.values(durations).reduce((sum, d) => sum + d, 0)
+}
+
+/**
+ * Get timer color status based on remaining time
+ */
+export function getTimerColorStatus(
+ remainingSeconds: number,
+ isOvertime: boolean
+): 'plenty' | 'warning' | 'critical' | 'overtime' {
+ if (isOvertime) return 'overtime'
+ if (remainingSeconds <= TIMER_CRITICAL_THRESHOLD) return 'critical'
+ if (remainingSeconds <= TIMER_WARNING_THRESHOLD) return 'warning'
+ return 'plenty'
+}
+
+/**
+ * Format seconds as MM:SS
+ */
+export function formatTime(seconds: number): string {
+ const absSeconds = Math.abs(seconds)
+ const mins = Math.floor(absSeconds / 60)
+ const secs = absSeconds % 60
+ const sign = seconds < 0 ? '-' : ''
+ return `${sign}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
+}
+
+/**
+ * Format minutes as "X Min"
+ */
+export function formatMinutes(minutes: number): string {
+ return `${minutes} Min`
+}
diff --git a/admin-v2/lib/companion/index.ts b/admin-v2/lib/companion/index.ts
new file mode 100644
index 0000000..f2fd34d
--- /dev/null
+++ b/admin-v2/lib/companion/index.ts
@@ -0,0 +1,2 @@
+export * from './types'
+export * from './constants'
diff --git a/admin-v2/lib/companion/types.ts b/admin-v2/lib/companion/types.ts
new file mode 100644
index 0000000..5a4405b
--- /dev/null
+++ b/admin-v2/lib/companion/types.ts
@@ -0,0 +1,329 @@
+/**
+ * TypeScript Types for Companion Module
+ * Migration from Flask companion.py/companion_js.py
+ */
+
+// ============================================================================
+// Phase System
+// ============================================================================
+
+export type PhaseId = 'einstieg' | 'erarbeitung' | 'sicherung' | 'transfer' | 'reflexion'
+
+export interface Phase {
+ id: PhaseId
+ shortName: string // E, A, S, T, R
+ displayName: string
+ duration: number // minutes
+ status: 'planned' | 'active' | 'completed'
+ actualTime?: number // seconds (actual time spent)
+ color: string // hex color
+}
+
+export interface PhaseContext {
+ currentPhase: PhaseId
+ phaseDisplayName: string
+}
+
+// ============================================================================
+// Dashboard / Companion Mode
+// ============================================================================
+
+export interface CompanionStats {
+ classesCount: number
+ studentsCount: number
+ learningUnitsCreated: number
+ gradesEntered: number
+}
+
+export interface Progress {
+ percentage: number
+ completed: number
+ total: number
+}
+
+export type SuggestionPriority = 'urgent' | 'high' | 'medium' | 'low'
+
+export interface Suggestion {
+ id: string
+ title: string
+ description: string
+ priority: SuggestionPriority
+ icon: string // lucide icon name
+ actionTarget: string // navigation path
+ estimatedTime: number // minutes
+}
+
+export type EventType = 'exam' | 'parent_meeting' | 'deadline' | 'other'
+
+export interface UpcomingEvent {
+ id: string
+ title: string
+ date: string // ISO date string
+ type: EventType
+ inDays: number
+}
+
+export interface CompanionData {
+ context: PhaseContext
+ stats: CompanionStats
+ phases: Phase[]
+ progress: Progress
+ suggestions: Suggestion[]
+ upcomingEvents: UpcomingEvent[]
+}
+
+// ============================================================================
+// Lesson Mode
+// ============================================================================
+
+export type LessonStatus =
+ | 'not_started'
+ | 'in_progress'
+ | 'paused'
+ | 'completed'
+ | 'overtime'
+
+export interface LessonPhase {
+ phase: PhaseId
+ duration: number // planned duration in minutes
+ status: 'planned' | 'active' | 'completed' | 'skipped'
+ actualTime: number // actual time spent in seconds
+ startedAt?: string // ISO timestamp
+ completedAt?: string // ISO timestamp
+}
+
+export interface Homework {
+ id: string
+ title: string
+ description?: string
+ dueDate: string // ISO date
+ attachments?: string[]
+ completed?: boolean
+}
+
+export interface Material {
+ id: string
+ title: string
+ type: 'document' | 'video' | 'presentation' | 'link' | 'other'
+ url?: string
+ fileName?: string
+}
+
+export interface LessonReflection {
+ rating: number // 1-5 stars
+ notes: string
+ nextSteps: string
+ savedAt?: string
+}
+
+export interface LessonSession {
+ sessionId: string
+ classId: string
+ className: string
+ subject: string
+ topic?: string
+ startTime: string // ISO timestamp
+ endTime?: string // ISO timestamp
+ phases: LessonPhase[]
+ totalPlannedDuration: number // minutes
+ currentPhaseIndex: number
+ elapsedTime: number // seconds
+ isPaused: boolean
+ pausedAt?: string
+ pauseDuration: number // total pause time in seconds
+ overtimeMinutes: number
+ status: LessonStatus
+ homeworkList: Homework[]
+ materials: Material[]
+ reflection?: LessonReflection
+}
+
+// ============================================================================
+// Lesson Templates
+// ============================================================================
+
+export interface PhaseDurations {
+ einstieg: number
+ erarbeitung: number
+ sicherung: number
+ transfer: number
+ reflexion: number
+}
+
+export interface LessonTemplate {
+ templateId: string
+ name: string
+ description?: string
+ subject?: string
+ durations: PhaseDurations
+ isSystemTemplate: boolean
+ createdBy?: string
+ createdAt?: string
+}
+
+// ============================================================================
+// Settings
+// ============================================================================
+
+export interface TeacherSettings {
+ defaultPhaseDurations: PhaseDurations
+ preferredLessonLength: number // minutes (default 45)
+ autoAdvancePhases: boolean
+ soundNotifications: boolean
+ showKeyboardShortcuts: boolean
+ highContrastMode: boolean
+ onboardingCompleted: boolean
+ selectedTemplateId?: string
+}
+
+// ============================================================================
+// Timer State
+// ============================================================================
+
+export type TimerColorStatus = 'plenty' | 'warning' | 'critical' | 'overtime'
+
+export interface TimerState {
+ isRunning: boolean
+ isPaused: boolean
+ elapsedSeconds: number
+ remainingSeconds: number
+ totalSeconds: number
+ progress: number // 0-1
+ colorStatus: TimerColorStatus
+ currentPhase: LessonPhase | null
+}
+
+// ============================================================================
+// Forms
+// ============================================================================
+
+export interface LessonStartFormData {
+ classId: string
+ subject: string
+ topic?: string
+ templateId?: string
+ customDurations?: PhaseDurations
+}
+
+export interface Class {
+ id: string
+ name: string
+ grade: string
+ studentCount: number
+}
+
+// ============================================================================
+// Feedback
+// ============================================================================
+
+export type FeedbackType = 'bug' | 'feature' | 'feedback'
+
+export interface FeedbackSubmission {
+ type: FeedbackType
+ title: string
+ description: string
+ screenshot?: string // base64
+ sessionId?: string
+ metadata?: Record
+}
+
+// ============================================================================
+// Onboarding
+// ============================================================================
+
+export interface OnboardingStep {
+ step: number
+ title: string
+ description: string
+ completed: boolean
+}
+
+export interface OnboardingState {
+ currentStep: number
+ totalSteps: number
+ steps: OnboardingStep[]
+ selectedState?: string // Bundesland
+ selectedSchoolType?: string
+ completed: boolean
+}
+
+// ============================================================================
+// WebSocket Messages
+// ============================================================================
+
+export type WSMessageType =
+ | 'phase_update'
+ | 'timer_tick'
+ | 'overtime_warning'
+ | 'pause_toggle'
+ | 'session_end'
+ | 'sync_request'
+
+export interface WSMessage {
+ type: WSMessageType
+ payload: {
+ sessionId: string
+ phase?: number
+ elapsed?: number
+ isPaused?: boolean
+ overtimeMinutes?: number
+ [key: string]: unknown
+ }
+ timestamp: string
+}
+
+// ============================================================================
+// API Responses
+// ============================================================================
+
+export interface APIResponse {
+ success: boolean
+ data?: T
+ error?: string
+ message?: string
+}
+
+export interface DashboardResponse extends APIResponse {}
+
+export interface LessonResponse extends APIResponse {}
+
+export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
+
+export interface SettingsResponse extends APIResponse {}
+
+// ============================================================================
+// Component Props
+// ============================================================================
+
+export type CompanionMode = 'companion' | 'lesson' | 'classic'
+
+export interface ModeToggleProps {
+ currentMode: CompanionMode
+ onModeChange: (mode: CompanionMode) => void
+}
+
+export interface PhaseTimelineProps {
+ phases: Phase[]
+ currentPhaseIndex: number
+ onPhaseClick?: (index: number) => void
+}
+
+export interface VisualPieTimerProps {
+ progress: number // 0-1
+ remainingSeconds: number
+ totalSeconds: number
+ colorStatus: TimerColorStatus
+ isPaused: boolean
+ currentPhaseName: string
+ phaseColor: string
+}
+
+export interface QuickActionsBarProps {
+ onExtend: (minutes: number) => void
+ onPause: () => void
+ onResume: () => void
+ onSkip: () => void
+ isPaused: boolean
+ isLastPhase: boolean
+ disabled?: boolean
+}
diff --git a/admin-v2/lib/navigation.ts b/admin-v2/lib/navigation.ts
index b56a34f..de74b3f 100644
--- a/admin-v2/lib/navigation.ts
+++ b/admin-v2/lib/navigation.ts
@@ -486,15 +486,6 @@ export const navigation: NavCategory[] = [
audience: ['Lehrer', 'Entwickler'],
oldAdminPath: '/admin/klausur-korrektur',
},
- {
- id: 'companion',
- name: 'Companion',
- href: '/education/companion',
- description: 'Unterrichts-Timer & Phasen',
- purpose: 'Strukturierter Unterricht mit 5-Phasen-Modell (E-A-S-T-R). Visual Timer, Hausaufgaben-Tracking und Reflexion.',
- audience: ['Lehrer'],
- oldAdminPath: '/admin/companion',
- },
],
},
// =========================================================================
diff --git a/admin-v2/lib/sdk/compliance-scope-engine.ts b/admin-v2/lib/sdk/compliance-scope-engine.ts
new file mode 100644
index 0000000..5011c39
--- /dev/null
+++ b/admin-v2/lib/sdk/compliance-scope-engine.ts
@@ -0,0 +1,1508 @@
+import type {
+ ComplianceDepthLevel,
+ ComplianceScores,
+ ScopeProfilingAnswer,
+ ScopeDecision,
+ HardTriggerRule,
+ TriggeredHardTrigger,
+ RequiredDocument,
+ RiskFlag,
+ ScopeGap,
+ NextAction,
+ ScopeReasoning,
+ ScopeDocumentType,
+ DocumentScopeRequirement,
+} from './compliance-scope-types'
+import {
+ getDepthLevelNumeric,
+ depthLevelFromNumeric,
+ maxDepthLevel,
+ createEmptyScopeDecision,
+ DOCUMENT_SCOPE_MATRIX,
+ DOCUMENT_TYPE_LABELS,
+ DOCUMENT_SDK_STEP_MAP,
+} from './compliance-scope-types'
+
+// ============================================================================
+// SCORE WEIGHTS PRO FRAGE
+// ============================================================================
+
+export const QUESTION_SCORE_WEIGHTS: Record<
+ string,
+ { risk: number; complexity: number; assurance: number }
+> = {
+ // Organisationsprofil (6 Fragen)
+ org_employee_count: { risk: 3, complexity: 5, assurance: 4 },
+ org_industry: { risk: 6, complexity: 4, assurance: 5 },
+ org_business_model: { risk: 5, complexity: 3, assurance: 4 },
+ org_customer_count: { risk: 4, complexity: 6, assurance: 5 },
+ org_cert_target: { risk: 2, complexity: 8, assurance: 9 },
+ org_has_dpo: { risk: 7, complexity: 2, assurance: 8 },
+
+ // Datenarten (5 Fragen)
+ data_art9: { risk: 10, complexity: 7, assurance: 9 },
+ data_minors: { risk: 10, complexity: 6, assurance: 9 },
+ data_volume: { risk: 6, complexity: 7, assurance: 6 },
+ data_retention_years: { risk: 5, complexity: 4, assurance: 5 },
+ data_sources: { risk: 4, complexity: 5, assurance: 4 },
+
+ // Verarbeitungszwecke (9 Fragen)
+ proc_adm_scoring: { risk: 9, complexity: 7, assurance: 8 },
+ proc_ai_usage: { risk: 8, complexity: 8, assurance: 8 },
+ proc_video_surveillance: { risk: 7, complexity: 5, assurance: 7 },
+ proc_employee_monitoring: { risk: 7, complexity: 5, assurance: 7 },
+ proc_tracking: { risk: 6, complexity: 4, assurance: 6 },
+ proc_dsar_process: { risk: 8, complexity: 6, assurance: 8 },
+ proc_deletion_concept: { risk: 7, complexity: 5, assurance: 7 },
+ proc_incident_response: { risk: 9, complexity: 6, assurance: 9 },
+ proc_regular_audits: { risk: 5, complexity: 7, assurance: 8 },
+
+ // Technik (7 Fragen)
+ tech_hosting_location: { risk: 7, complexity: 5, assurance: 7 },
+ tech_third_country: { risk: 8, complexity: 6, assurance: 8 },
+ tech_encryption_transit: { risk: 8, complexity: 4, assurance: 8 },
+ tech_encryption_rest: { risk: 8, complexity: 4, assurance: 8 },
+ tech_access_control: { risk: 7, complexity: 5, assurance: 7 },
+ tech_logging: { risk: 6, complexity: 5, assurance: 7 },
+ tech_backup_recovery: { risk: 6, complexity: 5, assurance: 7 },
+
+ // Produkt/Features (5 Fragen)
+ prod_webshop: { risk: 5, complexity: 4, assurance: 5 },
+ prod_data_broker: { risk: 9, complexity: 7, assurance: 8 },
+ prod_api_external: { risk: 6, complexity: 5, assurance: 6 },
+ prod_consent_management: { risk: 7, complexity: 5, assurance: 8 },
+ prod_data_portability: { risk: 4, complexity: 5, assurance: 5 },
+
+ // Compliance Reife (3 Fragen)
+ comp_training: { risk: 5, complexity: 4, assurance: 7 },
+ comp_vendor_management: { risk: 6, complexity: 6, assurance: 7 },
+ comp_documentation_level: { risk: 6, complexity: 7, assurance: 8 },
+}
+
+// ============================================================================
+// ANSWER MULTIPLIERS FΓR SINGLE-CHOICE FRAGEN
+// ============================================================================
+
+export const ANSWER_MULTIPLIERS: Record> = {
+ org_employee_count: {
+ '1-9': 0.1,
+ '10-49': 0.3,
+ '50-249': 0.5,
+ '250-999': 0.7,
+ '1000+': 1.0,
+ },
+ org_industry: {
+ tech: 0.4,
+ finance: 0.8,
+ healthcare: 0.9,
+ public: 0.7,
+ retail: 0.5,
+ education: 0.6,
+ other: 0.3,
+ },
+ org_business_model: {
+ b2b: 0.4,
+ b2c: 0.7,
+ b2b2c: 0.6,
+ internal: 0.3,
+ },
+ org_customer_count: {
+ '0-100': 0.1,
+ '100-1000': 0.2,
+ '1000-10000': 0.4,
+ '10000-100000': 0.7,
+ '100000+': 1.0,
+ },
+ data_volume: {
+ '<1000': 0.1,
+ '1000-10000': 0.2,
+ '10000-100000': 0.4,
+ '100000-1000000': 0.7,
+ '>1000000': 1.0,
+ },
+ data_retention_years: {
+ '<1': 0.2,
+ '1-3': 0.4,
+ '3-5': 0.6,
+ '5-10': 0.8,
+ '>10': 1.0,
+ },
+ tech_hosting_location: {
+ eu: 0.2,
+ eu_us_adequacy: 0.4,
+ us_adequacy: 0.6,
+ drittland: 1.0,
+ },
+ tech_access_control: {
+ none: 1.0,
+ basic: 0.6,
+ rbac: 0.3,
+ advanced: 0.1,
+ },
+ tech_logging: {
+ none: 1.0,
+ basic: 0.6,
+ comprehensive: 0.2,
+ },
+ tech_backup_recovery: {
+ none: 1.0,
+ basic: 0.5,
+ tested: 0.2,
+ },
+ comp_documentation_level: {
+ none: 1.0,
+ basic: 0.6,
+ structured: 0.3,
+ comprehensive: 0.1,
+ },
+}
+
+// ============================================================================
+// 50 HARD TRIGGER RULES
+// ============================================================================
+
+export const HARD_TRIGGER_RULES: HardTriggerRule[] = [
+ // ========== A: Art. 9 Besondere Kategorien (9 rules) ==========
+ {
+ id: 'HT-A01',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'gesundheit',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung von Gesundheitsdaten',
+ },
+ {
+ id: 'HT-A02',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'biometrie',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung biometrischer Daten zur eindeutigen Identifizierung',
+ },
+ {
+ id: 'HT-A03',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'genetik',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung genetischer Daten',
+ },
+ {
+ id: 'HT-A04',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'politisch',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung politischer Meinungen',
+ },
+ {
+ id: 'HT-A05',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'religion',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung religiΓΆser oder weltanschaulicher Γberzeugungen',
+ },
+ {
+ id: 'HT-A06',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'gewerkschaft',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung von GewerkschaftszugehΓΆrigkeit',
+ },
+ {
+ id: 'HT-A07',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'sexualleben',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung von Daten zum Sexualleben oder zur sexuellen Orientierung',
+ },
+ {
+ id: 'HT-A08',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'strafrechtlich',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 10 DSGVO',
+ description: 'Verarbeitung strafrechtlicher Verurteilungen',
+ },
+ {
+ id: 'HT-A09',
+ category: 'art9',
+ questionId: 'data_art9',
+ condition: 'CONTAINS',
+ conditionValue: 'ethnisch',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 Abs. 1 DSGVO',
+ description: 'Verarbeitung der rassischen oder ethnischen Herkunft',
+ },
+
+ // ========== B: Vulnerable Gruppen (3 rules) ==========
+ {
+ id: 'HT-B01',
+ category: 'vulnerable',
+ questionId: 'data_minors',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'DSE'],
+ legalReference: 'Art. 8 DSGVO',
+ description: 'Verarbeitung von Daten MinderjΓ€hriger',
+ },
+ {
+ id: 'HT-B02',
+ category: 'vulnerable',
+ questionId: 'data_minors',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L4',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'DSE'],
+ legalReference: 'Art. 8 + Art. 9 DSGVO',
+ description: 'Verarbeitung besonderer Kategorien von Daten MinderjΓ€hriger',
+ combineWithArt9: true,
+ },
+ {
+ id: 'HT-B03',
+ category: 'vulnerable',
+ questionId: 'data_minors',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L4',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'AI_ACT_DOKU'],
+ legalReference: 'Art. 8 DSGVO + AI Act',
+ description: 'KI-gestΓΌtzte Verarbeitung von Daten MinderjΓ€hriger',
+ combineWithAI: true,
+ },
+
+ // ========== C: ADM/KI (6 rules) ==========
+ {
+ id: 'HT-C01',
+ category: 'adm',
+ questionId: 'proc_adm_scoring',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 22 DSGVO',
+ description: 'Automatisierte Einzelentscheidung mit Rechtswirkung oder erheblicher BeeintrΓ€chtigung',
+ },
+ {
+ id: 'HT-C02',
+ category: 'adm',
+ questionId: 'proc_ai_usage',
+ condition: 'CONTAINS',
+ conditionValue: 'autonom',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'AI_ACT_DOKU'],
+ legalReference: 'Art. 22 DSGVO + AI Act',
+ description: 'Autonome KI-Systeme mit Entscheidungsbefugnis',
+ },
+ {
+ id: 'HT-C03',
+ category: 'adm',
+ questionId: 'proc_ai_usage',
+ condition: 'CONTAINS',
+ conditionValue: 'scoring',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM'],
+ legalReference: 'Art. 22 DSGVO',
+ description: 'KI-gestΓΌtztes Scoring',
+ },
+ {
+ id: 'HT-C04',
+ category: 'adm',
+ questionId: 'proc_ai_usage',
+ condition: 'CONTAINS',
+ conditionValue: 'profiling',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 22 DSGVO',
+ description: 'KI-gestΓΌtztes Profiling mit erheblicher Wirkung',
+ },
+ {
+ id: 'HT-C05',
+ category: 'adm',
+ questionId: 'proc_ai_usage',
+ condition: 'CONTAINS',
+ conditionValue: 'generativ',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'AI_ACT_DOKU'],
+ legalReference: 'AI Act',
+ description: 'Generative KI-Systeme',
+ },
+ {
+ id: 'HT-C06',
+ category: 'adm',
+ questionId: 'proc_ai_usage',
+ condition: 'CONTAINS',
+ conditionValue: 'chatbot',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'AI_ACT_DOKU'],
+ legalReference: 'AI Act',
+ description: 'Chatbots mit Personendatenverarbeitung',
+ },
+
+ // ========== D: Γberwachung (5 rules) ==========
+ {
+ id: 'HT-D01',
+ category: 'surveillance',
+ questionId: 'proc_video_surveillance',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSE'],
+ legalReference: 'Art. 6 DSGVO',
+ description: 'VideoΓΌberwachung',
+ },
+ {
+ id: 'HT-D02',
+ category: 'surveillance',
+ questionId: 'proc_employee_monitoring',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 88 DSGVO + BetrVG',
+ description: 'MitarbeiterΓΌberwachung',
+ },
+ {
+ id: 'HT-D03',
+ category: 'surveillance',
+ questionId: 'proc_tracking',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNGEN'],
+ legalReference: 'Art. 6 DSGVO + ePrivacy',
+ description: 'Online-Tracking',
+ },
+ {
+ id: 'HT-D04',
+ category: 'surveillance',
+ questionId: 'proc_video_surveillance',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 35 Abs. 3 DSGVO',
+ description: 'VideoΓΌberwachung kombiniert mit Mitarbeitermonitoring',
+ combineWithEmployeeMonitoring: true,
+ },
+ {
+ id: 'HT-D05',
+ category: 'surveillance',
+ questionId: 'proc_video_surveillance',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 35 Abs. 3 DSGVO',
+ description: 'VideoΓΌberwachung kombiniert mit automatisierter Bewertung',
+ combineWithADM: true,
+ },
+
+ // ========== E: Drittland (5 rules) ==========
+ {
+ id: 'HT-E01',
+ category: 'third_country',
+ questionId: 'tech_third_country',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TRANSFER_DOKU'],
+ legalReference: 'Art. 44 ff. DSGVO',
+ description: 'DatenΓΌbermittlung in Drittland',
+ },
+ {
+ id: 'HT-E02',
+ category: 'third_country',
+ questionId: 'tech_hosting_location',
+ condition: 'EQUALS',
+ conditionValue: 'drittland',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'TRANSFER_DOKU'],
+ legalReference: 'Art. 44 ff. DSGVO',
+ description: 'Hosting in Drittland',
+ },
+ {
+ id: 'HT-E03',
+ category: 'third_country',
+ questionId: 'tech_hosting_location',
+ condition: 'EQUALS',
+ conditionValue: 'us_adequacy',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TRANSFER_DOKU'],
+ legalReference: 'Art. 45 DSGVO',
+ description: 'Hosting in USA mit Angemessenheitsbeschluss',
+ },
+ {
+ id: 'HT-E04',
+ category: 'third_country',
+ questionId: 'tech_third_country',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'TRANSFER_DOKU', 'DSFA'],
+ legalReference: 'Art. 44 ff. + Art. 9 DSGVO',
+ description: 'Drittlandtransfer besonderer Kategorien',
+ combineWithArt9: true,
+ },
+ {
+ id: 'HT-E05',
+ category: 'third_country',
+ questionId: 'tech_third_country',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'TRANSFER_DOKU', 'DSFA'],
+ legalReference: 'Art. 44 ff. + Art. 8 DSGVO',
+ description: 'Drittlandtransfer von Daten MinderjΓ€hriger',
+ combineWithMinors: true,
+ },
+
+ // ========== F: Zertifizierung (5 rules) ==========
+ {
+ id: 'HT-F01',
+ category: 'certification',
+ questionId: 'org_cert_target',
+ condition: 'CONTAINS',
+ conditionValue: 'ISO27001',
+ minimumLevel: 'L4',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST'],
+ legalReference: 'ISO/IEC 27001',
+ description: 'Angestrebte ISO 27001 Zertifizierung',
+ },
+ {
+ id: 'HT-F02',
+ category: 'certification',
+ questionId: 'org_cert_target',
+ condition: 'CONTAINS',
+ conditionValue: 'ISO27701',
+ minimumLevel: 'L4',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TOM', 'VVT', 'AUDIT_CHECKLIST'],
+ legalReference: 'ISO/IEC 27701',
+ description: 'Angestrebte ISO 27701 Zertifizierung',
+ },
+ {
+ id: 'HT-F03',
+ category: 'certification',
+ questionId: 'org_cert_target',
+ condition: 'CONTAINS',
+ conditionValue: 'SOC2',
+ minimumLevel: 'L4',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST'],
+ legalReference: 'SOC 2 Type II',
+ description: 'Angestrebte SOC 2 Zertifizierung',
+ },
+ {
+ id: 'HT-F04',
+ category: 'certification',
+ questionId: 'org_cert_target',
+ condition: 'CONTAINS',
+ conditionValue: 'TISAX',
+ minimumLevel: 'L4',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST', 'VENDOR_MANAGEMENT'],
+ legalReference: 'TISAX',
+ description: 'Angestrebte TISAX Zertifizierung',
+ },
+ {
+ id: 'HT-F05',
+ category: 'certification',
+ questionId: 'org_cert_target',
+ condition: 'CONTAINS',
+ conditionValue: 'BSI-Grundschutz',
+ minimumLevel: 'L4',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST'],
+ legalReference: 'BSI IT-Grundschutz',
+ description: 'Angestrebte BSI-Grundschutz Zertifizierung',
+ },
+
+ // ========== G: Volumen/Skala (5 rules) ==========
+ {
+ id: 'HT-G01',
+ category: 'scale',
+ questionId: 'data_volume',
+ condition: 'EQUALS',
+ conditionValue: '>1000000',
+ minimumLevel: 'L3',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'LOESCHKONZEPT'],
+ legalReference: 'Art. 35 Abs. 3 lit. b DSGVO',
+ description: 'Umfangreiche Verarbeitung personenbezogener Daten (>1 Mio. DatensΓ€tze)',
+ },
+ {
+ id: 'HT-G02',
+ category: 'scale',
+ questionId: 'data_volume',
+ condition: 'EQUALS',
+ conditionValue: '100000-1000000',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM'],
+ legalReference: 'Art. 35 Abs. 3 lit. b DSGVO',
+ description: 'GroΓvolumige Datenverarbeitung (100k-1M DatensΓ€tze)',
+ },
+ {
+ id: 'HT-G03',
+ category: 'scale',
+ questionId: 'org_customer_count',
+ condition: 'EQUALS',
+ conditionValue: '100000+',
+ minimumLevel: 'L3',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSR_PROZESS'],
+ legalReference: 'Art. 15-22 DSGVO',
+ description: 'GroΓer Kundenstamm (>100k) mit hoher Betroffenenanzahl',
+ },
+ {
+ id: 'HT-G04',
+ category: 'scale',
+ questionId: 'org_employee_count',
+ condition: 'GREATER_THAN',
+ conditionValue: 249,
+ minimumLevel: 'L3',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'LOESCHKONZEPT', 'NOTFALLPLAN'],
+ legalReference: 'Art. 37 DSGVO',
+ description: 'GroΓe Organisation (>250 Mitarbeiter) mit erhΓΆhten Compliance-Anforderungen',
+ },
+ {
+ id: 'HT-G05',
+ category: 'scale',
+ questionId: 'org_employee_count',
+ condition: 'GREATER_THAN',
+ conditionValue: 999,
+ minimumLevel: 'L4',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'LOESCHKONZEPT'],
+ legalReference: 'Art. 35 + Art. 37 DSGVO',
+ description: 'Sehr groΓe Organisation (>1000 Mitarbeiter) mit Art. 9 Daten',
+ combineWithArt9: true,
+ },
+
+ // ========== H: Produkt/Business (7 rules) ==========
+ {
+ id: 'HT-H01',
+ category: 'product',
+ questionId: 'prod_webshop',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['DSE', 'AGB', 'COOKIE_BANNER', 'EINWILLIGUNGEN', 'VERBRAUCHERSCHUTZ'],
+ legalReference: 'Art. 6 DSGVO + eCommerce',
+ description: 'E-Commerce / Webshop-Betrieb',
+ },
+ {
+ id: 'HT-H02',
+ category: 'product',
+ questionId: 'prod_data_broker',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNGEN'],
+ legalReference: 'Art. 35 Abs. 3 DSGVO',
+ description: 'Datenhandel oder Datenmakler-TΓ€tigkeit',
+ },
+ {
+ id: 'HT-H03',
+ category: 'product',
+ questionId: 'prod_api_external',
+ condition: 'EQUALS',
+ conditionValue: true,
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['TOM', 'AVV'],
+ legalReference: 'Art. 28 DSGVO',
+ description: 'Externe API mit Datenweitergabe',
+ },
+ {
+ id: 'HT-H04',
+ category: 'product',
+ questionId: 'org_business_model',
+ condition: 'EQUALS',
+ conditionValue: 'b2c',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['DSE', 'COOKIE_BANNER', 'EINWILLIGUNGEN'],
+ legalReference: 'Art. 6 DSGVO',
+ description: 'B2C-GeschΓ€ftsmodell mit Endkundenkontakt',
+ },
+ {
+ id: 'HT-H05',
+ category: 'product',
+ questionId: 'org_industry',
+ condition: 'EQUALS',
+ conditionValue: 'finance',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM'],
+ legalReference: 'Art. 6 DSGVO + Finanzaufsicht',
+ description: 'Finanzbranche mit erhΓΆhten regulatorischen Anforderungen',
+ },
+ {
+ id: 'HT-H06',
+ category: 'product',
+ questionId: 'org_industry',
+ condition: 'EQUALS',
+ conditionValue: 'healthcare',
+ minimumLevel: 'L3',
+ requiresDSFA: true,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
+ legalReference: 'Art. 9 DSGVO + Gesundheitsrecht',
+ description: 'Gesundheitsbranche mit sensiblen Daten',
+ },
+ {
+ id: 'HT-H07',
+ category: 'product',
+ questionId: 'org_industry',
+ condition: 'EQUALS',
+ conditionValue: 'public',
+ minimumLevel: 'L2',
+ requiresDSFA: false,
+ mandatoryDocuments: ['VVT', 'TOM', 'DSR_PROZESS'],
+ legalReference: 'Art. 6 Abs. 1 lit. e DSGVO',
+ description: 'Γffentlicher Sektor',
+ },
+
+ // ========== I: Prozessreife - Gap Flags (5 rules) ==========
+ {
+ id: 'HT-I01',
+ category: 'process_maturity',
+ questionId: 'proc_dsar_process',
+ condition: 'EQUALS',
+ conditionValue: false,
+ minimumLevel: 'L1',
+ requiresDSFA: false,
+ mandatoryDocuments: [],
+ legalReference: 'Art. 15-22 DSGVO',
+ description: 'Fehlender Prozess fΓΌr Betroffenenrechte',
+ },
+ {
+ id: 'HT-I02',
+ category: 'process_maturity',
+ questionId: 'proc_deletion_concept',
+ condition: 'EQUALS',
+ conditionValue: false,
+ minimumLevel: 'L1',
+ requiresDSFA: false,
+ mandatoryDocuments: [],
+ legalReference: 'Art. 17 DSGVO',
+ description: 'Fehlendes LΓΆschkonzept',
+ },
+ {
+ id: 'HT-I03',
+ category: 'process_maturity',
+ questionId: 'proc_incident_response',
+ condition: 'EQUALS',
+ conditionValue: false,
+ minimumLevel: 'L1',
+ requiresDSFA: false,
+ mandatoryDocuments: [],
+ legalReference: 'Art. 33 DSGVO',
+ description: 'Fehlender Incident-Response-Prozess',
+ },
+ {
+ id: 'HT-I04',
+ category: 'process_maturity',
+ questionId: 'proc_regular_audits',
+ condition: 'EQUALS',
+ conditionValue: false,
+ minimumLevel: 'L1',
+ requiresDSFA: false,
+ mandatoryDocuments: [],
+ legalReference: 'Art. 24 DSGVO',
+ description: 'Fehlende regelmΓ€Γige Audits',
+ },
+ {
+ id: 'HT-I05',
+ category: 'process_maturity',
+ questionId: 'comp_training',
+ condition: 'EQUALS',
+ conditionValue: false,
+ minimumLevel: 'L1',
+ requiresDSFA: false,
+ mandatoryDocuments: [],
+ legalReference: 'Art. 39 Abs. 1 lit. b DSGVO',
+ description: 'Fehlende Schulungen zum Datenschutz',
+ },
+]
+
+// ============================================================================
+// COMPLIANCE SCOPE ENGINE
+// ============================================================================
+
+export class ComplianceScopeEngine {
+ /**
+ * Haupteinstiegspunkt: Evaluiert alle Profiling-Antworten und produziert eine ScopeDecision
+ */
+ evaluate(answers: ScopeProfilingAnswer[]): ScopeDecision {
+ const decision = createEmptyScopeDecision()
+
+ // 1. Scores berechnen
+ decision.scores = this.calculateScores(answers)
+
+ // 2. Hard Triggers prΓΌfen
+ decision.triggeredHardTriggers = this.evaluateHardTriggers(answers)
+
+ // 3. Finales Level bestimmen
+ decision.determinedLevel = this.determineLevel(
+ decision.scores,
+ decision.triggeredHardTriggers
+ )
+
+ // 4. Dokumenten-Scope aufbauen
+ decision.requiredDocuments = this.buildDocumentScope(
+ decision.determinedLevel,
+ decision.triggeredHardTriggers,
+ answers
+ )
+
+ // 5. Risk Flags ermitteln
+ decision.riskFlags = this.evaluateRiskFlags(answers, decision.determinedLevel)
+
+ // 6. Gaps berechnen
+ decision.gaps = this.calculateGaps(answers, decision.determinedLevel)
+
+ // 7. Next Actions ableiten
+ decision.nextActions = this.buildNextActions(
+ decision.requiredDocuments,
+ decision.gaps
+ )
+
+ // 8. Reasoning (Audit Trail) aufbauen
+ decision.reasoning = this.buildReasoning(
+ decision.scores,
+ decision.triggeredHardTriggers,
+ decision.determinedLevel,
+ decision.requiredDocuments
+ )
+
+ decision.evaluatedAt = new Date().toISOString()
+
+ return decision
+ }
+
+ /**
+ * Berechnet Risk-, Complexity- und Assurance-Scores aus den Profiling-Antworten
+ */
+ calculateScores(answers: ScopeProfilingAnswer[]): ComplianceScores {
+ let riskSum = 0
+ let complexitySum = 0
+ let assuranceSum = 0
+ let riskWeightSum = 0
+ let complexityWeightSum = 0
+ let assuranceWeightSum = 0
+
+ for (const answer of answers) {
+ const weights = QUESTION_SCORE_WEIGHTS[answer.questionId]
+ if (!weights) continue
+
+ const multiplier = this.getAnswerMultiplier(answer)
+
+ riskSum += weights.risk * multiplier
+ complexitySum += weights.complexity * multiplier
+ assuranceSum += weights.assurance * multiplier
+
+ riskWeightSum += weights.risk
+ complexityWeightSum += weights.complexity
+ assuranceWeightSum += weights.assurance
+ }
+
+ const riskScore =
+ riskWeightSum > 0 ? (riskSum / riskWeightSum) * 10 : 0
+ const complexityScore =
+ complexityWeightSum > 0 ? (complexitySum / complexityWeightSum) * 10 : 0
+ const assuranceScore =
+ assuranceWeightSum > 0 ? (assuranceSum / assuranceWeightSum) * 10 : 0
+
+ const composite = riskScore * 0.4 + complexityScore * 0.3 + assuranceScore * 0.3
+
+ return {
+ risk: Math.round(riskScore * 10) / 10,
+ complexity: Math.round(complexityScore * 10) / 10,
+ assurance: Math.round(assuranceScore * 10) / 10,
+ composite: Math.round(composite * 10) / 10,
+ }
+ }
+
+ /**
+ * Bestimmt den Multiplikator fΓΌr eine Antwort (0.0 - 1.0)
+ */
+ private getAnswerMultiplier(answer: ScopeProfilingAnswer): number {
+ const { questionId, answerValue } = answer
+
+ // Boolean
+ if (typeof answerValue === 'boolean') {
+ return answerValue ? 1.0 : 0.0
+ }
+
+ // Number
+ if (typeof answerValue === 'number') {
+ return this.normalizeNumericAnswer(questionId, answerValue)
+ }
+
+ // Single choice
+ if (typeof answerValue === 'string') {
+ const multipliers = ANSWER_MULTIPLIERS[questionId]
+ if (multipliers && multipliers[answerValue] !== undefined) {
+ return multipliers[answerValue]
+ }
+ return 0.5 // Fallback
+ }
+
+ // Multi choice
+ if (Array.isArray(answerValue)) {
+ if (answerValue.length === 0) return 0.0
+ // Simplified: count selected items
+ return Math.min(answerValue.length / 5, 1.0)
+ }
+
+ return 0.0
+ }
+
+ /**
+ * Normalisiert numerische Antworten
+ */
+ private normalizeNumericAnswer(questionId: string, value: number): number {
+ // Hier kΓΆnnten spezifische Ranges definiert werden
+ // Vereinfacht: logarithmische Normalisierung
+ if (value <= 0) return 0.0
+ if (value >= 1000) return 1.0
+ return Math.log10(value + 1) / 3 // 0-1000 β ~0-1
+ }
+
+ /**
+ * Evaluiert Hard Trigger Rules
+ */
+ evaluateHardTriggers(answers: ScopeProfilingAnswer[]): TriggeredHardTrigger[] {
+ const triggered: TriggeredHardTrigger[] = []
+ const answerMap = new Map(answers.map((a) => [a.questionId, a.answerValue]))
+
+ for (const rule of HARD_TRIGGER_RULES) {
+ const isTriggered = this.checkTriggerCondition(rule, answerMap, answers)
+
+ if (isTriggered) {
+ triggered.push({
+ ruleId: rule.id,
+ category: rule.category,
+ description: rule.description,
+ legalReference: rule.legalReference,
+ minimumLevel: rule.minimumLevel,
+ requiresDSFA: rule.requiresDSFA,
+ mandatoryDocuments: rule.mandatoryDocuments,
+ })
+ }
+ }
+
+ return triggered
+ }
+
+ /**
+ * PrΓΌft, ob eine Trigger-Regel erfΓΌllt ist
+ */
+ private checkTriggerCondition(
+ rule: HardTriggerRule,
+ answerMap: Map,
+ answers: ScopeProfilingAnswer[]
+ ): boolean {
+ const answerValue = answerMap.get(rule.questionId)
+ if (answerValue === undefined) return false
+
+ // Basis-Check
+ let baseCondition = false
+
+ switch (rule.condition) {
+ case 'EQUALS':
+ baseCondition = answerValue === rule.conditionValue
+ break
+ case 'CONTAINS':
+ if (Array.isArray(answerValue)) {
+ baseCondition = answerValue.includes(rule.conditionValue)
+ } else if (typeof answerValue === 'string') {
+ baseCondition = answerValue.includes(rule.conditionValue)
+ }
+ break
+ case 'IN':
+ if (Array.isArray(rule.conditionValue)) {
+ baseCondition = rule.conditionValue.includes(answerValue)
+ }
+ break
+ case 'GREATER_THAN':
+ if (typeof answerValue === 'number' && typeof rule.conditionValue === 'number') {
+ baseCondition = answerValue > rule.conditionValue
+ } else if (typeof answerValue === 'string') {
+ // Parse employee count from string like "1000+"
+ const parsed = this.parseEmployeeCount(answerValue)
+ baseCondition = parsed > (rule.conditionValue as number)
+ }
+ break
+ case 'NOT_EQUALS':
+ baseCondition = answerValue !== rule.conditionValue
+ break
+ }
+
+ if (!baseCondition) return false
+
+ // Combined checks
+ if (rule.combineWithArt9) {
+ const art9 = answerMap.get('data_art9')
+ if (!art9 || (Array.isArray(art9) && art9.length === 0)) return false
+ }
+
+ if (rule.combineWithMinors) {
+ const minors = answerMap.get('data_minors')
+ if (minors !== true) return false
+ }
+
+ if (rule.combineWithAI) {
+ const ai = answerMap.get('proc_ai_usage')
+ if (!ai || (Array.isArray(ai) && (ai.length === 0 || ai.includes('keine')))) {
+ return false
+ }
+ }
+
+ if (rule.combineWithEmployeeMonitoring) {
+ const empMon = answerMap.get('proc_employee_monitoring')
+ if (empMon !== true) return false
+ }
+
+ if (rule.combineWithADM) {
+ const adm = answerMap.get('proc_adm_scoring')
+ if (adm !== true) return false
+ }
+
+ return true
+ }
+
+ /**
+ * Parsed Mitarbeiterzahl aus String
+ */
+ private parseEmployeeCount(value: string): number {
+ if (value === '1-9') return 9
+ if (value === '10-49') return 49
+ if (value === '50-249') return 249
+ if (value === '250-999') return 999
+ if (value === '1000+') return 1000
+ return 0
+ }
+
+ /**
+ * Bestimmt das finale Compliance-Level basierend auf Scores und Triggers
+ */
+ determineLevel(
+ scores: ComplianceScores,
+ triggers: TriggeredHardTrigger[]
+ ): ComplianceDepthLevel {
+ // Score-basiertes Level
+ let levelFromScore: ComplianceDepthLevel
+ if (scores.composite <= 25) levelFromScore = 'L1'
+ else if (scores.composite <= 50) levelFromScore = 'L2'
+ else if (scores.composite <= 75) levelFromScore = 'L3'
+ else levelFromScore = 'L4'
+
+ // HΓΆchstes Level aus Triggers
+ let maxTriggerLevel: ComplianceDepthLevel = 'L1'
+ for (const trigger of triggers) {
+ if (getDepthLevelNumeric(trigger.minimumLevel) > getDepthLevelNumeric(maxTriggerLevel)) {
+ maxTriggerLevel = trigger.minimumLevel
+ }
+ }
+
+ // Maximum von beiden
+ return maxDepthLevel(levelFromScore, maxTriggerLevel)
+ }
+
+ /**
+ * Baut den Dokumenten-Scope basierend auf Level und Triggers
+ */
+ buildDocumentScope(
+ level: ComplianceDepthLevel,
+ triggers: TriggeredHardTrigger[],
+ answers: ScopeProfilingAnswer[]
+ ): RequiredDocument[] {
+ const requiredDocs: RequiredDocument[] = []
+ const mandatoryFromTriggers = new Set()
+
+ // Sammle mandatory docs aus Triggern
+ for (const trigger of triggers) {
+ for (const doc of trigger.mandatoryDocuments) {
+ mandatoryFromTriggers.add(doc as ScopeDocumentType)
+ }
+ }
+
+ // FΓΌr jeden Dokumenttyp prΓΌfen
+ for (const docType of Object.keys(DOCUMENT_SCOPE_MATRIX) as ScopeDocumentType[]) {
+ const requirement = DOCUMENT_SCOPE_MATRIX[docType][level]
+ const isMandatoryFromTrigger = mandatoryFromTriggers.has(docType)
+
+ if (requirement === 'mandatory' || isMandatoryFromTrigger) {
+ requiredDocs.push({
+ documentType: docType,
+ label: DOCUMENT_TYPE_LABELS[docType],
+ requirement: 'mandatory',
+ priority: this.getDocumentPriority(docType, isMandatoryFromTrigger),
+ estimatedEffort: this.estimateEffort(docType),
+ sdkStepUrl: DOCUMENT_SDK_STEP_MAP[docType],
+ triggeredBy: isMandatoryFromTrigger
+ ? triggers
+ .filter((t) => t.mandatoryDocuments.includes(docType as any))
+ .map((t) => t.ruleId)
+ : [],
+ })
+ } else if (requirement === 'recommended') {
+ requiredDocs.push({
+ documentType: docType,
+ label: DOCUMENT_TYPE_LABELS[docType],
+ requirement: 'recommended',
+ priority: 'medium',
+ estimatedEffort: this.estimateEffort(docType),
+ sdkStepUrl: DOCUMENT_SDK_STEP_MAP[docType],
+ triggeredBy: [],
+ })
+ }
+ }
+
+ // Sortieren: mandatory zuerst, dann nach Priority
+ requiredDocs.sort((a, b) => {
+ if (a.requirement === 'mandatory' && b.requirement !== 'mandatory') return -1
+ if (a.requirement !== 'mandatory' && b.requirement === 'mandatory') return 1
+
+ const priorityOrder: Record = { high: 3, medium: 2, low: 1 }
+ return priorityOrder[b.priority] - priorityOrder[a.priority]
+ })
+
+ return requiredDocs
+ }
+
+ /**
+ * Bestimmt die PrioritΓ€t eines Dokuments
+ */
+ private getDocumentPriority(
+ docType: ScopeDocumentType,
+ isMandatoryFromTrigger: boolean
+ ): 'high' | 'medium' | 'low' {
+ if (isMandatoryFromTrigger) return 'high'
+
+ // Basis-Dokumente haben hohe PrioritΓ€t
+ if (['VVT', 'TOM', 'DSE'].includes(docType)) return 'high'
+ if (['DSFA', 'AVV', 'EINWILLIGUNGEN'].includes(docType)) return 'high'
+
+ return 'medium'
+ }
+
+ /**
+ * SchΓ€tzt den Aufwand fΓΌr ein Dokument (in Stunden)
+ */
+ private estimateEffort(docType: ScopeDocumentType): number {
+ const effortMap: Record = {
+ VVT: 8,
+ TOM: 12,
+ DSFA: 16,
+ AVV: 4,
+ DSE: 6,
+ EINWILLIGUNGEN: 6,
+ LOESCHKONZEPT: 10,
+ TRANSFER_DOKU: 8,
+ DSR_PROZESS: 8,
+ NOTFALLPLAN: 12,
+ COOKIE_BANNER: 4,
+ AGB: 6,
+ VERBRAUCHERSCHUTZ: 4,
+ AUDIT_CHECKLIST: 8,
+ VENDOR_MANAGEMENT: 10,
+ AI_ACT_DOKU: 12,
+ }
+ return effortMap[docType] || 6
+ }
+
+ /**
+ * Evaluiert Risk Flags basierend auf Process Maturity Gaps und anderen Risiken
+ */
+ evaluateRiskFlags(
+ answers: ScopeProfilingAnswer[],
+ level: ComplianceDepthLevel
+ ): RiskFlag[] {
+ const flags: RiskFlag[] = []
+ const answerMap = new Map(answers.map((a) => [a.questionId, a.answerValue]))
+
+ // Process Maturity Gaps (Kategorie I Trigger)
+ const maturityRules = HARD_TRIGGER_RULES.filter((r) => r.category === 'process_maturity')
+ for (const rule of maturityRules) {
+ if (this.checkTriggerCondition(rule, answerMap, answers)) {
+ flags.push({
+ severity: 'medium',
+ category: 'process',
+ message: rule.description,
+ legalReference: rule.legalReference,
+ recommendation: this.getMaturityRecommendation(rule.id),
+ })
+ }
+ }
+
+ // VerschlΓΌsselung fehlt bei L2+
+ if (getDepthLevelNumeric(level) >= 2) {
+ const encTransit = answerMap.get('tech_encryption_transit')
+ const encRest = answerMap.get('tech_encryption_rest')
+
+ if (encTransit === false) {
+ flags.push({
+ severity: 'high',
+ category: 'technical',
+ message: 'Fehlende VerschlΓΌsselung bei DatenΓΌbertragung',
+ legalReference: 'Art. 32 DSGVO',
+ recommendation: 'TLS 1.2+ fΓΌr alle DatenΓΌbertragungen implementieren',
+ })
+ }
+
+ if (encRest === false) {
+ flags.push({
+ severity: 'high',
+ category: 'technical',
+ message: 'Fehlende VerschlΓΌsselung gespeicherter Daten',
+ legalReference: 'Art. 32 DSGVO',
+ recommendation: 'VerschlΓΌsselung at-rest fΓΌr sensitive Daten implementieren',
+ })
+ }
+ }
+
+ // Drittland ohne adΓ€quate Grundlage
+ const thirdCountry = answerMap.get('tech_third_country')
+ const hostingLocation = answerMap.get('tech_hosting_location')
+ if (
+ thirdCountry === true &&
+ hostingLocation !== 'eu' &&
+ hostingLocation !== 'eu_us_adequacy'
+ ) {
+ flags.push({
+ severity: 'high',
+ category: 'legal',
+ message: 'Drittlandtransfer ohne angemessene Garantien',
+ legalReference: 'Art. 44 ff. DSGVO',
+ recommendation:
+ 'Standardvertragsklauseln (SCCs) oder Binding Corporate Rules (BCRs) implementieren',
+ })
+ }
+
+ // Fehlender DSB bei groΓen Organisationen
+ const hasDPO = answerMap.get('org_has_dpo')
+ const employeeCount = answerMap.get('org_employee_count')
+ if (hasDPO === false && this.parseEmployeeCount(employeeCount as string) >= 250) {
+ flags.push({
+ severity: 'medium',
+ category: 'organizational',
+ message: 'Kein Datenschutzbeauftragter bei groΓer Organisation',
+ legalReference: 'Art. 37 DSGVO',
+ recommendation: 'Bestellung eines Datenschutzbeauftragten prΓΌfen',
+ })
+ }
+
+ return flags
+ }
+
+ /**
+ * Gibt Empfehlung fΓΌr Maturity Gap
+ */
+ private getMaturityRecommendation(ruleId: string): string {
+ const recommendations: Record = {
+ 'HT-I01': 'Prozess fΓΌr Betroffenenrechte (DSAR) etablieren und dokumentieren',
+ 'HT-I02': 'Lâschkonzept gemÀà Art. 17 DSGVO entwickeln und implementieren',
+ 'HT-I03':
+ 'Incident-Response-Plan fΓΌr Datenschutzverletzungen (Art. 33 DSGVO) erstellen',
+ 'HT-I04': 'RegelmΓ€Γige interne Audits und Reviews einfΓΌhren',
+ 'HT-I05': 'Schulungsprogramm fΓΌr Mitarbeiter zum Datenschutz etablieren',
+ }
+ return recommendations[ruleId] || 'Prozess etablieren und dokumentieren'
+ }
+
+ /**
+ * Berechnet Gaps zwischen Ist-Zustand und Soll-Anforderungen
+ */
+ calculateGaps(
+ answers: ScopeProfilingAnswer[],
+ level: ComplianceDepthLevel
+ ): ScopeGap[] {
+ const gaps: ScopeGap[] = []
+ const answerMap = new Map(answers.map((a) => [a.questionId, a.answerValue]))
+
+ // DSFA Gap (bei L3+)
+ if (getDepthLevelNumeric(level) >= 3) {
+ const hasDSFA = answerMap.get('proc_regular_audits') // Proxy
+ if (hasDSFA === false) {
+ gaps.push({
+ gapType: 'documentation',
+ severity: 'high',
+ description: 'Datenschutz-FolgenabschΓ€tzung (DSFA) fehlt',
+ requiredFor: level,
+ currentState: 'Keine DSFA durchgefΓΌhrt',
+ targetState: 'DSFA fΓΌr Hochrisiko-Verarbeitungen durchgefΓΌhrt und dokumentiert',
+ effort: 16,
+ priority: 'high',
+ })
+ }
+ }
+
+ // LΓΆschkonzept Gap
+ const hasDeletion = answerMap.get('proc_deletion_concept')
+ if (hasDeletion === false && getDepthLevelNumeric(level) >= 2) {
+ gaps.push({
+ gapType: 'process',
+ severity: 'medium',
+ description: 'LΓΆschkonzept fehlt',
+ requiredFor: level,
+ currentState: 'Kein systematisches LΓΆschkonzept',
+ targetState: 'Dokumentiertes LΓΆschkonzept mit definierten Fristen',
+ effort: 10,
+ priority: 'high',
+ })
+ }
+
+ // DSAR Prozess Gap
+ const hasDSAR = answerMap.get('proc_dsar_process')
+ if (hasDSAR === false) {
+ gaps.push({
+ gapType: 'process',
+ severity: 'high',
+ description: 'Prozess fΓΌr Betroffenenrechte fehlt',
+ requiredFor: level,
+ currentState: 'Kein etablierter DSAR-Prozess',
+ targetState: 'Dokumentierter Prozess zur Bearbeitung von Betroffenenrechten',
+ effort: 8,
+ priority: 'high',
+ })
+ }
+
+ // Incident Response Gap
+ const hasIncident = answerMap.get('proc_incident_response')
+ if (hasIncident === false) {
+ gaps.push({
+ gapType: 'process',
+ severity: 'high',
+ description: 'Incident-Response-Plan fehlt',
+ requiredFor: level,
+ currentState: 'Kein Prozess fΓΌr Datenschutzverletzungen',
+ targetState: 'Dokumentierter Incident-Response-Plan gemÀà Art. 33 DSGVO',
+ effort: 12,
+ priority: 'high',
+ })
+ }
+
+ // Schulungen Gap
+ const hasTraining = answerMap.get('comp_training')
+ if (hasTraining === false && getDepthLevelNumeric(level) >= 2) {
+ gaps.push({
+ gapType: 'organizational',
+ severity: 'medium',
+ description: 'Datenschutzschulungen fehlen',
+ requiredFor: level,
+ currentState: 'Keine regelmΓ€Γigen Schulungen',
+ targetState: 'Etabliertes Schulungsprogramm fΓΌr alle Mitarbeiter',
+ effort: 6,
+ priority: 'medium',
+ })
+ }
+
+ return gaps
+ }
+
+ /**
+ * Baut priorisierte Next Actions aus Required Documents und Gaps
+ */
+ buildNextActions(
+ requiredDocuments: RequiredDocument[],
+ gaps: ScopeGap[]
+ ): NextAction[] {
+ const actions: NextAction[] = []
+
+ // Dokumente zu Actions
+ for (const doc of requiredDocuments) {
+ if (doc.requirement === 'mandatory') {
+ actions.push({
+ actionType: 'create_document',
+ title: `${doc.label} erstellen`,
+ description: `Pflichtdokument fΓΌr Compliance-Level erstellen`,
+ priority: doc.priority,
+ estimatedEffort: doc.estimatedEffort,
+ documentType: doc.documentType,
+ sdkStepUrl: doc.sdkStepUrl,
+ blockers: [],
+ })
+ }
+ }
+
+ // Gaps zu Actions
+ for (const gap of gaps) {
+ let actionType: NextAction['actionType'] = 'establish_process'
+ if (gap.gapType === 'documentation') actionType = 'create_document'
+ else if (gap.gapType === 'technical') actionType = 'implement_technical'
+ else if (gap.gapType === 'organizational') actionType = 'organizational_change'
+
+ actions.push({
+ actionType,
+ title: `Gap schlieΓen: ${gap.description}`,
+ description: `Von "${gap.currentState}" zu "${gap.targetState}"`,
+ priority: gap.priority,
+ estimatedEffort: gap.effort,
+ blockers: [],
+ })
+ }
+
+ // Nach Priority sortieren
+ const priorityOrder: Record = { high: 3, medium: 2, low: 1 }
+ actions.sort((a, b) => priorityOrder[b.priority] - priorityOrder[a.priority])
+
+ return actions
+ }
+
+ /**
+ * Baut Reasoning (Audit Trail) fΓΌr Transparenz
+ */
+ buildReasoning(
+ scores: ComplianceScores,
+ triggers: TriggeredHardTrigger[],
+ level: ComplianceDepthLevel,
+ docs: RequiredDocument[]
+ ): ScopeReasoning[] {
+ const reasoning: ScopeReasoning[] = []
+
+ // 1. Score-Berechnung
+ reasoning.push({
+ step: 'score_calculation',
+ description: 'Risikobasierte Score-Berechnung aus Profiling-Antworten',
+ factors: [
+ `Risiko-Score: ${scores.risk}/10`,
+ `KomplexitΓ€ts-Score: ${scores.complexity}/10`,
+ `Assurance-Score: ${scores.assurance}/10`,
+ `Composite Score: ${scores.composite}/10`,
+ ],
+ impact: `Score-basiertes Level: ${this.getLevelFromScore(scores.composite)}`,
+ })
+
+ // 2. Hard Trigger Evaluation
+ if (triggers.length > 0) {
+ reasoning.push({
+ step: 'hard_trigger_evaluation',
+ description: `${triggers.length} Hard Trigger Rule(s) aktiviert`,
+ factors: triggers.map(
+ (t) => `${t.ruleId}: ${t.description} (${t.legalReference})`
+ ),
+ impact: `HΓΆchstes Trigger-Level: ${this.getMaxTriggerLevel(triggers)}`,
+ })
+ }
+
+ // 3. Level-Bestimmung
+ reasoning.push({
+ step: 'level_determination',
+ description: 'Finales Compliance-Level durch Maximum aus Score und Triggers',
+ factors: [
+ `Score-Level: ${this.getLevelFromScore(scores.composite)}`,
+ `Trigger-Level: ${this.getMaxTriggerLevel(triggers)}`,
+ ],
+ impact: `Finales Level: ${level}`,
+ })
+
+ // 4. Dokumenten-Scope
+ const mandatoryDocs = docs.filter((d) => d.requirement === 'mandatory')
+ reasoning.push({
+ step: 'document_scope',
+ description: `Dokumenten-Scope fΓΌr ${level} bestimmt`,
+ factors: [
+ `${mandatoryDocs.length} Pflichtdokumente`,
+ `${docs.length - mandatoryDocs.length} empfohlene Dokumente`,
+ ],
+ impact: `Gesamtaufwand: ~${docs.reduce((sum, d) => sum + d.estimatedEffort, 0)} Stunden`,
+ })
+
+ return reasoning
+ }
+
+ /**
+ * Hilfsfunktion: Level aus Score ableiten
+ */
+ private getLevelFromScore(composite: number): ComplianceDepthLevel {
+ if (composite <= 25) return 'L1'
+ if (composite <= 50) return 'L2'
+ if (composite <= 75) return 'L3'
+ return 'L4'
+ }
+
+ /**
+ * Hilfsfunktion: HΓΆchstes Level aus Triggern
+ */
+ private getMaxTriggerLevel(triggers: TriggeredHardTrigger[]): ComplianceDepthLevel {
+ if (triggers.length === 0) return 'L1'
+ let max: ComplianceDepthLevel = 'L1'
+ for (const t of triggers) {
+ if (getDepthLevelNumeric(t.minimumLevel) > getDepthLevelNumeric(max)) {
+ max = t.minimumLevel
+ }
+ }
+ return max
+ }
+}
+
+// ============================================================================
+// SINGLETON EXPORT
+// ============================================================================
+
+export const complianceScopeEngine = new ComplianceScopeEngine()
diff --git a/admin-v2/lib/sdk/compliance-scope-golden-tests.ts b/admin-v2/lib/sdk/compliance-scope-golden-tests.ts
new file mode 100644
index 0000000..6a31b7c
--- /dev/null
+++ b/admin-v2/lib/sdk/compliance-scope-golden-tests.ts
@@ -0,0 +1,722 @@
+import type { ScopeProfilingAnswer, ComplianceDepthLevel, ScopeDocumentType } from './compliance-scope-types'
+
+export interface GoldenTest {
+ id: string
+ name: string
+ description: string
+ answers: ScopeProfilingAnswer[]
+ expectedLevel: ComplianceDepthLevel | null // null for prefill tests
+ expectedMinDocuments?: ScopeDocumentType[]
+ expectedHardTriggerIds?: string[]
+ expectedDsfaRequired?: boolean
+ tags: string[]
+}
+
+export const GOLDEN_TESTS: GoldenTest[] = [
+ // GT-01: 2-Person Freelancer, nur B2B, DE-Hosting β L1
+ {
+ id: 'GT-01',
+ name: '2-Person Freelancer B2B',
+ description: 'Kleinstes Setup ohne besondere Risiken',
+ answers: [
+ { questionId: 'org_employee_count', value: '2' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'consulting' },
+ { questionId: 'data_health', value: false },
+ { questionId: 'data_genetic', value: false },
+ { questionId: 'data_biometric', value: false },
+ { questionId: 'data_racial_ethnic', value: false },
+ { questionId: 'data_political_opinion', value: false },
+ { questionId: 'data_religious', value: false },
+ { questionId: 'data_union_membership', value: false },
+ { questionId: 'data_sexual_orientation', value: false },
+ { questionId: 'data_criminal', value: false },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ { questionId: 'process_has_dsfa', value: true },
+ { questionId: 'process_has_incident_plan', value: true },
+ { questionId: 'data_volume', value: '<1000' },
+ { questionId: 'org_customer_count', value: '<100' },
+ ],
+ expectedLevel: 'L1',
+ expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
+ expectedHardTriggerIds: [],
+ expectedDsfaRequired: false,
+ tags: ['baseline', 'freelancer', 'b2b'],
+ },
+
+ // GT-02: Solo IT-Berater β L1
+ {
+ id: 'GT-02',
+ name: 'Solo IT-Berater',
+ description: 'Einzelperson, minimale Datenverarbeitung',
+ answers: [
+ { questionId: 'org_employee_count', value: '1' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'it_services' },
+ { questionId: 'data_health', value: false },
+ { questionId: 'data_genetic', value: false },
+ { questionId: 'data_biometric', value: false },
+ { questionId: 'data_volume', value: '<1000' },
+ { questionId: 'org_customer_count', value: '<50' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L1',
+ expectedHardTriggerIds: [],
+ tags: ['baseline', 'solo', 'minimal'],
+ },
+
+ // GT-03: 5-Person Agentur, Website, kein Tracking β L1
+ {
+ id: 'GT-03',
+ name: '5-Person Agentur ohne Tracking',
+ description: 'Kleine Agentur, einfache Website ohne Analytics',
+ answers: [
+ { questionId: 'org_employee_count', value: '5' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'marketing' },
+ { questionId: 'tech_has_website', value: true },
+ { questionId: 'tech_has_tracking', value: false },
+ { questionId: 'data_volume', value: '1000-10000' },
+ { questionId: 'org_customer_count', value: '100-1000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L1',
+ expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
+ tags: ['baseline', 'agency', 'simple'],
+ },
+
+ // GT-04: 30-Person SaaS B2B, EU-Cloud β L2 (scale trigger)
+ {
+ id: 'GT-04',
+ name: '30-Person SaaS B2B',
+ description: 'Scale-Trigger durch Mitarbeiterzahl',
+ answers: [
+ { questionId: 'org_employee_count', value: '30' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'software' },
+ { questionId: 'tech_has_cloud', value: true },
+ { questionId: 'data_volume', value: '10000-100000' },
+ { questionId: 'org_customer_count', value: '1000-10000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ { questionId: 'process_has_dsfa', value: false },
+ ],
+ expectedLevel: 'L2',
+ expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER'],
+ tags: ['scale', 'saas', 'growth'],
+ },
+
+ // GT-05: 50-Person Handel B2C, Webshop β L2 (B2C+Webshop)
+ {
+ id: 'GT-05',
+ name: '50-Person E-Commerce B2C',
+ description: 'B2C mit Webshop erhΓΆht Anforderungen',
+ answers: [
+ { questionId: 'org_employee_count', value: '50' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'retail' },
+ { questionId: 'tech_has_webshop', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'org_customer_count', value: '10000-100000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L2',
+ expectedHardTriggerIds: ['HT-H01'],
+ expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER', 'EINWILLIGUNG'],
+ tags: ['b2c', 'webshop', 'retail'],
+ },
+
+ // GT-06: 80-Person Dienstleister, Cloud β L2 (scale)
+ {
+ id: 'GT-06',
+ name: '80-Person Dienstleister',
+ description: 'GrΓΆΓerer Betrieb mit Cloud-Services',
+ answers: [
+ { questionId: 'org_employee_count', value: '80' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'professional_services' },
+ { questionId: 'tech_has_cloud', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'org_customer_count', value: '1000-10000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L2',
+ expectedMinDocuments: ['VVT', 'TOM', 'AVV'],
+ tags: ['scale', 'services'],
+ },
+
+ // GT-07: 20-Person Startup mit GA4 Tracking β L2 (tracking)
+ {
+ id: 'GT-07',
+ name: 'Startup mit Google Analytics',
+ description: 'Tracking-Tools erhΓΆhen Compliance-Anforderungen',
+ answers: [
+ { questionId: 'org_employee_count', value: '20' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'technology' },
+ { questionId: 'tech_has_website', value: true },
+ { questionId: 'tech_has_tracking', value: true },
+ { questionId: 'tech_tracking_tools', value: 'google_analytics' },
+ { questionId: 'data_volume', value: '10000-100000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L2',
+ expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNG'],
+ tags: ['tracking', 'analytics', 'startup'],
+ },
+
+ // GT-08: Kita-App (Minderjaehrige) β L3 (HT-B01)
+ {
+ id: 'GT-08',
+ name: 'Kita-App fΓΌr Eltern',
+ description: 'Datenverarbeitung von MinderjΓ€hrigen unter 16',
+ answers: [
+ { questionId: 'org_employee_count', value: '15' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'education' },
+ { questionId: 'data_subjects_minors', value: true },
+ { questionId: 'data_subjects_minors_age', value: '<16' },
+ { questionId: 'data_volume', value: '1000-10000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-B01'],
+ expectedDsfaRequired: true,
+ expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNG', 'AVV'],
+ tags: ['hard-trigger', 'minors', 'education'],
+ },
+
+ // GT-09: Krankenhaus-Software β L3 (HT-A01)
+ {
+ id: 'GT-09',
+ name: 'Krankenhaus-Verwaltungssoftware',
+ description: 'Gesundheitsdaten Art. 9 DSGVO',
+ answers: [
+ { questionId: 'org_employee_count', value: '200' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'healthcare' },
+ { questionId: 'data_health', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'org_customer_count', value: '10-50' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-A01'],
+ expectedDsfaRequired: true,
+ expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
+ tags: ['hard-trigger', 'health', 'art9'],
+ },
+
+ // GT-10: HR-Scoring-Plattform β L3 (HT-C01)
+ {
+ id: 'GT-10',
+ name: 'HR-Scoring fΓΌr Bewerbungen',
+ description: 'Automatisierte Entscheidungen im HR-Bereich',
+ answers: [
+ { questionId: 'org_employee_count', value: '40' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'hr_tech' },
+ { questionId: 'tech_has_adm', value: true },
+ { questionId: 'tech_adm_type', value: 'profiling' },
+ { questionId: 'tech_adm_impact', value: 'employment' },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-C01'],
+ expectedDsfaRequired: true,
+ expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
+ tags: ['hard-trigger', 'adm', 'profiling'],
+ },
+
+ // GT-11: Fintech Kreditscoring β L3 (HT-H05 + C01)
+ {
+ id: 'GT-11',
+ name: 'Fintech Kreditscoring',
+ description: 'Finanzsektor mit automatisierten Entscheidungen',
+ answers: [
+ { questionId: 'org_employee_count', value: '120' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'finance' },
+ { questionId: 'tech_has_adm', value: true },
+ { questionId: 'tech_adm_type', value: 'scoring' },
+ { questionId: 'tech_adm_impact', value: 'credit' },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-H05', 'HT-C01'],
+ expectedDsfaRequired: true,
+ expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
+ tags: ['hard-trigger', 'finance', 'scoring'],
+ },
+
+ // GT-12: Bildungsplattform Minderjaehrige β L3 (HT-B01)
+ {
+ id: 'GT-12',
+ name: 'Online-Lernplattform fΓΌr SchΓΌler',
+ description: 'Bildungssektor mit minderjΓ€hrigen Nutzern',
+ answers: [
+ { questionId: 'org_employee_count', value: '35' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'education' },
+ { questionId: 'data_subjects_minors', value: true },
+ { questionId: 'data_subjects_minors_age', value: '<16' },
+ { questionId: 'tech_has_tracking', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-B01'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'education', 'minors'],
+ },
+
+ // GT-13: Datenbroker β L3 (HT-H02)
+ {
+ id: 'GT-13',
+ name: 'Datenbroker / Adresshandel',
+ description: 'GeschΓ€ftsmodell basiert auf Datenhandel',
+ answers: [
+ { questionId: 'org_employee_count', value: '25' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'data_broker' },
+ { questionId: 'data_is_core_business', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'org_customer_count', value: '100-1000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-H02'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'data-broker'],
+ },
+
+ // GT-14: Video + ADM β L3 (HT-D05)
+ {
+ id: 'GT-14',
+ name: 'VideoΓΌberwachung mit Gesichtserkennung',
+ description: 'Biometrische Daten mit automatisierter Verarbeitung',
+ answers: [
+ { questionId: 'org_employee_count', value: '60' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'security' },
+ { questionId: 'data_biometric', value: true },
+ { questionId: 'tech_has_video_surveillance', value: true },
+ { questionId: 'tech_has_adm', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-D05'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'biometric', 'video'],
+ },
+
+ // GT-15: 500-MA Konzern ohne Zert β L3 (HT-G04)
+ {
+ id: 'GT-15',
+ name: 'GroΓunternehmen ohne Zertifizierung',
+ description: 'Scale-Trigger durch UnternehmensgrΓΆΓe',
+ answers: [
+ { questionId: 'org_employee_count', value: '500' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'manufacturing' },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'org_customer_count', value: '>100000' },
+ { questionId: 'cert_has_iso27001', value: false },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-G04'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'scale', 'enterprise'],
+ },
+
+ // GT-16: ISO 27001 Anbieter β L4 (HT-F01)
+ {
+ id: 'GT-16',
+ name: 'ISO 27001 zertifizierter Cloud-Provider',
+ description: 'Zertifizierung erfordert hΓΆchste Compliance',
+ answers: [
+ { questionId: 'org_employee_count', value: '150' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'cloud_services' },
+ { questionId: 'cert_has_iso27001', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ { questionId: 'process_has_dsfa', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-F01'],
+ expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV', 'CERT_ISO27001'],
+ tags: ['hard-trigger', 'certification', 'iso'],
+ },
+
+ // GT-17: TISAX Automobilzulieferer β L4 (HT-F04)
+ {
+ id: 'GT-17',
+ name: 'TISAX-zertifizierter Automobilzulieferer',
+ description: 'Automotive-Branche mit TISAX-Anforderungen',
+ answers: [
+ { questionId: 'org_employee_count', value: '300' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'automotive' },
+ { questionId: 'cert_has_tisax', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'org_customer_count', value: '10-50' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-F04'],
+ tags: ['hard-trigger', 'certification', 'tisax'],
+ },
+
+ // GT-18: ISO 27701 Cloud-Provider β L4 (HT-F02)
+ {
+ id: 'GT-18',
+ name: 'ISO 27701 Privacy-zertifiziert',
+ description: 'Privacy-spezifische Zertifizierung',
+ answers: [
+ { questionId: 'org_employee_count', value: '200' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'cloud_services' },
+ { questionId: 'cert_has_iso27701', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ { questionId: 'process_has_dsfa', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-F02'],
+ tags: ['hard-trigger', 'certification', 'privacy'],
+ },
+
+ // GT-19: Grosskonzern + Art.9 + >1M DS β L4 (HT-G05)
+ {
+ id: 'GT-19',
+ name: 'Konzern mit sensiblen Massendaten',
+ description: 'Kombination aus Scale und Art. 9 Daten',
+ answers: [
+ { questionId: 'org_employee_count', value: '2000' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'insurance' },
+ { questionId: 'data_health', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'org_customer_count', value: '>100000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-G05'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'scale', 'art9'],
+ },
+
+ // GT-20: Nur B2C Webshop β L2 (HT-H01)
+ {
+ id: 'GT-20',
+ name: 'Reiner B2C Webshop',
+ description: 'B2C-Trigger ohne weitere Risiken',
+ answers: [
+ { questionId: 'org_employee_count', value: '12' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'retail' },
+ { questionId: 'tech_has_webshop', value: true },
+ { questionId: 'data_volume', value: '10000-100000' },
+ { questionId: 'org_customer_count', value: '1000-10000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L2',
+ expectedHardTriggerIds: ['HT-H01'],
+ tags: ['b2c', 'webshop'],
+ },
+
+ // GT-21: Keine Daten, keine MA β L1
+ {
+ id: 'GT-21',
+ name: 'Minimale Datenverarbeitung',
+ description: 'Absolute Baseline ohne Risiken',
+ answers: [
+ { questionId: 'org_employee_count', value: '1' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'consulting' },
+ { questionId: 'data_volume', value: '<1000' },
+ { questionId: 'org_customer_count', value: '<50' },
+ { questionId: 'tech_has_website', value: false },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L1',
+ expectedHardTriggerIds: [],
+ tags: ['baseline', 'minimal'],
+ },
+
+ // GT-22: Alle Art.9 Kategorien β L3 (HT-A09)
+ {
+ id: 'GT-22',
+ name: 'Alle Art. 9 Kategorien',
+ description: 'Multiple sensible Datenkategorien',
+ answers: [
+ { questionId: 'org_employee_count', value: '50' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'research' },
+ { questionId: 'data_health', value: true },
+ { questionId: 'data_genetic', value: true },
+ { questionId: 'data_biometric', value: true },
+ { questionId: 'data_racial_ethnic', value: true },
+ { questionId: 'data_political_opinion', value: true },
+ { questionId: 'data_religious', value: true },
+ { questionId: 'data_union_membership', value: true },
+ { questionId: 'data_sexual_orientation', value: true },
+ { questionId: 'data_criminal', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-A09'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'art9', 'multiple-categories'],
+ },
+
+ // GT-23: Drittland + Art.9 β L3 (HT-E04)
+ {
+ id: 'GT-23',
+ name: 'Drittlandtransfer mit Art. 9 Daten',
+ description: 'Kombination aus Drittland und sensiblen Daten',
+ answers: [
+ { questionId: 'org_employee_count', value: '45' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'us' },
+ { questionId: 'org_industry', value: 'healthcare' },
+ { questionId: 'data_health', value: true },
+ { questionId: 'tech_has_third_country_transfer', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-E04'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'third-country', 'art9'],
+ },
+
+ // GT-24: Minderjaehrige + Art.9 β L4 (HT-B02)
+ {
+ id: 'GT-24',
+ name: 'MinderjΓ€hrige mit Gesundheitsdaten',
+ description: 'Kombination aus vulnerabler Gruppe und Art. 9',
+ answers: [
+ { questionId: 'org_employee_count', value: '30' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'healthcare' },
+ { questionId: 'data_subjects_minors', value: true },
+ { questionId: 'data_subjects_minors_age', value: '<16' },
+ { questionId: 'data_health', value: true },
+ { questionId: 'data_volume', value: '10000-100000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-B02'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'minors', 'health', 'combined-risk'],
+ },
+
+ // GT-25: KI autonome Entscheidungen β L3 (HT-C02)
+ {
+ id: 'GT-25',
+ name: 'KI mit autonomen Entscheidungen',
+ description: 'AI Act relevante autonome Systeme',
+ answers: [
+ { questionId: 'org_employee_count', value: '70' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'ai_services' },
+ { questionId: 'tech_has_adm', value: true },
+ { questionId: 'tech_adm_type', value: 'autonomous_decision' },
+ { questionId: 'tech_has_ai', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-C02'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'ai', 'adm'],
+ },
+
+ // GT-26: Multiple Zertifizierungen β L4 (HT-F01-05)
+ {
+ id: 'GT-26',
+ name: 'Multiple Zertifizierungen',
+ description: 'Mehrere Zertifizierungen kombiniert',
+ answers: [
+ { questionId: 'org_employee_count', value: '250' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'cloud_services' },
+ { questionId: 'cert_has_iso27001', value: true },
+ { questionId: 'cert_has_iso27701', value: true },
+ { questionId: 'cert_has_soc2', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ { questionId: 'process_has_dsfa', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-F01', 'HT-F02', 'HT-F03'],
+ tags: ['hard-trigger', 'certification', 'multiple'],
+ },
+
+ // GT-27: Oeffentlicher Sektor + Gesundheit β L3 (HT-H07 + A01)
+ {
+ id: 'GT-27',
+ name: 'Γffentlicher Sektor mit Gesundheitsdaten',
+ description: 'BehΓΆrde mit Art. 9 Datenverarbeitung',
+ answers: [
+ { questionId: 'org_employee_count', value: '120' },
+ { questionId: 'org_business_model', value: 'b2g' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'public_sector' },
+ { questionId: 'org_is_public_sector', value: true },
+ { questionId: 'data_health', value: true },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-H07', 'HT-A01'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'public-sector', 'health'],
+ },
+
+ // GT-28: Bildung + KI + Minderjaehrige β L4 (HT-B03)
+ {
+ id: 'GT-28',
+ name: 'EdTech mit KI fΓΌr MinderjΓ€hrige',
+ description: 'Triple-Risiko: Bildung, KI, vulnerable Gruppe',
+ answers: [
+ { questionId: 'org_employee_count', value: '55' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'education' },
+ { questionId: 'data_subjects_minors', value: true },
+ { questionId: 'data_subjects_minors_age', value: '<16' },
+ { questionId: 'tech_has_ai', value: true },
+ { questionId: 'tech_has_adm', value: true },
+ { questionId: 'data_volume', value: '100000-1000000' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L4',
+ expectedHardTriggerIds: ['HT-B03'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'education', 'ai', 'minors', 'triple-risk'],
+ },
+
+ // GT-29: Freelancer mit 1 Art.9 β L3 (hard trigger override despite low score)
+ {
+ id: 'GT-29',
+ name: 'Freelancer mit Gesundheitsdaten',
+ description: 'Hard Trigger ΓΌberschreibt niedrige Score-Bewertung',
+ answers: [
+ { questionId: 'org_employee_count', value: '1' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'de' },
+ { questionId: 'org_industry', value: 'healthcare' },
+ { questionId: 'data_health', value: true },
+ { questionId: 'data_volume', value: '<1000' },
+ { questionId: 'org_customer_count', value: '<50' },
+ { questionId: 'process_has_vvt', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-A01'],
+ expectedDsfaRequired: true,
+ tags: ['hard-trigger', 'override', 'art9', 'freelancer'],
+ },
+
+ // GT-30: Enterprise, alle Prozesse vorhanden β L3 (good process maturity)
+ {
+ id: 'GT-30',
+ name: 'Enterprise mit reifer Prozesslandschaft',
+ description: 'GroΓe Organisation mit allen Compliance-Prozessen',
+ answers: [
+ { questionId: 'org_employee_count', value: '450' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ { questionId: 'org_industry', value: 'manufacturing' },
+ { questionId: 'data_volume', value: '>1000000' },
+ { questionId: 'org_customer_count', value: '10000-100000' },
+ { questionId: 'process_has_vvt', value: true },
+ { questionId: 'process_has_tom', value: true },
+ { questionId: 'process_has_dsfa', value: true },
+ { questionId: 'process_has_incident_plan', value: true },
+ { questionId: 'process_has_dsb', value: true },
+ { questionId: 'process_has_training', value: true },
+ ],
+ expectedLevel: 'L3',
+ expectedHardTriggerIds: ['HT-G04'],
+ tags: ['enterprise', 'mature', 'all-processes'],
+ },
+
+ // GT-31: SMB, nur 1 Block beantwortet β L1 (graceful degradation)
+ {
+ id: 'GT-31',
+ name: 'UnvollstΓ€ndige Profilerstellung',
+ description: 'Test fΓΌr graceful degradation bei unvollstΓ€ndigen Antworten',
+ answers: [
+ { questionId: 'org_employee_count', value: '8' },
+ { questionId: 'org_business_model', value: 'b2b' },
+ { questionId: 'org_industry', value: 'consulting' },
+ // Nur Block 1 (Organization) beantwortet, Rest fehlt
+ ],
+ expectedLevel: 'L1',
+ expectedHardTriggerIds: [],
+ tags: ['incomplete', 'degradation', 'edge-case'],
+ },
+
+ // GT-32: CompanyProfile Prefill Konsistenz β null (prefill test, no expected level)
+ {
+ id: 'GT-32',
+ name: 'CompanyProfile Prefill Test',
+ description: 'PrΓΌft ob CompanyProfile-Daten korrekt in ScopeProfile ΓΌbernommen werden',
+ answers: [
+ { questionId: 'org_employee_count', value: '25' },
+ { questionId: 'org_business_model', value: 'b2c' },
+ { questionId: 'org_industry', value: 'retail' },
+ { questionId: 'tech_hosting_location', value: 'eu' },
+ // Diese Werte sollten mit CompanyProfile-Prefill ΓΌbereinstimmen
+ ],
+ expectedLevel: null,
+ tags: ['prefill', 'integration', 'consistency'],
+ },
+]
diff --git a/admin-v2/lib/sdk/compliance-scope-profiling.ts b/admin-v2/lib/sdk/compliance-scope-profiling.ts
new file mode 100644
index 0000000..a6548c2
--- /dev/null
+++ b/admin-v2/lib/sdk/compliance-scope-profiling.ts
@@ -0,0 +1,821 @@
+import type {
+ ScopeQuestionBlock,
+ ScopeQuestionBlockId,
+ ScopeProfilingQuestion,
+ ScopeProfilingAnswer,
+ ComplianceScopeState,
+} from './compliance-scope-types'
+import type { CompanyProfile } from './types'
+
+/**
+ * Block 1: Organisation & Reife
+ */
+const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
+ id: 'organisation',
+ title: 'Organisation & Reife',
+ description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen',
+ order: 1,
+ questions: [
+ {
+ id: 'org_employee_count',
+ type: 'number',
+ label: 'Wie viele Mitarbeiter hat Ihre Organisation?',
+ helpText: 'Geben Sie die Gesamtzahl aller BeschΓ€ftigten an (inkl. Teilzeit, Minijobs)',
+ required: true,
+ scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
+ mapsToCompanyProfile: 'employeeCount',
+ },
+ {
+ id: 'org_customer_count',
+ type: 'single',
+ label: 'Wie viele Kunden/Nutzer betreuen Sie?',
+ helpText: 'SchΓ€tzen Sie die Anzahl aktiver Kunden oder Nutzer',
+ required: true,
+ options: [
+ { value: '<100', label: 'Weniger als 100' },
+ { value: '100-1000', label: '100 bis 1.000' },
+ { value: '1000-10000', label: '1.000 bis 10.000' },
+ { value: '10000-100000', label: '10.000 bis 100.000' },
+ { value: '100000+', label: 'Mehr als 100.000' },
+ ],
+ scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
+ },
+ {
+ id: 'org_annual_revenue',
+ type: 'single',
+ label: 'Wie hoch ist Ihr jΓ€hrlicher Umsatz?',
+ helpText: 'WΓ€hlen Sie die zutreffende Umsatzklasse',
+ required: true,
+ options: [
+ { value: '<2Mio', label: 'Unter 2 Mio. EUR' },
+ { value: '2-10Mio', label: '2 bis 10 Mio. EUR' },
+ { value: '10-50Mio', label: '10 bis 50 Mio. EUR' },
+ { value: '>50Mio', label: 'Γber 50 Mio. EUR' },
+ ],
+ scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
+ mapsToCompanyProfile: 'annualRevenue',
+ },
+ {
+ id: 'org_cert_target',
+ type: 'multi',
+ label: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
+ helpText: 'Mehrfachauswahl mΓΆglich. Zertifizierungen erhΓΆhen den Assurance-Bedarf',
+ required: false,
+ options: [
+ { value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
+ { value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' },
+ { value: 'TISAX', label: 'TISAX (Automotive)' },
+ { value: 'SOC2', label: 'SOC 2 (US-Standard)' },
+ { value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' },
+ { value: 'Keine', label: 'Keine Zertifizierung geplant' },
+ ],
+ scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
+ },
+ {
+ id: 'org_industry',
+ type: 'single',
+ label: 'In welcher Branche sind Sie tΓ€tig?',
+ helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen',
+ required: true,
+ options: [
+ { value: 'it_software', label: 'IT & Software' },
+ { value: 'healthcare', label: 'Gesundheitswesen' },
+ { value: 'education', label: 'Bildung & Forschung' },
+ { value: 'finance', label: 'Finanzdienstleistungen' },
+ { value: 'retail', label: 'Einzelhandel & E-Commerce' },
+ { value: 'manufacturing', label: 'Produktion & Fertigung' },
+ { value: 'consulting', label: 'Beratung & Dienstleistungen' },
+ { value: 'public', label: 'Γffentliche Verwaltung' },
+ { value: 'other', label: 'Sonstige' },
+ ],
+ scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
+ mapsToCompanyProfile: 'industry',
+ mapsToVVTQuestion: 'org_industry',
+ mapsToLFQuestion: 'org-branche',
+ },
+ {
+ id: 'org_business_model',
+ type: 'single',
+ label: 'Was ist Ihr primΓ€res GeschΓ€ftsmodell?',
+ helpText: 'B2C-Modelle haben hΓΆhere Datenschutzanforderungen',
+ required: true,
+ options: [
+ { value: 'b2b', label: 'B2B (Business-to-Business)' },
+ { value: 'b2c', label: 'B2C (Business-to-Consumer)' },
+ { value: 'both', label: 'B2B und B2C gemischt' },
+ { value: 'b2g', label: 'B2G (Business-to-Government)' },
+ ],
+ scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
+ mapsToCompanyProfile: 'businessModel',
+ mapsToVVTQuestion: 'org_b2b_b2c',
+ mapsToLFQuestion: 'org-geschaeftsmodell',
+ },
+ {
+ id: 'org_has_dsb',
+ type: 'boolean',
+ label: 'Haben Sie einen Datenschutzbeauftragten bestellt?',
+ helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmΓ€Γiger Datenverarbeitung Pflicht',
+ required: true,
+ scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
+ },
+ ],
+}
+
+/**
+ * Block 2: Daten & Betroffene
+ */
+const BLOCK_2_DATA: ScopeQuestionBlock = {
+ id: 'data',
+ title: 'Daten & Betroffene',
+ description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
+ order: 2,
+ questions: [
+ {
+ id: 'data_minors',
+ type: 'boolean',
+ label: 'Verarbeiten Sie Daten von MinderjΓ€hrigen?',
+ helpText: 'Besondere Schutzpflichten fΓΌr unter 16-JΓ€hrige (bzw. 13-JΓ€hrige bei Online-Diensten)',
+ required: true,
+ scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
+ mapsToVVTQuestion: 'data_minors',
+ },
+ {
+ id: 'data_art9',
+ type: 'multi',
+ label: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
+ helpText: 'Diese Daten unterliegen erhΓΆhten Schutzanforderungen',
+ required: true,
+ options: [
+ { value: 'gesundheit', label: 'Gesundheitsdaten' },
+ { value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
+ { value: 'genetik', label: 'Genetische Daten' },
+ { value: 'politisch', label: 'Politische Meinungen' },
+ { value: 'religion', label: 'ReligiΓΆse/weltanschauliche Γberzeugungen' },
+ { value: 'gewerkschaft', label: 'GewerkschaftszugehΓΆrigkeit' },
+ { value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
+ { value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
+ { value: 'ethnisch', label: 'Ethnische Herkunft' },
+ ],
+ scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
+ mapsToVVTQuestion: 'data_health',
+ },
+ {
+ id: 'data_hr',
+ type: 'boolean',
+ label: 'Verarbeiten Sie Personaldaten (HR)?',
+ helpText: 'Bewerberdaten, GehΓ€lter, Leistungsbeurteilungen etc.',
+ required: true,
+ scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
+ mapsToVVTQuestion: 'dept_hr',
+ mapsToLFQuestion: 'data-hr',
+ },
+ {
+ id: 'data_communication',
+ type: 'boolean',
+ label: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
+ helpText: 'Inhalte oder Metadaten von KommunikationsvorgΓ€ngen',
+ required: true,
+ scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
+ },
+ {
+ id: 'data_financial',
+ type: 'boolean',
+ label: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
+ helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
+ required: true,
+ scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
+ mapsToVVTQuestion: 'dept_finance',
+ mapsToLFQuestion: 'data-buchhaltung',
+ },
+ {
+ id: 'data_volume',
+ type: 'single',
+ label: 'Wie viele PersonendatensΓ€tze verarbeiten Sie insgesamt?',
+ helpText: 'SchΓ€tzen Sie die Gesamtzahl betroffener Personen',
+ required: true,
+ options: [
+ { value: '<1000', label: 'Unter 1.000' },
+ { value: '1000-10000', label: '1.000 bis 10.000' },
+ { value: '10000-100000', label: '10.000 bis 100.000' },
+ { value: '100000-1000000', label: '100.000 bis 1 Mio.' },
+ { value: '>1000000', label: 'Γber 1 Mio.' },
+ ],
+ scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
+ },
+ ],
+}
+
+/**
+ * Block 3: Verarbeitung & Zweck
+ */
+const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
+ id: 'processing',
+ title: 'Verarbeitung & Zweck',
+ description: 'Wie und wofΓΌr werden personenbezogene Daten verarbeitet?',
+ order: 3,
+ questions: [
+ {
+ id: 'proc_tracking',
+ type: 'boolean',
+ label: 'Setzen Sie Tracking oder Profiling ein?',
+ helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
+ required: true,
+ scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
+ },
+ {
+ id: 'proc_adm_scoring',
+ type: 'boolean',
+ label: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
+ helpText: 'Scoring, BonitΓ€tsprΓΌfung, automatische Ablehnung ohne menschliche Beteiligung',
+ required: true,
+ scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
+ },
+ {
+ id: 'proc_ai_usage',
+ type: 'multi',
+ label: 'Setzen Sie KI-Systeme ein?',
+ helpText: 'KI-Einsatz kann zusΓ€tzliche Anforderungen (EU AI Act) auslΓΆsen',
+ required: true,
+ options: [
+ { value: 'keine', label: 'Keine KI im Einsatz' },
+ { value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
+ { value: 'scoring', label: 'Scoring/Risikobewertung' },
+ { value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
+ { value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
+ { value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
+ ],
+ scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
+ },
+ {
+ id: 'proc_data_combination',
+ type: 'boolean',
+ label: 'FΓΌhren Sie Daten aus verschiedenen Quellen zusammen?',
+ helpText: 'Data Matching, Anreicherung aus externen Quellen',
+ required: true,
+ scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
+ },
+ {
+ id: 'proc_employee_monitoring',
+ type: 'boolean',
+ label: 'Γberwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
+ helpText: 'BeschΓ€ftigtendatenschutz nach Β§ 26 BDSG',
+ required: true,
+ scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
+ },
+ {
+ id: 'proc_video_surveillance',
+ type: 'boolean',
+ label: 'Setzen Sie VideoΓΌberwachung ein?',
+ helpText: 'Kameras in BΓΌros, ProduktionsstΓ€tten, VerkaufsrΓ€umen etc.',
+ required: true,
+ scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
+ mapsToVVTQuestion: 'special_video_surveillance',
+ mapsToLFQuestion: 'data-video',
+ },
+ ],
+}
+
+/**
+ * Block 4: Technik/Hosting/Transfers
+ */
+const BLOCK_4_TECH: ScopeQuestionBlock = {
+ id: 'tech',
+ title: 'Technik, Hosting & Transfers',
+ description: 'Technische Infrastruktur und DatenΓΌbermittlung',
+ order: 4,
+ questions: [
+ {
+ id: 'tech_hosting_location',
+ type: 'single',
+ label: 'Wo werden Ihre Daten primΓ€r gehostet?',
+ helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
+ required: true,
+ options: [
+ { value: 'de', label: 'Deutschland' },
+ { value: 'eu', label: 'EU (ohne Deutschland)' },
+ { value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
+ { value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
+ { value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
+ ],
+ scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
+ },
+ {
+ id: 'tech_subprocessors',
+ type: 'boolean',
+ label: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
+ helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. β erfordert AVV nach Art. 28 DSGVO',
+ required: true,
+ scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
+ },
+ {
+ id: 'tech_third_country',
+ type: 'boolean',
+ label: 'Γbermitteln Sie Daten in DrittlΓ€nder?',
+ helpText: 'Transfer auΓerhalb EU/EWR erfordert SchutzmaΓnahmen (SCC, BCR etc.)',
+ required: true,
+ scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
+ mapsToVVTQuestion: 'transfer_cloud_us',
+ },
+ {
+ id: 'tech_encryption_rest',
+ type: 'boolean',
+ label: 'Sind Daten im Ruhezustand verschlΓΌsselt (at rest)?',
+ helpText: 'Datenbank-, Dateisystem- oder Volume-VerschlΓΌsselung',
+ required: true,
+ scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
+ },
+ {
+ id: 'tech_encryption_transit',
+ type: 'boolean',
+ label: 'Sind Daten bei Γbertragung verschlΓΌsselt (in transit)?',
+ helpText: 'TLS/SSL fΓΌr alle Verbindungen',
+ required: true,
+ scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
+ },
+ {
+ id: 'tech_cloud_providers',
+ type: 'multi',
+ label: 'Welche Cloud-Anbieter nutzen Sie?',
+ helpText: 'Mehrfachauswahl mΓΆglich',
+ required: false,
+ options: [
+ { value: 'aws', label: 'Amazon Web Services (AWS)' },
+ { value: 'azure', label: 'Microsoft Azure' },
+ { value: 'gcp', label: 'Google Cloud Platform (GCP)' },
+ { value: 'hetzner', label: 'Hetzner' },
+ { value: 'ionos', label: 'IONOS' },
+ { value: 'ovh', label: 'OVH' },
+ { value: 'andere', label: 'Andere Anbieter' },
+ { value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
+ ],
+ scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
+ },
+ ],
+}
+
+/**
+ * Block 5: Rechte & Prozesse
+ */
+const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
+ id: 'processes',
+ title: 'Rechte & Prozesse',
+ description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
+ order: 5,
+ questions: [
+ {
+ id: 'proc_dsar_process',
+ type: 'boolean',
+ label: 'Haben Sie einen Prozess fΓΌr Betroffenenrechte (DSAR)?',
+ helpText: 'Auskunft, LΓΆschung, Berichtigung, Widerspruch etc. β Art. 15-22 DSGVO',
+ required: true,
+ scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
+ },
+ {
+ id: 'proc_deletion_concept',
+ type: 'boolean',
+ label: 'Haben Sie ein LΓΆschkonzept?',
+ helpText: 'Definierte LΓΆschfristen und automatisierte LΓΆschroutinen',
+ required: true,
+ scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
+ },
+ {
+ id: 'proc_incident_response',
+ type: 'boolean',
+ label: 'Haben Sie einen Notfallplan fΓΌr DatenschutzvorfΓ€lle?',
+ helpText: 'Incident Response Plan, 72h-Meldepflicht an AufsichtsbehΓΆrde (Art. 33 DSGVO)',
+ required: true,
+ scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
+ },
+ {
+ id: 'proc_regular_audits',
+ type: 'boolean',
+ label: 'FΓΌhren Sie regelmΓ€Γige Datenschutz-Audits durch?',
+ helpText: 'Interne oder externe PrΓΌfungen mindestens jΓ€hrlich',
+ required: true,
+ scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
+ },
+ {
+ id: 'proc_training',
+ type: 'boolean',
+ label: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
+ helpText: 'Awareness-Trainings, Onboarding, jΓ€hrliche Auffrischung',
+ required: true,
+ scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
+ },
+ ],
+}
+
+/**
+ * Block 6: Produktkontext
+ */
+const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
+ id: 'product',
+ title: 'Produktkontext',
+ description: 'Spezifische Merkmale Ihrer Produkte und Services',
+ order: 6,
+ questions: [
+ {
+ id: 'prod_type',
+ type: 'multi',
+ label: 'Welche Art von Produkten/Services bieten Sie an?',
+ helpText: 'Mehrfachauswahl mΓΆglich',
+ required: true,
+ options: [
+ { value: 'webapp', label: 'Web-Anwendung' },
+ { value: 'mobile', label: 'Mobile App (iOS/Android)' },
+ { value: 'saas', label: 'SaaS-Plattform' },
+ { value: 'onpremise', label: 'On-Premise Software' },
+ { value: 'api', label: 'API/Schnittstellen' },
+ { value: 'iot', label: 'IoT/Hardware' },
+ { value: 'beratung', label: 'Beratungsleistungen' },
+ { value: 'handel', label: 'Handel/Vertrieb' },
+ ],
+ scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
+ },
+ {
+ id: 'prod_cookies_consent',
+ type: 'boolean',
+ label: 'BenΓΆtigen Sie Cookie-Consent (Tracking-Cookies)?',
+ helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
+ required: true,
+ scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
+ },
+ {
+ id: 'prod_webshop',
+ type: 'boolean',
+ label: 'Betreiben Sie einen Online-Shop?',
+ helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung',
+ required: true,
+ scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
+ },
+ {
+ id: 'prod_api_external',
+ type: 'boolean',
+ label: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
+ helpText: 'Programmierschnittstellen fΓΌr Partner, Entwickler etc.',
+ required: true,
+ scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
+ },
+ {
+ id: 'prod_data_broker',
+ type: 'boolean',
+ label: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
+ helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
+ required: true,
+ scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
+ },
+ ],
+}
+
+/**
+ * All question blocks in order
+ */
+export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
+ BLOCK_1_ORGANISATION,
+ BLOCK_2_DATA,
+ BLOCK_3_PROCESSING,
+ BLOCK_4_TECH,
+ BLOCK_5_PROCESSES,
+ BLOCK_6_PRODUCT,
+]
+
+/**
+ * Prefill scope answers from CompanyProfile
+ */
+export function prefillFromCompanyProfile(
+ profile: CompanyProfile
+): ScopeProfilingAnswer[] {
+ const answers: ScopeProfilingAnswer[] = []
+
+ // employeeCount
+ if (profile.employeeCount != null) {
+ answers.push({
+ questionId: 'org_employee_count',
+ value: profile.employeeCount,
+ })
+ }
+
+ // annualRevenue
+ if (profile.annualRevenue) {
+ answers.push({
+ questionId: 'org_annual_revenue',
+ value: profile.annualRevenue,
+ })
+ }
+
+ // industry
+ if (profile.industry) {
+ answers.push({
+ questionId: 'org_industry',
+ value: profile.industry,
+ })
+ }
+
+ // businessModel
+ if (profile.businessModel) {
+ answers.push({
+ questionId: 'org_business_model',
+ value: profile.businessModel,
+ })
+ }
+
+ // dpoName -> org_has_dsb
+ if (profile.dpoName && profile.dpoName.trim() !== '') {
+ answers.push({
+ questionId: 'org_has_dsb',
+ value: true,
+ })
+ }
+
+ // usesAI -> proc_ai_usage
+ if (profile.usesAI === true) {
+ // We don't know which specific AI type, so just mark as "generativ" as a default
+ answers.push({
+ questionId: 'proc_ai_usage',
+ value: ['generativ'],
+ })
+ } else if (profile.usesAI === false) {
+ answers.push({
+ questionId: 'proc_ai_usage',
+ value: ['keine'],
+ })
+ }
+
+ // offerings -> prod_type mapping
+ if (profile.offerings && profile.offerings.length > 0) {
+ const prodTypes: string[] = []
+ const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
+
+ if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
+ prodTypes.push('webapp')
+ }
+ if (
+ offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
+ ) {
+ prodTypes.push('mobile')
+ }
+ if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
+ prodTypes.push('saas')
+ }
+ if (
+ offeringsLower.some(
+ (o) => o.includes('onpremise') || o.includes('on-premise')
+ )
+ ) {
+ prodTypes.push('onpremise')
+ }
+ if (offeringsLower.some((o) => o.includes('api'))) {
+ prodTypes.push('api')
+ }
+ if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
+ prodTypes.push('iot')
+ }
+ if (
+ offeringsLower.some(
+ (o) => o.includes('beratung') || o.includes('consulting')
+ )
+ ) {
+ prodTypes.push('beratung')
+ }
+ if (
+ offeringsLower.some(
+ (o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
+ )
+ ) {
+ prodTypes.push('handel')
+ }
+
+ if (prodTypes.length > 0) {
+ answers.push({
+ questionId: 'prod_type',
+ value: prodTypes,
+ })
+ }
+ }
+
+ return answers
+}
+
+/**
+ * Prefill scope answers from VVT profiling answers
+ */
+export function prefillFromVVTAnswers(
+ vvtAnswers: Record
+): ScopeProfilingAnswer[] {
+ const answers: ScopeProfilingAnswer[] = []
+
+ // Build reverse mapping: VVT question -> Scope question
+ const reverseMap: Record = {}
+ for (const block of SCOPE_QUESTION_BLOCKS) {
+ for (const q of block.questions) {
+ if (q.mapsToVVTQuestion) {
+ reverseMap[q.mapsToVVTQuestion] = q.id
+ }
+ }
+ }
+
+ // Map VVT answers to scope answers
+ for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
+ const scopeQuestionId = reverseMap[vvtQuestionId]
+ if (scopeQuestionId) {
+ answers.push({
+ questionId: scopeQuestionId,
+ value: vvtValue,
+ })
+ }
+ }
+
+ return answers
+}
+
+/**
+ * Prefill scope answers from Loeschfristen profiling answers
+ */
+export function prefillFromLoeschfristenAnswers(
+ lfAnswers: Array<{ questionId: string; value: unknown }>
+): ScopeProfilingAnswer[] {
+ const answers: ScopeProfilingAnswer[] = []
+
+ // Build reverse mapping: LF question -> Scope question
+ const reverseMap: Record = {}
+ for (const block of SCOPE_QUESTION_BLOCKS) {
+ for (const q of block.questions) {
+ if (q.mapsToLFQuestion) {
+ reverseMap[q.mapsToLFQuestion] = q.id
+ }
+ }
+ }
+
+ // Map LF answers to scope answers
+ for (const lfAnswer of lfAnswers) {
+ const scopeQuestionId = reverseMap[lfAnswer.questionId]
+ if (scopeQuestionId) {
+ answers.push({
+ questionId: scopeQuestionId,
+ value: lfAnswer.value,
+ })
+ }
+ }
+
+ return answers
+}
+
+/**
+ * Export scope answers in VVT format
+ */
+export function exportToVVTAnswers(
+ scopeAnswers: ScopeProfilingAnswer[]
+): Record {
+ const vvtAnswers: Record = {}
+
+ for (const answer of scopeAnswers) {
+ // Find the question
+ let question: ScopeProfilingQuestion | undefined
+ for (const block of SCOPE_QUESTION_BLOCKS) {
+ question = block.questions.find((q) => q.id === answer.questionId)
+ if (question) break
+ }
+
+ if (question?.mapsToVVTQuestion) {
+ vvtAnswers[question.mapsToVVTQuestion] = answer.value
+ }
+ }
+
+ return vvtAnswers
+}
+
+/**
+ * Export scope answers in Loeschfristen format
+ */
+export function exportToLoeschfristenAnswers(
+ scopeAnswers: ScopeProfilingAnswer[]
+): Array<{ questionId: string; value: unknown }> {
+ const lfAnswers: Array<{ questionId: string; value: unknown }> = []
+
+ for (const answer of scopeAnswers) {
+ // Find the question
+ let question: ScopeProfilingQuestion | undefined
+ for (const block of SCOPE_QUESTION_BLOCKS) {
+ question = block.questions.find((q) => q.id === answer.questionId)
+ if (question) break
+ }
+
+ if (question?.mapsToLFQuestion) {
+ lfAnswers.push({
+ questionId: question.mapsToLFQuestion,
+ value: answer.value,
+ })
+ }
+ }
+
+ return lfAnswers
+}
+
+/**
+ * Export scope answers for TOM generator
+ */
+export function exportToTOMProfile(
+ scopeAnswers: ScopeProfilingAnswer[]
+): Record {
+ const tomProfile: Record = {}
+
+ // Get answer values
+ const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
+
+ // Map relevant scope answers to TOM profile fields
+ tomProfile.industry = getVal('org_industry')
+ tomProfile.employeeCount = getVal('org_employee_count')
+ tomProfile.hasDataMinors = getVal('data_minors')
+ tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
+ ? (getVal('data_art9') as string[]).length > 0
+ : false
+ tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
+ tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
+ ? !(getVal('proc_ai_usage') as string[]).includes('keine')
+ : false
+ tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
+ tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
+ tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
+ tomProfile.hasIncidentResponse = getVal('proc_incident_response')
+ tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
+ tomProfile.hasRegularAudits = getVal('proc_regular_audits')
+ tomProfile.hasTraining = getVal('proc_training')
+
+ return tomProfile
+}
+
+/**
+ * Check if a block is complete (all required questions answered)
+ */
+export function isBlockComplete(
+ answers: ScopeProfilingAnswer[],
+ blockId: ScopeQuestionBlockId
+): boolean {
+ const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
+ if (!block) return false
+
+ const requiredQuestions = block.questions.filter((q) => q.required)
+ const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
+
+ return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
+}
+
+/**
+ * Get progress for a specific block (0-100)
+ */
+export function getBlockProgress(
+ answers: ScopeProfilingAnswer[],
+ blockId: ScopeQuestionBlockId
+): number {
+ const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
+ if (!block) return 0
+
+ const requiredQuestions = block.questions.filter((q) => q.required)
+ if (requiredQuestions.length === 0) return 100
+
+ const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
+ const answeredCount = requiredQuestions.filter((q) =>
+ answeredQuestionIds.has(q.id)
+ ).length
+
+ return Math.round((answeredCount / requiredQuestions.length) * 100)
+}
+
+/**
+ * Get total progress across all blocks (0-100)
+ */
+export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
+ let totalRequired = 0
+ let totalAnswered = 0
+
+ const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
+
+ for (const block of SCOPE_QUESTION_BLOCKS) {
+ const requiredQuestions = block.questions.filter((q) => q.required)
+ totalRequired += requiredQuestions.length
+ totalAnswered += requiredQuestions.filter((q) =>
+ answeredQuestionIds.has(q.id)
+ ).length
+ }
+
+ if (totalRequired === 0) return 100
+ return Math.round((totalAnswered / totalRequired) * 100)
+}
+
+/**
+ * Get answer value for a specific question
+ */
+export function getAnswerValue(
+ answers: ScopeProfilingAnswer[],
+ questionId: string
+): unknown {
+ const answer = answers.find((a) => a.questionId === questionId)
+ return answer?.value
+}
+
+/**
+ * Get all questions as a flat array
+ */
+export function getAllQuestions(): ScopeProfilingQuestion[] {
+ return SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions)
+}
diff --git a/admin-v2/lib/sdk/compliance-scope-types.ts b/admin-v2/lib/sdk/compliance-scope-types.ts
new file mode 100644
index 0000000..6b1d338
--- /dev/null
+++ b/admin-v2/lib/sdk/compliance-scope-types.ts
@@ -0,0 +1,1355 @@
+/**
+ * Compliance Scope Engine - Type Definitions
+ *
+ * Definiert alle Typen fΓΌr das Compliance-Tiefenbestimmungssystem.
+ * ErmΓΆglicht die Bestimmung des optimalen Compliance-Levels (L1-L4) basierend auf
+ * Risiko, KomplexitΓ€t und Assurance-Bedarf einer Organisation.
+ */
+
+// ============================================================================
+// Core Level Types
+// ============================================================================
+
+/**
+ * Compliance-Tiefenstufen
+ * - L1: Lean Startup - Minimalansatz fΓΌr kleine Organisationen
+ * - L2: KMU Standard - Standard-Compliance fΓΌr mittelstΓ€ndische Unternehmen
+ * - L3: Erweitert - Erweiterte Compliance fΓΌr grΓΆΓere/risikoreichere Organisationen
+ * - L4: Zertifizierungsbereit - VollstΓ€ndige Compliance fΓΌr Zertifizierungen
+ */
+export type ComplianceDepthLevel = 'L1' | 'L2' | 'L3' | 'L4';
+
+/**
+ * Compliance-Scores zur Bestimmung der optimalen Tiefe
+ * Alle Werte zwischen 0-100
+ */
+export interface ComplianceScores {
+ /** Risiko-Score (0-100): HΓΆhere Werte = hΓΆheres Risiko */
+ risk_score: number;
+ /** KomplexitΓ€ts-Score (0-100): HΓΆhere Werte = komplexere Verarbeitung */
+ complexity_score: number;
+ /** Assurance-Bedarf (0-100): HΓΆhere Werte = hΓΆherer Nachweis-/Zertifizierungsbedarf */
+ assurance_need: number;
+ /** Zusammengesetzter Score (0-100): Gewichtete Kombination aller Scores */
+ composite_score: number;
+}
+
+// ============================================================================
+// Question & Profiling Types
+// ============================================================================
+
+/**
+ * IDs der FragenblΓΆcke fΓΌr das Scope-Profiling
+ */
+export type ScopeQuestionBlockId =
+ | 'org_reife' // Organisatorische Reife
+ | 'daten_betroffene' // Daten & Betroffene
+ | 'verarbeitung_zweck' // Verarbeitung & Zweck
+ | 'technik_hosting' // Technik & Hosting
+ | 'rechte_prozesse' // Rechte & Prozesse
+ | 'produktkontext'; // Produktkontext
+
+/**
+ * Eine einzelne Frage im Scope-Profiling
+ */
+export interface ScopeProfilingQuestion {
+ /** Eindeutige ID der Frage */
+ id: string;
+ /** ZugehΓΆriger Block */
+ block: ScopeQuestionBlockId;
+ /** Fragetext */
+ question: string;
+ /** Optional: Hilfetext/ErklΓ€rung */
+ helpText?: string;
+ /** Antworttyp */
+ type: 'single' | 'multi' | 'boolean' | 'number' | 'text';
+ /** Antwortoptionen (fΓΌr single/multi) */
+ options?: Array<{ value: string; label: string }>;
+ /** Ist die Frage erforderlich? */
+ required: boolean;
+ /** Gewichtung fΓΌr Score-Berechnung */
+ scoreWeights?: {
+ risk?: number; // Einfluss auf Risiko-Score
+ complexity?: number; // Einfluss auf KomplexitΓ€ts-Score
+ assurance?: number; // Einfluss auf Assurance-Bedarf
+ };
+ /** Mapping zu Firmenprofil-Feldern */
+ mapsToCompanyProfile?: string;
+ /** Mapping zu VVT-Fragen */
+ mapsToVVTQuestion?: string;
+ /** Mapping zu LF-Fragen */
+ mapsToLFQuestion?: string;
+ /** Mapping zu TOM-Profil */
+ mapsToTOMProfile?: string;
+}
+
+/**
+ * Antwort auf eine Profiling-Frage
+ */
+export interface ScopeProfilingAnswer {
+ /** ID der beantworteten Frage */
+ questionId: string;
+ /** Antwortwert (Typ abhΓ€ngig von Fragentyp) */
+ value: string | string[] | boolean | number;
+}
+
+/**
+ * Ein Block von zusammengehΓΆrigen Fragen
+ */
+export interface ScopeQuestionBlock {
+ /** Block-ID */
+ id: ScopeQuestionBlockId;
+ /** Block-Titel */
+ title: string;
+ /** Block-Beschreibung */
+ description: string;
+ /** Fragen in diesem Block */
+ questions: ScopeProfilingQuestion[];
+}
+
+// ============================================================================
+// Hard Trigger Types
+// ============================================================================
+
+/**
+ * Bedingungsoperatoren fΓΌr Hard Trigger
+ */
+export type HardTriggerOperator =
+ | 'EQUALS' // Exakte Γbereinstimmung
+ | 'CONTAINS' // EnthΓ€lt (fΓΌr Arrays/Strings)
+ | 'IN' // Ist in Liste enthalten
+ | 'GREATER_THAN' // GrΓΆΓer als (numerisch)
+ | 'NOT_EQUALS'; // Ungleich
+
+/**
+ * Hard Trigger Regel - erzwingt Mindest-Compliance-Level
+ */
+export interface HardTriggerRule {
+ /** Eindeutige ID der Regel */
+ id: string;
+ /** Kurze Bezeichnung */
+ label: string;
+ /** Detaillierte Beschreibung */
+ description: string;
+ /** Feld, das geprΓΌft wird (questionId oder company_profile Feld) */
+ conditionField: string;
+ /** Bedingungsoperator */
+ conditionOperator: HardTriggerOperator;
+ /** Wert, der geprΓΌft wird */
+ conditionValue: unknown;
+ /** Minimal erforderliches Level */
+ minimumLevel: ComplianceDepthLevel;
+ /** Pflichtdokumente bei Trigger */
+ mandatoryDocuments: ScopeDocumentType[];
+ /** DSFA erforderlich? */
+ dsfaRequired: boolean;
+ /** Rechtsgrundlage */
+ legalReference: string;
+}
+
+/**
+ * Getriggerter Hard Trigger mit Kontext
+ */
+export interface TriggeredHardTrigger {
+ /** Die getriggerte Regel */
+ rule: HardTriggerRule;
+ /** Der tatsΓ€chlich gefundene Wert */
+ matchedValue: unknown;
+ /** ErklΓ€rung warum getriggert */
+ explanation: string;
+}
+
+// ============================================================================
+// Document Types
+// ============================================================================
+
+/**
+ * Alle verfΓΌgbaren Dokumenttypen im SDK
+ */
+export type ScopeDocumentType =
+ | 'vvt' // Verzeichnis von VerarbeitungstΓ€tigkeiten
+ | 'lf' // LΓΆschfristenkonzept
+ | 'tom' // Technische und organisatorische MaΓnahmen
+ | 'av_vertrag' // Auftragsverarbeitungsvertrag
+ | 'dsi' // Datenschutz-Informationen (Privacy Policy)
+ | 'betroffenenrechte' // Betroffenenrechte-Prozess
+ | 'dsfa' // Datenschutz-FolgenabschΓ€tzung
+ | 'daten_transfer' // Drittlandtransfer-Dokumentation
+ | 'datenpannen' // Datenpannen-Prozess
+ | 'einwilligung' // Einwilligungsmanagement
+ | 'vertragsmanagement' // Vertragsmanagement-Prozess
+ | 'schulung' // Mitarbeiterschulung
+ | 'audit_log' // Audit & Logging Konzept
+ | 'risikoanalyse' // Risikoanalyse
+ | 'notfallplan' // Notfall- & Krisenplan
+ | 'zertifizierung' // Zertifizierungsvorbereitung
+ | 'datenschutzmanagement'; // Datenschutzmanagement-System (DSMS)
+
+// ============================================================================
+// Decision & Output Types
+// ============================================================================
+
+/**
+ * Die finale Scope-Entscheidung mit allen Details
+ */
+export interface ScopeDecision {
+ /** Eindeutige ID dieser Entscheidung */
+ id: string;
+ /** Bestimmtes Compliance-Level */
+ determinedLevel: ComplianceDepthLevel;
+ /** Berechnete Scores */
+ scores: ComplianceScores;
+ /** Getriggerte Hard Trigger */
+ triggeredHardTriggers: TriggeredHardTrigger[];
+ /** Erforderliche Dokumente mit Details */
+ requiredDocuments: RequiredDocument[];
+ /** Identifizierte Risiko-Flags */
+ riskFlags: RiskFlag[];
+ /** Identifizierte LΓΌcken */
+ gaps: ScopeGap[];
+ /** Empfohlene nΓ€chste Schritte */
+ nextActions: NextAction[];
+ /** BegrΓΌndung der Entscheidung */
+ reasoning: ScopeReasoning[];
+ /** Zeitstempel Erstellung */
+ createdAt: string;
+ /** Zeitstempel letzte Γnderung */
+ updatedAt: string;
+}
+
+/**
+ * Erforderliches Dokument mit Detailtiefe
+ */
+export interface RequiredDocument {
+ /** Dokumenttyp */
+ documentType: ScopeDocumentType;
+ /** Anzeigename */
+ label: string;
+ /** Ist Pflicht? */
+ required: boolean;
+ /** Erforderliche Tiefe (z.B. "Basis", "Standard", "Detailliert") */
+ depth: string;
+ /** Konkrete Anforderungen/Inhalte */
+ detailItems: string[];
+ /** GeschΓ€tzter Aufwand */
+ estimatedEffort: string;
+ /** Von welchen Triggern/Regeln gefordert */
+ triggeredBy: string[];
+ /** Link zum SDK-Schritt */
+ sdkStepUrl?: string;
+}
+
+/**
+ * Risiko-Flag
+ */
+export interface RiskFlag {
+ /** Eindeutige ID */
+ id: string;
+ /** Schweregrad */
+ severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
+ /** Titel */
+ title: string;
+ /** Beschreibung */
+ description: string;
+ /** Rechtsgrundlage */
+ legalReference?: string;
+ /** Empfehlung zur Behebung */
+ recommendation: string;
+}
+
+/**
+ * Identifizierte LΓΌcke in der Compliance
+ */
+export interface ScopeGap {
+ /** Eindeutige ID */
+ id: string;
+ /** Schweregrad */
+ severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
+ /** Titel */
+ title: string;
+ /** Beschreibung */
+ description: string;
+ /** Empfehlung zur SchlieΓung */
+ recommendation: string;
+ /** Betroffene Dokumente */
+ relatedDocuments: ScopeDocumentType[];
+}
+
+/**
+ * NΓ€chster empfohlener Schritt
+ */
+export interface NextAction {
+ /** Eindeutige ID */
+ id: string;
+ /** PrioritΓ€t (1 = hΓΆchste) */
+ priority: number;
+ /** Titel */
+ title: string;
+ /** Beschreibung */
+ description: string;
+ /** GeschΓ€tzter Aufwand */
+ estimatedEffort: string;
+ /** Betroffene Dokumente */
+ relatedDocuments: ScopeDocumentType[];
+ /** Link zum SDK-Schritt */
+ sdkStepUrl?: string;
+}
+
+/**
+ * BegrΓΌndungsschritt fΓΌr die Entscheidung
+ */
+export interface ScopeReasoning {
+ /** Schritt-Nummer/ID */
+ step: string;
+ /** Kurzbeschreibung */
+ description: string;
+ /** Detaillierte Punkte */
+ details: string[];
+}
+
+// ============================================================================
+// Document Scope Requirements
+// ============================================================================
+
+/**
+ * Anforderungen an ein Dokument pro Level
+ */
+export interface DocumentDepthRequirement {
+ /** Ist auf diesem Level erforderlich? */
+ required: boolean;
+ /** Tiefenbezeichnung */
+ depth: string;
+ /** Konkrete Anforderungen */
+ detailItems: string[];
+ /** GeschΓ€tzter Aufwand */
+ estimatedEffort: string;
+}
+
+/**
+ * VollstΓ€ndige Scope-Anforderungen fΓΌr ein Dokument
+ */
+export interface DocumentScopeRequirement {
+ /** L1 Anforderungen */
+ L1: DocumentDepthRequirement;
+ /** L2 Anforderungen */
+ L2: DocumentDepthRequirement;
+ /** L3 Anforderungen */
+ L3: DocumentDepthRequirement;
+ /** L4 Anforderungen */
+ L4: DocumentDepthRequirement;
+}
+
+// ============================================================================
+// State Management Types
+// ============================================================================
+
+/**
+ * Gesamter Zustand des Compliance Scope
+ */
+export interface ComplianceScopeState {
+ /** Alle gegebenen Antworten */
+ answers: ScopeProfilingAnswer[];
+ /** Aktuelle Entscheidung (null wenn noch nicht berechnet) */
+ decision: ScopeDecision | null;
+ /** Zeitpunkt der letzten Evaluierung */
+ lastEvaluatedAt: string | null;
+ /** Sind alle Pflichtfragen beantwortet? */
+ isComplete: boolean;
+}
+
+// ============================================================================
+// Constants - Labels & Descriptions
+// ============================================================================
+
+/**
+ * Deutsche Bezeichnungen fΓΌr Compliance-Levels
+ */
+export const DEPTH_LEVEL_LABELS: Record = {
+ L1: 'Lean Startup',
+ L2: 'KMU Standard',
+ L3: 'Erweitert',
+ L4: 'Zertifizierungsbereit',
+};
+
+/**
+ * Detaillierte Beschreibungen der Compliance-Levels
+ */
+export const DEPTH_LEVEL_DESCRIPTIONS: Record = {
+ L1: 'Minimalansatz fΓΌr kleine Organisationen und Startups. Fokus auf gesetzliche Pflichten mit pragmatischen LΓΆsungen.',
+ L2: 'Standard-Compliance fΓΌr mittelstΓ€ndische Unternehmen. Ausgewogenes VerhΓ€ltnis zwischen Aufwand und Compliance-QualitΓ€t.',
+ L3: 'Erweiterte Compliance fΓΌr grΓΆΓere oder risikoreichere Organisationen. Detaillierte Dokumentation und Prozesse.',
+ L4: 'VollstΓ€ndige Compliance fΓΌr Zertifizierungen und hΓΆchste Anforderungen. Audit-ready Dokumentation.',
+};
+
+/**
+ * Farben fΓΌr Compliance-Levels (Tailwind-kompatibel)
+ */
+export const DEPTH_LEVEL_COLORS: Record = {
+ L1: 'green',
+ L2: 'blue',
+ L3: 'amber',
+ L4: 'red',
+};
+
+/**
+ * Deutsche Bezeichnungen fΓΌr alle Dokumenttypen
+ */
+export const DOCUMENT_TYPE_LABELS: Record = {
+ vvt: 'Verzeichnis von VerarbeitungstΓ€tigkeiten (VVT)',
+ lf: 'LΓΆschfristenkonzept',
+ tom: 'Technische und organisatorische MaΓnahmen (TOM)',
+ av_vertrag: 'Auftragsverarbeitungsvertrag (AVV)',
+ dsi: 'Datenschutz-Informationen (Privacy Policy)',
+ betroffenenrechte: 'Betroffenenrechte-Prozess',
+ dsfa: 'Datenschutz-FolgenabschΓ€tzung (DSFA)',
+ daten_transfer: 'Drittlandtransfer-Dokumentation',
+ datenpannen: 'Datenpannen-Prozess',
+ einwilligung: 'Einwilligungsmanagement',
+ vertragsmanagement: 'Vertragsmanagement-Prozess',
+ schulung: 'Mitarbeiterschulung',
+ audit_log: 'Audit & Logging Konzept',
+ risikoanalyse: 'Risikoanalyse',
+ notfallplan: 'Notfall- & Krisenplan',
+ zertifizierung: 'Zertifizierungsvorbereitung',
+ datenschutzmanagement: 'Datenschutzmanagement-System (DSMS)',
+};
+
+/**
+ * Status-Labels fΓΌr Scope-Zustand
+ */
+export const SCOPE_STATUS_LABELS = {
+ NOT_STARTED: 'Nicht begonnen',
+ IN_PROGRESS: 'In Bearbeitung',
+ COMPLETE: 'Abgeschlossen',
+ NEEDS_UPDATE: 'Aktualisierung erforderlich',
+};
+
+/**
+ * LocalStorage Key fΓΌr Scope State
+ */
+export const STORAGE_KEY = 'bp_compliance_scope';
+
+// ============================================================================
+// Document Scope Matrix
+// ============================================================================
+
+/**
+ * VollstΓ€ndige Matrix aller Dokumentanforderungen pro Level
+ */
+export const DOCUMENT_SCOPE_MATRIX: Record = {
+ vvt: {
+ L1: {
+ required: true,
+ depth: 'Basis',
+ detailItems: [
+ 'Liste aller VerarbeitungstΓ€tigkeiten',
+ 'Grundlegende Angaben zu Zweck und Rechtsgrundlage',
+ 'Kategorien betroffener Personen und Daten',
+ 'Einfache Tabellenform ausreichend',
+ ],
+ estimatedEffort: '2-4 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierte Beschreibung der Verarbeitungszwecke',
+ 'EmpfΓ€ngerkategorien',
+ 'Speicherfristen',
+ 'TOM-Referenzen',
+ 'Strukturiertes Format',
+ ],
+ estimatedEffort: '4-8 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'VollstΓ€ndige Rechtsgrundlagen mit BegrΓΌndung',
+ 'Detaillierte Datenkategorien',
+ 'VerknΓΌpfung mit DSFA wo relevant',
+ 'Versionierung und Γnderungshistorie',
+ 'Freigabeprozess dokumentiert',
+ ],
+ estimatedEffort: '8-16 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndige Nachweiskette fΓΌr alle Angaben',
+ 'Integration mit Risikobewertung',
+ 'RegelmΓ€Γige Review-Zyklen dokumentiert',
+ 'Audit-Trail fΓΌr alle Γnderungen',
+ 'Compliance-Nachweise fΓΌr jede Verarbeitung',
+ ],
+ estimatedEffort: '16-24 Stunden',
+ },
+ },
+ lf: {
+ L1: {
+ required: true,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegende LΓΆschfristen fΓΌr Hauptdatenkategorien',
+ 'Einfache Tabelle oder Liste',
+ 'Bezug auf gesetzliche Aufbewahrungsfristen',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierte LΓΆschfristen pro VerarbeitungstΓ€tigkeit',
+ 'BegrΓΌndung der Fristen',
+ 'Technischer LΓΆschprozess beschrieben',
+ 'Verantwortlichkeiten festgelegt',
+ ],
+ estimatedEffort: '3-6 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Ausnahmen und SonderfΓ€lle dokumentiert',
+ 'Automatisierte LΓΆschprozesse beschrieben',
+ 'Nachweis regelmΓ€Γiger LΓΆschungen',
+ 'Eskalationsprozess bei Problemen',
+ ],
+ estimatedEffort: '6-10 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndiger Audit-Trail aller LΓΆschvorgΓ€nge',
+ 'RegelmΓ€Γige Audits dokumentiert',
+ 'Compliance-Nachweise fΓΌr alle LΓΆschfristen',
+ 'Integration mit Backup-Konzept',
+ ],
+ estimatedEffort: '10-16 Stunden',
+ },
+ },
+ tom: {
+ L1: {
+ required: true,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegende technische MaΓnahmen aufgelistet',
+ 'Organisatorische GrundmaΓnahmen',
+ 'Einfache Checkliste oder Tabelle',
+ ],
+ estimatedEffort: '2-3 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierte Beschreibung aller TOM',
+ 'Zuordnung zu Art. 32 DSGVO Kategorien',
+ 'Verantwortlichkeiten und Umsetzungsstatus',
+ 'Einfache Wirksamkeitsbewertung',
+ ],
+ estimatedEffort: '4-8 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Risikobewertung fΓΌr jede MaΓnahme',
+ 'Nachweis der Umsetzung',
+ 'RegelmΓ€Γige ΓberprΓΌfungszyklen',
+ 'VerbesserungsmaΓnahmen dokumentiert',
+ 'VerknΓΌpfung mit VVT',
+ ],
+ estimatedEffort: '8-12 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndige Wirksamkeitsnachweise',
+ 'Externe Audits dokumentiert',
+ 'Compliance-Matrix zu Standards (ISO 27001, etc.)',
+ 'Kontinuierliches Monitoring nachgewiesen',
+ ],
+ estimatedEffort: '12-20 Stunden',
+ },
+ },
+ av_vertrag: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Standard-AVV-Vorlage verwenden',
+ 'Grundlegende Angaben zu Auftragsverarbeiter',
+ 'Wesentliche Pflichten aufgefΓΌhrt',
+ ],
+ estimatedEffort: '1-2 Stunden pro Vertrag',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierte Beschreibung der Verarbeitung',
+ 'TOM des Auftragsverarbeiters geprΓΌft',
+ 'Unterschriebene VertrΓ€ge vollstΓ€ndig',
+ 'Register aller AVV gefΓΌhrt',
+ ],
+ estimatedEffort: '2-4 Stunden pro Vertrag',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Risikobewertung fΓΌr jeden Auftragsverarbeiter',
+ 'RegelmΓ€Γige ΓberprΓΌfungen dokumentiert',
+ 'Sub-Auftragsverarbeiter erfasst',
+ 'Audit-Rechte vereinbart und dokumentiert',
+ ],
+ estimatedEffort: '4-6 Stunden pro Vertrag',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'RegelmΓ€Γige Audits durchgefΓΌhrt und dokumentiert',
+ 'Compliance-Nachweise vom Auftragsverarbeiter',
+ 'VollstΓ€ndiges Vertragsmanagement-System',
+ 'Eskalations- und KΓΌndigungsprozesse dokumentiert',
+ ],
+ estimatedEffort: '6-10 Stunden pro Vertrag',
+ },
+ },
+ dsi: {
+ L1: {
+ required: true,
+ depth: 'Basis',
+ detailItems: [
+ 'DatenschutzerklΓ€rung auf Website',
+ 'Pflichtangaben nach Art. 13/14 DSGVO',
+ 'VerstΓ€ndliche Sprache',
+ 'Kontaktdaten DSB/Verantwortlicher',
+ ],
+ estimatedEffort: '2-4 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierte Beschreibung aller Verarbeitungen',
+ 'Rechtsgrundlagen erklΓ€rt',
+ 'Informationen zu Betroffenenrechten',
+ 'Cookie-/Tracking-Informationen',
+ 'RegelmΓ€Γige Aktualisierung',
+ ],
+ estimatedEffort: '4-8 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Mehrsprachige Versionen wo erforderlich',
+ 'Layered Notices (mehrstufige Informationen)',
+ 'Spezifische Informationen fΓΌr verschiedene Verarbeitungen',
+ 'Versionierung und Γnderungshistorie',
+ 'Consent Management Integration',
+ ],
+ estimatedEffort: '8-12 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndige Nachweiskette fΓΌr alle Informationen',
+ 'Audit-Trail fΓΌr Γnderungen',
+ 'Compliance mit internationalen Standards',
+ 'RegelmΓ€Γige rechtliche Reviews dokumentiert',
+ ],
+ estimatedEffort: '12-16 Stunden',
+ },
+ },
+ betroffenenrechte: {
+ L1: {
+ required: true,
+ depth: 'Basis',
+ detailItems: [
+ 'Prozess fΓΌr Auskunftsanfragen definiert',
+ 'KontaktmΓΆglichkeit bereitgestellt',
+ 'Grundlegende Fristen bekannt',
+ 'Einfaches Formular oder E-Mail-Vorlage',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Prozesse fΓΌr alle Betroffenenrechte (Auskunft, LΓΆschung, Berichtigung, etc.)',
+ 'Verantwortlichkeiten festgelegt',
+ 'Standardvorlagen fΓΌr Antworten',
+ 'Tracking von Anfragen',
+ ],
+ estimatedEffort: '3-6 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Detaillierte Prozessbeschreibungen',
+ 'Eskalationsprozesse bei komplexen FΓ€llen',
+ 'Schulung der Mitarbeiter dokumentiert',
+ 'Audit-Trail aller Anfragen',
+ 'Nachweis der Fristeneinhaltung',
+ ],
+ estimatedEffort: '6-10 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndiges Ticket-/Case-Management-System',
+ 'RegelmΓ€Γige Audits der Prozesse',
+ 'Compliance-Kennzahlen und Reporting',
+ 'Integration mit allen relevanten Systemen',
+ ],
+ estimatedEffort: '10-16 Stunden',
+ },
+ },
+ dsfa: {
+ L1: {
+ required: false,
+ depth: 'Nicht erforderlich',
+ detailItems: ['Nur bei Hard Trigger erforderlich'],
+ estimatedEffort: 'N/A',
+ },
+ L2: {
+ required: false,
+ depth: 'Bei Bedarf',
+ detailItems: [
+ 'DSFA-Schwellwertanalyse durchfΓΌhren',
+ 'Bei Erforderlichkeit: Basis-DSFA',
+ 'Risiken identifiziert und bewertet',
+ 'MaΓnahmen zur Risikominimierung',
+ ],
+ estimatedEffort: '4-8 Stunden pro DSFA',
+ },
+ L3: {
+ required: false,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Detaillierte Risikobewertung',
+ 'Konsultation der Betroffenen wo sinnvoll',
+ 'Dokumentation der Entscheidungsprozesse',
+ 'RegelmΓ€Γige ΓberprΓΌfung',
+ ],
+ estimatedEffort: '8-16 Stunden pro DSFA',
+ },
+ L4: {
+ required: true,
+ depth: 'VollstΓ€ndig',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'Strukturierter DSFA-Prozess etabliert',
+ 'Vorabkonsultation der AufsichtsbehΓΆrde wo erforderlich',
+ 'VollstΓ€ndige Dokumentation aller Schritte',
+ 'Integration in Projektmanagement',
+ ],
+ estimatedEffort: '16-24 Stunden pro DSFA',
+ },
+ },
+ daten_transfer: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Liste aller Drittlandtransfers',
+ 'Grundlegende Rechtsgrundlage identifiziert',
+ 'Standard-Vertragsklauseln wo nΓΆtig',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierte Dokumentation aller Transfers',
+ 'AngemessenheitsbeschlΓΌsse oder geeignete Garantien',
+ 'Informationen an Betroffene bereitgestellt',
+ 'Register gefΓΌhrt',
+ ],
+ estimatedEffort: '3-6 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Transfer Impact Assessment (TIA) durchgefΓΌhrt',
+ 'ZusΓ€tzliche SchutzmaΓnahmen dokumentiert',
+ 'RegelmΓ€Γige ΓberprΓΌfung der Rechtsgrundlagen',
+ 'Risikobewertung fΓΌr jedes Zielland',
+ ],
+ estimatedEffort: '6-12 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndige TIA-Dokumentation',
+ 'RegelmΓ€Γige Reviews dokumentiert',
+ 'Rechtliche Expertise nachgewiesen',
+ 'Compliance-Nachweise fΓΌr alle Transfers',
+ ],
+ estimatedEffort: '12-20 Stunden',
+ },
+ },
+ datenpannen: {
+ L1: {
+ required: true,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegender Prozess fΓΌr Datenpannen',
+ 'Kontakt zur AufsichtsbehΓΆrde bekannt',
+ 'Verantwortlichkeiten grob definiert',
+ 'Einfache Checkliste',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Detaillierter Incident-Response-Plan',
+ 'Bewertungskriterien fΓΌr Meldepflicht',
+ 'Vorlagen fΓΌr Meldungen (BehΓΆrde & Betroffene)',
+ 'Dokumentationspflichten klar definiert',
+ ],
+ estimatedEffort: '3-6 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Incident-Management-System etabliert',
+ 'RegelmΓ€Γige Γbungen durchgefΓΌhrt',
+ 'Eskalationsprozesse dokumentiert',
+ 'Post-Incident-Review-Prozess',
+ 'Lessons Learned dokumentiert',
+ ],
+ estimatedEffort: '6-10 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'VollstΓ€ndiges Breach-Log gefΓΌhrt',
+ 'Integration mit IT-Security-Incident-Response',
+ 'RegelmΓ€Γige Audits des Prozesses',
+ 'Compliance-Nachweise fΓΌr alle VorfΓ€lle',
+ ],
+ estimatedEffort: '10-16 Stunden',
+ },
+ },
+ einwilligung: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Einwilligungsformulare DSGVO-konform',
+ 'Opt-in statt Opt-out',
+ 'WiderrufsmΓΆglichkeit bereitgestellt',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Granulare Einwilligungen',
+ 'Nachweisbarkeit der Einwilligung',
+ 'Dokumentation des Einwilligungsprozesses',
+ 'RegelmΓ€Γige ΓberprΓΌfung',
+ ],
+ estimatedEffort: '3-6 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Consent-Management-System implementiert',
+ 'VollstΓ€ndiger Audit-Trail',
+ 'A/B-Testing dokumentiert',
+ 'Integration mit allen Datenverarbeitungen',
+ 'RegelmΓ€Γige Revalidierung',
+ ],
+ estimatedEffort: '6-12 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'Enterprise Consent Management Platform',
+ 'VollstΓ€ndige Nachweiskette fΓΌr alle Einwilligungen',
+ 'Compliance-Dashboard',
+ 'RegelmΓ€Γige externe Audits',
+ ],
+ estimatedEffort: '12-20 Stunden',
+ },
+ },
+ vertragsmanagement: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Einfaches Register wichtiger VertrΓ€ge',
+ 'Ablage datenschutzrelevanter VertrΓ€ge',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'VollstΓ€ndiges Vertragsregister',
+ 'Datenschutzklauseln in StandardvertrΓ€gen',
+ 'ΓberprΓΌfungsprozess fΓΌr neue VertrΓ€ge',
+ 'Ablaufdaten und KΓΌndigungsfristen getrackt',
+ ],
+ estimatedEffort: '3-6 Stunden Setup',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Vertragsmanagement-System implementiert',
+ 'Automatische Erinnerungen fΓΌr Reviews',
+ 'Risikobewertung fΓΌr Vertragspartner',
+ 'Compliance-Checks vor Vertragsabschluss',
+ ],
+ estimatedEffort: '6-12 Stunden Setup',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'Enterprise Contract Management System',
+ 'VollstΓ€ndiger Audit-Trail',
+ 'Integration mit Procurement',
+ 'RegelmΓ€Γige Compliance-Audits',
+ ],
+ estimatedEffort: '12-20 Stunden Setup',
+ },
+ },
+ schulung: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegende Datenschutz-Awareness',
+ 'Informationsblatt fΓΌr Mitarbeiter',
+ 'Kontaktperson benannt',
+ ],
+ estimatedEffort: '1-2 Stunden Vorbereitung',
+ },
+ L2: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'JΓ€hrliche Datenschutzschulung',
+ 'Schulungsunterlagen erstellt',
+ 'Teilnahme dokumentiert',
+ 'Rollenspezifische Inhalte',
+ ],
+ estimatedEffort: '4-8 Stunden Vorbereitung',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'E-Learning-Plattform oder strukturiertes Schulungsprogramm',
+ 'Wissenstests durchgefΓΌhrt',
+ 'Auffrischungsschulungen',
+ 'Spezialschulungen fΓΌr SchlΓΌsselpersonal',
+ 'Schulungsplan erstellt',
+ ],
+ estimatedEffort: '8-16 Stunden Vorbereitung',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'Umfassendes Schulungsprogramm',
+ 'Externe Schulungen wo erforderlich',
+ 'Zertifizierungen fΓΌr SchlΓΌsselpersonal',
+ 'VollstΓ€ndige Dokumentation aller Schulungen',
+ 'Wirksamkeitsmessung',
+ ],
+ estimatedEffort: '16-24 Stunden Vorbereitung',
+ },
+ },
+ audit_log: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegendes Logging aktiviert',
+ 'Zugriffsprotokolle fΓΌr kritische Systeme',
+ ],
+ estimatedEffort: '2-4 Stunden',
+ },
+ L2: {
+ required: false,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Strukturiertes Logging-Konzept',
+ 'Aufbewahrungsfristen definiert',
+ 'Zugriffskontrolle auf Logs',
+ 'RegelmΓ€Γige ΓberprΓΌfung',
+ ],
+ estimatedEffort: '4-8 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Zentralisiertes Logging-System',
+ 'Automatische Alerts bei Anomalien',
+ 'Audit-Trail fΓΌr alle datenschutzrelevanten VorgΓ€nge',
+ 'Compliance-Reporting',
+ ],
+ estimatedEffort: '8-16 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'Enterprise SIEM-System',
+ 'VollstΓ€ndige Nachvollziehbarkeit aller Zugriffe',
+ 'RegelmΓ€Γige Log-Audits dokumentiert',
+ 'Integration mit Incident Response',
+ ],
+ estimatedEffort: '16-24 Stunden',
+ },
+ },
+ risikoanalyse: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegende Risikoidentifikation',
+ 'Einfache Bewertung nach Eintrittswahrscheinlichkeit und Auswirkung',
+ ],
+ estimatedEffort: '2-4 Stunden',
+ },
+ L2: {
+ required: false,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Strukturierte Risikoanalyse',
+ 'Risikomatrix erstellt',
+ 'MaΓnahmen zur Risikominimierung definiert',
+ 'JΓ€hrliche ΓberprΓΌfung',
+ ],
+ estimatedEffort: '4-8 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Umfassende Risikoanalyse nach Standard-Framework',
+ 'Integration mit VVT und DSFA',
+ 'Risikomanagement-Prozess etabliert',
+ 'RegelmΓ€Γige Reviews',
+ 'Risiko-Dashboard',
+ ],
+ estimatedEffort: '8-16 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'Enterprise Risk Management System',
+ 'VollstΓ€ndige Integration mit ISMS',
+ 'Kontinuierliche RisikoΓΌberwachung',
+ 'RegelmΓ€Γige externe Assessments',
+ ],
+ estimatedEffort: '16-24 Stunden',
+ },
+ },
+ notfallplan: {
+ L1: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegende Notfallkontakte definiert',
+ 'Einfacher Backup-Prozess',
+ ],
+ estimatedEffort: '1-2 Stunden',
+ },
+ L2: {
+ required: false,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L1-Anforderungen',
+ 'Notfall- und Krisenplan erstellt',
+ 'Business Continuity Grundlagen',
+ 'Backup und Recovery dokumentiert',
+ 'Verantwortlichkeiten festgelegt',
+ ],
+ estimatedEffort: '3-6 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Detailliert',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Detaillierter Business Continuity Plan',
+ 'Disaster Recovery Plan',
+ 'RegelmΓ€Γige Tests durchgefΓΌhrt',
+ 'Eskalationsprozesse dokumentiert',
+ 'Externe Kommunikation geplant',
+ ],
+ estimatedEffort: '6-12 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'Audit-Ready',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'ISO 22301 konformes BCMS',
+ 'RegelmΓ€Γige Γbungen und Audits',
+ 'VollstΓ€ndige Dokumentation',
+ 'Integration mit IT-Disaster-Recovery',
+ ],
+ estimatedEffort: '12-20 Stunden',
+ },
+ },
+ zertifizierung: {
+ L1: {
+ required: false,
+ depth: 'Nicht relevant',
+ detailItems: ['Keine Zertifizierung erforderlich'],
+ estimatedEffort: 'N/A',
+ },
+ L2: {
+ required: false,
+ depth: 'Nicht relevant',
+ detailItems: ['Keine Zertifizierung erforderlich'],
+ estimatedEffort: 'N/A',
+ },
+ L3: {
+ required: false,
+ depth: 'Optional',
+ detailItems: [
+ 'Evaluierung mΓΆglicher Zertifizierungen',
+ 'Gap-Analyse durchgefΓΌhrt',
+ 'Entscheidung fΓΌr/gegen Zertifizierung dokumentiert',
+ ],
+ estimatedEffort: '4-8 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'VollstΓ€ndig',
+ detailItems: [
+ 'Zertifizierungsvorbereitung (ISO 27001, ISO 27701, etc.)',
+ 'Gap-Analyse abgeschlossen',
+ 'MaΓnahmenplan erstellt',
+ 'Interne Audits durchgefΓΌhrt',
+ 'Dokumentation audit-ready',
+ 'Zertifizierungsstelle ausgewΓ€hlt',
+ ],
+ estimatedEffort: '40-80 Stunden',
+ },
+ },
+ datenschutzmanagement: {
+ L1: {
+ required: false,
+ depth: 'Nicht erforderlich',
+ detailItems: ['Kein formales DSMS notwendig'],
+ estimatedEffort: 'N/A',
+ },
+ L2: {
+ required: false,
+ depth: 'Basis',
+ detailItems: [
+ 'Grundlegendes Datenschutzmanagement',
+ 'Verantwortlichkeiten definiert',
+ 'RegelmΓ€Γige Reviews geplant',
+ ],
+ estimatedEffort: '2-4 Stunden',
+ },
+ L3: {
+ required: true,
+ depth: 'Standard',
+ detailItems: [
+ 'Alle L2-Anforderungen',
+ 'Strukturiertes DSMS etabliert',
+ 'Datenschutz-Policy erstellt',
+ 'RegelmΓ€Γige Management-Reviews',
+ 'KPIs fΓΌr Datenschutz definiert',
+ 'Verbesserungsprozess etabliert',
+ ],
+ estimatedEffort: '8-16 Stunden',
+ },
+ L4: {
+ required: true,
+ depth: 'VollstΓ€ndig',
+ detailItems: [
+ 'Alle L3-Anforderungen',
+ 'ISO 27701 oder vergleichbares DSMS',
+ 'Integration mit ISMS',
+ 'VollstΓ€ndige Dokumentation aller Prozesse',
+ 'RegelmΓ€Γige interne und externe Audits',
+ 'Kontinuierliche Verbesserung nachgewiesen',
+ ],
+ estimatedEffort: '24-40 Stunden',
+ },
+ },
+};
+
+// ============================================================================
+// Document to SDK Step URL Mapping
+// ============================================================================
+
+/**
+ * Mapping von Dokumenttypen zu SDK-Schritt-URLs
+ */
+export const DOCUMENT_SDK_STEP_MAP: Partial> = {
+ vvt: '/sdk/vvt',
+ lf: '/sdk/loeschkonzept',
+ tom: '/sdk/tom',
+ av_vertrag: '/sdk/auftragsverarbeitung',
+ dsi: '/sdk/datenschutzinformation',
+ betroffenenrechte: '/sdk/betroffenenrechte',
+ dsfa: '/sdk/dsfa',
+ daten_transfer: '/sdk/drittland',
+ datenpannen: '/sdk/datenpanne',
+ einwilligung: '/sdk/einwilligung',
+ vertragsmanagement: '/sdk/vertragsmanagement',
+ schulung: '/sdk/schulung',
+ audit_log: '/sdk/audit-logging',
+ risikoanalyse: '/sdk/risikoanalyse',
+ notfallplan: '/sdk/notfallplan',
+ zertifizierung: '/sdk/zertifizierung',
+ datenschutzmanagement: '/sdk/dsms',
+};
+
+// ============================================================================
+// Helper Functions
+// ============================================================================
+
+/**
+ * Erstellt einen leeren Scope State
+ */
+export function createEmptyScopeState(): ComplianceScopeState {
+ return {
+ answers: [],
+ decision: null,
+ lastEvaluatedAt: null,
+ isComplete: false,
+ };
+}
+
+/**
+ * Erstellt eine leere Scope Decision mit Default-Werten
+ */
+export function createEmptyScopeDecision(): ScopeDecision {
+ return {
+ id: `decision_${Date.now()}`,
+ determinedLevel: 'L1',
+ scores: {
+ risk_score: 0,
+ complexity_score: 0,
+ assurance_need: 0,
+ composite_score: 0,
+ },
+ triggeredHardTriggers: [],
+ requiredDocuments: [],
+ riskFlags: [],
+ gaps: [],
+ nextActions: [],
+ reasoning: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+}
+
+/**
+ * Gibt das hΓΆhere von zwei Depth Levels zurΓΌck
+ */
+export function maxDepthLevel(
+ a: ComplianceDepthLevel,
+ b: ComplianceDepthLevel
+): ComplianceDepthLevel {
+ const levels: ComplianceDepthLevel[] = ['L1', 'L2', 'L3', 'L4'];
+ const indexA = levels.indexOf(a);
+ const indexB = levels.indexOf(b);
+ return levels[Math.max(indexA, indexB)];
+}
+
+/**
+ * Konvertiert Depth Level zu numerischem Wert (1-4)
+ */
+export function getDepthLevelNumeric(level: ComplianceDepthLevel): number {
+ const map: Record = {
+ L1: 1,
+ L2: 2,
+ L3: 3,
+ L4: 4,
+ };
+ return map[level];
+}
+
+/**
+ * Konvertiert numerischen Wert (1-4) zu Depth Level
+ */
+export function depthLevelFromNumeric(n: number): ComplianceDepthLevel {
+ const map: Record = {
+ 1: 'L1',
+ 2: 'L2',
+ 3: 'L3',
+ 4: 'L4',
+ };
+ return map[Math.max(1, Math.min(4, Math.round(n)))] || 'L1';
+}
diff --git a/admin-v2/lib/sdk/context.tsx b/admin-v2/lib/sdk/context.tsx
index 235504f..1c9bc6b 100644
--- a/admin-v2/lib/sdk/context.tsx
+++ b/admin-v2/lib/sdk/context.tsx
@@ -62,6 +62,9 @@ const initialState: SDKState = {
// Company Profile
companyProfile: null,
+ // Compliance Scope
+ complianceScope: null,
+
// Progress
currentPhase: 1,
currentStep: 'company-profile',
@@ -179,6 +182,16 @@ function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState {
: null,
})
+ case 'SET_COMPLIANCE_SCOPE':
+ return updateState({ complianceScope: action.payload })
+
+ case 'UPDATE_COMPLIANCE_SCOPE':
+ return updateState({
+ complianceScope: state.complianceScope
+ ? { ...state.complianceScope, ...action.payload }
+ : null,
+ })
+
case 'ADD_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: [...state.importedDocuments, action.payload],
@@ -448,6 +461,10 @@ interface SDKContextValue {
setCompanyProfile: (profile: CompanyProfile) => void
updateCompanyProfile: (updates: Partial) => void
+ // Compliance Scope
+ setComplianceScope: (scope: import('./compliance-scope-types').ComplianceScopeState) => void
+ updateComplianceScope: (updates: Partial) => void
+
// Import (for existing customers)
addImportedDocument: (doc: ImportedDocument) => void
setGapAnalysis: (analysis: GapAnalysis) => void
@@ -740,6 +757,15 @@ export function SDKProvider({
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates })
}, [])
+ // Compliance Scope
+ const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => {
+ dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope })
+ }, [])
+
+ const updateComplianceScope = useCallback((updates: Partial) => {
+ dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates })
+ }, [])
+
// Import Document
const addImportedDocument = useCallback((doc: ImportedDocument) => {
dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc })
@@ -1040,6 +1066,8 @@ export function SDKProvider({
setCustomerType,
setCompanyProfile,
updateCompanyProfile,
+ setComplianceScope,
+ updateComplianceScope,
addImportedDocument,
setGapAnalysis,
validateCheckpoint,
diff --git a/admin-v2/lib/sdk/dsfa/index.ts b/admin-v2/lib/sdk/dsfa/index.ts
index 774d2d5..8f2520c 100644
--- a/admin-v2/lib/sdk/dsfa/index.ts
+++ b/admin-v2/lib/sdk/dsfa/index.ts
@@ -6,3 +6,5 @@
export * from './types'
export * from './api'
+export * from './risk-catalog'
+export * from './mitigation-library'
diff --git a/admin-v2/lib/sdk/dsfa/mitigation-library.ts b/admin-v2/lib/sdk/dsfa/mitigation-library.ts
new file mode 100644
index 0000000..d462663
--- /dev/null
+++ b/admin-v2/lib/sdk/dsfa/mitigation-library.ts
@@ -0,0 +1,694 @@
+/**
+ * DSFA Massnahmenbibliothek - Vordefinierte Massnahmen
+ *
+ * ~50 Massnahmen gegliedert nach SDM-Gewaehrleistungszielen
+ * (Vertraulichkeit, Integritaet, Verfuegbarkeit, Datenminimierung,
+ * Transparenz, Nichtverkettung, Intervenierbarkeit) sowie
+ * Automatisierung/KI, Rechtlich/Organisatorisch.
+ *
+ * Quellen: Art. 25/32 DSGVO, SDM V2.0, BSI Grundschutz,
+ * Baseline-DSFA Katalog
+ */
+
+import type { DSFAMitigationType } from './types'
+import type { SDMGoal } from './types'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export interface CatalogMitigation {
+ id: string
+ type: DSFAMitigationType
+ sdmGoals: SDMGoal[]
+ title: string
+ description: string
+ legalBasis: string
+ evidenceTypes: string[]
+ addressesRiskIds: string[]
+ effectiveness: 'low' | 'medium' | 'high'
+}
+
+// =============================================================================
+// MASSNAHMENBIBLIOTHEK
+// =============================================================================
+
+export const MITIGATION_LIBRARY: CatalogMitigation[] = [
+ // =========================================================================
+ // VERTRAULICHKEIT (Access Control & Encryption)
+ // =========================================================================
+ {
+ id: 'M-ACC-01',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Multi-Faktor-Authentifizierung (MFA) & Conditional Access',
+ description: 'Einfuehrung von MFA fuer alle Benutzerkonten mit Zugriff auf personenbezogene Daten. Conditional Access Policies beschraenken den Zugriff basierend auf Standort, Geraet und Risikobewertung.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['MFA-Policy-Screenshot', 'Conditional-Access-Regeln', 'Login-Statistiken'],
+ addressesRiskIds: ['R-CONF-02', 'R-CONF-06'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-ACC-02',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Passwort-Policy & Credential-Schutz',
+ description: 'Durchsetzung starker Passwort-Richtlinien, Credential-Rotation, Einsatz eines Passwort-Managers und Monitoring auf kompromittierte Zugangsdaten (Breach Detection).',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['Passwort-Policy-Dokument', 'Breach-Detection-Report'],
+ addressesRiskIds: ['R-CONF-02'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-CONF-01',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Rollenbasierte Zugriffskontrolle (RBAC) & Least Privilege',
+ description: 'Implementierung eines RBAC-Systems mit dem Prinzip der minimalen Berechtigung. Jeder Benutzer erhaelt nur die Rechte, die fuer seine Aufgabe erforderlich sind.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO, Art. 25 Abs. 2 DSGVO',
+ evidenceTypes: ['Rollen-Matrix', 'Berechtigungs-Audit-Report', 'Access-Review-Protokoll'],
+ addressesRiskIds: ['R-CONF-01', 'R-CONF-03', 'R-INT-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-CONF-02',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Security Configuration Management',
+ description: 'Regelmaessige Ueberpruefung und Haertung der Systemkonfiguration. Automatisierte Konfigurationschecks (CIS Benchmarks) und Monitoring auf Konfigurationsaenderungen.',
+ legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
+ evidenceTypes: ['CIS-Benchmark-Report', 'Konfigurationsaenderungs-Log'],
+ addressesRiskIds: ['R-CONF-01'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-CONF-03',
+ type: 'organizational',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Regelmaessige Zugriffsrechte-Ueberpruefung (Access Review)',
+ description: 'Quartalsweiser Review aller Zugriffsberechtigungen durch Vorgesetzte. Entzug nicht mehr benoetigter Rechte, Offboarding-Prozess bei Mitarbeiteraustritt.',
+ legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
+ evidenceTypes: ['Access-Review-Protokoll', 'Offboarding-Checkliste'],
+ addressesRiskIds: ['R-CONF-01', 'R-CONF-03'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-CONF-04',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit', 'integritaet'],
+ title: 'Privileged Access Management (PAM)',
+ description: 'Absicherung administrativer Zugriffe durch Just-in-Time-Elevation, Session-Recording und Break-Glass-Prozeduren fuer Notfallzugriffe.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['PAM-Policy', 'Session-Recording-Logs', 'Break-Glass-Protokolle'],
+ addressesRiskIds: ['R-CONF-03', 'R-INT-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-CONF-05',
+ type: 'organizational',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Vier-Augen-Prinzip fuer sensible Operationen',
+ description: 'Fuer den Zugriff auf besonders schutzwuerdige Daten oder kritische Systemoperationen ist die Genehmigung durch eine zweite Person erforderlich.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['Prozessbeschreibung', 'Genehmigungsprotokoll'],
+ addressesRiskIds: ['R-CONF-03'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-CONF-06',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Verschluesselung at-rest und in-transit',
+ description: 'Vollstaendige Verschluesselung personenbezogener Daten bei Speicherung (AES-256) und Uebertragung (TLS 1.3). Verwaltung der Schluessel ueber ein zentrales Key-Management-System.',
+ legalBasis: 'Art. 32 Abs. 1 lit. a DSGVO',
+ evidenceTypes: ['Verschluesselungs-Policy', 'TLS-Konfigurationsreport', 'KMS-Audit'],
+ addressesRiskIds: ['R-CONF-04', 'R-TRANS-01', 'R-AUTO-05'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-CONF-07',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'End-to-End-Verschluesselung fuer Kommunikation',
+ description: 'Einsatz von End-to-End-Verschluesselung fuer sensible Kommunikation (E-Mail, Messaging), sodass auch der Betreiber keinen Zugriff auf die Inhalte hat.',
+ legalBasis: 'Art. 32 Abs. 1 lit. a DSGVO',
+ evidenceTypes: ['E2E-Konfiguration', 'Testbericht'],
+ addressesRiskIds: ['R-CONF-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-CONF-08',
+ type: 'technical',
+ sdmGoals: ['vertraulichkeit', 'datenminimierung'],
+ title: 'Log-Sanitization & PII-Filtering',
+ description: 'Automatische Filterung personenbezogener Daten aus Logs, Fehlermeldungen und Debug-Ausgaben. Einsatz von Tokenisierung oder Maskierung.',
+ legalBasis: 'Art. 25 Abs. 1 DSGVO, Art. 32 Abs. 1 lit. a DSGVO',
+ evidenceTypes: ['Log-Policy', 'PII-Filter-Konfiguration', 'Stichproben-Audit'],
+ addressesRiskIds: ['R-CONF-07'],
+ effectiveness: 'medium',
+ },
+
+ // =========================================================================
+ // INTEGRITAET (Audit, Monitoring, Integrity Checks)
+ // =========================================================================
+ {
+ id: 'M-INT-01',
+ type: 'technical',
+ sdmGoals: ['integritaet'],
+ title: 'Input-Validierung & Injection-Schutz',
+ description: 'Konsequente Validierung aller Eingaben, Prepared Statements fuer Datenbankzugriffe, Content Security Policy und Output-Encoding zum Schutz vor Injection-Angriffen.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['SAST-Report', 'Penetrationstest-Bericht', 'WAF-Regeln'],
+ addressesRiskIds: ['R-INT-01'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-INT-02',
+ type: 'technical',
+ sdmGoals: ['integritaet', 'transparenz'],
+ title: 'Audit-Logging & SIEM-Integration',
+ description: 'Lueckenlose Protokollierung aller sicherheitsrelevanten Ereignisse mit Weiterleitung an ein SIEM-System. Manipulation-sichere Logs mit Integritaetspruefung.',
+ legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
+ evidenceTypes: ['SIEM-Dashboard-Screenshot', 'Audit-Log-Beispiel', 'Alert-Regeln'],
+ addressesRiskIds: ['R-INT-01', 'R-INT-04', 'R-INT-05', 'R-CONF-03', 'R-ORG-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-INT-03',
+ type: 'technical',
+ sdmGoals: ['integritaet'],
+ title: 'Web Application Firewall (WAF) & API-Gateway',
+ description: 'Einsatz einer WAF zum Schutz vor OWASP Top 10 Angriffen und eines API-Gateways fuer Rate-Limiting, Schema-Validierung und Anomalie-Erkennung.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['WAF-Regelset', 'API-Gateway-Konfiguration', 'Blockierungs-Statistiken'],
+ addressesRiskIds: ['R-INT-01'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-INT-04',
+ type: 'technical',
+ sdmGoals: ['integritaet'],
+ title: 'Daten-Synchronisations-Monitoring & Integritaetspruefung',
+ description: 'Automatische Ueberwachung von Synchronisationsprozessen mit Checksummen-Vergleich, Konflikterkennung und Alerting bei Inkonsistenzen.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['Sync-Monitoring-Dashboard', 'Checksummen-Report', 'Incident-Log'],
+ addressesRiskIds: ['R-INT-02'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-INT-05',
+ type: 'technical',
+ sdmGoals: ['integritaet'],
+ title: 'Versionierung & Change-Tracking fuer personenbezogene Daten',
+ description: 'Alle Aenderungen an personenbezogenen Daten werden versioniert gespeichert (Audit-Trail). Wer hat wann was geaendert ist jederzeit nachvollziehbar.',
+ legalBasis: 'Art. 5 Abs. 1 lit. f DSGVO',
+ evidenceTypes: ['Versionierungs-Schema', 'Change-Log-Beispiel'],
+ addressesRiskIds: ['R-INT-02', 'R-INT-05'],
+ effectiveness: 'medium',
+ },
+
+ // =========================================================================
+ // VERFUEGBARKEIT (Backup, Recovery, Redundancy)
+ // =========================================================================
+ {
+ id: 'M-AVAIL-01',
+ type: 'technical',
+ sdmGoals: ['verfuegbarkeit'],
+ title: 'Backup-Strategie mit 3-2-1-Regel',
+ description: 'Implementierung einer Backup-Strategie nach der 3-2-1-Regel: 3 Kopien, 2 verschiedene Medien, 1 Offsite. Verschluesselte Backups mit regelmaessiger Integritaetspruefung.',
+ legalBasis: 'Art. 32 Abs. 1 lit. c DSGVO',
+ evidenceTypes: ['Backup-Policy', 'Backup-Monitoring-Report', 'Offsite-Nachweis'],
+ addressesRiskIds: ['R-AVAIL-01', 'R-AVAIL-03', 'R-INT-03'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-AVAIL-02',
+ type: 'organizational',
+ sdmGoals: ['verfuegbarkeit'],
+ title: 'Regelmaessige Restore-Tests & Disaster Recovery Uebungen',
+ description: 'Mindestens quartalsweise Durchfuehrung von Restore-Tests und jaehrliche Disaster-Recovery-Uebungen. Dokumentation der Ergebnisse und Lessons Learned.',
+ legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
+ evidenceTypes: ['Restore-Test-Protokoll', 'DR-Uebungs-Dokumentation', 'RTO/RPO-Nachweis'],
+ addressesRiskIds: ['R-AVAIL-01', 'R-AVAIL-03', 'R-INT-03'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-AVAIL-03',
+ type: 'technical',
+ sdmGoals: ['verfuegbarkeit'],
+ title: 'Endpoint Protection & Anti-Ransomware',
+ description: 'Einsatz von Endpoint-Detection-and-Response (EDR) Loesungen mit spezifischem Ransomware-Schutz, Verhaltensanalyse und automatischer Isolation kompromittierter Systeme.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['EDR-Dashboard', 'Threat-Detection-Statistiken', 'Incident-Response-Plan'],
+ addressesRiskIds: ['R-AVAIL-01'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-AVAIL-04',
+ type: 'technical',
+ sdmGoals: ['verfuegbarkeit'],
+ title: 'Redundanz & High-Availability-Architektur',
+ description: 'Redundante Systemauslegung mit automatischem Failover, Load-Balancing und geo-redundanter Datenhaltung zur Sicherstellung der Verfuegbarkeit.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['HA-Architekturdiagramm', 'Failover-Testprotokoll', 'SLA-Dokumentation'],
+ addressesRiskIds: ['R-AVAIL-02', 'R-AVAIL-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-AVAIL-05',
+ type: 'organizational',
+ sdmGoals: ['verfuegbarkeit', 'intervenierbarkeit'],
+ title: 'Exit-Strategie & Datenportabilitaetsplan',
+ description: 'Dokumentierte Exit-Strategie fuer jeden kritischen Anbieter mit Datenexport-Verfahren, Migrationsplan und Uebergangsfristen. Regelmaessiger Export-Test.',
+ legalBasis: 'Art. 28 Abs. 3 lit. g DSGVO, Art. 20 DSGVO',
+ evidenceTypes: ['Exit-Plan-Dokument', 'Export-Test-Protokoll', 'Vertragliche-Regelung'],
+ addressesRiskIds: ['R-AVAIL-02', 'R-AVAIL-05'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-AVAIL-06',
+ type: 'technical',
+ sdmGoals: ['verfuegbarkeit'],
+ title: 'DDoS-Schutz & Rate-Limiting',
+ description: 'Einsatz von DDoS-Mitigation-Services, CDN-basiertem Schutz und anwendungsspezifischem Rate-Limiting zur Abwehr von Verfuegbarkeitsangriffen.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['DDoS-Schutz-Konfiguration', 'Rate-Limit-Regeln', 'Traffic-Analyse'],
+ addressesRiskIds: ['R-AVAIL-04'],
+ effectiveness: 'high',
+ },
+
+ // =========================================================================
+ // DATENMINIMIERUNG (Retention, Anonymization, Purpose Limitation)
+ // =========================================================================
+ {
+ id: 'M-DMIN-01',
+ type: 'technical',
+ sdmGoals: ['datenminimierung'],
+ title: 'Privacy by Design: Datenerhebung auf das Minimum beschraenken',
+ description: 'Technische Massnahmen zur Beschraenkung der Datenerhebung: Pflichtfelder minimieren, optionale Felder deutlich kennzeichnen, Default-Einstellungen datenschutzfreundlich.',
+ legalBasis: 'Art. 25 Abs. 1 DSGVO',
+ evidenceTypes: ['Formular-Review', 'Default-Settings-Dokumentation'],
+ addressesRiskIds: ['R-RIGHTS-07', 'R-CONF-07'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-DMIN-02',
+ type: 'technical',
+ sdmGoals: ['datenminimierung', 'nichtverkettung'],
+ title: 'Pseudonymisierung & Anonymisierung',
+ description: 'Einsatz von Pseudonymisierungsverfahren (Token-basiert, Hash-basiert) und k-Anonymity/Differential Privacy bei der Weitergabe oder Analyse von Daten.',
+ legalBasis: 'Art. 25 Abs. 1 DSGVO, Art. 32 Abs. 1 lit. a DSGVO',
+ evidenceTypes: ['Pseudonymisierungs-Konzept', 'Re-Identifizierungs-Risiko-Analyse'],
+ addressesRiskIds: ['R-RIGHTS-04', 'R-RIGHTS-07'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-DMIN-03',
+ type: 'technical',
+ sdmGoals: ['datenminimierung'],
+ title: 'Automatisiertes Loeschkonzept mit Aufbewahrungsfristen',
+ description: 'Implementierung automatischer Loeschroutinen basierend auf definierten Aufbewahrungsfristen. Monitoring der Loeschvorgaenge und Nachweis der Loeschung.',
+ legalBasis: 'Art. 5 Abs. 1 lit. e DSGVO, Art. 17 DSGVO',
+ evidenceTypes: ['Loeschkonzept-Dokument', 'Loeschfrist-Uebersicht', 'Loeschprotokoll'],
+ addressesRiskIds: ['R-RIGHTS-07', 'R-ORG-02'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-DMIN-04',
+ type: 'organizational',
+ sdmGoals: ['datenminimierung'],
+ title: 'Regelmaessige Ueberpruefung der Datenbestaende',
+ description: 'Jaehrlicher Review aller gespeicherten personenbezogenen Daten auf Erforderlichkeit. Identifikation und Bereinigung von Altbestaenden, verwaisten Datensaetzen und redundanten Kopien.',
+ legalBasis: 'Art. 5 Abs. 1 lit. e DSGVO',
+ evidenceTypes: ['Datenbestand-Review-Bericht', 'Bereinigungs-Protokoll'],
+ addressesRiskIds: ['R-ORG-02'],
+ effectiveness: 'medium',
+ },
+
+ // =========================================================================
+ // TRANSPARENZ (Information, Documentation, Auditability)
+ // =========================================================================
+ {
+ id: 'M-TRANS-01',
+ type: 'organizational',
+ sdmGoals: ['transparenz'],
+ title: 'Datenschutzhinweise & Privacy Notices',
+ description: 'Umfassende, verstaendliche Datenschutzhinweise gemaess Art. 13/14 DSGVO an allen Erhebungsstellen. Layered-Approach fuer unterschiedliche Detailstufen.',
+ legalBasis: 'Art. 13, Art. 14 DSGVO',
+ evidenceTypes: ['Privacy-Notice-Review', 'Zustellungs-Nachweis', 'Usability-Test'],
+ addressesRiskIds: ['R-CONF-05', 'R-RIGHTS-02', 'R-RIGHTS-03', 'R-RIGHTS-06', 'R-TRANS-03', 'R-SPEC-02'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-TRANS-02',
+ type: 'technical',
+ sdmGoals: ['transparenz'],
+ title: 'Vollstaendiger Audit-Trail fuer personenbezogene Daten',
+ description: 'Lueckenloser, manipulationssicherer Audit-Trail fuer alle Verarbeitungsvorgaenge personenbezogener Daten. Wer hat wann auf welche Daten zugegriffen oder sie veraendert.',
+ legalBasis: 'Art. 5 Abs. 2 DSGVO (Rechenschaftspflicht)',
+ evidenceTypes: ['Audit-Trail-Architektur', 'Log-Integritaets-Nachweis', 'Beispiel-Audit-Export'],
+ addressesRiskIds: ['R-INT-05'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-TRANS-03',
+ type: 'technical',
+ sdmGoals: ['transparenz'],
+ title: 'Erklaerbarkeit von KI-Entscheidungen (Explainability)',
+ description: 'Implementierung von Erklaerungsverfahren (SHAP, LIME, Feature-Importance) fuer automatisierte Entscheidungen. Bereitstellung verstaendlicher Begruendungen fuer Betroffene.',
+ legalBasis: 'Art. 22 Abs. 3 DSGVO, Art. 13 Abs. 2 lit. f DSGVO',
+ evidenceTypes: ['XAI-Konzept', 'Erklaerbarkeits-Beispiel', 'Betroffenen-Information'],
+ addressesRiskIds: ['R-AUTO-01', 'R-AUTO-03', 'R-RIGHTS-01'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-TRANS-04',
+ type: 'organizational',
+ sdmGoals: ['transparenz'],
+ title: 'Ueberwachungs-Folgenabschaetzung & Informationspflicht',
+ description: 'Bei systematischer Ueberwachung: Gesonderte Folgenabschaetzung, klare Beschilderung/Information, Verhaeltnismaessigkeitspruefung und zeitliche Begrenzung.',
+ legalBasis: 'Art. 35 Abs. 3 lit. c DSGVO, Art. 13 DSGVO',
+ evidenceTypes: ['Ueberwachungs-DSFA', 'Beschilderungs-Nachweis', 'Verhaeltnismaessigkeits-Bewertung'],
+ addressesRiskIds: ['R-RIGHTS-03'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-TRANS-05',
+ type: 'organizational',
+ sdmGoals: ['transparenz'],
+ title: 'Verzeichnis von Verarbeitungstaetigkeiten (VVT) pflegen',
+ description: 'Vollstaendiges und aktuelles VVT gemaess Art. 30 DSGVO fuer alle Verarbeitungstaetigkeiten. Regelmaessige Aktualisierung bei Aenderungen.',
+ legalBasis: 'Art. 30 DSGVO',
+ evidenceTypes: ['VVT-Export', 'Aktualisierungs-Log'],
+ addressesRiskIds: ['R-RIGHTS-06'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-TRANS-06',
+ type: 'legal',
+ sdmGoals: ['transparenz', 'vertraulichkeit'],
+ title: 'Transfer Impact Assessment (TIA) fuer Drittlandtransfer',
+ description: 'Durchfuehrung eines Transfer Impact Assessments vor jedem Drittlandtransfer. Bewertung des Schutzniveaus im Empfaengerland und Festlegung zusaetzlicher Garantien.',
+ legalBasis: 'Art. 46 DSGVO, Schrems-II-Urteil',
+ evidenceTypes: ['TIA-Dokument', 'Schutzniveau-Analyse', 'Zusaetzliche-Garantien-Vereinbarung'],
+ addressesRiskIds: ['R-TRANS-01', 'R-TRANS-02'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-TRANS-07',
+ type: 'legal',
+ sdmGoals: ['vertraulichkeit'],
+ title: 'Standardvertragsklauseln (SCC) & Supplementary Measures',
+ description: 'Abschluss aktueller EU-Standardvertragsklauseln (2021/914) mit Auftragsverarbeitern im Drittland. Ergaenzende technische und organisatorische Massnahmen (Verschluesselung, Pseudonymisierung).',
+ legalBasis: 'Art. 46 Abs. 2 lit. c DSGVO',
+ evidenceTypes: ['Unterzeichnete SCC', 'Supplementary-Measures-Dokumentation'],
+ addressesRiskIds: ['R-TRANS-01', 'R-TRANS-02'],
+ effectiveness: 'medium',
+ },
+
+ // =========================================================================
+ // NICHTVERKETTUNG (Purpose Limitation, Data Separation, DLP)
+ // =========================================================================
+ {
+ id: 'M-NONL-01',
+ type: 'technical',
+ sdmGoals: ['nichtverkettung'],
+ title: 'Zweckbindung & Consent-Management',
+ description: 'Technische Durchsetzung der Zweckbindung: Daten werden nur fuer den erhobenen Zweck verwendet. Consent-Management-System protokolliert und erzwingt Einwilligungen.',
+ legalBasis: 'Art. 5 Abs. 1 lit. b DSGVO, Art. 6 Abs. 1 lit. a DSGVO',
+ evidenceTypes: ['Consent-Management-System', 'Zweckbindungs-Matrix', 'Consent-Protokolle'],
+ addressesRiskIds: ['R-CONF-05', 'R-RIGHTS-02', 'R-RIGHTS-03'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-NONL-02',
+ type: 'technical',
+ sdmGoals: ['nichtverkettung'],
+ title: 'Data Loss Prevention (DLP) & Datenklassifikation',
+ description: 'Implementierung von DLP-Regeln zur Verhinderung unkontrollierter Datenweitergabe. Datenklassifikation (oeffentlich, intern, vertraulich, streng vertraulich) als Grundlage.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['DLP-Policy', 'Datenklassifikations-Schema', 'DLP-Incident-Report'],
+ addressesRiskIds: ['R-RIGHTS-02'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-NONL-03',
+ type: 'technical',
+ sdmGoals: ['nichtverkettung', 'datenminimierung'],
+ title: 'Differential Privacy & k-Anonymity bei Datenanalysen',
+ description: 'Einsatz von Differential Privacy oder k-Anonymity-Verfahren bei der Analyse personenbezogener Daten, um Re-Identifizierung zu verhindern.',
+ legalBasis: 'Art. 25 Abs. 1 DSGVO',
+ evidenceTypes: ['Anonymisierungs-Konzept', 'Privacy-Budget-Berechnung', 'k-Anonymity-Nachweis'],
+ addressesRiskIds: ['R-RIGHTS-04', 'R-AUTO-05'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-NONL-04',
+ type: 'technical',
+ sdmGoals: ['nichtverkettung'],
+ title: 'Mandantentrennung & Datenisolierung',
+ description: 'Strikte logische oder physische Trennung personenbezogener Daten verschiedener Mandanten/Zwecke. Verhinderung unbeabsichtigter Zusammenfuehrung.',
+ legalBasis: 'Art. 5 Abs. 1 lit. b DSGVO',
+ evidenceTypes: ['Mandantentrennungs-Konzept', 'Isolierungs-Test-Bericht'],
+ addressesRiskIds: ['R-RIGHTS-04'],
+ effectiveness: 'high',
+ },
+
+ // =========================================================================
+ // INTERVENIERBARKEIT (Data Subject Rights, Correction, Deletion)
+ // =========================================================================
+ {
+ id: 'M-INTERV-01',
+ type: 'technical',
+ sdmGoals: ['intervenierbarkeit'],
+ title: 'DSAR-Workflow (Data Subject Access Request)',
+ description: 'Automatisierter Workflow fuer Betroffenenanfragen (Auskunft, Loeschung, Berichtigung, Export). Fristenmanagement (1 Monat), Identitaetspruefung und Dokumentation.',
+ legalBasis: 'Art. 15-22 DSGVO, Art. 12 Abs. 3 DSGVO',
+ evidenceTypes: ['DSAR-Workflow-Dokumentation', 'Bearbeitungszeiten-Statistik', 'Audit-Trail'],
+ addressesRiskIds: ['R-RIGHTS-05', 'R-AVAIL-05'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-INTERV-02',
+ type: 'technical',
+ sdmGoals: ['intervenierbarkeit'],
+ title: 'Self-Service Datenverwaltung fuer Betroffene',
+ description: 'Bereitstellung eines Self-Service-Portals, ueber das Betroffene ihre Daten einsehen, korrigieren, exportieren und die Loeschung beantragen koennen.',
+ legalBasis: 'Art. 15-20 DSGVO',
+ evidenceTypes: ['Portal-Screenshot', 'Funktions-Testprotokoll', 'Nutzungs-Statistik'],
+ addressesRiskIds: ['R-RIGHTS-05'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-INTERV-03',
+ type: 'organizational',
+ sdmGoals: ['intervenierbarkeit'],
+ title: 'Widerspruchs- und Einschraenkungsprozess',
+ description: 'Definierter Prozess fuer die Bearbeitung von Widerspruechen (Art. 21) und Einschraenkungsersuchen (Art. 18). Technische Moeglichkeit zur Sperrung einzelner Datensaetze.',
+ legalBasis: 'Art. 18, Art. 21 DSGVO',
+ evidenceTypes: ['Prozessbeschreibung', 'Sperr-Funktionalitaets-Nachweis'],
+ addressesRiskIds: ['R-RIGHTS-05'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-INTERV-04',
+ type: 'organizational',
+ sdmGoals: ['intervenierbarkeit'],
+ title: 'Human-in-the-Loop bei automatisierten Entscheidungen',
+ description: 'Sicherstellung menschlicher Ueberpruefung bei automatisierten Entscheidungen mit erheblicher Auswirkung. Eskalationsprozess und Einspruchsmoeglichkeit fuer Betroffene.',
+ legalBasis: 'Art. 22 Abs. 3 DSGVO',
+ evidenceTypes: ['HITL-Prozessbeschreibung', 'Eskalations-Statistik', 'Einspruchs-Protokoll'],
+ addressesRiskIds: ['R-AUTO-04'],
+ effectiveness: 'high',
+ },
+
+ // =========================================================================
+ // AUTOMATISIERUNG / KI
+ // =========================================================================
+ {
+ id: 'M-AUTO-01',
+ type: 'technical',
+ sdmGoals: ['nichtverkettung', 'transparenz'],
+ title: 'Bias-Monitoring & Fairness-Tests',
+ description: 'Regelmaessige Ueberpruefung von KI-Modellen auf Bias und Diskriminierung. Fairness-Metriken (Demographic Parity, Equal Opportunity) und Korrekturmassnahmen bei Abweichungen.',
+ legalBasis: 'Art. 22 Abs. 3 DSGVO, AI Act Art. 10',
+ evidenceTypes: ['Bias-Audit-Report', 'Fairness-Metriken-Dashboard', 'Korrektur-Dokumentation'],
+ addressesRiskIds: ['R-RIGHTS-01', 'R-AUTO-01', 'R-AUTO-02'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-AUTO-02',
+ type: 'technical',
+ sdmGoals: ['transparenz'],
+ title: 'KI-Modell-Dokumentation & Model Cards',
+ description: 'Ausfuehrliche Dokumentation aller KI-Modelle: Trainingsdaten, Architektur, Performance-Metriken, bekannte Einschraenkungen, Einsatzzweck (Model Cards).',
+ legalBasis: 'Art. 13 Abs. 2 lit. f DSGVO, AI Act Art. 11',
+ evidenceTypes: ['Model-Card', 'Performance-Report', 'Einsatzbereich-Dokumentation'],
+ addressesRiskIds: ['R-AUTO-01', 'R-AUTO-03'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-AUTO-03',
+ type: 'organizational',
+ sdmGoals: ['intervenierbarkeit', 'transparenz'],
+ title: 'KI-Governance-Framework & Human Oversight Board',
+ description: 'Etablierung eines KI-Governance-Frameworks mit einem Human Oversight Board, das alle KI-Systeme mit hohem Risiko ueberwacht und Interventionsmoeglichkeiten hat.',
+ legalBasis: 'Art. 22 DSGVO, AI Act Art. 14',
+ evidenceTypes: ['Governance-Policy', 'Oversight-Board-Protokolle', 'Interventions-Log'],
+ addressesRiskIds: ['R-AUTO-01', 'R-AUTO-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-AUTO-04',
+ type: 'technical',
+ sdmGoals: ['nichtverkettung', 'datenminimierung'],
+ title: 'Datenschutzkonformes KI-Training (Privacy-Preserving ML)',
+ description: 'Einsatz von Federated Learning, Differential Privacy beim Training oder synthetischen Trainingsdaten, um personenbezogene Daten im Modell zu schuetzen.',
+ legalBasis: 'Art. 25 Abs. 1 DSGVO',
+ evidenceTypes: ['Privacy-Preserving-ML-Konzept', 'Training-Daten-Analyse', 'Modell-Invertierbarkeiots-Test'],
+ addressesRiskIds: ['R-AUTO-02', 'R-AUTO-05'],
+ effectiveness: 'high',
+ },
+
+ // =========================================================================
+ // ORGANISATORISCHE MASSNAHMEN
+ // =========================================================================
+ {
+ id: 'M-ORG-01',
+ type: 'organizational',
+ sdmGoals: ['vertraulichkeit', 'integritaet'],
+ title: 'Datenschutz-Schulungen & Awareness-Programm',
+ description: 'Regelmaessige verpflichtende Datenschutz-Schulungen fuer alle Mitarbeiter. Awareness-Kampagnen zu Phishing, Social Engineering und sicherem Datenumgang.',
+ legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO, Art. 39 Abs. 1 lit. a DSGVO',
+ evidenceTypes: ['Schulungsplan', 'Teilnahmequoten', 'Phishing-Simulations-Ergebnis'],
+ addressesRiskIds: ['R-CONF-06', 'R-ORG-03'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-ORG-02',
+ type: 'organizational',
+ sdmGoals: ['integritaet'],
+ title: 'Verpflichtung auf Vertraulichkeit & Datenschutz-Policy',
+ description: 'Schriftliche Verpflichtung aller Mitarbeiter und externen Dienstleister auf Vertraulichkeit und Einhaltung der Datenschutz-Policies.',
+ legalBasis: 'Art. 28 Abs. 3 lit. b DSGVO, Art. 29 DSGVO',
+ evidenceTypes: ['Unterzeichnete-Verpflichtungserklaerung', 'Datenschutz-Policy'],
+ addressesRiskIds: ['R-ORG-03'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-ORG-03',
+ type: 'organizational',
+ sdmGoals: ['transparenz'],
+ title: 'Datenpannen-Erkennungs- und Meldeprozess (Incident Response)',
+ description: 'Definierter Incident-Response-Prozess mit klaren Eskalationswegen, 72h-Meldepflicht-Tracking, Klassifizierungsschema und Kommunikationsplan.',
+ legalBasis: 'Art. 33, Art. 34 DSGVO',
+ evidenceTypes: ['Incident-Response-Plan', 'Melde-Template', 'Uebungs-Protokoll'],
+ addressesRiskIds: ['R-ORG-04'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-ORG-04',
+ type: 'technical',
+ sdmGoals: ['transparenz', 'verfuegbarkeit'],
+ title: 'Automatisiertes Breach-Detection & Alerting',
+ description: 'Automatische Erkennung von Datenpannen durch Anomalie-Detection, ungewoehnliche Zugriffsmuster und Datenexfiltrations-Erkennung mit sofortigem Alert an den Incident-Response-Team.',
+ legalBasis: 'Art. 33 Abs. 1 DSGVO',
+ evidenceTypes: ['Alert-Regeln', 'Detection-Dashboard', 'Reaktionszeiten-Statistik'],
+ addressesRiskIds: ['R-ORG-04'],
+ effectiveness: 'high',
+ },
+
+ // =========================================================================
+ // RECHTLICHE MASSNAHMEN
+ // =========================================================================
+ {
+ id: 'M-LEGAL-01',
+ type: 'legal',
+ sdmGoals: ['transparenz'],
+ title: 'Angemessenheitsbeschluss oder Binding Corporate Rules (BCR)',
+ description: 'Sicherstellung, dass Drittlandtransfers auf einem Angemessenheitsbeschluss oder genehmigten BCRs basieren. Laufende Ueberwachung des Schutzniveaus.',
+ legalBasis: 'Art. 45, Art. 47 DSGVO',
+ evidenceTypes: ['Angemessenheitsbeschluss-Referenz', 'BCR-Genehmigung'],
+ addressesRiskIds: ['R-TRANS-02'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-LEGAL-02',
+ type: 'legal',
+ sdmGoals: ['transparenz'],
+ title: 'Auftragsverarbeitungsvertrag (AVV) nach Art. 28 DSGVO',
+ description: 'Abschluss vollstaendiger AVVs mit allen Auftragsverarbeitern. Regelung von Zweck, Dauer, Datenkategorien, Weisungsbindung, Sub-Auftragsverarbeiter und Audit-Rechten.',
+ legalBasis: 'Art. 28 Abs. 3 DSGVO',
+ evidenceTypes: ['Unterzeichneter-AVV', 'Sub-Auftragsverarbeiter-Liste', 'Audit-Bericht'],
+ addressesRiskIds: ['R-ORG-01', 'R-TRANS-03'],
+ effectiveness: 'high',
+ },
+ {
+ id: 'M-LEGAL-03',
+ type: 'legal',
+ sdmGoals: ['transparenz'],
+ title: 'Regelmaessige Auftragsverarbeiter-Audits',
+ description: 'Jaehrliche Ueberpruefung der Auftragsverarbeiter auf Einhaltung der AVV-Vorgaben. Dokumentierte Audits vor Ort oder anhand von Zertifizierungen (SOC 2, ISO 27001).',
+ legalBasis: 'Art. 28 Abs. 3 lit. h DSGVO',
+ evidenceTypes: ['Audit-Bericht', 'Zertifizierungs-Nachweis', 'Massnahmenplan'],
+ addressesRiskIds: ['R-ORG-01'],
+ effectiveness: 'medium',
+ },
+ {
+ id: 'M-LEGAL-04',
+ type: 'legal',
+ sdmGoals: ['intervenierbarkeit', 'transparenz'],
+ title: 'Altersverifikation & Eltern-Einwilligung (Art. 8)',
+ description: 'Implementierung einer altersgerechten Verifikation und Einholung der Eltern-Einwilligung bei Minderjaehrigen unter 16 Jahren. Kindgerechte Datenschutzinformationen.',
+ legalBasis: 'Art. 8 DSGVO, EG 38 DSGVO',
+ evidenceTypes: ['Altersverifikations-Konzept', 'Eltern-Einwilligungs-Formular', 'Kindgerechte-Privacy-Notice'],
+ addressesRiskIds: ['R-SPEC-02'],
+ effectiveness: 'medium',
+ },
+]
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+export function getMitigationsBySDMGoal(goal: SDMGoal): CatalogMitigation[] {
+ return MITIGATION_LIBRARY.filter(m => m.sdmGoals.includes(goal))
+}
+
+export function getMitigationsByType(type: DSFAMitigationType): CatalogMitigation[] {
+ return MITIGATION_LIBRARY.filter(m => m.type === type)
+}
+
+export function getMitigationsForRisk(riskId: string): CatalogMitigation[] {
+ return MITIGATION_LIBRARY.filter(m => m.addressesRiskIds.includes(riskId))
+}
+
+export function getCatalogMitigationById(id: string): CatalogMitigation | undefined {
+ return MITIGATION_LIBRARY.find(m => m.id === id)
+}
+
+export function getMitigationsByEffectiveness(effectiveness: 'low' | 'medium' | 'high'): CatalogMitigation[] {
+ return MITIGATION_LIBRARY.filter(m => m.effectiveness === effectiveness)
+}
+
+export const MITIGATION_TYPE_LABELS: Record = {
+ technical: 'Technisch',
+ organizational: 'Organisatorisch',
+ legal: 'Rechtlich',
+}
+
+export const SDM_GOAL_LABELS: Record = {
+ datenminimierung: 'Datenminimierung',
+ verfuegbarkeit: 'Verfuegbarkeit',
+ integritaet: 'Integritaet',
+ vertraulichkeit: 'Vertraulichkeit',
+ nichtverkettung: 'Nichtverkettung',
+ transparenz: 'Transparenz',
+ intervenierbarkeit: 'Intervenierbarkeit',
+}
+
+export const EFFECTIVENESS_LABELS: Record = {
+ low: 'Gering',
+ medium: 'Mittel',
+ high: 'Hoch',
+}
diff --git a/admin-v2/lib/sdk/dsfa/risk-catalog.ts b/admin-v2/lib/sdk/dsfa/risk-catalog.ts
new file mode 100644
index 0000000..4e99dcd
--- /dev/null
+++ b/admin-v2/lib/sdk/dsfa/risk-catalog.ts
@@ -0,0 +1,615 @@
+/**
+ * DSFA Risikokatalog - Vordefinierte Risikoszenarien
+ *
+ * ~40 Risiken gegliedert nach Vertraulichkeit, Integritaet, Verfuegbarkeit,
+ * Rechte & Freiheiten, Drittlandtransfer und Automatisierung.
+ *
+ * Quellen: EG 75 DSGVO, Art. 32 DSGVO, Art. 28/46 DSGVO, Art. 22 DSGVO,
+ * Baseline-DSFA Katalog, SDM V2.0
+ */
+
+import type { DSFARiskCategory } from './types'
+import type { SDMGoal } from './types'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export interface CatalogRisk {
+ id: string
+ category: DSFARiskCategory
+ sdmGoal: SDMGoal
+ title: string
+ description: string
+ impactExamples: string[]
+ typicalLikelihood: 'low' | 'medium' | 'high'
+ typicalImpact: 'low' | 'medium' | 'high'
+ wp248Criteria: string[]
+ applicableTo: string[]
+ mitigationIds: string[]
+}
+
+// =============================================================================
+// RISIKOKATALOG
+// =============================================================================
+
+export const RISK_CATALOG: CatalogRisk[] = [
+ // =========================================================================
+ // VERTRAULICHKEIT (Confidentiality)
+ // =========================================================================
+ {
+ id: 'R-CONF-01',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Unbefugte Offenlegung durch Fehlkonfiguration',
+ description: 'Personenbezogene Daten werden durch fehlerhafte Systemkonfiguration (z.B. offene APIs, fehlerhafte Zugriffsrechte, oeffentliche Cloud-Speicher) unbefugt zugaenglich.',
+ impactExamples: ['Identitaetsdiebstahl', 'Reputationsschaden', 'Diskriminierung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K5'],
+ applicableTo: ['cloud_storage', 'web_application', 'api_service'],
+ mitigationIds: ['M-CONF-01', 'M-CONF-02', 'M-CONF-03'],
+ },
+ {
+ id: 'R-CONF-02',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Account Takeover / Credential Stuffing',
+ description: 'Angreifer uebernehmen Benutzerkonten durch gestohlene Zugangsdaten, Brute-Force-Angriffe oder Phishing und erlangen Zugriff auf personenbezogene Daten.',
+ impactExamples: ['Kontrollverlust ueber eigene Daten', 'Finanzieller Schaden', 'Missbrauch der Identitaet'],
+ typicalLikelihood: 'high',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K7'],
+ applicableTo: ['identity', 'web_application', 'email_service'],
+ mitigationIds: ['M-ACC-01', 'M-ACC-02'],
+ },
+ {
+ id: 'R-CONF-03',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Unbefugter Zugriff durch Support-/Administrationspersonal',
+ description: 'Administratoren oder Support-Mitarbeiter greifen ohne dienstliche Notwendigkeit auf personenbezogene Daten zu (Insider-Bedrohung).',
+ impactExamples: ['Verletzung der Privatsphaere', 'Datenmissbrauch', 'Vertrauensverlust'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K4'],
+ applicableTo: ['identity', 'crm', 'cloud_storage', 'support_system'],
+ mitigationIds: ['M-CONF-04', 'M-CONF-05', 'M-INT-02'],
+ },
+ {
+ id: 'R-CONF-04',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Datenleck durch unzureichende Verschluesselung',
+ description: 'Personenbezogene Daten werden bei Uebertragung oder Speicherung nicht oder unzureichend verschluesselt und koennen abgefangen werden.',
+ impactExamples: ['Man-in-the-Middle-Angriff', 'Datendiebstahl bei Speichermedien-Verlust'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K8'],
+ applicableTo: ['cloud_storage', 'email_service', 'mobile_app', 'api_service'],
+ mitigationIds: ['M-CONF-06', 'M-CONF-07'],
+ },
+ {
+ id: 'R-CONF-05',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Unkontrollierte Datenweitergabe an Dritte',
+ description: 'Personenbezogene Daten werden ohne Rechtsgrundlage oder ueber das vereinbarte Mass hinaus an Dritte weitergegeben (z.B. durch Tracking, Analyse-Tools, Sub-Auftragsverarbeiter).',
+ impactExamples: ['Unerwuenschte Werbung', 'Profiling ohne Wissen', 'Kontrollverlust'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K1', 'K6'],
+ applicableTo: ['web_application', 'analytics', 'marketing', 'crm'],
+ mitigationIds: ['M-NONL-01', 'M-TRANS-01'],
+ },
+ {
+ id: 'R-CONF-06',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Social Engineering / Phishing gegen Betroffene',
+ description: 'Betroffene werden durch manipulative Kommunikation dazu verleitet, personenbezogene Daten preiszugeben oder Zugriff zu gewaehren.',
+ impactExamples: ['Identitaetsdiebstahl', 'Finanzieller Schaden', 'Uebernahme von Konten'],
+ typicalLikelihood: 'high',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K7'],
+ applicableTo: ['email_service', 'web_application', 'identity'],
+ mitigationIds: ['M-ACC-01', 'M-ORG-01'],
+ },
+ {
+ id: 'R-CONF-07',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Unbeabsichtigte Offenlegung in Logs/Debugging',
+ description: 'Personenbezogene Daten gelangen in Protokolldateien, Fehlermeldungen oder Debug-Ausgaben und werden dort nicht geschuetzt.',
+ impactExamples: ['Zugriff durch Unbefugte auf Logdaten', 'Langzeitspeicherung ohne Rechtsgrundlage'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K4'],
+ applicableTo: ['api_service', 'web_application', 'cloud_storage'],
+ mitigationIds: ['M-CONF-08', 'M-DMIN-01'],
+ },
+
+ // =========================================================================
+ // INTEGRITAET (Integrity)
+ // =========================================================================
+ {
+ id: 'R-INT-01',
+ category: 'integrity',
+ sdmGoal: 'integritaet',
+ title: 'Datenmanipulation durch externen Angriff',
+ description: 'Personenbezogene Daten werden durch einen Cyberangriff (SQL-Injection, API-Manipulation) veraendert, ohne dass dies erkannt wird.',
+ impactExamples: ['Falsche Entscheidungen auf Basis manipulierter Daten', 'Rufschaedigung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K8'],
+ applicableTo: ['api_service', 'web_application', 'database'],
+ mitigationIds: ['M-INT-01', 'M-INT-02', 'M-INT-03'],
+ },
+ {
+ id: 'R-INT-02',
+ category: 'integrity',
+ sdmGoal: 'integritaet',
+ title: 'Fehlerhafte Synchronisation zwischen Systemen',
+ description: 'Bei der Synchronisation personenbezogener Daten zwischen verschiedenen Systemen kommt es zu Inkonsistenzen, Duplikaten oder Datenverlust.',
+ impactExamples: ['Falsche Kontaktdaten', 'Doppelte Verarbeitung', 'Falsche Auskuenfte'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K6'],
+ applicableTo: ['crm', 'cloud_storage', 'erp', 'identity'],
+ mitigationIds: ['M-INT-04', 'M-INT-05'],
+ },
+ {
+ id: 'R-INT-03',
+ category: 'integrity',
+ sdmGoal: 'integritaet',
+ title: 'Backup-Korruption oder fehlerhafte Wiederherstellung',
+ description: 'Backups personenbezogener Daten sind beschaedigt, unvollstaendig oder veraltet, sodass eine zuverlaessige Wiederherstellung nicht moeglich ist.',
+ impactExamples: ['Datenverlust bei Wiederherstellung', 'Veraltete Datenbasis', 'Compliance-Verstoss'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K5'],
+ applicableTo: ['database', 'cloud_storage', 'erp'],
+ mitigationIds: ['M-AVAIL-01', 'M-AVAIL-02'],
+ },
+ {
+ id: 'R-INT-04',
+ category: 'integrity',
+ sdmGoal: 'integritaet',
+ title: 'Unbemerkte Aenderung von Zugriffsrechten',
+ description: 'Zugriffsberechtigungen werden unbefugt oder fehlerhaft geaendert, wodurch unberechtigte Personen Zugang zu personenbezogenen Daten erhalten.',
+ impactExamples: ['Privilege Escalation', 'Unbefugter Datenzugriff'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4'],
+ applicableTo: ['identity', 'cloud_storage', 'api_service'],
+ mitigationIds: ['M-INT-02', 'M-CONF-04'],
+ },
+ {
+ id: 'R-INT-05',
+ category: 'integrity',
+ sdmGoal: 'integritaet',
+ title: 'Fehlende Nachvollziehbarkeit von Datenveraenderungen',
+ description: 'Aenderungen an personenbezogenen Daten werden nicht protokolliert, sodass Manipulationen oder Fehler nicht erkannt oder nachvollzogen werden koennen.',
+ impactExamples: ['Unmoeglich festzustellen wer/wann Daten geaendert hat', 'Audit-Versagen'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K3'],
+ applicableTo: ['database', 'crm', 'erp', 'web_application'],
+ mitigationIds: ['M-INT-02', 'M-TRANS-02'],
+ },
+
+ // =========================================================================
+ // VERFUEGBARKEIT (Availability)
+ // =========================================================================
+ {
+ id: 'R-AVAIL-01',
+ category: 'availability',
+ sdmGoal: 'verfuegbarkeit',
+ title: 'Ransomware-Angriff mit Datenverschluesselung',
+ description: 'Schadsoftware verschluesselt personenbezogene Daten und macht sie unzugaenglich. Die Wiederherstellung erfordert entweder Loesegeldzahlung oder Backup-Restore.',
+ impactExamples: ['Verlust des Zugangs zu eigenen Daten', 'Betriebsunterbrechung', 'Loesegeld-Erpressung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K5', 'K8'],
+ applicableTo: ['cloud_storage', 'database', 'erp', 'web_application'],
+ mitigationIds: ['M-AVAIL-01', 'M-AVAIL-02', 'M-AVAIL-03'],
+ },
+ {
+ id: 'R-AVAIL-02',
+ category: 'availability',
+ sdmGoal: 'verfuegbarkeit',
+ title: 'Provider-Ausfall / Cloud-Service Nichtverfuegbarkeit',
+ description: 'Der Cloud-/Hosting-Provider faellt aus, was den Zugang zu personenbezogenen Daten verhindert. Betroffene koennen ihre Rechte nicht ausueben.',
+ impactExamples: ['Keine Auskunft moeglich', 'Vertragsverletzung', 'Geschaeftsunterbrechung'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K5', 'K9'],
+ applicableTo: ['cloud_storage', 'web_application', 'api_service'],
+ mitigationIds: ['M-AVAIL-04', 'M-AVAIL-05'],
+ },
+ {
+ id: 'R-AVAIL-03',
+ category: 'availability',
+ sdmGoal: 'verfuegbarkeit',
+ title: 'Datenverlust durch fehlende oder ungetestete Backups',
+ description: 'Personenbezogene Daten gehen unwiederbringlich verloren, weil keine ausreichenden Backups existieren oder Restore-Prozesse nicht getestet werden.',
+ impactExamples: ['Unwiderruflicher Datenverlust', 'Verlust von Beweismitteln', 'Compliance-Verstoss'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K5'],
+ applicableTo: ['database', 'cloud_storage', 'erp'],
+ mitigationIds: ['M-AVAIL-01', 'M-AVAIL-02'],
+ },
+ {
+ id: 'R-AVAIL-04',
+ category: 'availability',
+ sdmGoal: 'verfuegbarkeit',
+ title: 'DDoS-Angriff auf oeffentliche Dienste',
+ description: 'Ein Distributed-Denial-of-Service-Angriff verhindert den Zugang zu Systemen, die personenbezogene Daten verarbeiten.',
+ impactExamples: ['Betroffene koennen Rechte nicht ausueben', 'Geschaeftsausfall'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K5', 'K9'],
+ applicableTo: ['web_application', 'api_service'],
+ mitigationIds: ['M-AVAIL-06', 'M-AVAIL-04'],
+ },
+ {
+ id: 'R-AVAIL-05',
+ category: 'availability',
+ sdmGoal: 'verfuegbarkeit',
+ title: 'Vendor Lock-in mit Kontrollverlust',
+ description: 'Abhaengigkeit von einem einzelnen Anbieter erschwert oder verhindert den Zugang zu personenbezogenen Daten bei Vertragsbeendigung oder Anbieterwechsel.',
+ impactExamples: ['Datenexport nicht moeglich', 'Erzwungene Weiternutzung', 'Datenverlust bei Kuendigung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K9'],
+ applicableTo: ['cloud_storage', 'erp', 'crm'],
+ mitigationIds: ['M-AVAIL-05', 'M-INTERV-01'],
+ },
+
+ // =========================================================================
+ // RECHTE & FREIHEITEN (Rights & Freedoms)
+ // =========================================================================
+ {
+ id: 'R-RIGHTS-01',
+ category: 'rights_freedoms',
+ sdmGoal: 'nichtverkettung',
+ title: 'Diskriminierung durch automatisierte Verarbeitung',
+ description: 'Automatisierte Entscheidungssysteme fuehren zu einer diskriminierenden Behandlung bestimmter Personengruppen aufgrund von Merkmalen wie Alter, Geschlecht, Herkunft oder Gesundheitszustand.',
+ impactExamples: ['Benachteiligung bei Kreditvergabe', 'Ausschluss von Dienstleistungen', 'Ungleichbehandlung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K1', 'K2', 'K7'],
+ applicableTo: ['ai_ml', 'scoring', 'identity'],
+ mitigationIds: ['M-AUTO-01', 'M-AUTO-02', 'M-TRANS-03'],
+ },
+ {
+ id: 'R-RIGHTS-02',
+ category: 'rights_freedoms',
+ sdmGoal: 'nichtverkettung',
+ title: 'Unzulaessiges Profiling ohne Einwilligung',
+ description: 'Nutzerverhalten wird systematisch analysiert und zu Profilen zusammengefuehrt, ohne dass eine Rechtsgrundlage oder Einwilligung vorliegt.',
+ impactExamples: ['Persoenlichkeitsprofile ohne Wissen', 'Gezielte Manipulation', 'Filterblase'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K1', 'K3', 'K6'],
+ applicableTo: ['analytics', 'marketing', 'web_application', 'ai_ml'],
+ mitigationIds: ['M-NONL-01', 'M-NONL-02', 'M-TRANS-01'],
+ },
+ {
+ id: 'R-RIGHTS-03',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'Systematische Ueberwachung von Betroffenen',
+ description: 'Betroffene werden systematisch ueberwacht (z.B. durch Standorttracking, E-Mail-Monitoring, Videoueberwachung), ohne angemessene Transparenz oder Rechtsgrundlage.',
+ impactExamples: ['Einschuechterungseffekt (Chilling Effect)', 'Verletzung der Privatsphaere', 'Vertrauensverlust'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K3', 'K4', 'K7'],
+ applicableTo: ['monitoring', 'hr_system', 'mobile_app'],
+ mitigationIds: ['M-TRANS-01', 'M-TRANS-04', 'M-NONL-01'],
+ },
+ {
+ id: 'R-RIGHTS-04',
+ category: 'rights_freedoms',
+ sdmGoal: 'nichtverkettung',
+ title: 'Re-Identifizierung pseudonymisierter Daten',
+ description: 'Pseudonymisierte oder anonymisierte Daten werden durch Zusammenfuehrung mit anderen Datenquellen re-identifiziert, wodurch der Schutz der Betroffenen aufgehoben wird.',
+ impactExamples: ['Verlust der Anonymitaet', 'Unerwuenschte Identifizierung', 'Zweckentfremdung'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K1', 'K6', 'K8'],
+ applicableTo: ['analytics', 'ai_ml', 'research'],
+ mitigationIds: ['M-NONL-03', 'M-NONL-04', 'M-DMIN-02'],
+ },
+ {
+ id: 'R-RIGHTS-05',
+ category: 'rights_freedoms',
+ sdmGoal: 'intervenierbarkeit',
+ title: 'Hinderung bei Ausuebung von Betroffenenrechten',
+ description: 'Betroffene werden an der Ausuebung ihrer Rechte (Auskunft, Loeschung, Berichtigung, Widerspruch) gehindert β z.B. durch fehlende Prozesse, technische Huerden oder Verzoegerungen.',
+ impactExamples: ['Keine Loeschung moeglich', 'Verzoegerte Auskunft', 'Bussgeld gem. Art. 83'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K9'],
+ applicableTo: ['web_application', 'crm', 'identity', 'cloud_storage'],
+ mitigationIds: ['M-INTERV-01', 'M-INTERV-02', 'M-INTERV-03'],
+ },
+ {
+ id: 'R-RIGHTS-06',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'Fehlende oder unzureichende Informationspflichten',
+ description: 'Betroffene werden nicht oder unzureichend ueber die Verarbeitung ihrer Daten informiert (Verstoss gegen Art. 13/14 DSGVO).',
+ impactExamples: ['Keine informierte Einwilligung moeglich', 'Vertrauensverlust', 'Bussgeld'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K9'],
+ applicableTo: ['web_application', 'mobile_app', 'marketing'],
+ mitigationIds: ['M-TRANS-01', 'M-TRANS-05'],
+ },
+ {
+ id: 'R-RIGHTS-07',
+ category: 'rights_freedoms',
+ sdmGoal: 'datenminimierung',
+ title: 'Uebermassige Datenerhebung (Verstoss Datenminimierung)',
+ description: 'Es werden mehr personenbezogene Daten erhoben als fuer den Verarbeitungszweck notwendig (Verstoss gegen Art. 5 Abs. 1 lit. c DSGVO).',
+ impactExamples: ['Unnoetige Risikoexposition', 'Hoeherer Schaden bei Datenpanne'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K5'],
+ applicableTo: ['web_application', 'mobile_app', 'crm', 'hr_system'],
+ mitigationIds: ['M-DMIN-01', 'M-DMIN-02', 'M-DMIN-03'],
+ },
+
+ // =========================================================================
+ // DRITTLANDTRANSFER
+ // =========================================================================
+ {
+ id: 'R-TRANS-01',
+ category: 'rights_freedoms',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Zugriff durch Drittland-Behoerden (FISA/CLOUD Act)',
+ description: 'Behoerden eines Drittlandes (z.B. USA) greifen auf personenbezogene Daten zu, die bei einem Cloud-Provider in der EU oder im Drittland gespeichert sind.',
+ impactExamples: ['Ueberwachung ohne Wissen', 'Kein Rechtsschutz', 'Schrems-II-Risiko'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K5', 'K7'],
+ applicableTo: ['cloud_storage', 'email_service', 'crm', 'analytics'],
+ mitigationIds: ['M-TRANS-06', 'M-TRANS-07', 'M-CONF-06'],
+ },
+ {
+ id: 'R-TRANS-02',
+ category: 'rights_freedoms',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Unzureichende Schutzgarantien bei Drittlandtransfer',
+ description: 'Personenbezogene Daten werden in Drittlaender uebermittelt, ohne dass angemessene Garantien (SCC, BCR, Angemessenheitsbeschluss) vorhanden sind.',
+ impactExamples: ['Rechtswidriger Transfer', 'Bussgeld', 'Untersagung der Verarbeitung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K5', 'K7'],
+ applicableTo: ['cloud_storage', 'email_service', 'crm', 'analytics'],
+ mitigationIds: ['M-TRANS-06', 'M-TRANS-07', 'M-LEGAL-01'],
+ },
+ {
+ id: 'R-TRANS-03',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'Intransparente Sub-Auftragsverarbeiter-Kette',
+ description: 'Die Kette der Sub-Auftragsverarbeiter ist nicht transparent. Betroffene und Verantwortliche wissen nicht, wo ihre Daten tatsaechlich verarbeitet werden.',
+ impactExamples: ['Unkontrollierte Datenweitergabe', 'Unbekannter Verarbeitungsort'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K5'],
+ applicableTo: ['cloud_storage', 'crm', 'analytics'],
+ mitigationIds: ['M-TRANS-01', 'M-LEGAL-02'],
+ },
+
+ // =========================================================================
+ // AUTOMATISIERUNG / KI
+ // =========================================================================
+ {
+ id: 'R-AUTO-01',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'KI-Fehlentscheidung mit erheblicher Auswirkung',
+ description: 'Ein KI-System trifft eine fehlerhafte automatisierte Entscheidung (z.B. Ablehnung, Sperrung, Bewertung), die erhebliche Auswirkungen auf eine betroffene Person hat.',
+ impactExamples: ['Unrechtmaessige Ablehnung', 'Falsche Risikoeinstufung', 'Benachteiligung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K1', 'K2', 'K8'],
+ applicableTo: ['ai_ml', 'scoring', 'hr_system'],
+ mitigationIds: ['M-AUTO-01', 'M-AUTO-02', 'M-AUTO-03'],
+ },
+ {
+ id: 'R-AUTO-02',
+ category: 'rights_freedoms',
+ sdmGoal: 'nichtverkettung',
+ title: 'Algorithmischer Bias in Trainingsdaten',
+ description: 'KI-Modelle spiegeln Vorurteile in den Trainingsdaten wider und treffen diskriminierende Entscheidungen bezueglich geschuetzter Merkmale.',
+ impactExamples: ['Diskriminierung nach Geschlecht/Herkunft', 'Systematische Benachteiligung'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K1', 'K2', 'K7', 'K8'],
+ applicableTo: ['ai_ml', 'scoring'],
+ mitigationIds: ['M-AUTO-01', 'M-AUTO-04'],
+ },
+ {
+ id: 'R-AUTO-03',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'Fehlende Erklaerbarkeit automatisierter Entscheidungen',
+ description: 'Automatisierte Entscheidungen koennen den Betroffenen nicht erklaert werden ("Black Box"), sodass der Anspruch auf aussagekraeftige Informationen (Art. 22 Abs. 3) nicht erfuellt wird.',
+ impactExamples: ['Keine Anfechtbarkeit', 'Vertrauensverlust', 'Verstoss gegen Art. 22'],
+ typicalLikelihood: 'high',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K2', 'K8'],
+ applicableTo: ['ai_ml', 'scoring'],
+ mitigationIds: ['M-AUTO-02', 'M-TRANS-03'],
+ },
+ {
+ id: 'R-AUTO-04',
+ category: 'rights_freedoms',
+ sdmGoal: 'intervenierbarkeit',
+ title: 'Fehlende menschliche Aufsicht bei KI-Entscheidungen',
+ description: 'Automatisierte Entscheidungen werden ohne menschliche Ueberpruefung oder Interventionsmoeglichkeit getroffen, obwohl dies erforderlich waere.',
+ impactExamples: ['Keine Korrekturmoeglichkeit', 'Eskalation von Fehlern'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K2', 'K8'],
+ applicableTo: ['ai_ml', 'scoring', 'hr_system'],
+ mitigationIds: ['M-AUTO-03', 'M-INTERV-04'],
+ },
+ {
+ id: 'R-AUTO-05',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Datenleck durch KI-Training mit personenbezogenen Daten',
+ description: 'Personenbezogene Daten, die fuer das Training von KI-Modellen verwendet werden, koennen durch das Modell reproduziert oder extrahiert werden (Model Inversion, Membership Inference).',
+ impactExamples: ['Offenlegung von Trainingsdaten', 'Re-Identifizierung'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K8'],
+ applicableTo: ['ai_ml'],
+ mitigationIds: ['M-CONF-06', 'M-NONL-03', 'M-AUTO-04'],
+ },
+
+ // =========================================================================
+ // ORGANISATORISCHE RISIKEN
+ // =========================================================================
+ {
+ id: 'R-ORG-01',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'Fehlende oder fehlerhafte Auftragsverarbeitungsvertraege',
+ description: 'Mit Auftragsverarbeitern existieren keine oder unzureichende Vertraege gemaess Art. 28 DSGVO, sodass Pflichten und Rechte nicht geregelt sind.',
+ impactExamples: ['Keine Kontrolle ueber Verarbeiter', 'Bussgeld', 'Datenmissbrauch durch Verarbeiter'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K5'],
+ applicableTo: ['cloud_storage', 'crm', 'analytics', 'email_service'],
+ mitigationIds: ['M-LEGAL-02', 'M-LEGAL-03'],
+ },
+ {
+ id: 'R-ORG-02',
+ category: 'rights_freedoms',
+ sdmGoal: 'datenminimierung',
+ title: 'Fehlende Loeschprozesse / Ueberschreitung von Aufbewahrungsfristen',
+ description: 'Personenbezogene Daten werden laenger als notwendig gespeichert, weil keine automatischen Loeschprozesse oder Aufbewahrungsfristen definiert sind.',
+ impactExamples: ['Unnoetige Risikoexposition', 'Verstoss gegen Speicherbegrenzung', 'Bussgeld'],
+ typicalLikelihood: 'high',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K5'],
+ applicableTo: ['database', 'cloud_storage', 'crm', 'erp', 'email_service'],
+ mitigationIds: ['M-DMIN-03', 'M-DMIN-04'],
+ },
+ {
+ id: 'R-ORG-03',
+ category: 'integrity',
+ sdmGoal: 'integritaet',
+ title: 'Unzureichende Schulung/Sensibilisierung der Mitarbeiter',
+ description: 'Mitarbeiter sind nicht ausreichend im Umgang mit personenbezogenen Daten geschult und verursachen durch Unkenntnis Datenpannen oder Verarbeitungsfehler.',
+ impactExamples: ['Versehentliche Datenweitergabe', 'Phishing-Erfolg', 'Fehlerhafte Verarbeitung'],
+ typicalLikelihood: 'high',
+ typicalImpact: 'medium',
+ wp248Criteria: ['K5', 'K7'],
+ applicableTo: ['hr_system', 'email_service', 'crm', 'web_application'],
+ mitigationIds: ['M-ORG-01', 'M-ORG-02'],
+ },
+ {
+ id: 'R-ORG-04',
+ category: 'rights_freedoms',
+ sdmGoal: 'transparenz',
+ title: 'Fehlende Datenpannen-Erkennung und -Meldung',
+ description: 'Datenpannen werden nicht rechtzeitig erkannt oder nicht innerhalb der 72-Stunden-Frist (Art. 33 DSGVO) an die Aufsichtsbehoerde gemeldet.',
+ impactExamples: ['Verspaetete Meldung', 'Bussgeld', 'Verzoegerte Benachrichtigung Betroffener'],
+ typicalLikelihood: 'medium',
+ typicalImpact: 'high',
+ wp248Criteria: ['K5'],
+ applicableTo: ['web_application', 'cloud_storage', 'database', 'api_service'],
+ mitigationIds: ['M-ORG-03', 'M-ORG-04', 'M-INT-02'],
+ },
+
+ // =========================================================================
+ // BESONDERE DATENKATEGORIEN
+ // =========================================================================
+ {
+ id: 'R-SPEC-01',
+ category: 'confidentiality',
+ sdmGoal: 'vertraulichkeit',
+ title: 'Kompromittierung besonderer Datenkategorien (Art. 9)',
+ description: 'Besonders schutzwuerdige Daten (Gesundheit, Religion, Biometrie, Gewerkschaftszugehoerigkeit) werden offengelegt oder missbraucht.',
+ impactExamples: ['Schwerwiegende Diskriminierung', 'Existenzielle Bedrohung', 'Soziale Ausgrenzung'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K7'],
+ applicableTo: ['hr_system', 'health_system', 'identity'],
+ mitigationIds: ['M-CONF-06', 'M-CONF-01', 'M-CONF-04'],
+ },
+ {
+ id: 'R-SPEC-02',
+ category: 'rights_freedoms',
+ sdmGoal: 'intervenierbarkeit',
+ title: 'Verarbeitung von Kinderdaten ohne angemessenen Schutz',
+ description: 'Daten von Minderjaehrigen werden verarbeitet, ohne die besonderen Schutzmassnahmen fuer Kinder (Art. 8, EG 38 DSGVO) zu beachten.',
+ impactExamples: ['Langzeitfolgen fuer Minderjaehrige', 'Einschraenkung der Entwicklung', 'Manipulation'],
+ typicalLikelihood: 'low',
+ typicalImpact: 'high',
+ wp248Criteria: ['K4', 'K7'],
+ applicableTo: ['web_application', 'mobile_app', 'education'],
+ mitigationIds: ['M-LEGAL-04', 'M-DMIN-01', 'M-TRANS-01'],
+ },
+]
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+export function getRisksByCategory(category: DSFARiskCategory): CatalogRisk[] {
+ return RISK_CATALOG.filter(r => r.category === category)
+}
+
+export function getRisksBySDMGoal(goal: SDMGoal): CatalogRisk[] {
+ return RISK_CATALOG.filter(r => r.sdmGoal === goal)
+}
+
+export function getRisksByWP248Criterion(criterionCode: string): CatalogRisk[] {
+ return RISK_CATALOG.filter(r => r.wp248Criteria.includes(criterionCode))
+}
+
+export function getRisksByComponent(component: string): CatalogRisk[] {
+ return RISK_CATALOG.filter(r => r.applicableTo.includes(component))
+}
+
+export function getCatalogRiskById(id: string): CatalogRisk | undefined {
+ return RISK_CATALOG.find(r => r.id === id)
+}
+
+export const RISK_CATEGORY_LABELS: Record = {
+ confidentiality: 'Vertraulichkeit',
+ integrity: 'Integritaet',
+ availability: 'Verfuegbarkeit',
+ rights_freedoms: 'Rechte & Freiheiten',
+}
+
+export const COMPONENT_FAMILY_LABELS: Record = {
+ identity: 'Identitaet & Zugang',
+ cloud_storage: 'Cloud-Speicher',
+ web_application: 'Web-Anwendung',
+ api_service: 'API-Service',
+ email_service: 'E-Mail-Dienst',
+ mobile_app: 'Mobile App',
+ database: 'Datenbank',
+ crm: 'CRM-System',
+ erp: 'ERP-System',
+ analytics: 'Analyse/Tracking',
+ marketing: 'Marketing',
+ ai_ml: 'KI / Machine Learning',
+ scoring: 'Scoring / Bewertung',
+ hr_system: 'HR-System',
+ health_system: 'Gesundheitssystem',
+ monitoring: 'Ueberwachungssystem',
+ support_system: 'Support-System',
+ education: 'Bildungsplattform',
+ research: 'Forschung',
+}
diff --git a/admin-v2/lib/sdk/dsfa/types.ts b/admin-v2/lib/sdk/dsfa/types.ts
index e56a5da..88d8f08 100644
--- a/admin-v2/lib/sdk/dsfa/types.ts
+++ b/admin-v2/lib/sdk/dsfa/types.ts
@@ -5,6 +5,57 @@
* aligned with the backend Go models.
*/
+// =============================================================================
+// SDM GEWAEHRLEISTUNGSZIELE (Standard-Datenschutzmodell V2.0)
+// =============================================================================
+
+export type SDMGoal =
+ | 'datenminimierung'
+ | 'verfuegbarkeit'
+ | 'integritaet'
+ | 'vertraulichkeit'
+ | 'nichtverkettung'
+ | 'transparenz'
+ | 'intervenierbarkeit'
+
+export const SDM_GOALS: Record = {
+ datenminimierung: {
+ name: 'Datenminimierung',
+ description: 'Verarbeitung personenbezogener Daten auf das dem Zweck angemessene, erhebliche und notwendige Mass beschraenken.',
+ article: 'Art. 5 Abs. 1 lit. c DSGVO',
+ },
+ verfuegbarkeit: {
+ name: 'Verfuegbarkeit',
+ description: 'Personenbezogene Daten muessen dem Verantwortlichen zur Verfuegung stehen und ordnungsgemaess im vorgesehenen Prozess verwendet werden koennen.',
+ article: 'Art. 32 Abs. 1 lit. b DSGVO',
+ },
+ integritaet: {
+ name: 'Integritaet',
+ description: 'Personenbezogene Daten bleiben waehrend der Verarbeitung unversehrt, vollstaendig und aktuell.',
+ article: 'Art. 5 Abs. 1 lit. d DSGVO',
+ },
+ vertraulichkeit: {
+ name: 'Vertraulichkeit',
+ description: 'Kein unbefugter Zugriff auf personenbezogene Daten. Nur befugte Personen koennen auf Daten zugreifen.',
+ article: 'Art. 32 Abs. 1 lit. b DSGVO',
+ },
+ nichtverkettung: {
+ name: 'Nichtverkettung',
+ description: 'Personenbezogene Daten duerfen nicht ohne Weiteres fuer einen anderen als den erhobenen Zweck zusammengefuehrt werden (Zweckbindung).',
+ article: 'Art. 5 Abs. 1 lit. b DSGVO',
+ },
+ transparenz: {
+ name: 'Transparenz',
+ description: 'Die Verarbeitung personenbezogener Daten muss fuer Betroffene und Aufsichtsbehoerden nachvollziehbar sein.',
+ article: 'Art. 5 Abs. 1 lit. a DSGVO',
+ },
+ intervenierbarkeit: {
+ name: 'Intervenierbarkeit',
+ description: 'Den Betroffenen werden wirksame Moeglichkeiten der Einflussnahme (Auskunft, Berichtigung, Loeschung, Widerspruch) auf die Verarbeitung gewaehrt.',
+ article: 'Art. 15-21 DSGVO',
+ },
+}
+
// =============================================================================
// ENUMS & CONSTANTS
// =============================================================================
diff --git a/admin-v2/lib/sdk/index.ts b/admin-v2/lib/sdk/index.ts
index 57c7e7f..ae7cff0 100644
--- a/admin-v2/lib/sdk/index.ts
+++ b/admin-v2/lib/sdk/index.ts
@@ -55,6 +55,25 @@ export type {
GenerateResponse,
} from './sdk-client'
+// Compliance Scope Engine
+export type {
+ ComplianceDepthLevel,
+ ComplianceScores,
+ ComplianceScopeState,
+ ScopeDecision,
+ ScopeProfilingAnswer,
+ ScopeDocumentType,
+} from './compliance-scope-types'
+export {
+ DEPTH_LEVEL_LABELS,
+ DEPTH_LEVEL_DESCRIPTIONS,
+ DEPTH_LEVEL_COLORS,
+ DOCUMENT_TYPE_LABELS,
+ STORAGE_KEY as SCOPE_STORAGE_KEY,
+ createEmptyScopeState,
+} from './compliance-scope-types'
+export { complianceScopeEngine } from './compliance-scope-engine'
+
// Demo Data Seeding (stored via API like real customer data)
export {
generateDemoState,
diff --git a/admin-v2/lib/sdk/loeschfristen-baseline-catalog.ts b/admin-v2/lib/sdk/loeschfristen-baseline-catalog.ts
new file mode 100644
index 0000000..63367a6
--- /dev/null
+++ b/admin-v2/lib/sdk/loeschfristen-baseline-catalog.ts
@@ -0,0 +1,578 @@
+/**
+ * Loeschfristen Baseline-Katalog
+ *
+ * 18 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
+ * Datenobjekte in deutschen Unternehmen. Basierend auf AO, HGB,
+ * UStG, BGB, ArbZG, AGG, BDSG und BSIG.
+ *
+ * Werden genutzt, um neue Loeschfrist-Policies schnell aus
+ * bewaehrten Vorlagen zu erstellen.
+ */
+
+import type {
+ LoeschfristPolicy,
+ RetentionDriverType,
+ DeletionMethodType,
+ StorageLocation,
+ PolicyStatus,
+ ReviewInterval,
+ RetentionUnit,
+ DeletionTriggerLevel,
+} from './loeschfristen-types'
+
+import { createEmptyPolicy } from './loeschfristen-types'
+
+// =============================================================================
+// BASELINE TEMPLATE INTERFACE
+// =============================================================================
+
+export interface BaselineTemplate {
+ templateId: string
+ dataObjectName: string
+ description: string
+ affectedGroups: string[]
+ dataCategories: string[]
+ primaryPurpose: string
+ deletionTrigger: DeletionTriggerLevel
+ retentionDriver: RetentionDriverType | null
+ retentionDriverDetail: string
+ retentionDuration: number | null
+ retentionUnit: RetentionUnit | null
+ retentionDescription: string
+ startEvent: string
+ deletionMethod: DeletionMethodType
+ deletionMethodDetail: string
+ responsibleRole: string
+ reviewInterval: ReviewInterval
+ tags: string[]
+}
+
+// =============================================================================
+// BASELINE TEMPLATES (18 Vorlagen)
+// =============================================================================
+
+export const BASELINE_TEMPLATES: BaselineTemplate[] = [
+ // ==================== 1. Personalakten ====================
+ {
+ templateId: 'personal-akten',
+ dataObjectName: 'Personalakten',
+ description:
+ 'Vollstaendige Personalakten inkl. Arbeitsvertraege, Zeugnisse, Abmahnungen und sonstige beschaeftigungsrelevante Dokumente.',
+ affectedGroups: ['Mitarbeiter'],
+ dataCategories: ['Stammdaten', 'Vertragsdaten', 'Gehaltsdaten', 'Zeugnisse'],
+ primaryPurpose:
+ 'Dokumentation und Nachweisfuehrung des Beschaeftigungsverhaeltnisses sowie Erfuellung steuerrechtlicher Aufbewahrungspflichten.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'AO_147',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 147 AO fuer steuerlich relevante Unterlagen der Personalakte.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
+ startEvent: 'Ende des Beschaeftigungsverhaeltnisses',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung aller digitalen Personalakten-Dokumente nach Ablauf der Aufbewahrungsfrist. Papierakten werden datenschutzkonform vernichtet.',
+ responsibleRole: 'HR-Abteilung',
+ reviewInterval: 'ANNUAL',
+ tags: ['hr', 'steuer'],
+ },
+
+ // ==================== 2. Buchhaltungsbelege ====================
+ {
+ templateId: 'buchhaltungsbelege',
+ dataObjectName: 'Buchhaltungsbelege',
+ description:
+ 'Buchungsbelege, Kontoauszuege, Kassenbuecher und sonstige Belege der laufenden Buchhaltung.',
+ affectedGroups: ['Kunden', 'Lieferanten'],
+ dataCategories: ['Finanzdaten', 'Transaktionsdaten', 'Kontodaten'],
+ primaryPurpose:
+ 'Ordnungsgemaesse Buchfuehrung und Erfuellung handelsrechtlicher Aufbewahrungspflichten nach HGB.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'HGB_257',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 257 HGB fuer Handelsbuecher und Buchungsbelege.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre nach Ende des Geschaeftsjahres',
+ startEvent: 'Ende des Geschaeftsjahres',
+ deletionMethod: 'MANUAL_REVIEW_DELETE',
+ deletionMethodDetail:
+ 'Manuelle Pruefung durch die Buchhaltung vor Loeschung, um sicherzustellen, dass keine laufenden Pruefungen oder Rechtsstreitigkeiten bestehen.',
+ responsibleRole: 'Buchhaltung',
+ reviewInterval: 'ANNUAL',
+ tags: ['finanzen', 'hgb'],
+ },
+
+ // ==================== 3. Rechnungen ====================
+ {
+ templateId: 'rechnungen',
+ dataObjectName: 'Rechnungen',
+ description:
+ 'Eingangs- und Ausgangsrechnungen inkl. Rechnungsanhaenge und rechnungsbegruendende Unterlagen.',
+ affectedGroups: ['Kunden', 'Lieferanten'],
+ dataCategories: ['Rechnungsdaten', 'Umsatzsteuerdaten', 'Adressdaten'],
+ primaryPurpose:
+ 'Dokumentation umsatzsteuerrelevanter Vorgaenge und Erfuellung der Aufbewahrungspflicht nach UStG.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'USTG_14B',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 14b UStG fuer Rechnungen und rechnungsbegruendende Unterlagen.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre ab Rechnungsdatum',
+ startEvent: 'Rechnungsdatum',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung nach Ablauf der 10-Jahres-Frist. Vor Loeschung wird geprueft, ob Rechnungen in laufenden Betriebspruefungen benoetigt werden.',
+ responsibleRole: 'Buchhaltung',
+ reviewInterval: 'ANNUAL',
+ tags: ['finanzen', 'ustg'],
+ },
+
+ // ==================== 4. Geschaeftsbriefe ====================
+ {
+ templateId: 'geschaeftsbriefe',
+ dataObjectName: 'Geschaeftsbriefe',
+ description:
+ 'Empfangene und versandte Handelsbriefe, Geschaeftskorrespondenz und geschaeftsrelevante E-Mails.',
+ affectedGroups: ['Kunden', 'Lieferanten'],
+ dataCategories: ['Korrespondenz', 'Vertragskommunikation', 'Angebote'],
+ primaryPurpose:
+ 'Nachweisfuehrung geschaeftlicher Kommunikation und Erfuellung der handelsrechtlichen Aufbewahrungspflicht fuer Handelsbriefe.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'HGB_257',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 257 HGB fuer empfangene und versandte Handelsbriefe (6 Jahre).',
+ retentionDuration: 6,
+ retentionUnit: 'YEARS',
+ retentionDescription: '6 Jahre ab Eingang oder Versand des Geschaeftsbriefes',
+ startEvent: 'Eingang bzw. Versand des Geschaeftsbriefes',
+ deletionMethod: 'MANUAL_REVIEW_DELETE',
+ deletionMethodDetail:
+ 'Manuelle Pruefung durch die Geschaeftsleitung, da Geschaeftsbriefe ggf. als Beweismittel in Rechtsstreitigkeiten dienen koennen.',
+ responsibleRole: 'Geschaeftsleitung',
+ reviewInterval: 'ANNUAL',
+ tags: ['kommunikation', 'hgb'],
+ },
+
+ // ==================== 5. Bewerbungsunterlagen ====================
+ {
+ templateId: 'bewerbungsunterlagen',
+ dataObjectName: 'Bewerbungsunterlagen',
+ description:
+ 'Eingereichte Bewerbungsunterlagen inkl. Anschreiben, Lebenslauf, Zeugnisse und Korrespondenz mit Bewerbern.',
+ affectedGroups: ['Bewerber'],
+ dataCategories: ['Bewerbungsdaten', 'Qualifikationen', 'Kontaktdaten'],
+ primaryPurpose:
+ 'Durchfuehrung des Bewerbungsverfahrens und Absicherung gegen Entschaedigungsansprueche nach dem AGG.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'AGG_15',
+ retentionDriverDetail:
+ 'Aufbewahrung fuer 6 Monate nach Absage gemaess 15 Abs. 4 AGG (Frist fuer Geltendmachung von Entschaedigungsanspruechen).',
+ retentionDuration: 6,
+ retentionUnit: 'MONTHS',
+ retentionDescription: '6 Monate nach Absage oder Stellenbesetzung',
+ startEvent: 'Absage oder endgueltige Stellenbesetzung',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung aller Bewerbungsunterlagen und zugehoeriger Kommunikation nach Ablauf der 6-Monats-Frist.',
+ responsibleRole: 'HR-Abteilung',
+ reviewInterval: 'QUARTERLY',
+ tags: ['hr', 'bewerbung'],
+ },
+
+ // ==================== 6. Kundenstammdaten ====================
+ {
+ templateId: 'kundenstammdaten',
+ dataObjectName: 'Kundenstammdaten',
+ description:
+ 'Stammdaten von Kunden inkl. Kontaktdaten, Anschrift, Kundennummer und Kommunikationspraeferenzen.',
+ affectedGroups: ['Kunden'],
+ dataCategories: ['Stammdaten', 'Kontaktdaten', 'Adressdaten'],
+ primaryPurpose:
+ 'Pflege der Kundenbeziehung, Vertragserfuellung und Absicherung gegen Verjaehrung vertraglicher Ansprueche.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BGB_195',
+ retentionDriverDetail:
+ 'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB (3 Jahre).',
+ retentionDuration: 3,
+ retentionUnit: 'YEARS',
+ retentionDescription: '3 Jahre nach letzter geschaeftlicher Interaktion',
+ startEvent: 'Letzte geschaeftliche Interaktion mit dem Kunden',
+ deletionMethod: 'MANUAL_REVIEW_DELETE',
+ deletionMethodDetail:
+ 'Manuelle Pruefung durch den Vertrieb vor Loeschung, um sicherzustellen, dass keine aktiven Geschaeftsbeziehungen oder offenen Forderungen bestehen.',
+ responsibleRole: 'Vertrieb',
+ reviewInterval: 'ANNUAL',
+ tags: ['crm', 'kunden'],
+ },
+
+ // ==================== 7. Newsletter-Einwilligungen ====================
+ {
+ templateId: 'newsletter-einwilligungen',
+ dataObjectName: 'Newsletter-Einwilligungen',
+ description:
+ 'Einwilligungserklaerungen fuer den Newsletter-Versand inkl. Double-Opt-in-Nachweis und Abmeldezeitpunkt.',
+ affectedGroups: ['Abonnenten'],
+ dataCategories: ['Einwilligungsdaten', 'E-Mail-Adresse', 'Opt-in-Nachweis'],
+ primaryPurpose:
+ 'Nachweis der wirksamen Einwilligung zum Newsletter-Versand gemaess Art. 7 DSGVO und Dokumentation des Widerrufs.',
+ deletionTrigger: 'PURPOSE_END',
+ retentionDriver: null,
+ retentionDriverDetail:
+ 'Keine gesetzliche Aufbewahrungspflicht. Daten werden bis zum Widerruf der Einwilligung gespeichert.',
+ retentionDuration: null,
+ retentionUnit: null,
+ retentionDescription: 'Bis zum Widerruf der Einwilligung durch den Abonnenten',
+ startEvent: 'Widerruf der Einwilligung durch den Abonnenten',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung der personenbezogenen Daten nach Eingang des Widerrufs. Der Einwilligungsnachweis selbst wird fuer die Dauer der Nachweispflicht aufbewahrt.',
+ responsibleRole: 'Marketing',
+ reviewInterval: 'SEMI_ANNUAL',
+ tags: ['marketing', 'einwilligung'],
+ },
+
+ // ==================== 8. Webserver-Logs ====================
+ {
+ templateId: 'webserver-logs',
+ dataObjectName: 'Webserver-Logs',
+ description:
+ 'Server-Zugriffsprotokolle inkl. IP-Adressen, Zeitstempel, aufgerufene URLs und HTTP-Statuscodes.',
+ affectedGroups: ['Website-Besucher'],
+ dataCategories: ['IP-Adressen', 'Zugriffszeitpunkte', 'User-Agent-Daten'],
+ primaryPurpose:
+ 'Sicherstellung der IT-Sicherheit, Erkennung von Angriffen und Stoerungen sowie Erfuellung der Protokollierungspflicht nach BSIG.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BSIG',
+ retentionDriverDetail:
+ 'Aufbewahrung gemaess BSI-Gesetz / IT-Sicherheitsgesetz 2.0 fuer die Analyse von Sicherheitsvorfaellen.',
+ retentionDuration: 7,
+ retentionUnit: 'DAYS',
+ retentionDescription: '7 Tage nach Zeitpunkt des Zugriffs',
+ startEvent: 'Zeitpunkt des Server-Zugriffs',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Rotation und Loeschung der Logdateien nach 7 Tagen durch den Webserver (logrotate).',
+ responsibleRole: 'IT-Abteilung',
+ reviewInterval: 'QUARTERLY',
+ tags: ['it', 'logs'],
+ },
+
+ // ==================== 9. Videoueberwachung ====================
+ {
+ templateId: 'videoueberwachung',
+ dataObjectName: 'Videoueberwachung',
+ description:
+ 'Aufnahmen der Videoueberwachung in Geschaeftsraeumen, Eingangsbereichen und Parkplaetzen.',
+ affectedGroups: ['Besucher', 'Mitarbeiter'],
+ dataCategories: ['Videodaten', 'Bilddaten', 'Zeitstempel'],
+ primaryPurpose:
+ 'Schutz des Eigentums und der Sicherheit von Personen sowie Aufklaerung von Vorfaellen in den ueberwachten Bereichen.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BDSG_35',
+ retentionDriverDetail:
+ 'Unverzuegliche Loeschung nach Zweckwegfall gemaess 35 BDSG bzw. Art. 17 DSGVO. Maximale Speicherdauer 48 Stunden.',
+ retentionDuration: 2,
+ retentionUnit: 'DAYS',
+ retentionDescription: '48 Stunden (2 Tage) nach Aufnahmezeitpunkt',
+ startEvent: 'Aufnahmezeitpunkt der Videosequenz',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatisches Ueberschreiben der Aufnahmen durch das Videomanagementsystem nach Ablauf der 48-Stunden-Frist.',
+ responsibleRole: 'Facility Management',
+ reviewInterval: 'QUARTERLY',
+ tags: ['sicherheit', 'video'],
+ },
+
+ // ==================== 10. Gehaltsabrechnungen ====================
+ {
+ templateId: 'gehaltsabrechnungen',
+ dataObjectName: 'Gehaltsabrechnungen',
+ description:
+ 'Monatliche Gehaltsabrechnungen, Lohnsteuerbescheinigungen und Sozialversicherungsmeldungen.',
+ affectedGroups: ['Mitarbeiter'],
+ dataCategories: ['Gehaltsdaten', 'Steuerdaten', 'Sozialversicherungsdaten'],
+ primaryPurpose:
+ 'Dokumentation der Lohn- und Gehaltszahlungen sowie Erfuellung steuerrechtlicher und sozialversicherungsrechtlicher Aufbewahrungspflichten.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'AO_147',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 147 AO fuer lohnsteuerrelevante Unterlagen und Gehaltsbuchungen.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre nach Ende des Geschaeftsjahres',
+ startEvent: 'Ende des Geschaeftsjahres der jeweiligen Abrechnung',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung der digitalen Gehaltsabrechnungen nach Ablauf der Aufbewahrungsfrist. Papierbelege werden datenschutzkonform vernichtet.',
+ responsibleRole: 'Lohnbuchhaltung',
+ reviewInterval: 'ANNUAL',
+ tags: ['hr', 'steuer'],
+ },
+
+ // ==================== 11. Vertraege ====================
+ {
+ templateId: 'vertraege',
+ dataObjectName: 'Vertraege',
+ description:
+ 'Geschaeftsvertraege, Rahmenvereinbarungen, Dienstleistungsvertraege und zugehoerige Anlagen und Nachtraege.',
+ affectedGroups: ['Vertragspartner'],
+ dataCategories: ['Vertragsdaten', 'Kontaktdaten', 'Konditionen'],
+ primaryPurpose:
+ 'Dokumentation vertraglicher Vereinbarungen und Sicherung von Beweismitteln fuer die Dauer moeglicher Rechtsstreitigkeiten.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'HGB_257',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 257 HGB fuer handelsrechtlich relevante Vertragsunterlagen.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre nach Ende der Vertragslaufzeit',
+ startEvent: 'Ende der Vertragslaufzeit bzw. Vertragsbeendigung',
+ deletionMethod: 'MANUAL_REVIEW_DELETE',
+ deletionMethodDetail:
+ 'Manuelle Pruefung durch die Rechtsabteilung vor Loeschung, um sicherzustellen, dass keine laufenden oder angedrohten Rechtsstreitigkeiten bestehen.',
+ responsibleRole: 'Rechtsabteilung',
+ reviewInterval: 'ANNUAL',
+ tags: ['recht', 'vertraege'],
+ },
+
+ // ==================== 12. Zeiterfassungsdaten ====================
+ {
+ templateId: 'zeiterfassung',
+ dataObjectName: 'Zeiterfassungsdaten',
+ description:
+ 'Arbeitszeitaufzeichnungen inkl. Beginn, Ende, Pausen und Ueberstunden der Beschaeftigten.',
+ affectedGroups: ['Mitarbeiter'],
+ dataCategories: ['Arbeitszeiten', 'Pausenzeiten', 'Ueberstunden'],
+ primaryPurpose:
+ 'Erfuellung der gesetzlichen Aufzeichnungspflicht fuer Arbeitszeiten und Nachweis der Einhaltung des Arbeitszeitgesetzes.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'ARBZG_16',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 16 Abs. 2 ArbZG fuer Aufzeichnungen ueber die Arbeitszeit.',
+ retentionDuration: 2,
+ retentionUnit: 'YEARS',
+ retentionDescription: '2 Jahre nach Ende des Erfassungszeitraums',
+ startEvent: 'Ende des jeweiligen Erfassungszeitraums',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung der Zeiterfassungsdaten nach Ablauf der 2-Jahres-Frist im Zeiterfassungssystem.',
+ responsibleRole: 'HR-Abteilung',
+ reviewInterval: 'ANNUAL',
+ tags: ['hr', 'arbzg'],
+ },
+
+ // ==================== 13. Krankmeldungen ====================
+ {
+ templateId: 'krankmeldungen',
+ dataObjectName: 'Krankmeldungen',
+ description:
+ 'Arbeitsunfaehigkeitsbescheinigungen, Krankmeldungen und zugehoerige Abwesenheitsdokumentationen.',
+ affectedGroups: ['Mitarbeiter'],
+ dataCategories: ['Gesundheitsdaten', 'Abwesenheitszeiten', 'AU-Bescheinigungen'],
+ primaryPurpose:
+ 'Dokumentation von Fehlzeiten, Entgeltfortzahlung im Krankheitsfall und Absicherung gegen Verjaehrung arbeitsrechtlicher Ansprueche.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BGB_195',
+ retentionDriverDetail:
+ 'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB zur Absicherung von Erstattungsanspruechen.',
+ retentionDuration: 3,
+ retentionUnit: 'YEARS',
+ retentionDescription: '3 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
+ startEvent: 'Ende des Beschaeftigungsverhaeltnisses',
+ deletionMethod: 'MANUAL_REVIEW_DELETE',
+ deletionMethodDetail:
+ 'Manuelle Pruefung durch die HR-Abteilung vor Loeschung, da Krankmeldungen besondere Kategorien personenbezogener Daten (Gesundheitsdaten) enthalten.',
+ responsibleRole: 'HR-Abteilung',
+ reviewInterval: 'ANNUAL',
+ tags: ['hr', 'gesundheit'],
+ },
+
+ // ==================== 14. Steuererklaerungen ====================
+ {
+ templateId: 'steuererklaerungen',
+ dataObjectName: 'Steuererklaerungen',
+ description:
+ 'Koerperschaftsteuer-, Gewerbesteuer- und Umsatzsteuererklaerungen inkl. Anlagen und Bescheide.',
+ affectedGroups: ['Unternehmen'],
+ dataCategories: ['Steuerdaten', 'Finanzkennzahlen', 'Bescheide'],
+ primaryPurpose:
+ 'Erfuellung steuerrechtlicher Dokumentationspflichten und Nachweisfuehrung gegenueber den Finanzbehoerden.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'AO_147',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 147 AO fuer Steuererklaerungen und zugehoerige Unterlagen.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre ab dem jeweiligen Steuerjahr',
+ startEvent: 'Ende des betreffenden Steuerjahres',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung nach Ablauf der 10-Jahres-Frist, sofern keine laufende Betriebspruefung oder Einspruchsverfahren vorliegen.',
+ responsibleRole: 'Steuerberater/Buchhaltung',
+ reviewInterval: 'ANNUAL',
+ tags: ['finanzen', 'steuer'],
+ },
+
+ // ==================== 15. Gesellschafterprotokolle ====================
+ {
+ templateId: 'protokolle-gesellschafter',
+ dataObjectName: 'Gesellschafterprotokolle',
+ description:
+ 'Protokolle der Gesellschafterversammlungen, Beschluesse, Abstimmungsergebnisse und notarielle Urkunden.',
+ affectedGroups: ['Gesellschafter'],
+ dataCategories: ['Beschlussdaten', 'Abstimmungsergebnisse', 'Protokolle'],
+ primaryPurpose:
+ 'Dokumentation gesellschaftsrechtlicher Beschluesse und Erfuellung handelsrechtlicher Aufbewahrungspflichten.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'HGB_257',
+ retentionDriverDetail:
+ 'Aufbewahrungspflicht gemaess 257 HGB fuer Eroeffnungsbilanzen, Jahresabschluesse und zugehoerige Beschluesse.',
+ retentionDuration: 10,
+ retentionUnit: 'YEARS',
+ retentionDescription: '10 Jahre ab Beschlussdatum',
+ startEvent: 'Datum des jeweiligen Gesellschafterbeschlusses',
+ deletionMethod: 'PHYSICAL_DESTROY',
+ deletionMethodDetail:
+ 'Physische Vernichtung der Papieroriginale durch zertifizierten Aktenvernichtungsdienstleister (DIN 66399, Sicherheitsstufe P-4). Digitale Kopien werden parallel geloescht.',
+ responsibleRole: 'Geschaeftsleitung',
+ reviewInterval: 'ANNUAL',
+ tags: ['recht', 'gesellschaft'],
+ },
+
+ // ==================== 16. CRM-Kontakthistorie ====================
+ {
+ templateId: 'crm-kontakthistorie',
+ dataObjectName: 'CRM-Kontakthistorie',
+ description:
+ 'Kontaktverlauf im CRM-System inkl. Anrufe, E-Mails, Termine, Notizen und Angebotsverlauf.',
+ affectedGroups: ['Kunden', 'Interessenten'],
+ dataCategories: ['Kommunikationsdaten', 'Interaktionshistorie', 'Angebotsdaten'],
+ primaryPurpose:
+ 'Pflege der Kundenbeziehung und Nachverfolgung geschaeftlicher Interaktionen fuer Vertriebs- und Servicezwecke.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BGB_195',
+ retentionDriverDetail:
+ 'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB zur Absicherung vertraglicher Ansprueche.',
+ retentionDuration: 3,
+ retentionUnit: 'YEARS',
+ retentionDescription: '3 Jahre nach letztem Kontakt mit dem Kunden oder Interessenten',
+ startEvent: 'Letzter dokumentierter Kontakt im CRM-System',
+ deletionMethod: 'ANONYMIZATION',
+ deletionMethodDetail:
+ 'Anonymisierung der personenbezogenen Daten im CRM-System, sodass statistische Auswertungen weiterhin moeglich sind, aber kein Personenbezug mehr hergestellt werden kann.',
+ responsibleRole: 'Vertrieb',
+ reviewInterval: 'SEMI_ANNUAL',
+ tags: ['crm', 'kunden'],
+ },
+
+ // ==================== 17. Backup-Daten ====================
+ {
+ templateId: 'backup-daten',
+ dataObjectName: 'Backup-Daten',
+ description:
+ 'Vollstaendige und inkrementelle Sicherungskopien aller Systeme, Datenbanken und Dateisysteme.',
+ affectedGroups: ['Alle Betroffenengruppen'],
+ dataCategories: ['Systemsicherungen', 'Datenbankkopien', 'Dateisystemsicherungen'],
+ primaryPurpose:
+ 'Sicherstellung der Datenwiederherstellung im Katastrophenfall und Gewaehrleistung der Geschaeftskontinuitaet.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BSIG',
+ retentionDriverDetail:
+ 'Aufbewahrung von Backups fuer 90 Tage gemaess BSI-Grundschutz-Empfehlungen zur Sicherstellung der Wiederherstellbarkeit.',
+ retentionDuration: 90,
+ retentionUnit: 'DAYS',
+ retentionDescription: '90 Tage nach Erstellung des Backups',
+ startEvent: 'Erstellungsdatum des jeweiligen Backups',
+ deletionMethod: 'CRYPTO_ERASE',
+ deletionMethodDetail:
+ 'Kryptographische Loeschung durch Vernichtung der Verschluesselungsschluessel, sodass die verschluesselten Backup-Daten nicht mehr entschluesselt werden koennen.',
+ responsibleRole: 'IT-Abteilung',
+ reviewInterval: 'QUARTERLY',
+ tags: ['it', 'backup'],
+ },
+
+ // ==================== 18. Cookie-Consent-Nachweise ====================
+ {
+ templateId: 'cookie-consent-logs',
+ dataObjectName: 'Cookie-Consent-Nachweise',
+ description:
+ 'Nachweise ueber Cookie-Einwilligungen der Website-Besucher inkl. Consent-ID, Zeitstempel, gesetzte Praeferenzen und IP-Adresse.',
+ affectedGroups: ['Website-Besucher'],
+ dataCategories: ['Consent-Daten', 'IP-Adressen', 'Zeitstempel', 'Praeferenzen'],
+ primaryPurpose:
+ 'Nachweisfuehrung der Einwilligung in die Cookie-Nutzung gemaess Art. 7 Abs. 1 DSGVO und ePrivacy-Richtlinie.',
+ deletionTrigger: 'RETENTION_DRIVER',
+ retentionDriver: 'BGB_195',
+ retentionDriverDetail:
+ 'Aufbewahrung der Consent-Nachweise fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB zur Absicherung gegen Abmahnungen.',
+ retentionDuration: 3,
+ retentionUnit: 'YEARS',
+ retentionDescription: '3 Jahre nach Zeitpunkt der Einwilligung',
+ startEvent: 'Zeitpunkt der Cookie-Einwilligung (Consent-Zeitstempel)',
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail:
+ 'Automatische Loeschung der Consent-Nachweise nach Ablauf der 3-Jahres-Frist durch das Consent-Management-System.',
+ responsibleRole: 'Datenschutzbeauftragter',
+ reviewInterval: 'ANNUAL',
+ tags: ['datenschutz', 'consent'],
+ },
+]
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+/**
+ * Erstellt eine vollstaendige LoeschfristPolicy aus einem BaselineTemplate.
+ * Nutzt createEmptyPolicy() als Basis und ueberlagert die Template-Felder.
+ */
+export function templateToPolicy(template: BaselineTemplate): LoeschfristPolicy {
+ const base = createEmptyPolicy()
+
+ return {
+ ...base,
+ dataObjectName: template.dataObjectName,
+ description: template.description,
+ affectedGroups: [...template.affectedGroups],
+ dataCategories: [...template.dataCategories],
+ primaryPurpose: template.primaryPurpose,
+ deletionTrigger: template.deletionTrigger,
+ retentionDriver: template.retentionDriver,
+ retentionDriverDetail: template.retentionDriverDetail,
+ retentionDuration: template.retentionDuration,
+ retentionUnit: template.retentionUnit,
+ retentionDescription: template.retentionDescription,
+ startEvent: template.startEvent,
+ deletionMethod: template.deletionMethod,
+ deletionMethodDetail: template.deletionMethodDetail,
+ responsibleRole: template.responsibleRole,
+ reviewInterval: template.reviewInterval,
+ tags: [...template.tags],
+ }
+}
+
+/**
+ * Gibt alle Templates zurueck, die einen bestimmten Tag enthalten.
+ */
+export function getTemplatesByTag(tag: string): BaselineTemplate[] {
+ return BASELINE_TEMPLATES.filter(t => t.tags.includes(tag))
+}
+
+/**
+ * Findet ein Template anhand seiner templateId.
+ */
+export function getTemplateById(templateId: string): BaselineTemplate | undefined {
+ return BASELINE_TEMPLATES.find(t => t.templateId === templateId)
+}
+
+/**
+ * Gibt alle im Katalog verwendeten Tags als sortierte Liste zurueck.
+ */
+export function getAllTemplateTags(): string[] {
+ const tags = new Set()
+ BASELINE_TEMPLATES.forEach(t => t.tags.forEach(tag => tags.add(tag)))
+ return Array.from(tags).sort()
+}
diff --git a/admin-v2/lib/sdk/loeschfristen-compliance.ts b/admin-v2/lib/sdk/loeschfristen-compliance.ts
new file mode 100644
index 0000000..d3d3b07
--- /dev/null
+++ b/admin-v2/lib/sdk/loeschfristen-compliance.ts
@@ -0,0 +1,325 @@
+// =============================================================================
+// Loeschfristen Module - Compliance Check Engine
+// Prueft Policies auf Vollstaendigkeit, Konsistenz und DSGVO-Konformitaet
+// =============================================================================
+
+import {
+ LoeschfristPolicy,
+ PolicyStatus,
+ isPolicyOverdue,
+ getActiveLegalHolds,
+} from './loeschfristen-types'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export type ComplianceIssueType =
+ | 'MISSING_TRIGGER'
+ | 'MISSING_LEGAL_BASIS'
+ | 'OVERDUE_REVIEW'
+ | 'NO_RESPONSIBLE'
+ | 'LEGAL_HOLD_CONFLICT'
+ | 'STALE_DRAFT'
+ | 'UNCOVERED_VVT_CATEGORY'
+
+export type ComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
+
+export interface ComplianceIssue {
+ id: string
+ policyId: string
+ policyName: string
+ type: ComplianceIssueType
+ severity: ComplianceIssueSeverity
+ title: string
+ description: string
+ recommendation: string
+}
+
+export interface ComplianceCheckResult {
+ issues: ComplianceIssue[]
+ score: number // 0-100
+ stats: {
+ total: number
+ passed: number
+ failed: number
+ bySeverity: Record
+ }
+}
+
+// =============================================================================
+// HELPERS
+// =============================================================================
+
+let issueCounter = 0
+
+function createIssueId(): string {
+ issueCounter++
+ return `CI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
+}
+
+function createIssue(
+ policy: LoeschfristPolicy,
+ type: ComplianceIssueType,
+ severity: ComplianceIssueSeverity,
+ title: string,
+ description: string,
+ recommendation: string
+): ComplianceIssue {
+ return {
+ id: createIssueId(),
+ policyId: policy.policyId,
+ policyName: policy.dataObjectName || policy.policyId,
+ type,
+ severity,
+ title,
+ description,
+ recommendation,
+ }
+}
+
+function daysBetween(dateStr: string, now: Date): number {
+ const date = new Date(dateStr)
+ const diffMs = now.getTime() - date.getTime()
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24))
+}
+
+// =============================================================================
+// INDIVIDUAL CHECKS
+// =============================================================================
+
+/**
+ * Check 1: MISSING_TRIGGER (HIGH)
+ * Policy has no deletionTrigger set, or trigger is PURPOSE_END but no startEvent defined.
+ */
+function checkMissingTrigger(policy: LoeschfristPolicy): ComplianceIssue | null {
+ if (!policy.deletionTrigger) {
+ return createIssue(
+ policy,
+ 'MISSING_TRIGGER',
+ 'HIGH',
+ 'Kein Loeschtrigger definiert',
+ `Die Policy "${policy.dataObjectName}" hat keinen Loeschtrigger gesetzt. Ohne Trigger ist unklar, wann die Daten geloescht werden.`,
+ 'Definieren Sie einen Loeschtrigger (Zweckende, Aufbewahrungspflicht oder Legal Hold) fuer diese Policy.'
+ )
+ }
+
+ if (policy.deletionTrigger === 'PURPOSE_END' && !policy.startEvent.trim()) {
+ return createIssue(
+ policy,
+ 'MISSING_TRIGGER',
+ 'HIGH',
+ 'Zweckende ohne Startereignis',
+ `Die Policy "${policy.dataObjectName}" nutzt "Zweckende" als Trigger, hat aber kein Startereignis definiert. Ohne Startereignis laesst sich der Loeschzeitpunkt nicht berechnen.`,
+ 'Definieren Sie ein konkretes Startereignis (z.B. "Vertragsende", "Abmeldung", "Projektabschluss").'
+ )
+ }
+
+ return null
+}
+
+/**
+ * Check 2: MISSING_LEGAL_BASIS (HIGH)
+ * Policy with RETENTION_DRIVER trigger but no retentionDriver set.
+ */
+function checkMissingLegalBasis(policy: LoeschfristPolicy): ComplianceIssue | null {
+ if (policy.deletionTrigger === 'RETENTION_DRIVER' && !policy.retentionDriver) {
+ return createIssue(
+ policy,
+ 'MISSING_LEGAL_BASIS',
+ 'HIGH',
+ 'Aufbewahrungspflicht ohne Rechtsgrundlage',
+ `Die Policy "${policy.dataObjectName}" hat "Aufbewahrungspflicht" als Trigger, aber keinen konkreten Aufbewahrungstreiber (z.B. AO 147, HGB 257) zugeordnet.`,
+ 'Waehlen Sie den passenden gesetzlichen Aufbewahrungstreiber aus oder wechseln Sie den Trigger-Typ.'
+ )
+ }
+
+ return null
+}
+
+/**
+ * Check 3: OVERDUE_REVIEW (MEDIUM)
+ * Policy where nextReviewDate is in the past.
+ */
+function checkOverdueReview(policy: LoeschfristPolicy): ComplianceIssue | null {
+ if (isPolicyOverdue(policy)) {
+ const overdueDays = daysBetween(policy.nextReviewDate, new Date())
+ return createIssue(
+ policy,
+ 'OVERDUE_REVIEW',
+ 'MEDIUM',
+ 'Ueberfaellige Pruefung',
+ `Die Policy "${policy.dataObjectName}" haette am ${new Date(policy.nextReviewDate).toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig.`,
+ 'Fuehren Sie umgehend eine Pruefung dieser Policy durch und aktualisieren Sie das naechste Pruefungsdatum.'
+ )
+ }
+
+ return null
+}
+
+/**
+ * Check 4: NO_RESPONSIBLE (MEDIUM)
+ * Policy with no responsiblePerson AND no responsibleRole.
+ */
+function checkNoResponsible(policy: LoeschfristPolicy): ComplianceIssue | null {
+ if (!policy.responsiblePerson.trim() && !policy.responsibleRole.trim()) {
+ return createIssue(
+ policy,
+ 'NO_RESPONSIBLE',
+ 'MEDIUM',
+ 'Keine verantwortliche Person/Rolle',
+ `Die Policy "${policy.dataObjectName}" hat weder eine verantwortliche Person noch eine verantwortliche Rolle zugewiesen. Ohne Verantwortlichkeit kann die Loeschung nicht zuverlaessig durchgefuehrt werden.`,
+ 'Weisen Sie eine verantwortliche Person oder zumindest eine verantwortliche Rolle (z.B. "Datenschutzbeauftragter", "IT-Leitung") zu.'
+ )
+ }
+
+ return null
+}
+
+/**
+ * Check 5: LEGAL_HOLD_CONFLICT (CRITICAL)
+ * Policy has active legal hold but deletionMethod is AUTO_DELETE.
+ */
+function checkLegalHoldConflict(policy: LoeschfristPolicy): ComplianceIssue | null {
+ const activeHolds = getActiveLegalHolds(policy)
+ if (activeHolds.length > 0 && policy.deletionMethod === 'AUTO_DELETE') {
+ const holdReasons = activeHolds.map((h) => h.reason).join(', ')
+ return createIssue(
+ policy,
+ 'LEGAL_HOLD_CONFLICT',
+ 'CRITICAL',
+ 'Legal Hold mit automatischer Loeschung',
+ `Die Policy "${policy.dataObjectName}" hat ${activeHolds.length} aktive(n) Legal Hold(s) (${holdReasons}), aber die Loeschmethode ist auf "Automatische Loeschung" gesetzt. Dies kann zu unbeabsichtigter Vernichtung von Beweismitteln fuehren.`,
+ 'Aendern Sie die Loeschmethode auf "Manuelle Pruefung & Loeschung" oder deaktivieren Sie die automatische Loeschung, solange der Legal Hold aktiv ist.'
+ )
+ }
+
+ return null
+}
+
+/**
+ * Check 6: STALE_DRAFT (LOW)
+ * Policy in DRAFT status older than 90 days.
+ */
+function checkStaleDraft(policy: LoeschfristPolicy): ComplianceIssue | null {
+ if (policy.status === 'DRAFT') {
+ const ageInDays = daysBetween(policy.createdAt, new Date())
+ if (ageInDays > 90) {
+ return createIssue(
+ policy,
+ 'STALE_DRAFT',
+ 'LOW',
+ 'Veralteter Entwurf',
+ `Die Policy "${policy.dataObjectName}" ist seit ${ageInDays} Tagen im Entwurfsstatus. Entwuerfe, die laenger als 90 Tage nicht finalisiert werden, deuten auf unvollstaendige Dokumentation hin.`,
+ 'Finalisieren Sie den Entwurf und setzen Sie den Status auf "Aktiv", oder archivieren Sie die Policy, falls sie nicht mehr benoetigt wird.'
+ )
+ }
+ }
+
+ return null
+}
+
+// =============================================================================
+// MAIN COMPLIANCE CHECK
+// =============================================================================
+
+/**
+ * Fuehrt einen vollstaendigen Compliance-Check ueber alle Policies durch.
+ *
+ * @param policies - Alle Loeschfrist-Policies
+ * @param vvtDataCategories - Optionale Datenkategorien aus dem VVT (localStorage)
+ * @returns ComplianceCheckResult mit Issues, Score und Statistiken
+ */
+export function runComplianceCheck(
+ policies: LoeschfristPolicy[],
+ vvtDataCategories?: string[]
+): ComplianceCheckResult {
+ // Reset counter for deterministic IDs within a single check run
+ issueCounter = 0
+
+ const issues: ComplianceIssue[] = []
+
+ // Run checks 1-6 for each policy
+ for (const policy of policies) {
+ const checks = [
+ checkMissingTrigger(policy),
+ checkMissingLegalBasis(policy),
+ checkOverdueReview(policy),
+ checkNoResponsible(policy),
+ checkLegalHoldConflict(policy),
+ checkStaleDraft(policy),
+ ]
+
+ for (const issue of checks) {
+ if (issue !== null) {
+ issues.push(issue)
+ }
+ }
+ }
+
+ // Check 7: UNCOVERED_VVT_CATEGORY (MEDIUM)
+ if (vvtDataCategories && vvtDataCategories.length > 0) {
+ const coveredCategories = new Set()
+ for (const policy of policies) {
+ for (const category of policy.dataCategories) {
+ coveredCategories.add(category.toLowerCase().trim())
+ }
+ }
+
+ for (const vvtCategory of vvtDataCategories) {
+ const normalized = vvtCategory.toLowerCase().trim()
+ if (!coveredCategories.has(normalized)) {
+ issues.push({
+ id: createIssueId(),
+ policyId: '-',
+ policyName: '-',
+ type: 'UNCOVERED_VVT_CATEGORY',
+ severity: 'MEDIUM',
+ title: `Datenkategorie ohne Loeschfrist: "${vvtCategory}"`,
+ description: `Die Datenkategorie "${vvtCategory}" ist im Verzeichnis der Verarbeitungstaetigkeiten (VVT) erfasst, hat aber keine zugehoerige Loeschfrist-Policy. Gemaess DSGVO Art. 5 Abs. 1 lit. e muss fuer jede Datenkategorie eine Speicherbegrenzung definiert sein.`,
+ recommendation: `Erstellen Sie eine neue Loeschfrist-Policy fuer die Datenkategorie "${vvtCategory}" oder ordnen Sie sie einer bestehenden Policy zu.`,
+ })
+ }
+ }
+ }
+
+ // Calculate score
+ const bySeverity: Record = {
+ LOW: 0,
+ MEDIUM: 0,
+ HIGH: 0,
+ CRITICAL: 0,
+ }
+
+ for (const issue of issues) {
+ bySeverity[issue.severity]++
+ }
+
+ const rawScore =
+ 100 -
+ (bySeverity.CRITICAL * 15 +
+ bySeverity.HIGH * 10 +
+ bySeverity.MEDIUM * 5 +
+ bySeverity.LOW * 2)
+
+ const score = Math.max(0, rawScore)
+
+ // Calculate pass/fail per policy
+ const failedPolicyIds = new Set(
+ issues.filter((i) => i.policyId !== '-').map((i) => i.policyId)
+ )
+ const totalPolicies = policies.length
+ const failedCount = failedPolicyIds.size
+ const passedCount = totalPolicies - failedCount
+
+ return {
+ issues,
+ score,
+ stats: {
+ total: totalPolicies,
+ passed: passedCount,
+ failed: failedCount,
+ bySeverity,
+ },
+ }
+}
diff --git a/admin-v2/lib/sdk/loeschfristen-export.ts b/admin-v2/lib/sdk/loeschfristen-export.ts
new file mode 100644
index 0000000..39a1136
--- /dev/null
+++ b/admin-v2/lib/sdk/loeschfristen-export.ts
@@ -0,0 +1,353 @@
+// =============================================================================
+// Loeschfristen Module - Export & Report Generation
+// JSON, CSV, Markdown-Compliance-Report und Browser-Download
+// =============================================================================
+
+import {
+ LoeschfristPolicy,
+ RETENTION_DRIVER_META,
+ DELETION_METHOD_LABELS,
+ STATUS_LABELS,
+ TRIGGER_LABELS,
+ formatRetentionDuration,
+ getEffectiveDeletionTrigger,
+} from './loeschfristen-types'
+
+import {
+ runComplianceCheck,
+ ComplianceCheckResult,
+ ComplianceIssueSeverity,
+} from './loeschfristen-compliance'
+
+// =============================================================================
+// JSON EXPORT
+// =============================================================================
+
+interface PolicyExportEnvelope {
+ exportDate: string
+ version: string
+ totalPolicies: number
+ policies: LoeschfristPolicy[]
+}
+
+/**
+ * Exportiert alle Policies als pretty-printed JSON.
+ * Enthaelt Metadaten (Exportdatum, Version, Anzahl).
+ */
+export function exportPoliciesAsJSON(policies: LoeschfristPolicy[]): string {
+ const exportData: PolicyExportEnvelope = {
+ exportDate: new Date().toISOString(),
+ version: '1.0',
+ totalPolicies: policies.length,
+ policies: policies,
+ }
+ return JSON.stringify(exportData, null, 2)
+}
+
+// =============================================================================
+// CSV EXPORT
+// =============================================================================
+
+/**
+ * Escapes a CSV field value according to RFC 4180.
+ * Fields containing commas, double quotes, or newlines are wrapped in quotes.
+ * Existing double quotes are doubled.
+ */
+function escapeCSVField(value: string): string {
+ if (
+ value.includes(',') ||
+ value.includes('"') ||
+ value.includes('\n') ||
+ value.includes('\r') ||
+ value.includes(';')
+ ) {
+ return `"${value.replace(/"/g, '""')}"`
+ }
+ return value
+}
+
+/**
+ * Formats a date string to German locale format (DD.MM.YYYY).
+ * Returns empty string for null/undefined/empty values.
+ */
+function formatDateDE(dateStr: string | null | undefined): string {
+ if (!dateStr) return ''
+ try {
+ const date = new Date(dateStr)
+ if (isNaN(date.getTime())) return ''
+ return date.toLocaleDateString('de-DE', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ })
+ } catch {
+ return ''
+ }
+}
+
+/**
+ * Exportiert alle Policies als CSV mit BOM fuer Excel-Kompatibilitaet.
+ * Trennzeichen ist Semikolon (;) fuer deutschsprachige Excel-Versionen.
+ */
+export function exportPoliciesAsCSV(policies: LoeschfristPolicy[]): string {
+ const BOM = '\uFEFF'
+ const SEPARATOR = ';'
+
+ const headers = [
+ 'LF-Nr.',
+ 'Datenobjekt',
+ 'Beschreibung',
+ 'Loeschtrigger',
+ 'Aufbewahrungstreiber',
+ 'Frist',
+ 'Startereignis',
+ 'Loeschmethode',
+ 'Verantwortlich',
+ 'Status',
+ 'Legal Hold aktiv',
+ 'Letzte Pruefung',
+ 'Naechste Pruefung',
+ ]
+
+ const rows: string[] = []
+
+ // Header row
+ rows.push(headers.map(escapeCSVField).join(SEPARATOR))
+
+ // Data rows
+ for (const policy of policies) {
+ const effectiveTrigger = getEffectiveDeletionTrigger(policy)
+ const triggerLabel = TRIGGER_LABELS[effectiveTrigger]
+
+ const driverLabel = policy.retentionDriver
+ ? RETENTION_DRIVER_META[policy.retentionDriver].label
+ : ''
+
+ const durationLabel = formatRetentionDuration(
+ policy.retentionDuration,
+ policy.retentionUnit
+ )
+
+ const methodLabel = DELETION_METHOD_LABELS[policy.deletionMethod]
+ const statusLabel = STATUS_LABELS[policy.status]
+
+ // Combine responsiblePerson and responsibleRole
+ const responsible = [policy.responsiblePerson, policy.responsibleRole]
+ .filter((s) => s.trim())
+ .join(' / ')
+
+ const legalHoldActive = policy.hasActiveLegalHold ? 'Ja' : 'Nein'
+
+ const row = [
+ policy.policyId,
+ policy.dataObjectName,
+ policy.description,
+ triggerLabel,
+ driverLabel,
+ durationLabel,
+ policy.startEvent,
+ methodLabel,
+ responsible || '-',
+ statusLabel,
+ legalHoldActive,
+ formatDateDE(policy.lastReviewDate),
+ formatDateDE(policy.nextReviewDate),
+ ]
+
+ rows.push(row.map(escapeCSVField).join(SEPARATOR))
+ }
+
+ return BOM + rows.join('\r\n')
+}
+
+// =============================================================================
+// COMPLIANCE SUMMARY (MARKDOWN)
+// =============================================================================
+
+const SEVERITY_LABELS: Record = {
+ CRITICAL: 'Kritisch',
+ HIGH: 'Hoch',
+ MEDIUM: 'Mittel',
+ LOW: 'Niedrig',
+}
+
+const SEVERITY_EMOJI: Record = {
+ CRITICAL: '[!!!]',
+ HIGH: '[!!]',
+ MEDIUM: '[!]',
+ LOW: '[i]',
+}
+
+/**
+ * Returns a textual rating based on the compliance score.
+ */
+function getScoreRating(score: number): string {
+ if (score >= 90) return 'Ausgezeichnet'
+ if (score >= 75) return 'Gut'
+ if (score >= 50) return 'Verbesserungswuerdig'
+ if (score >= 25) return 'Mangelhaft'
+ return 'Kritisch'
+}
+
+/**
+ * Generiert einen Markdown-formatierten Compliance-Bericht.
+ * Enthaelt: Uebersicht, Score, Issue-Liste, Empfehlungen.
+ */
+export function generateComplianceSummary(
+ policies: LoeschfristPolicy[],
+ vvtDataCategories?: string[]
+): string {
+ const result: ComplianceCheckResult = runComplianceCheck(policies, vvtDataCategories)
+ const now = new Date()
+
+ const lines: string[] = []
+
+ // Header
+ lines.push('# Compliance-Bericht: Loeschfristen')
+ lines.push('')
+ lines.push(
+ `**Erstellt am:** ${now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} um ${now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr`
+ )
+ lines.push('')
+
+ // Overview
+ lines.push('## Uebersicht')
+ lines.push('')
+ lines.push(`| Kennzahl | Wert |`)
+ lines.push(`|----------|------|`)
+ lines.push(`| Gepruefte Policies | ${result.stats.total} |`)
+ lines.push(`| Bestanden | ${result.stats.passed} |`)
+ lines.push(`| Beanstandungen | ${result.stats.failed} |`)
+ lines.push(`| Compliance-Score | **${result.score}/100** (${getScoreRating(result.score)}) |`)
+ lines.push('')
+
+ // Severity breakdown
+ lines.push('## Befunde nach Schweregrad')
+ lines.push('')
+ lines.push('| Schweregrad | Anzahl |')
+ lines.push('|-------------|--------|')
+
+ const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
+ for (const severity of severityOrder) {
+ const count = result.stats.bySeverity[severity]
+ lines.push(`| ${SEVERITY_LABELS[severity]} | ${count} |`)
+ }
+ lines.push('')
+
+ // Status distribution of policies
+ const statusCounts: Record = {}
+ for (const policy of policies) {
+ const label = STATUS_LABELS[policy.status]
+ statusCounts[label] = (statusCounts[label] || 0) + 1
+ }
+
+ lines.push('## Policy-Status-Verteilung')
+ lines.push('')
+ lines.push('| Status | Anzahl |')
+ lines.push('|--------|--------|')
+ for (const [label, count] of Object.entries(statusCounts)) {
+ lines.push(`| ${label} | ${count} |`)
+ }
+ lines.push('')
+
+ // Issues list
+ if (result.issues.length === 0) {
+ lines.push('## Befunde')
+ lines.push('')
+ lines.push('Keine Beanstandungen gefunden. Alle Policies sind konform.')
+ lines.push('')
+ } else {
+ lines.push('## Befunde')
+ lines.push('')
+
+ // Group issues by severity
+ for (const severity of severityOrder) {
+ const issuesForSeverity = result.issues.filter((i) => i.severity === severity)
+ if (issuesForSeverity.length === 0) continue
+
+ lines.push(`### ${SEVERITY_LABELS[severity]} ${SEVERITY_EMOJI[severity]}`)
+ lines.push('')
+
+ for (const issue of issuesForSeverity) {
+ const policyRef =
+ issue.policyId !== '-' ? ` (${issue.policyId})` : ''
+ lines.push(`**${issue.title}**${policyRef}`)
+ lines.push('')
+ lines.push(`> ${issue.description}`)
+ lines.push('')
+ lines.push(`Empfehlung: ${issue.recommendation}`)
+ lines.push('')
+ lines.push('---')
+ lines.push('')
+ }
+ }
+ }
+
+ // Recommendations summary
+ lines.push('## Zusammenfassung der Empfehlungen')
+ lines.push('')
+
+ if (result.stats.bySeverity.CRITICAL > 0) {
+ lines.push(
+ `1. **Sofortmassnahmen erforderlich:** ${result.stats.bySeverity.CRITICAL} kritische(r) Befund(e) muessen umgehend behoben werden (Legal Hold-Konflikte).`
+ )
+ }
+ if (result.stats.bySeverity.HIGH > 0) {
+ lines.push(
+ `${result.stats.bySeverity.CRITICAL > 0 ? '2' : '1'}. **Hohe Prioritaet:** ${result.stats.bySeverity.HIGH} Befund(e) mit hoher Prioritaet (fehlende Trigger/Rechtsgrundlagen) sollten zeitnah bearbeitet werden.`
+ )
+ }
+ if (result.stats.bySeverity.MEDIUM > 0) {
+ lines.push(
+ `- **Mittlere Prioritaet:** ${result.stats.bySeverity.MEDIUM} Befund(e) betreffen ueberfaellige Pruefungen, fehlende Verantwortlichkeiten oder nicht abgedeckte Datenkategorien.`
+ )
+ }
+ if (result.stats.bySeverity.LOW > 0) {
+ lines.push(
+ `- **Niedrige Prioritaet:** ${result.stats.bySeverity.LOW} Befund(e) betreffen veraltete Entwuerfe, die finalisiert oder archiviert werden sollten.`
+ )
+ }
+ if (result.issues.length === 0) {
+ lines.push(
+ 'Alle Policies sind konform. Stellen Sie sicher, dass die naechsten Pruefungstermine eingehalten werden.'
+ )
+ }
+ lines.push('')
+
+ // Footer
+ lines.push('---')
+ lines.push('')
+ lines.push(
+ '*Dieser Bericht wurde automatisch generiert und ersetzt keine rechtliche Beratung. Die Verantwortung fuer die DSGVO-Konformitaet liegt beim Verantwortlichen (Art. 4 Nr. 7 DSGVO).*'
+ )
+
+ return lines.join('\n')
+}
+
+// =============================================================================
+// BROWSER DOWNLOAD UTILITY
+// =============================================================================
+
+/**
+ * Loest einen Datei-Download im Browser aus.
+ * Erstellt ein temporaeres Blob-URL und simuliert einen Link-Klick.
+ *
+ * @param content - Der Dateiinhalt als String
+ * @param filename - Der gewuenschte Dateiname (z.B. "loeschfristen-export.json")
+ * @param mimeType - Der MIME-Typ (z.B. "application/json", "text/csv;charset=utf-8")
+ */
+export function downloadFile(
+ content: string,
+ filename: string,
+ mimeType: string
+): void {
+ const blob = new Blob([content], { type: mimeType })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+}
diff --git a/admin-v2/lib/sdk/loeschfristen-profiling.ts b/admin-v2/lib/sdk/loeschfristen-profiling.ts
new file mode 100644
index 0000000..5135f70
--- /dev/null
+++ b/admin-v2/lib/sdk/loeschfristen-profiling.ts
@@ -0,0 +1,538 @@
+// =============================================================================
+// Loeschfristen Module - Profiling Wizard
+// 4-Step Profiling (15 Fragen) zur Generierung von Baseline-Loeschrichtlinien
+// =============================================================================
+
+import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types'
+import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export type ProfilingStepId = 'organization' | 'data-categories' | 'systems' | 'special'
+
+export interface ProfilingQuestion {
+ id: string
+ step: ProfilingStepId
+ question: string // German
+ helpText?: string
+ type: 'single' | 'multi' | 'boolean' | 'number'
+ options?: { value: string; label: string }[]
+ required: boolean
+}
+
+export interface ProfilingAnswer {
+ questionId: string
+ value: string | string[] | boolean | number
+}
+
+export interface ProfilingStep {
+ id: ProfilingStepId
+ title: string
+ description: string
+ questions: ProfilingQuestion[]
+}
+
+export interface ProfilingResult {
+ matchedTemplates: BaselineTemplate[]
+ generatedPolicies: LoeschfristPolicy[]
+ additionalStorageLocations: StorageLocation[]
+ hasLegalHoldRequirement: boolean
+}
+
+// =============================================================================
+// PROFILING STEPS (4 Steps, 15 Questions)
+// =============================================================================
+
+export const PROFILING_STEPS: ProfilingStep[] = [
+ // =========================================================================
+ // Step 1: Organisation (4 Fragen)
+ // =========================================================================
+ {
+ id: 'organization',
+ title: 'Organisation',
+ description: 'Allgemeine Informationen zu Ihrem Unternehmen, um branchenspezifische Loeschfristen zu ermitteln.',
+ questions: [
+ {
+ id: 'org-branche',
+ step: 'organization',
+ question: 'In welcher Branche ist Ihr Unternehmen taetig?',
+ helpText: 'Die Branche bestimmt, welche branchenspezifischen Aufbewahrungspflichten relevant sind.',
+ type: 'single',
+ options: [
+ { value: 'it-software', label: 'IT / Software' },
+ { value: 'handel', label: 'Handel' },
+ { value: 'dienstleistung', label: 'Dienstleistung' },
+ { value: 'gesundheitswesen', label: 'Gesundheitswesen' },
+ { value: 'bildung', label: 'Bildung' },
+ { value: 'fertigung-industrie', label: 'Fertigung / Industrie' },
+ { value: 'finanzwesen', label: 'Finanzwesen' },
+ { value: 'oeffentlicher-sektor', label: 'Oeffentlicher Sektor' },
+ { value: 'sonstige', label: 'Sonstige' },
+ ],
+ required: true,
+ },
+ {
+ id: 'org-mitarbeiter',
+ step: 'organization',
+ question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?',
+ helpText: 'Die Unternehmensgroesse beeinflusst den Umfang der erforderlichen Loeschkonzepte.',
+ type: 'single',
+ options: [
+ { value: '<10', label: 'Weniger als 10' },
+ { value: '10-49', label: '10 bis 49' },
+ { value: '50-249', label: '50 bis 249' },
+ { value: '250+', label: '250 und mehr' },
+ ],
+ required: true,
+ },
+ {
+ id: 'org-geschaeftsmodell',
+ step: 'organization',
+ question: 'Welches Geschaeftsmodell verfolgen Sie?',
+ helpText: 'B2B und B2C haben unterschiedliche Anforderungen an die Datenhaltung.',
+ type: 'single',
+ options: [
+ { value: 'b2b', label: 'B2B (Geschaeftskunden)' },
+ { value: 'b2c', label: 'B2C (Endkunden)' },
+ { value: 'beides', label: 'Beides (B2B und B2C)' },
+ ],
+ required: true,
+ },
+ {
+ id: 'org-website',
+ step: 'organization',
+ question: 'Betreiben Sie eine Website oder Online-Praesenz?',
+ helpText: 'Websites erzeugen Webserver-Logs und erfordern Cookie-Consent-Verwaltung.',
+ type: 'boolean',
+ required: true,
+ },
+ ],
+ },
+
+ // =========================================================================
+ // Step 2: Datenkategorien (5 Fragen)
+ // =========================================================================
+ {
+ id: 'data-categories',
+ title: 'Datenkategorien',
+ description: 'Welche Arten personenbezogener Daten verarbeiten Sie? Dies bestimmt die relevanten Aufbewahrungsfristen.',
+ questions: [
+ {
+ id: 'data-hr',
+ step: 'data-categories',
+ question: 'Verarbeiten Sie HR-/Personaldaten (Personalakten, Gehaltsabrechnungen, Zeiterfassung)?',
+ helpText: 'Personalakten unterliegen umfangreichen gesetzlichen Aufbewahrungspflichten (bis zu 10 Jahre).',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'data-buchhaltung',
+ step: 'data-categories',
+ question: 'Fuehren Sie eine Buchhaltung mit Finanzdaten (Rechnungen, Belege, Steuererklarungen)?',
+ helpText: 'Buchhaltungsunterlagen muessen gemaess HGB und AO bis zu 10 Jahre aufbewahrt werden.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'data-vertraege',
+ step: 'data-categories',
+ question: 'Verwalten Sie Vertraege mit Kunden oder Lieferanten?',
+ helpText: 'Vertragsunterlagen und Geschaeftsbriefe haben spezifische Aufbewahrungspflichten.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'data-marketing',
+ step: 'data-categories',
+ question: 'Betreiben Sie Marketing-Aktivitaeten (Newsletter, CRM-Kampagnen)?',
+ helpText: 'Marketing-Einwilligungen und Kontakthistorien muessen dokumentiert und verwaltet werden.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'data-video',
+ step: 'data-categories',
+ question: 'Setzen Sie Videoueberwachung ein?',
+ helpText: 'Videoueberwachungsdaten haben besonders kurze Loeschfristen (in der Regel 72 Stunden).',
+ type: 'boolean',
+ required: true,
+ },
+ ],
+ },
+
+ // =========================================================================
+ // Step 3: Systeme (3 Fragen)
+ // =========================================================================
+ {
+ id: 'systems',
+ title: 'Systeme & Infrastruktur',
+ description: 'Welche IT-Systeme und Infrastruktur nutzen Sie? Dies beeinflusst die Speicherorte in Ihrem Loeschkonzept.',
+ questions: [
+ {
+ id: 'sys-cloud',
+ step: 'systems',
+ question: 'Nutzen Sie Cloud-Dienste zur Datenspeicherung oder -verarbeitung?',
+ helpText: 'Cloud-Speicherorte muessen in den Loeschrichtlinien als separate Speicherorte dokumentiert werden.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'sys-backup',
+ step: 'systems',
+ question: 'Haben Sie Backup-Systeme im Einsatz?',
+ helpText: 'Backups erfordern eine eigene Loeschstrategie, da Daten dort nach der primaeren Loeschung weiter existieren koennen.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'sys-erp',
+ step: 'systems',
+ question: 'Setzen Sie ein ERP- oder CRM-System ein?',
+ helpText: 'ERP-/CRM-Systeme sind haeufig zentrale Speicherorte fuer Kunden- und Geschaeftsdaten.',
+ type: 'boolean',
+ required: true,
+ },
+ ],
+ },
+
+ // =========================================================================
+ // Step 4: Spezielle Anforderungen (3 Fragen)
+ // =========================================================================
+ {
+ id: 'special',
+ title: 'Spezielle Anforderungen',
+ description: 'Gibt es besondere rechtliche oder organisatorische Anforderungen, die Ihr Loeschkonzept beeinflussen?',
+ questions: [
+ {
+ id: 'special-legal-hold',
+ step: 'special',
+ question: 'Gibt es Legal-Hold-Anforderungen (z.B. laufende Rechtsstreitigkeiten, behoerdliche Untersuchungen)?',
+ helpText: 'Bei einem Legal Hold muessen betroffene Daten trotz abgelaufener Loeschfristen aufbewahrt werden.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'special-archivierung',
+ step: 'special',
+ question: 'Benoetigen Sie eine Langzeitarchivierung von Dokumenten?',
+ helpText: 'Langzeitarchivierung kann ueber die gesetzlichen Mindestfristen hinausgehen und erfordert eine gesonderte Rechtfertigung.',
+ type: 'boolean',
+ required: true,
+ },
+ {
+ id: 'special-gesundheit',
+ step: 'special',
+ question: 'Verarbeiten Sie Gesundheitsdaten (z.B. Krankmeldungen, Arbeitsmedizin)?',
+ helpText: 'Gesundheitsdaten sind besonders schuetzenswerte Daten nach Art. 9 DSGVO und unterliegen strengeren Anforderungen.',
+ type: 'boolean',
+ required: true,
+ },
+ ],
+ },
+]
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+/**
+ * Retrieve the value of a specific answer by question ID.
+ */
+export function getAnswerValue(answers: ProfilingAnswer[], questionId: string): unknown {
+ const answer = answers.find(a => a.questionId === questionId)
+ return answer?.value ?? undefined
+}
+
+/**
+ * Check whether all required questions in a given step have been answered.
+ */
+export function isStepComplete(answers: ProfilingAnswer[], stepId: ProfilingStepId): boolean {
+ const step = PROFILING_STEPS.find(s => s.id === stepId)
+ if (!step) return false
+
+ return step.questions
+ .filter(q => q.required)
+ .every(q => {
+ const answer = answers.find(a => a.questionId === q.id)
+ if (!answer) return false
+
+ // Check that the value is not empty
+ const val = answer.value
+ if (val === undefined || val === null) return false
+ if (typeof val === 'string' && val.trim() === '') return false
+ if (Array.isArray(val) && val.length === 0) return false
+
+ return true
+ })
+}
+
+/**
+ * Calculate overall profiling progress as a percentage (0-100).
+ */
+export function getProfilingProgress(answers: ProfilingAnswer[]): number {
+ const totalRequired = PROFILING_STEPS.reduce(
+ (sum, step) => sum + step.questions.filter(q => q.required).length,
+ 0
+ )
+ if (totalRequired === 0) return 100
+
+ const answeredRequired = PROFILING_STEPS.reduce((sum, step) => {
+ return (
+ sum +
+ step.questions.filter(q => q.required).filter(q => {
+ const answer = answers.find(a => a.questionId === q.id)
+ if (!answer) return false
+ const val = answer.value
+ if (val === undefined || val === null) return false
+ if (typeof val === 'string' && val.trim() === '') return false
+ if (Array.isArray(val) && val.length === 0) return false
+ return true
+ }).length
+ )
+ }, 0)
+
+ return Math.round((answeredRequired / totalRequired) * 100)
+}
+
+// =============================================================================
+// CORE GENERATOR
+// =============================================================================
+
+/**
+ * Generate deletion policies based on the profiling answers.
+ *
+ * Logic:
+ * - Match baseline templates based on boolean and categorical answers
+ * - Deduplicate matched templates by templateId
+ * - Convert matched templates to full LoeschfristPolicy objects
+ * - Add additional storage locations (Cloud, Backup) if applicable
+ * - Detect legal hold requirements
+ */
+export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): ProfilingResult {
+ const matchedTemplateIds = new Set()
+
+ // -------------------------------------------------------------------------
+ // Helper to get a boolean answer
+ // -------------------------------------------------------------------------
+ const getBool = (questionId: string): boolean => {
+ const val = getAnswerValue(answers, questionId)
+ return val === true
+ }
+
+ const getString = (questionId: string): string => {
+ const val = getAnswerValue(answers, questionId)
+ return typeof val === 'string' ? val : ''
+ }
+
+ // -------------------------------------------------------------------------
+ // Always-included templates (universally recommended)
+ // -------------------------------------------------------------------------
+ matchedTemplateIds.add('protokolle-gesellschafter')
+
+ // -------------------------------------------------------------------------
+ // HR data (data-hr = true)
+ // -------------------------------------------------------------------------
+ if (getBool('data-hr')) {
+ matchedTemplateIds.add('personal-akten')
+ matchedTemplateIds.add('gehaltsabrechnungen')
+ matchedTemplateIds.add('zeiterfassung')
+ matchedTemplateIds.add('bewerbungsunterlagen')
+ matchedTemplateIds.add('krankmeldungen')
+ }
+
+ // -------------------------------------------------------------------------
+ // Buchhaltung (data-buchhaltung = true)
+ // -------------------------------------------------------------------------
+ if (getBool('data-buchhaltung')) {
+ matchedTemplateIds.add('buchhaltungsbelege')
+ matchedTemplateIds.add('rechnungen')
+ matchedTemplateIds.add('steuererklaerungen')
+ }
+
+ // -------------------------------------------------------------------------
+ // Vertraege (data-vertraege = true)
+ // -------------------------------------------------------------------------
+ if (getBool('data-vertraege')) {
+ matchedTemplateIds.add('vertraege')
+ matchedTemplateIds.add('geschaeftsbriefe')
+ matchedTemplateIds.add('kundenstammdaten')
+ }
+
+ // -------------------------------------------------------------------------
+ // Marketing (data-marketing = true)
+ // -------------------------------------------------------------------------
+ if (getBool('data-marketing')) {
+ matchedTemplateIds.add('newsletter-einwilligungen')
+ matchedTemplateIds.add('crm-kontakthistorie')
+ matchedTemplateIds.add('cookie-consent-logs')
+ }
+
+ // -------------------------------------------------------------------------
+ // Video (data-video = true)
+ // -------------------------------------------------------------------------
+ if (getBool('data-video')) {
+ matchedTemplateIds.add('videoueberwachung')
+ }
+
+ // -------------------------------------------------------------------------
+ // Website (org-website = true)
+ // -------------------------------------------------------------------------
+ if (getBool('org-website')) {
+ matchedTemplateIds.add('webserver-logs')
+ matchedTemplateIds.add('cookie-consent-logs')
+ }
+
+ // -------------------------------------------------------------------------
+ // ERP/CRM (sys-erp = true)
+ // -------------------------------------------------------------------------
+ if (getBool('sys-erp')) {
+ matchedTemplateIds.add('kundenstammdaten')
+ matchedTemplateIds.add('crm-kontakthistorie')
+ }
+
+ // -------------------------------------------------------------------------
+ // Backup (sys-backup = true)
+ // -------------------------------------------------------------------------
+ if (getBool('sys-backup')) {
+ matchedTemplateIds.add('backup-daten')
+ }
+
+ // -------------------------------------------------------------------------
+ // Gesundheitsdaten (special-gesundheit = true)
+ // -------------------------------------------------------------------------
+ if (getBool('special-gesundheit')) {
+ // Ensure krankmeldungen is included even without full HR data
+ matchedTemplateIds.add('krankmeldungen')
+ }
+
+ // -------------------------------------------------------------------------
+ // Resolve matched templates from catalog
+ // -------------------------------------------------------------------------
+ const matchedTemplates: BaselineTemplate[] = []
+ for (const templateId of matchedTemplateIds) {
+ const template = BASELINE_TEMPLATES.find(t => t.templateId === templateId)
+ if (template) {
+ matchedTemplates.push(template)
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Convert to policies
+ // -------------------------------------------------------------------------
+ const generatedPolicies: LoeschfristPolicy[] = matchedTemplates.map(template =>
+ templateToPolicy(template)
+ )
+
+ // -------------------------------------------------------------------------
+ // Additional storage locations
+ // -------------------------------------------------------------------------
+ const additionalStorageLocations: StorageLocation[] = []
+
+ if (getBool('sys-cloud')) {
+ const cloudLocation: StorageLocation = {
+ id: crypto.randomUUID(),
+ name: 'Cloud-Speicher',
+ type: 'CLOUD',
+ isBackup: false,
+ provider: null,
+ deletionCapable: true,
+ }
+ additionalStorageLocations.push(cloudLocation)
+
+ // Add Cloud storage location to all generated policies
+ for (const policy of generatedPolicies) {
+ policy.storageLocations.push({ ...cloudLocation, id: crypto.randomUUID() })
+ }
+ }
+
+ if (getBool('sys-backup')) {
+ const backupLocation: StorageLocation = {
+ id: crypto.randomUUID(),
+ name: 'Backup-System',
+ type: 'BACKUP',
+ isBackup: true,
+ provider: null,
+ deletionCapable: true,
+ }
+ additionalStorageLocations.push(backupLocation)
+
+ // Add Backup storage location to all generated policies
+ for (const policy of generatedPolicies) {
+ policy.storageLocations.push({ ...backupLocation, id: crypto.randomUUID() })
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Legal Hold
+ // -------------------------------------------------------------------------
+ const hasLegalHoldRequirement = getBool('special-legal-hold')
+
+ // If legal hold is active, mark all generated policies accordingly
+ if (hasLegalHoldRequirement) {
+ for (const policy of generatedPolicies) {
+ policy.hasActiveLegalHold = true
+ policy.deletionTrigger = 'LEGAL_HOLD'
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Tag policies with profiling metadata
+ // -------------------------------------------------------------------------
+ const branche = getString('org-branche')
+ const mitarbeiter = getString('org-mitarbeiter')
+
+ for (const policy of generatedPolicies) {
+ policy.tags = [
+ ...policy.tags,
+ 'profiling-generated',
+ ...(branche ? [`branche:${branche}`] : []),
+ ...(mitarbeiter ? [`groesse:${mitarbeiter}`] : []),
+ ]
+ }
+
+ return {
+ matchedTemplates,
+ generatedPolicies,
+ additionalStorageLocations,
+ hasLegalHoldRequirement,
+ }
+}
+
+// =============================================================================
+// COMPLIANCE SCOPE INTEGRATION
+// =============================================================================
+
+/**
+ * Prefill Loeschfristen profiling answers from Compliance Scope Engine answers.
+ * The Scope Engine acts as the "Single Source of Truth" for organizational questions.
+ */
+export function prefillFromScopeAnswers(
+ scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[]
+): ProfilingAnswer[] {
+ const { exportToLoeschfristenAnswers } = require('./compliance-scope-profiling')
+ const exported = exportToLoeschfristenAnswers(scopeAnswers) as Array<{ questionId: string; value: unknown }>
+ return exported.map(item => ({
+ questionId: item.questionId,
+ value: item.value as string | string[] | boolean | number,
+ }))
+}
+
+/**
+ * Get the list of Loeschfristen question IDs that are prefilled from Scope answers.
+ * These questions should show "Aus Scope-Analyse uebernommen" hint.
+ */
+export const SCOPE_PREFILLED_LF_QUESTIONS = [
+ 'org-branche',
+ 'org-mitarbeiter',
+ 'org-geschaeftsmodell',
+ 'org-website',
+ 'data-hr',
+ 'data-buchhaltung',
+ 'data-vertraege',
+ 'data-marketing',
+ 'data-video',
+ 'sys-cloud',
+ 'sys-erp',
+]
diff --git a/admin-v2/lib/sdk/loeschfristen-types.ts b/admin-v2/lib/sdk/loeschfristen-types.ts
new file mode 100644
index 0000000..1368b9d
--- /dev/null
+++ b/admin-v2/lib/sdk/loeschfristen-types.ts
@@ -0,0 +1,346 @@
+// =============================================================================
+// Loeschfristen Module - TypeScript Types
+// 3-Level Loeschlogik: Zweckende -> Aufbewahrungstreiber -> Legal Hold
+// =============================================================================
+
+// =============================================================================
+// ENUMS & LITERAL TYPES
+// =============================================================================
+
+export type DeletionTriggerLevel = 'PURPOSE_END' | 'RETENTION_DRIVER' | 'LEGAL_HOLD'
+
+export type RetentionDriverType =
+ | 'AO_147' // 10 Jahre Steuerunterlagen
+ | 'HGB_257' // 10/6 Jahre Handelsbuecher/-briefe
+ | 'USTG_14B' // 10 Jahre Rechnungen
+ | 'BGB_195' // 3 Jahre Verjaehrung
+ | 'ARBZG_16' // 2 Jahre Zeiterfassung
+ | 'AGG_15' // 6 Monate Bewerbungen
+ | 'BDSG_35' // Unverzuegliche Loeschung
+ | 'BSIG' // 90 Tage Sicherheitslogs
+ | 'CUSTOM'
+
+export type DeletionMethodType =
+ | 'AUTO_DELETE'
+ | 'MANUAL_REVIEW_DELETE'
+ | 'ANONYMIZATION'
+ | 'AGGREGATION'
+ | 'CRYPTO_ERASE'
+ | 'PHYSICAL_DESTROY'
+
+export type PolicyStatus = 'DRAFT' | 'ACTIVE' | 'REVIEW_NEEDED' | 'ARCHIVED'
+
+export type ReviewInterval = 'QUARTERLY' | 'SEMI_ANNUAL' | 'ANNUAL'
+
+export type RetentionUnit = 'DAYS' | 'MONTHS' | 'YEARS'
+
+export type StorageLocationType =
+ | 'DATABASE' | 'FILE_SYSTEM' | 'CLOUD' | 'EMAIL' | 'BACKUP' | 'PAPER' | 'OTHER'
+
+export type LegalHoldStatus = 'ACTIVE' | 'RELEASED' | 'EXPIRED'
+
+// =============================================================================
+// INTERFACES
+// =============================================================================
+
+export interface LegalHold {
+ id: string
+ reason: string
+ legalBasis: string
+ responsiblePerson: string
+ startDate: string
+ expectedEndDate: string | null
+ actualEndDate: string | null
+ status: LegalHoldStatus
+ affectedDataCategories: string[]
+}
+
+export interface StorageLocation {
+ id: string
+ name: string
+ type: StorageLocationType
+ isBackup: boolean
+ provider: string | null
+ deletionCapable: boolean
+}
+
+export interface LoeschfristPolicy {
+ id: string
+ policyId: string // LF-2026-001
+ dataObjectName: string
+ description: string
+ affectedGroups: string[]
+ dataCategories: string[]
+ primaryPurpose: string
+ // 3-Level Loeschlogik
+ deletionTrigger: DeletionTriggerLevel
+ retentionDriver: RetentionDriverType | null
+ retentionDriverDetail: string
+ retentionDuration: number | null
+ retentionUnit: RetentionUnit | null
+ retentionDescription: string
+ startEvent: string
+ hasActiveLegalHold: boolean
+ legalHolds: LegalHold[]
+ // Speicherorte & Loeschung
+ storageLocations: StorageLocation[]
+ deletionMethod: DeletionMethodType
+ deletionMethodDetail: string
+ // Verantwortung & Workflow
+ responsibleRole: string
+ responsiblePerson: string
+ releaseProcess: string
+ linkedVVTActivityIds: string[]
+ // Status & Review
+ status: PolicyStatus
+ lastReviewDate: string
+ nextReviewDate: string
+ reviewInterval: ReviewInterval
+ tags: string[]
+ createdAt: string
+ updatedAt: string
+}
+
+// =============================================================================
+// CONSTANTS
+// =============================================================================
+
+export interface RetentionDriverMeta {
+ label: string
+ statute: string
+ defaultDuration: number | null
+ defaultUnit: RetentionUnit | null
+ description: string
+}
+
+export const RETENTION_DRIVER_META: Record = {
+ AO_147: {
+ label: 'Abgabenordnung (AO) 147',
+ statute: '147 AO',
+ defaultDuration: 10,
+ defaultUnit: 'YEARS',
+ description: 'Aufbewahrung steuerrelevanter Unterlagen (Buchungsbelege, Bilanzen, Jahresabschluesse)',
+ },
+ HGB_257: {
+ label: 'Handelsgesetzbuch (HGB) 257',
+ statute: '257 HGB',
+ defaultDuration: 10,
+ defaultUnit: 'YEARS',
+ description: 'Handelsbuecher und Buchungsbelege (10 J.), empfangene/gesendete Handelsbriefe (6 J.)',
+ },
+ USTG_14B: {
+ label: 'Umsatzsteuergesetz (UStG) 14b',
+ statute: '14b UStG',
+ defaultDuration: 10,
+ defaultUnit: 'YEARS',
+ description: 'Aufbewahrung von Rechnungen und rechnungsbegruendenden Unterlagen',
+ },
+ BGB_195: {
+ label: 'Buergerliches Gesetzbuch (BGB) 195',
+ statute: '195 BGB',
+ defaultDuration: 3,
+ defaultUnit: 'YEARS',
+ description: 'Regelmaessige Verjaehrungsfrist fuer vertragliche Ansprueche',
+ },
+ ARBZG_16: {
+ label: 'Arbeitszeitgesetz (ArbZG) 16',
+ statute: '16 Abs. 2 ArbZG',
+ defaultDuration: 2,
+ defaultUnit: 'YEARS',
+ description: 'Aufbewahrung von Arbeitszeitaufzeichnungen',
+ },
+ AGG_15: {
+ label: 'Allg. Gleichbehandlungsgesetz (AGG) 15',
+ statute: '15 Abs. 4 AGG',
+ defaultDuration: 6,
+ defaultUnit: 'MONTHS',
+ description: 'Frist fuer Geltendmachung von Entschaedigungsanspruechen nach Absage',
+ },
+ BDSG_35: {
+ label: 'BDSG 35 / DSGVO Art. 17',
+ statute: '35 BDSG / Art. 17 DSGVO',
+ defaultDuration: null,
+ defaultUnit: null,
+ description: 'Unverzuegliche Loeschung nach Zweckwegfall (kein fester Zeitraum)',
+ },
+ BSIG: {
+ label: 'BSI-Gesetz (BSIG)',
+ statute: 'BSIG / IT-SiG 2.0',
+ defaultDuration: 90,
+ defaultUnit: 'DAYS',
+ description: 'Aufbewahrung von Sicherheitslogs fuer Vorfallsanalyse',
+ },
+ CUSTOM: {
+ label: 'Individuelle Frist',
+ statute: 'Individuell',
+ defaultDuration: null,
+ defaultUnit: null,
+ description: 'Benutzerdefinierte Aufbewahrungsfrist',
+ },
+}
+
+export const DELETION_METHOD_LABELS: Record = {
+ AUTO_DELETE: 'Automatische Loeschung',
+ MANUAL_REVIEW_DELETE: 'Manuelle Pruefung & Loeschung',
+ ANONYMIZATION: 'Anonymisierung',
+ AGGREGATION: 'Aggregation (statistische Verdichtung)',
+ CRYPTO_ERASE: 'Kryptographische Loeschung',
+ PHYSICAL_DESTROY: 'Physische Vernichtung',
+}
+
+export const STATUS_LABELS: Record = {
+ DRAFT: 'Entwurf',
+ ACTIVE: 'Aktiv',
+ REVIEW_NEEDED: 'Pruefung erforderlich',
+ ARCHIVED: 'Archiviert',
+}
+
+export const STATUS_COLORS: Record = {
+ DRAFT: 'bg-gray-100 text-gray-700 border-gray-200',
+ ACTIVE: 'bg-green-100 text-green-700 border-green-200',
+ REVIEW_NEEDED: 'bg-yellow-100 text-yellow-700 border-yellow-200',
+ ARCHIVED: 'bg-blue-100 text-blue-700 border-blue-200',
+}
+
+export const TRIGGER_LABELS: Record = {
+ PURPOSE_END: 'Zweckende',
+ RETENTION_DRIVER: 'Aufbewahrungspflicht',
+ LEGAL_HOLD: 'Legal Hold',
+}
+
+export const TRIGGER_COLORS: Record = {
+ PURPOSE_END: 'bg-green-100 text-green-700',
+ RETENTION_DRIVER: 'bg-blue-100 text-blue-700',
+ LEGAL_HOLD: 'bg-red-100 text-red-700',
+}
+
+export const REVIEW_INTERVAL_LABELS: Record = {
+ QUARTERLY: 'Vierteljaehrlich',
+ SEMI_ANNUAL: 'Halbjaehrlich',
+ ANNUAL: 'Jaehrlich',
+}
+
+export const STORAGE_LOCATION_LABELS: Record = {
+ DATABASE: 'Datenbank',
+ FILE_SYSTEM: 'Dateisystem',
+ CLOUD: 'Cloud-Speicher',
+ EMAIL: 'E-Mail-System',
+ BACKUP: 'Backup-System',
+ PAPER: 'Papierarchiv',
+ OTHER: 'Sonstiges',
+}
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+let policyCounter = 0
+
+export function generatePolicyId(): string {
+ policyCounter++
+ const year = new Date().getFullYear()
+ const num = String(policyCounter).padStart(3, '0')
+ return `LF-${year}-${num}`
+}
+
+export function createEmptyPolicy(): LoeschfristPolicy {
+ const now = new Date().toISOString()
+ const nextYear = new Date()
+ nextYear.setFullYear(nextYear.getFullYear() + 1)
+
+ return {
+ id: crypto.randomUUID(),
+ policyId: generatePolicyId(),
+ dataObjectName: '',
+ description: '',
+ affectedGroups: [],
+ dataCategories: [],
+ primaryPurpose: '',
+ deletionTrigger: 'PURPOSE_END',
+ retentionDriver: null,
+ retentionDriverDetail: '',
+ retentionDuration: null,
+ retentionUnit: null,
+ retentionDescription: '',
+ startEvent: '',
+ hasActiveLegalHold: false,
+ legalHolds: [],
+ storageLocations: [],
+ deletionMethod: 'AUTO_DELETE',
+ deletionMethodDetail: '',
+ responsibleRole: '',
+ responsiblePerson: '',
+ releaseProcess: '',
+ linkedVVTActivityIds: [],
+ status: 'DRAFT',
+ lastReviewDate: now,
+ nextReviewDate: nextYear.toISOString(),
+ reviewInterval: 'ANNUAL',
+ tags: [],
+ createdAt: now,
+ updatedAt: now,
+ }
+}
+
+export function createEmptyLegalHold(): LegalHold {
+ return {
+ id: crypto.randomUUID(),
+ reason: '',
+ legalBasis: '',
+ responsiblePerson: '',
+ startDate: new Date().toISOString().split('T')[0],
+ expectedEndDate: null,
+ actualEndDate: null,
+ status: 'ACTIVE',
+ affectedDataCategories: [],
+ }
+}
+
+export function createEmptyStorageLocation(): StorageLocation {
+ return {
+ id: crypto.randomUUID(),
+ name: '',
+ type: 'DATABASE',
+ isBackup: false,
+ provider: null,
+ deletionCapable: true,
+ }
+}
+
+export function formatRetentionDuration(
+ duration: number | null,
+ unit: RetentionUnit | null
+): string {
+ if (duration === null || unit === null) return 'Bis Zweckwegfall'
+ const unitLabels: Record = {
+ DAYS: duration === 1 ? 'Tag' : 'Tage',
+ MONTHS: duration === 1 ? 'Monat' : 'Monate',
+ YEARS: duration === 1 ? 'Jahr' : 'Jahre',
+ }
+ return `${duration} ${unitLabels[unit]}`
+}
+
+export function isPolicyOverdue(policy: LoeschfristPolicy): boolean {
+ if (!policy.nextReviewDate) return false
+ return new Date(policy.nextReviewDate) <= new Date()
+}
+
+export function getActiveLegalHolds(policy: LoeschfristPolicy): LegalHold[] {
+ return policy.legalHolds.filter(h => h.status === 'ACTIVE')
+}
+
+export function getEffectiveDeletionTrigger(policy: LoeschfristPolicy): DeletionTriggerLevel {
+ if (policy.hasActiveLegalHold && getActiveLegalHolds(policy).length > 0) {
+ return 'LEGAL_HOLD'
+ }
+ if (policy.retentionDriver && policy.retentionDriver !== 'CUSTOM') {
+ return 'RETENTION_DRIVER'
+ }
+ return 'PURPOSE_END'
+}
+
+// =============================================================================
+// LOCALSTORAGE KEY
+// =============================================================================
+
+export const LOESCHFRISTEN_STORAGE_KEY = 'bp_loeschfristen'
diff --git a/admin-v2/lib/sdk/tom-generator/context.tsx b/admin-v2/lib/sdk/tom-generator/context.tsx
index a33e481..fbb4920 100644
--- a/admin-v2/lib/sdk/tom-generator/context.tsx
+++ b/admin-v2/lib/sdk/tom-generator/context.tsx
@@ -63,6 +63,7 @@ type TOMGeneratorAction =
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial } }
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
| { type: 'ADD_EXPORT'; payload: ExportRecord }
+ | { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial }> } }
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
// =============================================================================
@@ -236,6 +237,16 @@ function tomGeneratorReducer(
})
}
+ case 'BULK_UPDATE_TOMS': {
+ let updatedTOMs = [...state.derivedTOMs]
+ for (const update of action.payload.updates) {
+ updatedTOMs = updatedTOMs.map((tom) =>
+ tom.id === update.id ? { ...tom, ...update.data } : tom
+ )
+ }
+ return updateState({ derivedTOMs: updatedTOMs })
+ }
+
case 'LOAD_STATE': {
return action.payload
}
@@ -283,6 +294,7 @@ interface TOMGeneratorContextValue {
// TOM derivation
deriveTOMs: () => void
updateDerivedTOM: (id: string, data: Partial) => void
+ bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial }>) => void
// Gap analysis
runGapAnalysis: () => void
diff --git a/admin-v2/lib/sdk/tom-generator/controls/loader.ts b/admin-v2/lib/sdk/tom-generator/controls/loader.ts
index 5fed41f..0236002 100644
--- a/admin-v2/lib/sdk/tom-generator/controls/loader.ts
+++ b/admin-v2/lib/sdk/tom-generator/controls/loader.ts
@@ -2072,6 +2072,287 @@ const CONTROL_LIBRARY_DATA: ControlLibrary = {
complexity: 'HIGH',
tags: ['dpia', 'dsfa', 'risk-assessment'],
},
+
+ // =========================================================================
+ // DELETION / VERNICHTUNG β Sichere Datenloeschung & Datentraegervernichtung
+ // =========================================================================
+ {
+ id: 'TOM-DL-01',
+ code: 'TOM-DL-01',
+ category: 'SEPARATION',
+ type: 'TECHNICAL',
+ name: {
+ de: 'Sichere Datenloeschung',
+ en: 'Secure Data Deletion',
+ },
+ description: {
+ de: 'Implementierung sicherer Loeschverfahren, die personenbezogene Daten unwiederbringlich entfernen (z.B. nach DIN 66399).',
+ en: 'Implementation of secure deletion procedures that irrecoverably remove personal data (e.g. per DIN 66399).',
+ },
+ mappings: [
+ { framework: 'GDPR_ART17', reference: 'Art. 17' },
+ { framework: 'GDPR_ART5', reference: 'Art. 5 Abs. 1 lit. e' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.8.10' },
+ { framework: 'BSI_C5', reference: 'SY-09' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'dataProfile.dataVolume',
+ operator: 'NOT_EQUALS',
+ value: 'NONE',
+ result: 'REQUIRED',
+ priority: 30,
+ },
+ ],
+ defaultApplicability: 'REQUIRED',
+ evidenceRequirements: [
+ 'Loeschkonzept / Loeschrichtlinie',
+ 'Loeschprotokolle mit Zeitstempeln',
+ 'DIN 66399 Konformitaetsnachweis',
+ ],
+ reviewFrequency: 'ANNUAL',
+ priority: 'HIGH',
+ complexity: 'MEDIUM',
+ tags: ['deletion', 'loeschung', 'data-lifecycle', 'din-66399'],
+ },
+ {
+ id: 'TOM-DL-02',
+ code: 'TOM-DL-02',
+ category: 'SEPARATION',
+ type: 'TECHNICAL',
+ name: {
+ de: 'Datentraegervernichtung',
+ en: 'Media Destruction',
+ },
+ description: {
+ de: 'Physische Vernichtung von Datentraegern (Festplatten, SSDs, USB-Sticks, Papier) gemaess DIN 66399 Schutzklassen.',
+ en: 'Physical destruction of storage media (hard drives, SSDs, USB sticks, paper) per DIN 66399 protection classes.',
+ },
+ mappings: [
+ { framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.7.14' },
+ { framework: 'BSI_C5', reference: 'AM-08' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'dataProfile.dataVolume',
+ operator: 'NOT_EQUALS',
+ value: 'NONE',
+ result: 'RECOMMENDED',
+ priority: 20,
+ },
+ ],
+ defaultApplicability: 'RECOMMENDED',
+ evidenceRequirements: [
+ 'Vernichtungsprotokoll mit Seriennummern',
+ 'Zertifikat des Vernichtungsdienstleisters',
+ 'DIN 66399 Sicherheitsstufe-Nachweis',
+ ],
+ reviewFrequency: 'ANNUAL',
+ priority: 'MEDIUM',
+ complexity: 'LOW',
+ tags: ['deletion', 'media-destruction', 'physical-security', 'din-66399'],
+ },
+ {
+ id: 'TOM-DL-03',
+ code: 'TOM-DL-03',
+ category: 'SEPARATION',
+ type: 'ORGANIZATIONAL',
+ name: {
+ de: 'Loeschprotokollierung',
+ en: 'Deletion Logging',
+ },
+ description: {
+ de: 'Systematische Protokollierung aller Loeschvorgaenge mit Zeitstempel, Verantwortlichem, Datenobjekt und Loeschmethode.',
+ en: 'Systematic logging of all deletion operations with timestamp, responsible person, data object, and deletion method.',
+ },
+ mappings: [
+ { framework: 'GDPR_ART5', reference: 'Art. 5 Abs. 2 (Rechenschaftspflicht)' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.8.10' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'dataProfile.dataVolume',
+ operator: 'NOT_EQUALS',
+ value: 'NONE',
+ result: 'REQUIRED',
+ priority: 25,
+ },
+ ],
+ defaultApplicability: 'REQUIRED',
+ evidenceRequirements: [
+ 'Loeschprotokoll-Template',
+ 'Archivierte Loeschprotokolle (Stichprobe)',
+ 'Automatisierungsnachweis (bei automatischen Loeschungen)',
+ ],
+ reviewFrequency: 'SEMI_ANNUAL',
+ priority: 'HIGH',
+ complexity: 'LOW',
+ tags: ['deletion', 'logging', 'accountability', 'documentation'],
+ },
+ {
+ id: 'TOM-DL-04',
+ code: 'TOM-DL-04',
+ category: 'SEPARATION',
+ type: 'TECHNICAL',
+ name: {
+ de: 'Backup-Bereinigung',
+ en: 'Backup Sanitization',
+ },
+ description: {
+ de: 'Sicherstellung, dass personenbezogene Daten auch in Backup-Systemen nach Ablauf der Loeschfrist entfernt werden.',
+ en: 'Ensuring that personal data is also removed from backup systems after the retention period expires.',
+ },
+ mappings: [
+ { framework: 'GDPR_ART17', reference: 'Art. 17 Abs. 2' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.8.13' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'techProfile.hasBackups',
+ operator: 'EQUALS',
+ value: true,
+ result: 'REQUIRED',
+ priority: 25,
+ },
+ ],
+ defaultApplicability: 'RECOMMENDED',
+ evidenceRequirements: [
+ 'Backup-Loeschkonzept',
+ 'Backup-Rotationsplan',
+ 'Nachweis der Backup-Bereinigung',
+ ],
+ reviewFrequency: 'SEMI_ANNUAL',
+ priority: 'MEDIUM',
+ complexity: 'HIGH',
+ tags: ['deletion', 'backup', 'data-lifecycle', 'retention'],
+ },
+
+ // =========================================================================
+ // SCHULUNG / VERTRAULICHKEIT β Training & Awareness
+ // =========================================================================
+ {
+ id: 'TOM-TR-01',
+ code: 'TOM-TR-01',
+ category: 'REVIEW',
+ type: 'ORGANIZATIONAL',
+ name: {
+ de: 'Datenschutzschulung',
+ en: 'Data Protection Training',
+ },
+ description: {
+ de: 'Regelmaessige Schulung aller Mitarbeiter zu Datenschutzgrundlagen, DSGVO-Anforderungen und betrieblichen Datenschutzrichtlinien.',
+ en: 'Regular training of all employees on data protection fundamentals, GDPR requirements, and organizational data protection policies.',
+ },
+ mappings: [
+ { framework: 'GDPR_ART39', reference: 'Art. 39 Abs. 1 lit. b' },
+ { framework: 'GDPR_ART47', reference: 'Art. 47 Abs. 2 lit. n' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.6.3' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'orgProfile.employeeCount',
+ operator: 'GREATER_THAN',
+ value: 0,
+ result: 'REQUIRED',
+ priority: 30,
+ },
+ ],
+ defaultApplicability: 'REQUIRED',
+ evidenceRequirements: [
+ 'Schulungsplan (jaehrlich)',
+ 'Teilnahmelisten / Schulungsnachweise',
+ 'Schulungsmaterialien / Praesentation',
+ 'Wissenstest-Ergebnisse (optional)',
+ ],
+ reviewFrequency: 'ANNUAL',
+ priority: 'HIGH',
+ complexity: 'LOW',
+ tags: ['training', 'schulung', 'awareness', 'organizational'],
+ },
+ {
+ id: 'TOM-TR-02',
+ code: 'TOM-TR-02',
+ category: 'REVIEW',
+ type: 'ORGANIZATIONAL',
+ name: {
+ de: 'Verpflichtung auf Datengeheimnis',
+ en: 'Confidentiality Obligation',
+ },
+ description: {
+ de: 'Schriftliche Verpflichtung aller Mitarbeiter und externen Dienstleister auf die Vertraulichkeit personenbezogener Daten.',
+ en: 'Written obligation of all employees and external service providers to maintain confidentiality of personal data.',
+ },
+ mappings: [
+ { framework: 'GDPR_ART28', reference: 'Art. 28 Abs. 3 lit. b' },
+ { framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 4' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.6.6' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'orgProfile.employeeCount',
+ operator: 'GREATER_THAN',
+ value: 0,
+ result: 'REQUIRED',
+ priority: 30,
+ },
+ ],
+ defaultApplicability: 'REQUIRED',
+ evidenceRequirements: [
+ 'Muster-Verpflichtungserklaerung',
+ 'Unterschriebene Verpflichtungserklaerungen',
+ 'Register der verpflichteten Personen',
+ ],
+ reviewFrequency: 'ANNUAL',
+ priority: 'HIGH',
+ complexity: 'LOW',
+ tags: ['training', 'confidentiality', 'vertraulichkeit', 'obligation'],
+ },
+ {
+ id: 'TOM-TR-03',
+ code: 'TOM-TR-03',
+ category: 'REVIEW',
+ type: 'ORGANIZATIONAL',
+ name: {
+ de: 'Security Awareness Programm',
+ en: 'Security Awareness Program',
+ },
+ description: {
+ de: 'Fortlaufendes Awareness-Programm zu IT-Sicherheit, Phishing-Erkennung, Social Engineering und sicherem Umgang mit Daten.',
+ en: 'Ongoing awareness program on IT security, phishing detection, social engineering, and safe data handling.',
+ },
+ mappings: [
+ { framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
+ { framework: 'ISO27001_ANNEX_A', reference: 'A.6.3' },
+ { framework: 'BSI_C5', reference: 'ORP.3' },
+ ],
+ applicabilityConditions: [
+ {
+ field: 'orgProfile.employeeCount',
+ operator: 'GREATER_THAN',
+ value: 10,
+ result: 'REQUIRED',
+ priority: 20,
+ },
+ {
+ field: 'orgProfile.employeeCount',
+ operator: 'GREATER_THAN',
+ value: 0,
+ result: 'RECOMMENDED',
+ priority: 15,
+ },
+ ],
+ defaultApplicability: 'RECOMMENDED',
+ evidenceRequirements: [
+ 'Awareness-Programm-Dokumentation',
+ 'Phishing-Simulationsergebnisse',
+ 'Teilnahmenachweise',
+ ],
+ reviewFrequency: 'SEMI_ANNUAL',
+ priority: 'MEDIUM',
+ complexity: 'MEDIUM',
+ tags: ['training', 'security-awareness', 'phishing', 'social-engineering'],
+ },
],
}
diff --git a/admin-v2/lib/sdk/tom-generator/sdm-mapping.ts b/admin-v2/lib/sdk/tom-generator/sdm-mapping.ts
new file mode 100644
index 0000000..cdea531
--- /dev/null
+++ b/admin-v2/lib/sdk/tom-generator/sdm-mapping.ts
@@ -0,0 +1,192 @@
+// =============================================================================
+// SDM (Standard-Datenschutzmodell) Mapping
+// Maps ControlCategories to SDM Gewaehrleistungsziele and Spec Modules
+// =============================================================================
+
+import { ControlCategory } from './types'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export type SDMGewaehrleistungsziel =
+ | 'Verfuegbarkeit'
+ | 'Integritaet'
+ | 'Vertraulichkeit'
+ | 'Nichtverkettung'
+ | 'Intervenierbarkeit'
+ | 'Transparenz'
+ | 'Datenminimierung'
+
+export type TOMModuleCategory =
+ | 'IDENTITY_AUTH'
+ | 'LOGGING'
+ | 'DOCUMENTATION'
+ | 'SEPARATION'
+ | 'RETENTION'
+ | 'DELETION'
+ | 'TRAINING'
+ | 'REVIEW'
+
+export const SDM_GOAL_LABELS: Record = {
+ Verfuegbarkeit: 'Verfuegbarkeit',
+ Integritaet: 'Integritaet',
+ Vertraulichkeit: 'Vertraulichkeit',
+ Nichtverkettung: 'Nichtverkettung',
+ Intervenierbarkeit: 'Intervenierbarkeit',
+ Transparenz: 'Transparenz',
+ Datenminimierung: 'Datenminimierung',
+}
+
+export const SDM_GOAL_DESCRIPTIONS: Record = {
+ Verfuegbarkeit: 'Personenbezogene Daten muessen zeitgerecht zur Verfuegung stehen und ordnungsgemaess verarbeitet werden koennen.',
+ Integritaet: 'Personenbezogene Daten muessen unversehrt, vollstaendig und aktuell bleiben.',
+ Vertraulichkeit: 'Nur Befugte duerfen personenbezogene Daten zur Kenntnis nehmen.',
+ Nichtverkettung: 'Daten duerfen nicht ohne Weiteres fuer andere Zwecke zusammengefuehrt werden.',
+ Intervenierbarkeit: 'Betroffene muessen ihre Rechte wahrnehmen koennen (Auskunft, Berichtigung, Loeschung).',
+ Transparenz: 'Verarbeitungsvorgaenge muessen nachvollziehbar dokumentiert sein.',
+ Datenminimierung: 'Nur die fuer den Zweck erforderlichen Daten duerfen verarbeitet werden.',
+}
+
+export const MODULE_LABELS: Record = {
+ IDENTITY_AUTH: 'Identitaet & Authentifizierung',
+ LOGGING: 'Protokollierung',
+ DOCUMENTATION: 'Dokumentation',
+ SEPARATION: 'Trennung',
+ RETENTION: 'Aufbewahrung',
+ DELETION: 'Loeschung & Vernichtung',
+ TRAINING: 'Schulung & Vertraulichkeit',
+ REVIEW: 'Ueberpruefung & Bewertung',
+}
+
+// =============================================================================
+// MAPPINGS
+// =============================================================================
+
+/**
+ * Maps ControlCategory to its primary SDM Gewaehrleistungsziele
+ */
+export const SDM_CATEGORY_MAPPING: Record = {
+ ACCESS_CONTROL: ['Vertraulichkeit'],
+ ADMISSION_CONTROL: ['Vertraulichkeit', 'Integritaet'],
+ ACCESS_AUTHORIZATION: ['Vertraulichkeit', 'Nichtverkettung'],
+ TRANSFER_CONTROL: ['Vertraulichkeit', 'Integritaet'],
+ INPUT_CONTROL: ['Integritaet', 'Transparenz'],
+ ORDER_CONTROL: ['Transparenz', 'Intervenierbarkeit'],
+ AVAILABILITY: ['Verfuegbarkeit'],
+ SEPARATION: ['Nichtverkettung', 'Datenminimierung'],
+ ENCRYPTION: ['Vertraulichkeit', 'Integritaet'],
+ PSEUDONYMIZATION: ['Datenminimierung', 'Nichtverkettung'],
+ RESILIENCE: ['Verfuegbarkeit'],
+ RECOVERY: ['Verfuegbarkeit', 'Integritaet'],
+ REVIEW: ['Transparenz', 'Intervenierbarkeit'],
+}
+
+/**
+ * Maps ControlCategory to Spec Module Categories
+ */
+export const MODULE_CATEGORY_MAPPING: Record = {
+ ACCESS_CONTROL: ['IDENTITY_AUTH'],
+ ADMISSION_CONTROL: ['IDENTITY_AUTH'],
+ ACCESS_AUTHORIZATION: ['IDENTITY_AUTH', 'DOCUMENTATION'],
+ TRANSFER_CONTROL: ['DOCUMENTATION'],
+ INPUT_CONTROL: ['LOGGING'],
+ ORDER_CONTROL: ['DOCUMENTATION'],
+ AVAILABILITY: ['REVIEW'],
+ SEPARATION: ['SEPARATION'],
+ ENCRYPTION: ['IDENTITY_AUTH'],
+ PSEUDONYMIZATION: ['SEPARATION', 'DELETION'],
+ RESILIENCE: ['REVIEW'],
+ RECOVERY: ['REVIEW'],
+ REVIEW: ['REVIEW', 'TRAINING'],
+}
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+import type { DerivedTOM, ControlLibraryEntry } from './types'
+import { getControlById } from './controls/loader'
+
+/**
+ * Get SDM goals for a given control (by looking up its category)
+ */
+export function getSDMGoalsForControl(controlId: string): SDMGewaehrleistungsziel[] {
+ const control = getControlById(controlId)
+ if (!control) return []
+ return SDM_CATEGORY_MAPPING[control.category] || []
+}
+
+/**
+ * Get derived TOMs that map to a specific SDM goal
+ */
+export function getTOMsBySDMGoal(
+ toms: DerivedTOM[],
+ goal: SDMGewaehrleistungsziel
+): DerivedTOM[] {
+ return toms.filter(tom => {
+ const goals = getSDMGoalsForControl(tom.controlId)
+ return goals.includes(goal)
+ })
+}
+
+/**
+ * Get derived TOMs belonging to a specific module
+ */
+export function getTOMsByModule(
+ toms: DerivedTOM[],
+ module: TOMModuleCategory
+): DerivedTOM[] {
+ return toms.filter(tom => {
+ const control = getControlById(tom.controlId)
+ if (!control) return false
+ const modules = MODULE_CATEGORY_MAPPING[control.category] || []
+ return modules.includes(module)
+ })
+}
+
+/**
+ * Get SDM goal coverage statistics
+ */
+export function getSDMCoverageStats(toms: DerivedTOM[]): Record {
+ const goals = Object.keys(SDM_GOAL_LABELS) as SDMGewaehrleistungsziel[]
+ const stats = {} as Record
+
+ for (const goal of goals) {
+ const goalTOMs = getTOMsBySDMGoal(toms, goal)
+ stats[goal] = {
+ total: goalTOMs.length,
+ implemented: goalTOMs.filter(t => t.implementationStatus === 'IMPLEMENTED').length,
+ partial: goalTOMs.filter(t => t.implementationStatus === 'PARTIAL').length,
+ missing: goalTOMs.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length,
+ }
+ }
+
+ return stats
+}
+
+/**
+ * Get module coverage statistics
+ */
+export function getModuleCoverageStats(toms: DerivedTOM[]): Record {
+ const modules = Object.keys(MODULE_LABELS) as TOMModuleCategory[]
+ const stats = {} as Record
+
+ for (const mod of modules) {
+ const modTOMs = getTOMsByModule(toms, mod)
+ stats[mod] = {
+ total: modTOMs.length,
+ implemented: modTOMs.filter(t => t.implementationStatus === 'IMPLEMENTED').length,
+ }
+ }
+
+ return stats
+}
diff --git a/admin-v2/lib/sdk/tom-generator/types.ts b/admin-v2/lib/sdk/tom-generator/types.ts
index 28dc7cf..d13cc63 100644
--- a/admin-v2/lib/sdk/tom-generator/types.ts
+++ b/admin-v2/lib/sdk/tom-generator/types.ts
@@ -899,3 +899,65 @@ export function createInitialTOMGeneratorState(
* Alias for createInitialTOMGeneratorState (for API compatibility)
*/
export const createEmptyTOMGeneratorState = createInitialTOMGeneratorState
+
+// =============================================================================
+// SDM TYPES (Standard-Datenschutzmodell)
+// =============================================================================
+
+export type SDMGewaehrleistungsziel =
+ | 'Verfuegbarkeit'
+ | 'Integritaet'
+ | 'Vertraulichkeit'
+ | 'Nichtverkettung'
+ | 'Intervenierbarkeit'
+ | 'Transparenz'
+ | 'Datenminimierung'
+
+export type TOMModuleCategory =
+ | 'IDENTITY_AUTH'
+ | 'LOGGING'
+ | 'DOCUMENTATION'
+ | 'SEPARATION'
+ | 'RETENTION'
+ | 'DELETION'
+ | 'TRAINING'
+ | 'REVIEW'
+
+/**
+ * Maps ControlCategory to SDM Gewaehrleistungsziele.
+ * Used by the TOM Dashboard to display SDM coverage.
+ */
+export const SDM_CATEGORY_MAPPING: Record = {
+ ACCESS_CONTROL: ['Vertraulichkeit'],
+ ADMISSION_CONTROL: ['Vertraulichkeit', 'Integritaet'],
+ ACCESS_AUTHORIZATION: ['Vertraulichkeit', 'Nichtverkettung'],
+ TRANSFER_CONTROL: ['Vertraulichkeit', 'Integritaet'],
+ INPUT_CONTROL: ['Integritaet', 'Transparenz'],
+ ORDER_CONTROL: ['Transparenz', 'Intervenierbarkeit'],
+ AVAILABILITY: ['Verfuegbarkeit'],
+ SEPARATION: ['Nichtverkettung', 'Datenminimierung'],
+ ENCRYPTION: ['Vertraulichkeit', 'Integritaet'],
+ PSEUDONYMIZATION: ['Datenminimierung', 'Nichtverkettung'],
+ RESILIENCE: ['Verfuegbarkeit'],
+ RECOVERY: ['Verfuegbarkeit', 'Integritaet'],
+ REVIEW: ['Transparenz', 'Intervenierbarkeit'],
+}
+
+/**
+ * Maps ControlCategory to Spec Module Categories.
+ */
+export const MODULE_CATEGORY_MAPPING: Record = {
+ ACCESS_CONTROL: ['IDENTITY_AUTH'],
+ ADMISSION_CONTROL: ['IDENTITY_AUTH'],
+ ACCESS_AUTHORIZATION: ['IDENTITY_AUTH', 'DOCUMENTATION'],
+ TRANSFER_CONTROL: ['DOCUMENTATION'],
+ INPUT_CONTROL: ['LOGGING'],
+ ORDER_CONTROL: ['DOCUMENTATION'],
+ AVAILABILITY: ['REVIEW'],
+ SEPARATION: ['SEPARATION'],
+ ENCRYPTION: ['IDENTITY_AUTH'],
+ PSEUDONYMIZATION: ['SEPARATION', 'DELETION'],
+ RESILIENCE: ['REVIEW'],
+ RECOVERY: ['REVIEW'],
+ REVIEW: ['REVIEW', 'TRAINING'],
+}
diff --git a/admin-v2/lib/sdk/types.ts b/admin-v2/lib/sdk/types.ts
index b939f1a..388d44f 100644
--- a/admin-v2/lib/sdk/types.ts
+++ b/admin-v2/lib/sdk/types.ts
@@ -314,10 +314,23 @@ export const SDK_STEPS: SDKStep[] = [
isOptional: false,
},
{
- id: 'use-case-assessment',
+ id: 'compliance-scope',
phase: 1,
package: 'vorbereitung',
order: 2,
+ name: 'Compliance Scope',
+ nameShort: 'Scope',
+ description: 'Umfang und Tiefe Ihrer Compliance-Dokumentation bestimmen',
+ url: '/sdk/compliance-scope',
+ checkpointId: 'CP-SCOPE',
+ prerequisiteSteps: ['company-profile'],
+ isOptional: false,
+ },
+ {
+ id: 'use-case-assessment',
+ phase: 1,
+ package: 'vorbereitung',
+ order: 3,
name: 'Anwendungsfall-Erfassung',
nameShort: 'Anwendung',
description: 'AI-AnwendungsfΓ€lle strukturiert dokumentieren',
@@ -330,7 +343,7 @@ export const SDK_STEPS: SDKStep[] = [
id: 'import',
phase: 1,
package: 'vorbereitung',
- order: 3,
+ order: 4,
name: 'Dokument-Import',
nameShort: 'Import',
description: 'Bestehende Dokumente hochladen (Bestandskunden)',
@@ -343,7 +356,7 @@ export const SDK_STEPS: SDKStep[] = [
id: 'screening',
phase: 1,
package: 'vorbereitung',
- order: 4,
+ order: 5,
name: 'System Screening',
nameShort: 'Screening',
description: 'SBOM + Security Check',
@@ -356,7 +369,7 @@ export const SDK_STEPS: SDKStep[] = [
id: 'modules',
phase: 1,
package: 'vorbereitung',
- order: 5,
+ order: 6,
name: 'Compliance Modules',
nameShort: 'Module',
description: 'Abgleich welche Regulierungen gelten',
@@ -365,6 +378,19 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['screening'],
isOptional: false,
},
+ {
+ id: 'source-policy',
+ phase: 1,
+ package: 'vorbereitung',
+ order: 7,
+ name: 'Source Policy',
+ nameShort: 'Quellen',
+ description: 'Datenquellen-Governance & Whitelist',
+ url: '/sdk/source-policy',
+ checkpointId: 'CP-SPOL',
+ prerequisiteSteps: ['modules'],
+ isOptional: false,
+ },
// =============================================================================
// PAKET 2: ANALYSE (Assessment)
@@ -379,7 +405,7 @@ export const SDK_STEPS: SDKStep[] = [
description: 'PrΓΌfaspekte aus Regulierungen ableiten',
url: '/sdk/requirements',
checkpointId: 'CP-REQ',
- prerequisiteSteps: ['modules'],
+ prerequisiteSteps: ['source-policy'],
isOptional: false,
},
{
@@ -447,6 +473,19 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['ai-act'],
isOptional: false,
},
+ {
+ id: 'audit-report',
+ phase: 1,
+ package: 'analyse',
+ order: 7,
+ name: 'Audit Report',
+ nameShort: 'Report',
+ description: 'Audit-Sitzungen & PDF-Report',
+ url: '/sdk/audit-report',
+ checkpointId: 'CP-AREP',
+ prerequisiteSteps: ['audit-checklist'],
+ isOptional: false,
+ },
// =============================================================================
// PAKET 3: DOKUMENTATION (Compliance Docs)
@@ -461,7 +500,7 @@ export const SDK_STEPS: SDKStep[] = [
description: 'NIS2, DSGVO, AI Act Pflichten',
url: '/sdk/obligations',
checkpointId: 'CP-OBL',
- prerequisiteSteps: ['audit-checklist'],
+ prerequisiteSteps: ['audit-report'],
isOptional: false,
},
{
@@ -572,6 +611,19 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['cookie-banner'],
isOptional: true,
},
+ {
+ id: 'workflow',
+ phase: 2,
+ package: 'rechtliche-texte',
+ order: 5,
+ name: 'Document Workflow',
+ nameShort: 'Workflow',
+ description: 'Versionierung & Freigabe-Workflow',
+ url: '/sdk/workflow',
+ checkpointId: 'CP-WRKF',
+ prerequisiteSteps: ['document-generator'],
+ isOptional: false,
+ },
// =============================================================================
// PAKET 5: BETRIEB (Operations)
@@ -586,7 +638,7 @@ export const SDK_STEPS: SDKStep[] = [
description: 'Betroffenenrechte-Portal',
url: '/sdk/dsr',
checkpointId: 'CP-DSR',
- prerequisiteSteps: ['cookie-banner'],
+ prerequisiteSteps: ['workflow'],
isOptional: false,
},
{
@@ -615,6 +667,32 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['escalations'],
isOptional: false,
},
+ {
+ id: 'consent-management',
+ phase: 2,
+ package: 'betrieb',
+ order: 4,
+ name: 'Consent Verwaltung',
+ nameShort: 'Consent Mgmt',
+ description: 'Dokument-Lifecycle & DSGVO-Prozesse',
+ url: '/sdk/consent-management',
+ checkpointId: 'CP-CMGMT',
+ prerequisiteSteps: ['vendor-compliance'],
+ isOptional: false,
+ },
+ {
+ id: 'notfallplan',
+ phase: 2,
+ package: 'betrieb',
+ order: 5,
+ name: 'Notfallplan & Breach Response',
+ nameShort: 'Notfallplan',
+ description: 'Datenpannen-Management nach Art. 33/34 DSGVO',
+ url: '/sdk/notfallplan',
+ checkpointId: 'CP-NOTF',
+ prerequisiteSteps: ['consent-management'],
+ isOptional: false,
+ },
]
// =============================================================================
@@ -1208,6 +1286,9 @@ export interface SDKState {
// Company Profile (collected before use cases)
companyProfile: CompanyProfile | null
+ // Compliance Scope (determines depth level L1-L4)
+ complianceScope: import('./compliance-scope-types').ComplianceScopeState | null
+
// Progress
currentPhase: SDKPhase
currentStep: string
@@ -1265,6 +1346,8 @@ export type SDKAction =
| { type: 'SET_CUSTOMER_TYPE'; payload: CustomerType }
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial }
+ | { type: 'SET_COMPLIANCE_SCOPE'; payload: import('./compliance-scope-types').ComplianceScopeState }
+ | { type: 'UPDATE_COMPLIANCE_SCOPE'; payload: Partial }
| { type: 'ADD_IMPORTED_DOCUMENT'; payload: ImportedDocument }
| { type: 'UPDATE_IMPORTED_DOCUMENT'; payload: { id: string; data: Partial } }
| { type: 'DELETE_IMPORTED_DOCUMENT'; payload: string }
@@ -1783,3 +1866,243 @@ export const JURISDICTION_LABELS: Record = {
US: 'United States',
INTL: 'International',
}
+
+// =============================================================================
+// DSFA RAG TYPES (Source Attribution & Corpus Management)
+// =============================================================================
+
+/**
+ * License codes for DSFA source documents
+ */
+export type DSFALicenseCode =
+ | 'DL-DE-BY-2.0' // Datenlizenz Deutschland β Namensnennung
+ | 'DL-DE-ZERO-2.0' // Datenlizenz Deutschland β Zero
+ | 'CC-BY-4.0' // Creative Commons Attribution 4.0
+ | 'EDPB-LICENSE' // EDPB Document License
+ | 'PUBLIC_DOMAIN' // Public Domain
+ | 'PROPRIETARY' // Internal/Proprietary
+
+/**
+ * Document types in the DSFA corpus
+ */
+export type DSFADocumentType = 'guideline' | 'checklist' | 'regulation' | 'template'
+
+/**
+ * Category for DSFA chunks (for filtering)
+ */
+export type DSFACategory =
+ | 'threshold_analysis'
+ | 'risk_assessment'
+ | 'mitigation'
+ | 'consultation'
+ | 'documentation'
+ | 'process'
+ | 'criteria'
+
+/**
+ * DSFA source registry entry
+ */
+export interface DSFASource {
+ id: string
+ sourceCode: string
+ name: string
+ fullName?: string
+ organization?: string
+ sourceUrl?: string
+ eurLexCelex?: string
+ licenseCode: DSFALicenseCode
+ licenseName: string
+ licenseUrl?: string
+ attributionRequired: boolean
+ attributionText: string
+ documentType?: DSFADocumentType
+ language: string
+}
+
+/**
+ * DSFA document entry
+ */
+export interface DSFADocument {
+ id: string
+ sourceId: string
+ title: string
+ description?: string
+ fileName?: string
+ fileType?: string
+ fileSizeBytes?: number
+ minioBucket: string
+ minioPath?: string
+ originalUrl?: string
+ ocrProcessed: boolean
+ textExtracted: boolean
+ chunksGenerated: number
+ lastIndexedAt?: string
+ metadata: Record
+ createdAt: string
+ updatedAt: string
+}
+
+/**
+ * DSFA chunk with full attribution
+ */
+export interface DSFAChunk {
+ chunkId: string
+ content: string
+ sectionTitle?: string
+ pageNumber?: number
+ category?: DSFACategory
+ documentId: string
+ documentTitle?: string
+ sourceId: string
+ sourceCode: string
+ sourceName: string
+ attributionText: string
+ licenseCode: DSFALicenseCode
+ licenseName: string
+ licenseUrl?: string
+ attributionRequired: boolean
+ sourceUrl?: string
+ documentType?: DSFADocumentType
+}
+
+/**
+ * DSFA search result with score and attribution
+ */
+export interface DSFASearchResult {
+ chunkId: string
+ content: string
+ score: number
+ sourceCode: string
+ sourceName: string
+ attributionText: string
+ licenseCode: DSFALicenseCode
+ licenseName: string
+ licenseUrl?: string
+ attributionRequired: boolean
+ sourceUrl?: string
+ documentType?: DSFADocumentType
+ category?: DSFACategory
+ sectionTitle?: string
+ pageNumber?: number
+}
+
+/**
+ * DSFA search response with aggregated attribution
+ */
+export interface DSFASearchResponse {
+ query: string
+ results: DSFASearchResult[]
+ totalResults: number
+ licensesUsed: string[]
+ attributionNotice: string
+}
+
+/**
+ * Source statistics for dashboard
+ */
+export interface DSFASourceStats {
+ sourceId: string
+ sourceCode: string
+ name: string
+ organization?: string
+ licenseCode: DSFALicenseCode
+ documentType?: DSFADocumentType
+ documentCount: number
+ chunkCount: number
+ lastIndexedAt?: string
+}
+
+/**
+ * Corpus statistics for dashboard
+ */
+export interface DSFACorpusStats {
+ sources: DSFASourceStats[]
+ totalSources: number
+ totalDocuments: number
+ totalChunks: number
+ qdrantCollection: string
+ qdrantPointsCount: number
+ qdrantStatus: string
+}
+
+/**
+ * License information
+ */
+export interface DSFALicenseInfo {
+ code: DSFALicenseCode
+ name: string
+ url?: string
+ attributionRequired: boolean
+ modificationAllowed: boolean
+ commercialUse: boolean
+}
+
+/**
+ * Ingestion request for DSFA documents
+ */
+export interface DSFAIngestRequest {
+ documentUrl?: string
+ documentText?: string
+ title?: string
+}
+
+/**
+ * Ingestion response
+ */
+export interface DSFAIngestResponse {
+ sourceCode: string
+ documentId?: string
+ chunksCreated: number
+ message: string
+}
+
+/**
+ * Props for SourceAttribution component
+ */
+export interface SourceAttributionProps {
+ sources: Array<{
+ sourceCode: string
+ sourceName: string
+ attributionText: string
+ licenseCode: DSFALicenseCode
+ sourceUrl?: string
+ score?: number
+ }>
+ compact?: boolean
+ showScores?: boolean
+}
+
+/**
+ * License code display labels
+ */
+export const DSFA_LICENSE_LABELS: Record = {
+ 'DL-DE-BY-2.0': 'Datenlizenz DE β Namensnennung 2.0',
+ 'DL-DE-ZERO-2.0': 'Datenlizenz DE β Zero 2.0',
+ 'CC-BY-4.0': 'CC BY 4.0 International',
+ 'EDPB-LICENSE': 'EDPB Document License',
+ 'PUBLIC_DOMAIN': 'Public Domain',
+ 'PROPRIETARY': 'Proprietary',
+}
+
+/**
+ * Document type display labels
+ */
+export const DSFA_DOCUMENT_TYPE_LABELS: Record = {
+ guideline: 'Leitlinie',
+ checklist: 'PrΓΌfliste',
+ regulation: 'Verordnung',
+ template: 'Vorlage',
+}
+
+/**
+ * Category display labels
+ */
+export const DSFA_CATEGORY_LABELS: Record = {
+ threshold_analysis: 'Schwellwertanalyse',
+ risk_assessment: 'Risikobewertung',
+ mitigation: 'Risikominderung',
+ consultation: 'BehΓΆrdenkonsultation',
+ documentation: 'Dokumentation',
+ process: 'Prozessschritte',
+ criteria: 'Kriterien',
+}
diff --git a/admin-v2/lib/sdk/vvt-baseline-catalog.ts b/admin-v2/lib/sdk/vvt-baseline-catalog.ts
new file mode 100644
index 0000000..85fcbc5
--- /dev/null
+++ b/admin-v2/lib/sdk/vvt-baseline-catalog.ts
@@ -0,0 +1,630 @@
+/**
+ * VVT Baseline-Katalog
+ *
+ * Vordefinierte Verarbeitungstaetigkeiten als Templates.
+ * Werden vom Profiling-Fragebogen (Generator) genutzt, um
+ * auf Basis der Antworten VVT-Eintraege vorzubefuellen.
+ */
+
+import type { VVTActivity, BusinessFunction } from './vvt-types'
+
+export interface BaselineTemplate {
+ templateId: string
+ businessFunction: BusinessFunction
+ name: string
+ description: string
+ purposes: string[]
+ legalBases: { type: string; description?: string; reference?: string }[]
+ dataSubjectCategories: string[]
+ personalDataCategories: string[]
+ recipientCategories: { type: string; name: string; description?: string }[]
+ retentionPeriod: { duration?: number; durationUnit?: string; description: string; legalBasis?: string; deletionProcedure?: string }
+ tomDescription: string
+ structuredToms: {
+ accessControl: string[]
+ confidentiality: string[]
+ integrity: string[]
+ availability: string[]
+ separation: string[]
+ }
+ typicalSystems: string[]
+ protectionLevel: 'LOW' | 'MEDIUM' | 'HIGH'
+ dpiaRequired: boolean
+ tags: string[]
+}
+
+// =============================================================================
+// BASELINE TEMPLATES
+// =============================================================================
+
+export const VVT_BASELINE_CATALOG: BaselineTemplate[] = [
+ // ==================== HR ====================
+ {
+ templateId: 'hr-mitarbeiterverwaltung',
+ businessFunction: 'hr',
+ name: 'Mitarbeiterverwaltung',
+ description: 'Verwaltung von Stammdaten, Vertraegen und Personalakten der Beschaeftigten',
+ purposes: ['Durchfuehrung des Beschaeftigungsverhaeltnisses', 'Personalverwaltung und -planung'],
+ legalBases: [
+ { type: 'CONTRACT', description: 'Arbeitsvertrag', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ { type: 'LEGAL_OBLIGATION', description: 'Arbeitsrechtliche Pflichten', reference: 'Β§ 26 BDSG' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES'],
+ personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'DOB', 'SOCIAL_SECURITY', 'TAX_ID', 'BANK_ACCOUNT', 'EMPLOYMENT_DATA'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Personalabteilung' },
+ { type: 'AUTHORITY', name: 'Finanzamt' },
+ { type: 'AUTHORITY', name: 'Sozialversicherungstraeger' },
+ ],
+ retentionPeriod: { duration: 10, durationUnit: 'YEARS', description: '10 Jahre nach Ende des Beschaeftigungsverhaeltnisses', legalBasis: 'HGB Β§ 257, AO Β§ 147', deletionProcedure: 'Sichere Loeschung nach Ablauf' },
+ tomDescription: 'Zugriffskontrolle auf Personalakten, Verschluesselung, Protokollierung',
+ structuredToms: {
+ accessControl: ['RBAC', 'Need-to-know-Prinzip', 'Personalakten nur fuer HR'],
+ confidentiality: ['Verschluesselung personenbezogener Daten', 'Vertraulichkeitsvereinbarungen'],
+ integrity: ['Aenderungsprotokollierung', 'Vier-Augen-Prinzip bei Gehaltsaenderungen'],
+ availability: ['Regelmaessige Backups', 'Redundante Speicherung'],
+ separation: ['Trennung Personal-/Gehaltsdaten'],
+ },
+ typicalSystems: ['HR-Software', 'Gehaltsabrechnung', 'Dokumentenmanagement'],
+ protectionLevel: 'HIGH',
+ dpiaRequired: false,
+ tags: ['hr', 'mitarbeiter', 'personal'],
+ },
+ {
+ templateId: 'hr-gehaltsabrechnung',
+ businessFunction: 'hr',
+ name: 'Gehaltsabrechnung',
+ description: 'Berechnung und Auszahlung von Gehaeltern, Sozialabgaben und Steuern',
+ purposes: ['Lohn- und Gehaltsabrechnung', 'Erfuellung steuer- und sozialversicherungsrechtlicher Pflichten'],
+ legalBases: [
+ { type: 'CONTRACT', description: 'Arbeitsvertrag', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ { type: 'LEGAL_OBLIGATION', description: 'Steuer-/Sozialversicherungsrecht', reference: 'Art. 6 Abs. 1 lit. c DSGVO' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES'],
+ personalDataCategories: ['NAME', 'ADDRESS', 'SOCIAL_SECURITY', 'TAX_ID', 'BANK_ACCOUNT', 'SALARY_DATA'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'Lohnbuero / Steuerberater' },
+ { type: 'AUTHORITY', name: 'Finanzamt' },
+ { type: 'AUTHORITY', name: 'Krankenkassen' },
+ ],
+ retentionPeriod: { duration: 10, durationUnit: 'YEARS', description: '10 Jahre (steuerrechtlich)', legalBasis: 'AO Β§ 147 Abs. 1 Nr. 1', deletionProcedure: 'Automatische Loeschung nach Fristablauf' },
+ tomDescription: 'Strenge Zugriffskontrolle, Verschluesselung, Trennung von Stamm- und Gehaltsdaten',
+ structuredToms: {
+ accessControl: ['Nur Lohnbuchhaltung/Steuerberater', 'MFA'],
+ confidentiality: ['Verschluesselung at-rest und in-transit', 'Vertraulichkeitsklausel'],
+ integrity: ['Revisionssichere Ablage', 'Pruefprotokoll'],
+ availability: ['Monatliche Backups', 'Jahresabschluss-Archiv'],
+ separation: ['Gehaltsdaten getrennt von allgemeinen Personaldaten'],
+ },
+ typicalSystems: ['DATEV', 'Lohnabrechnungssoftware'],
+ protectionLevel: 'HIGH',
+ dpiaRequired: false,
+ tags: ['hr', 'gehalt', 'lohn', 'steuer'],
+ },
+ {
+ templateId: 'hr-bewerbermanagement',
+ businessFunction: 'hr',
+ name: 'Bewerbermanagement',
+ description: 'Entgegennahme, Verwaltung und Bewertung von Bewerbungen',
+ purposes: ['Bearbeitung eingehender Bewerbungen', 'Bewerberauswahl'],
+ legalBases: [
+ { type: 'CONTRACT', description: 'Vorvertragliche Massnahmen', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ { type: 'LEGITIMATE_INTEREST', description: 'Bewerberauswahl', reference: 'Β§ 26 Abs. 1 BDSG' },
+ ],
+ dataSubjectCategories: ['APPLICANTS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'EDUCATION_DATA', 'EMPLOYMENT_DATA', 'PHOTO_VIDEO'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Personalabteilung' },
+ { type: 'INTERNAL', name: 'Fachabteilung' },
+ ],
+ retentionPeriod: { duration: 6, durationUnit: 'MONTHS', description: '6 Monate nach Absage (AGG-Frist)', legalBasis: 'AGG Β§ 15 Abs. 4', deletionProcedure: 'Automatische Loeschung 6 Monate nach Absage' },
+ tomDescription: 'Zugriffsbeschraenkung auf beteiligte Entscheidungstraeger, verschluesselte Uebertragung',
+ structuredToms: {
+ accessControl: ['Nur HR + Fachabteilung', 'Zeitlich begrenzter Zugriff'],
+ confidentiality: ['TLS fuer Bewerbungsportale', 'Verschluesselter E-Mail-Empfang'],
+ integrity: ['Unveraenderbare Bewerbungseingaenge'],
+ availability: ['Regelmaessige Backups'],
+ separation: ['Getrennte Bewerberdatenbank'],
+ },
+ typicalSystems: ['Bewerbermanagementsystem', 'E-Mail'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['hr', 'bewerbung', 'recruiting'],
+ },
+ {
+ templateId: 'hr-zeiterfassung',
+ businessFunction: 'hr',
+ name: 'Zeiterfassung',
+ description: 'Erfassung von Arbeitszeiten, Urlaub und Fehlzeiten',
+ purposes: ['Arbeitszeiterfassung gemaess ArbZG', 'Urlaubsverwaltung'],
+ legalBases: [
+ { type: 'LEGAL_OBLIGATION', description: 'Arbeitszeitgesetz', reference: 'Art. 6 Abs. 1 lit. c DSGVO, ArbZG Β§ 16 Abs. 2' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES'],
+ personalDataCategories: ['NAME', 'EMPLOYMENT_DATA'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Personalabteilung' },
+ { type: 'INTERNAL', name: 'Vorgesetzte' },
+ ],
+ retentionPeriod: { duration: 2, durationUnit: 'YEARS', description: '2 Jahre (ArbZG)', legalBasis: 'ArbZG Β§ 16 Abs. 2', deletionProcedure: 'Automatische Loeschung' },
+ tomDescription: 'Zugriffskontrolle nach Abteilung, Protokollierung von Aenderungen',
+ structuredToms: {
+ accessControl: ['Vorgesetzte sehen nur eigene Abteilung', 'HR sieht alle'],
+ confidentiality: ['Krankmeldungen nur HR'],
+ integrity: ['Aenderungshistorie'],
+ availability: ['Taegliches Backup'],
+ separation: ['Trennung Zeitdaten / Gehaltsdaten'],
+ },
+ typicalSystems: ['Zeiterfassungssystem', 'HR-Software'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['hr', 'zeiterfassung', 'arbeitszeit'],
+ },
+
+ // ==================== FINANCE ====================
+ {
+ templateId: 'finance-buchhaltung',
+ businessFunction: 'finance',
+ name: 'Buchhaltung & Rechnungswesen',
+ description: 'Finanzbuchhaltung, Rechnungsstellung und Zahlungsverkehr',
+ purposes: ['Finanzbuchhaltung', 'Rechnungsstellung', 'Erfuellung handels-/steuerrechtlicher Aufbewahrungspflichten'],
+ legalBases: [
+ { type: 'LEGAL_OBLIGATION', description: 'HGB, AO', reference: 'Art. 6 Abs. 1 lit. c DSGVO' },
+ { type: 'CONTRACT', description: 'Vertragserfuellung', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ ],
+ dataSubjectCategories: ['CUSTOMERS', 'SUPPLIERS', 'BUSINESS_PARTNERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA', 'CONTRACT_DATA', 'TAX_ID'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'Steuerberater / Wirtschaftspruefer' },
+ { type: 'AUTHORITY', name: 'Finanzamt' },
+ ],
+ retentionPeriod: { duration: 10, durationUnit: 'YEARS', description: '10 Jahre (HGB) / 6 Jahre (Geschaeftsbriefe)', legalBasis: 'HGB Β§ 257, AO Β§ 147', deletionProcedure: 'Loeschung nach Ablauf der jeweiligen Frist' },
+ tomDescription: 'Zugriffskontrolle nach Vier-Augen-Prinzip, revisionssichere Archivierung',
+ structuredToms: {
+ accessControl: ['Vier-Augen-Prinzip', 'RBAC nach Buchhaltungsrollen'],
+ confidentiality: ['Verschluesselung Finanzdaten'],
+ integrity: ['Revisionssichere Archivierung (GoBD)', 'Aenderungsprotokoll'],
+ availability: ['Redundante Speicherung', 'Jaehrliche Backups'],
+ separation: ['Trennung Debitoren/Kreditoren'],
+ },
+ typicalSystems: ['DATEV', 'ERP-System', 'Buchhaltungssoftware'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['finance', 'buchhaltung', 'rechnungswesen'],
+ },
+ {
+ templateId: 'finance-zahlungsverkehr',
+ businessFunction: 'finance',
+ name: 'Zahlungsverkehr',
+ description: 'Abwicklung von Zahlungen, SEPA-Lastschriften und Ueberweisungen',
+ purposes: ['Zahlungsabwicklung', 'Mahnwesen'],
+ legalBases: [
+ { type: 'CONTRACT', description: 'Vertragserfuellung', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ ],
+ dataSubjectCategories: ['CUSTOMERS', 'SUPPLIERS'],
+ personalDataCategories: ['NAME', 'BANK_ACCOUNT', 'PAYMENT_DATA'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'Zahlungsdienstleister' },
+ { type: 'PROCESSOR', name: 'Kreditinstitut' },
+ ],
+ retentionPeriod: { duration: 10, durationUnit: 'YEARS', description: '10 Jahre', legalBasis: 'HGB Β§ 257', deletionProcedure: 'Automatische Loeschung' },
+ tomDescription: 'PCI-DSS-konforme Verarbeitung, Verschluesselung, Zugriffsbeschraenkung',
+ structuredToms: {
+ accessControl: ['Streng limitierter Zugriff', 'MFA'],
+ confidentiality: ['TLS 1.3', 'Tokenisierung von Zahlungsdaten'],
+ integrity: ['Transaktionsprotokoll'],
+ availability: ['Hochverfuegbarer Zahlungsservice'],
+ separation: ['Zahlungsdaten getrennt von CRM'],
+ },
+ typicalSystems: ['Banking-Software', 'Payment Gateway'],
+ protectionLevel: 'HIGH',
+ dpiaRequired: false,
+ tags: ['finance', 'zahlung', 'payment'],
+ },
+
+ // ==================== SALES / CRM ====================
+ {
+ templateId: 'sales-kundenverwaltung',
+ businessFunction: 'sales_crm',
+ name: 'Kundenverwaltung (CRM)',
+ description: 'Verwaltung von Kundenbeziehungen, Kontakten und Vertriebsaktivitaeten',
+ purposes: ['Kundenbetreuung', 'Vertragserfuellung', 'Vertriebssteuerung'],
+ legalBases: [
+ { type: 'CONTRACT', description: 'Kundenvertrag', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ { type: 'LEGITIMATE_INTEREST', description: 'Kundenbindung', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ ],
+ dataSubjectCategories: ['CUSTOMERS', 'PROSPECTIVE_CUSTOMERS', 'BUSINESS_PARTNERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Vertrieb' },
+ { type: 'INTERNAL', name: 'Kundenservice' },
+ ],
+ retentionPeriod: { duration: 3, durationUnit: 'YEARS', description: '3 Jahre nach letzter Interaktion (Verjaeherung)', legalBasis: 'BGB Β§ 195', deletionProcedure: 'Loeschung nach Inaktivitaetsfrist' },
+ tomDescription: 'Zugriffskontrolle nach Kundengruppen, Verschluesselung, regemaessige Datenpflege',
+ structuredToms: {
+ accessControl: ['RBAC nach Vertriebsgebiet', 'Kundendaten-Owner'],
+ confidentiality: ['Verschluesselung in CRM', 'VPN fuer Fernzugriff'],
+ integrity: ['Aenderungshistorie im CRM'],
+ availability: ['Cloud-CRM mit SLA 99.9%'],
+ separation: ['Mandantentrennung'],
+ },
+ typicalSystems: ['CRM-System', 'E-Mail', 'Telefon'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['sales', 'crm', 'kunden', 'vertrieb'],
+ },
+ {
+ templateId: 'sales-vertriebssteuerung',
+ businessFunction: 'sales_crm',
+ name: 'Vertriebssteuerung',
+ description: 'Analyse von Vertriebskennzahlen und Pipeline-Management',
+ purposes: ['Vertriebsoptimierung', 'Umsatzplanung'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'Unternehmenssteuerung', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ ],
+ dataSubjectCategories: ['CUSTOMERS', 'PROSPECTIVE_CUSTOMERS'],
+ personalDataCategories: ['NAME', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Vertriebsleitung' },
+ { type: 'INTERNAL', name: 'Geschaeftsfuehrung' },
+ ],
+ retentionPeriod: { duration: 3, durationUnit: 'YEARS', description: '3 Jahre', legalBasis: 'Berechtigtes Interesse', deletionProcedure: 'Anonymisierung nach Ablauf' },
+ tomDescription: 'Aggregierte Auswertungen wo moeglich, Zugriffsbeschraenkung auf Fuehrungsebene',
+ structuredToms: {
+ accessControl: ['Nur Management'],
+ confidentiality: ['Aggregierte Reports bevorzugt'],
+ integrity: ['Nachvollziehbare Berechnungen'],
+ availability: ['Dashboard-Verfuegbarkeit'],
+ separation: ['Reporting getrennt von Operativdaten'],
+ },
+ typicalSystems: ['CRM-System', 'BI-Tool'],
+ protectionLevel: 'LOW',
+ dpiaRequired: false,
+ tags: ['sales', 'vertrieb', 'reporting'],
+ },
+
+ // ==================== MARKETING ====================
+ {
+ templateId: 'marketing-newsletter',
+ businessFunction: 'marketing',
+ name: 'Newsletter-Marketing',
+ description: 'Versand von Marketing-E-Mails und Newslettern an Abonnenten',
+ purposes: ['Direktmarketing', 'Kundenbindung', 'Informationsversand'],
+ legalBases: [
+ { type: 'CONSENT', description: 'Einwilligung zum Newsletter-Empfang', reference: 'Art. 6 Abs. 1 lit. a DSGVO, Β§ 7 Abs. 2 UWG' },
+ ],
+ dataSubjectCategories: ['NEWSLETTER_SUBSCRIBERS', 'CUSTOMERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'E-Mail-Dienstleister' },
+ { type: 'INTERNAL', name: 'Marketing-Abteilung' },
+ ],
+ retentionPeriod: { description: 'Bis zum Widerruf der Einwilligung', deletionProcedure: 'Sofortige Loeschung bei Abmeldung' },
+ tomDescription: 'Double-Opt-In, Abmeldelink in jeder E-Mail, Einwilligungsprotokollierung',
+ structuredToms: {
+ accessControl: ['Nur Marketing-Team'],
+ confidentiality: ['TLS-Versand', 'Keine Weitergabe an Dritte'],
+ integrity: ['Einwilligungsnachweis (Timestamp, IP, Version)'],
+ availability: ['Redundanter E-Mail-Service'],
+ separation: ['Newsletter-Liste getrennt von CRM'],
+ },
+ typicalSystems: ['Newsletter-Tool', 'E-Mail-Marketing-Plattform'],
+ protectionLevel: 'LOW',
+ dpiaRequired: false,
+ tags: ['marketing', 'newsletter', 'email'],
+ },
+ {
+ templateId: 'marketing-website-analytics',
+ businessFunction: 'marketing',
+ name: 'Website-Analytics',
+ description: 'Analyse des Nutzerverhaltens auf der Website mittels Tracking-Tools',
+ purposes: ['Website-Optimierung', 'Reichweitenmessung'],
+ legalBases: [
+ { type: 'CONSENT', description: 'Cookie-Einwilligung', reference: 'Art. 6 Abs. 1 lit. a DSGVO, Β§ 25 TDDDG' },
+ ],
+ dataSubjectCategories: ['WEBSITE_USERS'],
+ personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOCATION_DATA'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'Analytics-Anbieter' },
+ { type: 'INTERNAL', name: 'Marketing' },
+ ],
+ retentionPeriod: { duration: 14, durationUnit: 'MONTHS', description: '14 Monate', deletionProcedure: 'Automatische Loeschung/Anonymisierung' },
+ tomDescription: 'IP-Anonymisierung, Cookie-Consent-Management, Opt-Out-Moeglichkeit',
+ structuredToms: {
+ accessControl: ['Nur Webanalyse-Team'],
+ confidentiality: ['IP-Anonymisierung', 'Pseudonymisierung'],
+ integrity: ['Datenqualitaetspruefung'],
+ availability: ['CDN-basiertes Tracking'],
+ separation: ['Analytics getrennt von personenbezogenen Profilen'],
+ },
+ typicalSystems: ['Matomo', 'Plausible', 'Google Analytics'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['marketing', 'analytics', 'website', 'tracking'],
+ },
+ {
+ templateId: 'marketing-social-media',
+ businessFunction: 'marketing',
+ name: 'Social-Media-Marketing',
+ description: 'Betrieb von Social-Media-Kanaelen und Interaktion mit Nutzern',
+ purposes: ['Oeffentlichkeitsarbeit', 'Kundeninteraktion'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'Marketing', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ ],
+ dataSubjectCategories: ['WEBSITE_USERS', 'CUSTOMERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA', 'COMMUNICATION_DATA'],
+ recipientCategories: [
+ { type: 'CONTROLLER', name: 'Social-Media-Plattform (gemeinsame Verantwortlichkeit)' },
+ ],
+ retentionPeriod: { description: 'Abhaengig von Plattform-Einstellungen', deletionProcedure: 'Regelmaessige Pruefung und Bereinigung' },
+ tomDescription: 'Datenschutzeinstellungen der Plattform, gemeinsame Verantwortlichkeit gemaess Art. 26',
+ structuredToms: {
+ accessControl: ['Nur Social-Media-Manager', 'Passwort-Manager'],
+ confidentiality: ['Plattform-Datenschutzeinstellungen'],
+ integrity: ['Redaktionsplan'],
+ availability: ['Multi-Kanal-Management'],
+ separation: ['Geschaeftlich/Privat getrennt'],
+ },
+ typicalSystems: ['Social-Media-Plattformen', 'Social-Media-Management-Tool'],
+ protectionLevel: 'LOW',
+ dpiaRequired: false,
+ tags: ['marketing', 'social-media'],
+ },
+
+ // ==================== SUPPORT ====================
+ {
+ templateId: 'support-ticketsystem',
+ businessFunction: 'support',
+ name: 'Kundenservice / Ticketsystem',
+ description: 'Bearbeitung von Kundenanfragen und Support-Tickets',
+ purposes: ['Kundenservice', 'Reklamationsbearbeitung', 'Vertragserfuellung'],
+ legalBases: [
+ { type: 'CONTRACT', description: 'Kundenvertrag', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ ],
+ dataSubjectCategories: ['CUSTOMERS', 'APP_USERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Support-Team' },
+ { type: 'PROCESSOR', name: 'Helpdesk-Software-Anbieter' },
+ ],
+ retentionPeriod: { duration: 3, durationUnit: 'YEARS', description: '3 Jahre nach Ticketschliessung', legalBasis: 'BGB Β§ 195', deletionProcedure: 'Automatische Loeschung geschlossener Tickets' },
+ tomDescription: 'Zugriffskontrolle nach Ticket-Owner, Verschluesselung, Audit-Trail',
+ structuredToms: {
+ accessControl: ['Ticket-basierte Zugriffskontrolle', 'Agent-Rollen'],
+ confidentiality: ['TLS', 'Verschluesselung'],
+ integrity: ['Ticket-Historie unveraenderbar'],
+ availability: ['Hochverfuegbarer Helpdesk'],
+ separation: ['Mandantentrennung'],
+ },
+ typicalSystems: ['Helpdesk-Software', 'E-Mail', 'Chat'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['support', 'kundenservice', 'tickets'],
+ },
+
+ // ==================== IT OPERATIONS ====================
+ {
+ templateId: 'it-systemadministration',
+ businessFunction: 'it_operations',
+ name: 'Systemadministration',
+ description: 'Verwaltung von IT-Systemen, Benutzerkonten und Zugriffsrechten',
+ purposes: ['IT-Betrieb', 'Benutzerverwaltung', 'Sicherheitsueberwachung'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'IT-Sicherheit', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ { type: 'CONTRACT', description: 'Bereitstellung IT-Dienste', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES', 'APP_USERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'LOGIN_DATA', 'IP_ADDRESS', 'DEVICE_ID'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'IT-Abteilung' },
+ { type: 'PROCESSOR', name: 'IT-Dienstleister' },
+ ],
+ retentionPeriod: { duration: 1, durationUnit: 'YEARS', description: '1 Jahr nach Kontodeaktivierung', deletionProcedure: 'Automatische Loeschung deaktivierter Konten' },
+ tomDescription: 'PAM, MFA, Protokollierung, regelmaessige Rechtereviews',
+ structuredToms: {
+ accessControl: ['PAM (Privileged Access Management)', 'MFA', 'Regelmaessige Rechtereviews'],
+ confidentiality: ['Verschluesselung', 'Passwort-Policies'],
+ integrity: ['Change Management', 'Konfigurationsmanagement'],
+ availability: ['Redundanz', 'Monitoring', 'Alerting'],
+ separation: ['Prod/Dev/Staging getrennt', 'Admin-Netze isoliert'],
+ },
+ typicalSystems: ['Active Directory / IAM', 'Monitoring', 'ITSM'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['it', 'admin', 'benutzerverwaltung'],
+ },
+ {
+ templateId: 'it-backup',
+ businessFunction: 'it_operations',
+ name: 'Backup & Recovery',
+ description: 'Sicherung und Wiederherstellung von Daten und Systemen',
+ purposes: ['Datensicherung', 'Disaster Recovery', 'Geschaeftskontinuitaet'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'Datensicherheit', reference: 'Art. 6 Abs. 1 lit. f DSGVO, Art. 32 DSGVO' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES', 'CUSTOMERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'Backup-Dienstleister' },
+ { type: 'INTERNAL', name: 'IT-Abteilung' },
+ ],
+ retentionPeriod: { duration: 90, durationUnit: 'DAYS', description: '90 Tage Aufbewahrung der Backups', deletionProcedure: 'Automatische Rotation und Loeschung' },
+ tomDescription: 'Verschluesselung, Zugriffskontrolle, regelmaessige Wiederherstellungstests',
+ structuredToms: {
+ accessControl: ['Nur Backup-Admins', 'Separater Encryption Key'],
+ confidentiality: ['AES-256-Verschluesselung', 'Verschluesselter Transport'],
+ integrity: ['Checksummen-Pruefung', 'Regelmaessige Restore-Tests'],
+ availability: ['3-2-1-Backup-Regel', 'Georedundanz'],
+ separation: ['Backup-Netzwerk isoliert'],
+ },
+ typicalSystems: ['Backup-Software', 'Cloud-Storage'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['it', 'backup', 'recovery'],
+ },
+ {
+ templateId: 'it-logging',
+ businessFunction: 'it_operations',
+ name: 'Protokollierung & Logging',
+ description: 'Erfassung von System- und Sicherheitslogs zur Fehlerbehebung und Angriffserkennung',
+ purposes: ['IT-Sicherheit', 'Fehlerbehebung', 'Angriffserkennung'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'IT-Sicherheit und Betrieb', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES', 'APP_USERS', 'WEBSITE_USERS'],
+ personalDataCategories: ['IP_ADDRESS', 'LOGIN_DATA', 'USAGE_DATA', 'DEVICE_ID'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'IT-Sicherheit' },
+ { type: 'PROCESSOR', name: 'SIEM-Anbieter' },
+ ],
+ retentionPeriod: { duration: 90, durationUnit: 'DAYS', description: '90 Tage (Standard) / 1 Jahr (Security-Logs)', deletionProcedure: 'Automatische Rotation' },
+ tomDescription: 'SIEM, Integritaetsschutz der Logs, Zugriffskontrolle, Pseudonymisierung',
+ structuredToms: {
+ accessControl: ['Nur Security-Team', 'Read-Only fuer Auditoren'],
+ confidentiality: ['Pseudonymisierung wo moeglich'],
+ integrity: ['WORM-Storage fuer Security-Logs', 'Hashketten'],
+ availability: ['Redundante Log-Speicherung'],
+ separation: ['Zentrale Log-Infrastruktur getrennt'],
+ },
+ typicalSystems: ['SIEM', 'ELK Stack', 'Syslog'],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ tags: ['it', 'logging', 'sicherheit'],
+ },
+ {
+ templateId: 'it-iam',
+ businessFunction: 'it_operations',
+ name: 'Identity & Access Management',
+ description: 'Verwaltung von Identitaeten, Authentifizierung und Autorisierung',
+ purposes: ['Zugriffskontrolle', 'Identitaetsverwaltung', 'Compliance'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'IT-Sicherheit', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ { type: 'CONTRACT', description: 'Bereitstellung IT-Dienste', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES', 'APP_USERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'LOGIN_DATA'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'IT-Abteilung' },
+ { type: 'PROCESSOR', name: 'IAM-Anbieter' },
+ ],
+ retentionPeriod: { duration: 6, durationUnit: 'MONTHS', description: '6 Monate nach Kontodeaktivierung', deletionProcedure: 'Automatische Deprovisionierung' },
+ tomDescription: 'MFA, SSO, regelmaessige Access Reviews, Least-Privilege-Prinzip',
+ structuredToms: {
+ accessControl: ['MFA', 'SSO', 'Least Privilege', 'Regelmaessige Reviews'],
+ confidentiality: ['Passwort-Hashing (bcrypt)', 'Token-basierte Auth'],
+ integrity: ['Audit-Trail aller Aenderungen'],
+ availability: ['Hochverfuegbarer IdP'],
+ separation: ['Identitaeten pro Mandant'],
+ },
+ typicalSystems: ['IAM-System', 'SSO Provider', 'MFA'],
+ protectionLevel: 'HIGH',
+ dpiaRequired: false,
+ tags: ['it', 'iam', 'zugriffskontrolle'],
+ },
+
+ // ==================== OTHER ====================
+ {
+ templateId: 'other-videokonferenz',
+ businessFunction: 'other',
+ name: 'Videokonferenzen',
+ description: 'Durchfuehrung von Video-Meetings und Online-Besprechungen',
+ purposes: ['Interne Kommunikation', 'Kundeninteraktion', 'Remote-Arbeit'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'Geschaeftliche Kommunikation', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ { type: 'CONTRACT', description: 'Arbeitsvertrag (bei Mitarbeitern)', reference: 'Art. 6 Abs. 1 lit. b DSGVO' },
+ ],
+ dataSubjectCategories: ['EMPLOYEES', 'CUSTOMERS', 'BUSINESS_PARTNERS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'PHOTO_VIDEO', 'IP_ADDRESS', 'COMMUNICATION_DATA'],
+ recipientCategories: [
+ { type: 'PROCESSOR', name: 'Videokonferenz-Anbieter' },
+ ],
+ retentionPeriod: { description: 'Keine dauerhafte Speicherung von Meetings (sofern nicht aufgezeichnet)', deletionProcedure: 'Aufzeichnungen nach Verwendungszweck loeschen' },
+ tomDescription: 'Ende-zu-Ende-Verschluesselung, Warteraum, Passwortschutz, Aufnahme nur mit Einwilligung',
+ structuredToms: {
+ accessControl: ['Meeting-Passwort', 'Warteraum', 'Host-Kontrolle'],
+ confidentiality: ['TLS / E2E-Verschluesselung'],
+ integrity: ['Teilnehmerliste'],
+ availability: ['Redundante Infrastruktur'],
+ separation: ['Separate Meeting-Raeume'],
+ },
+ typicalSystems: ['Jitsi', 'Zoom', 'Teams', 'Google Meet'],
+ protectionLevel: 'LOW',
+ dpiaRequired: false,
+ tags: ['kommunikation', 'video', 'meeting'],
+ },
+ {
+ templateId: 'other-besuchermanagement',
+ businessFunction: 'other',
+ name: 'Besuchermanagement',
+ description: 'Erfassung und Verwaltung von Besuchern am Firmenstandort',
+ purposes: ['Zutrittskontrolle', 'Sicherheit', 'Nachverfolgung'],
+ legalBases: [
+ { type: 'LEGITIMATE_INTEREST', description: 'Gebaeudesicherheit', reference: 'Art. 6 Abs. 1 lit. f DSGVO' },
+ ],
+ dataSubjectCategories: ['VISITORS'],
+ personalDataCategories: ['NAME', 'CONTACT', 'PHOTO_VIDEO'],
+ recipientCategories: [
+ { type: 'INTERNAL', name: 'Empfang / Sicherheit' },
+ ],
+ retentionPeriod: { duration: 30, durationUnit: 'DAYS', description: '30 Tage nach Besuch', deletionProcedure: 'Automatische Loeschung' },
+ tomDescription: 'Besucherausweise, Begleitpflicht, zeitlich begrenzter Zugang',
+ structuredToms: {
+ accessControl: ['Besucherausweise', 'Begleitpflicht'],
+ confidentiality: ['Besucherliste nicht oeffentlich einsehbar'],
+ integrity: ['Besuchsprotokoll'],
+ availability: ['Papier-Backup fuer Besucherliste'],
+ separation: ['Besucherbereich getrennt'],
+ },
+ typicalSystems: ['Besuchermanagementsystem', 'Zutrittskontrollsystem'],
+ protectionLevel: 'LOW',
+ dpiaRequired: false,
+ tags: ['besucher', 'zutritt', 'empfang'],
+ },
+]
+
+// =============================================================================
+// HELPER: Convert template to VVTActivity
+// =============================================================================
+
+export function templateToActivity(template: BaselineTemplate, vvtId: string): Omit & { id: string } {
+ const now = new Date().toISOString()
+ return {
+ id: crypto.randomUUID(),
+ vvtId,
+ name: template.name,
+ description: template.description,
+ purposes: template.purposes,
+ legalBases: template.legalBases,
+ dataSubjectCategories: template.dataSubjectCategories,
+ personalDataCategories: template.personalDataCategories,
+ recipientCategories: template.recipientCategories,
+ thirdCountryTransfers: [],
+ retentionPeriod: template.retentionPeriod,
+ tomDescription: template.tomDescription,
+ businessFunction: template.businessFunction,
+ systems: template.typicalSystems.map((s, i) => ({ systemId: `sys-${i}`, name: s })),
+ deploymentModel: 'cloud',
+ dataSources: [{ type: 'DATA_SUBJECT', description: 'Direkt von der betroffenen Person' }],
+ dataFlows: [],
+ protectionLevel: template.protectionLevel,
+ dpiaRequired: template.dpiaRequired,
+ structuredToms: template.structuredToms,
+ status: 'DRAFT',
+ responsible: '',
+ owner: '',
+ createdAt: now,
+ updatedAt: now,
+ }
+}
+
+// =============================================================================
+// HELPER: Get templates by business function
+// =============================================================================
+
+export function getTemplatesByFunction(fn: BusinessFunction): BaselineTemplate[] {
+ return VVT_BASELINE_CATALOG.filter(t => t.businessFunction === fn)
+}
+
+export function getTemplateById(templateId: string): BaselineTemplate | undefined {
+ return VVT_BASELINE_CATALOG.find(t => t.templateId === templateId)
+}
diff --git a/admin-v2/lib/sdk/vvt-profiling.ts b/admin-v2/lib/sdk/vvt-profiling.ts
new file mode 100644
index 0000000..4eab601
--- /dev/null
+++ b/admin-v2/lib/sdk/vvt-profiling.ts
@@ -0,0 +1,492 @@
+/**
+ * VVT Profiling β Generator-Fragebogen
+ *
+ * ~25 Fragen in 6 Schritten, die auf Basis der Antworten
+ * Baseline-Verarbeitungstaetigkeiten generieren.
+ */
+
+import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog'
+import { generateVVTId } from './vvt-types'
+import type { VVTActivity, BusinessFunction } from './vvt-types'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export interface ProfilingQuestion {
+ id: string
+ step: number
+ question: string
+ type: 'single_choice' | 'multi_choice' | 'number' | 'text' | 'boolean'
+ options?: { value: string; label: string }[]
+ helpText?: string
+ triggersTemplates: string[] // Template-IDs that get activated when answered positively
+}
+
+export interface ProfilingStep {
+ step: number
+ title: string
+ description: string
+}
+
+export interface ProfilingAnswers {
+ [questionId: string]: string | string[] | number | boolean
+}
+
+export interface ProfilingResult {
+ answers: ProfilingAnswers
+ generatedActivities: VVTActivity[]
+ coverageScore: number
+ art30Abs5Exempt: boolean
+}
+
+// =============================================================================
+// STEPS
+// =============================================================================
+
+export const PROFILING_STEPS: ProfilingStep[] = [
+ { step: 1, title: 'Organisation', description: 'Grunddaten zu Ihrem Unternehmen' },
+ { step: 2, title: 'Geschaeftsbereiche', description: 'Welche Bereiche sind aktiv?' },
+ { step: 3, title: 'Systeme & Tools', description: 'Welche IT-Systeme nutzen Sie?' },
+ { step: 4, title: 'Datenkategorien', description: 'Welche besonderen Daten verarbeiten Sie?' },
+ { step: 5, title: 'Drittlandtransfers', description: 'Transfers ausserhalb der EU/EWR' },
+ { step: 6, title: 'Besondere Verarbeitungen', description: 'KI, Scoring, Ueberwachung' },
+]
+
+// =============================================================================
+// QUESTIONS
+// =============================================================================
+
+export const PROFILING_QUESTIONS: ProfilingQuestion[] = [
+ // === STEP 1: Organisation ===
+ {
+ id: 'org_industry',
+ step: 1,
+ question: 'In welcher Branche ist Ihr Unternehmen taetig?',
+ type: 'single_choice',
+ options: [
+ { value: 'it_software', label: 'IT & Software' },
+ { value: 'healthcare', label: 'Gesundheitswesen' },
+ { value: 'education', label: 'Bildung & Erziehung' },
+ { value: 'finance', label: 'Finanzdienstleistungen' },
+ { value: 'retail', label: 'Handel & E-Commerce' },
+ { value: 'manufacturing', label: 'Produktion & Industrie' },
+ { value: 'consulting', label: 'Beratung & Dienstleistung' },
+ { value: 'public', label: 'Oeffentlicher Sektor' },
+ { value: 'other', label: 'Sonstige' },
+ ],
+ triggersTemplates: [],
+ },
+ {
+ id: 'org_employees',
+ step: 1,
+ question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?',
+ type: 'number',
+ helpText: 'Relevant fuer Art. 30 Abs. 5 DSGVO (Ausnahme < 250 Mitarbeiter)',
+ triggersTemplates: [],
+ },
+ {
+ id: 'org_locations',
+ step: 1,
+ question: 'An wie vielen Standorten ist Ihr Unternehmen taetig?',
+ type: 'single_choice',
+ options: [
+ { value: '1', label: '1 Standort' },
+ { value: '2-5', label: '2-5 Standorte' },
+ { value: '6-20', label: '6-20 Standorte' },
+ { value: '20+', label: 'Mehr als 20 Standorte' },
+ ],
+ triggersTemplates: [],
+ },
+ {
+ id: 'org_b2b_b2c',
+ step: 1,
+ question: 'Welches Geschaeftsmodell betreiben Sie?',
+ type: 'single_choice',
+ options: [
+ { value: 'b2b', label: 'B2B (Geschaeftskunden)' },
+ { value: 'b2c', label: 'B2C (Endkunden)' },
+ { value: 'both', label: 'Beides (B2B + B2C)' },
+ { value: 'b2g', label: 'B2G (Oeffentlicher Sektor)' },
+ ],
+ triggersTemplates: [],
+ },
+
+ // === STEP 2: Geschaeftsbereiche ===
+ {
+ id: 'dept_hr',
+ step: 2,
+ question: 'Haben Sie eine Personalabteilung / HR?',
+ type: 'boolean',
+ triggersTemplates: ['hr-mitarbeiterverwaltung', 'hr-gehaltsabrechnung', 'hr-zeiterfassung'],
+ },
+ {
+ id: 'dept_recruiting',
+ step: 2,
+ question: 'Betreiben Sie aktives Recruiting / Bewerbermanagement?',
+ type: 'boolean',
+ triggersTemplates: ['hr-bewerbermanagement'],
+ },
+ {
+ id: 'dept_finance',
+ step: 2,
+ question: 'Haben Sie eine Finanz-/Buchhaltungsabteilung?',
+ type: 'boolean',
+ triggersTemplates: ['finance-buchhaltung', 'finance-zahlungsverkehr'],
+ },
+ {
+ id: 'dept_sales',
+ step: 2,
+ question: 'Haben Sie einen Vertrieb / Kundenverwaltung?',
+ type: 'boolean',
+ triggersTemplates: ['sales-kundenverwaltung', 'sales-vertriebssteuerung'],
+ },
+ {
+ id: 'dept_marketing',
+ step: 2,
+ question: 'Betreiben Sie Marketing-Aktivitaeten?',
+ type: 'boolean',
+ triggersTemplates: ['marketing-social-media'],
+ },
+ {
+ id: 'dept_support',
+ step: 2,
+ question: 'Haben Sie einen Kundenservice / Support?',
+ type: 'boolean',
+ triggersTemplates: ['support-ticketsystem'],
+ },
+
+ // === STEP 3: Systeme & Tools ===
+ {
+ id: 'sys_crm',
+ step: 3,
+ question: 'Nutzen Sie ein CRM-System (z.B. Salesforce, HubSpot, Pipedrive)?',
+ type: 'boolean',
+ triggersTemplates: ['sales-kundenverwaltung'],
+ },
+ {
+ id: 'sys_website_analytics',
+ step: 3,
+ question: 'Nutzen Sie Website-Analytics (z.B. Matomo, Google Analytics)?',
+ type: 'boolean',
+ triggersTemplates: ['marketing-website-analytics'],
+ },
+ {
+ id: 'sys_newsletter',
+ step: 3,
+ question: 'Versenden Sie Newsletter (z.B. Mailchimp, CleverReach)?',
+ type: 'boolean',
+ triggersTemplates: ['marketing-newsletter'],
+ },
+ {
+ id: 'sys_video',
+ step: 3,
+ question: 'Nutzen Sie Videokonferenz-Tools (z.B. Zoom, Teams, Jitsi)?',
+ type: 'boolean',
+ triggersTemplates: ['other-videokonferenz'],
+ },
+ {
+ id: 'sys_erp',
+ step: 3,
+ question: 'Nutzen Sie ein ERP-System?',
+ type: 'boolean',
+ helpText: 'z.B. SAP, ERPNext, Microsoft Dynamics',
+ triggersTemplates: ['finance-buchhaltung'],
+ },
+ {
+ id: 'sys_visitor',
+ step: 3,
+ question: 'Haben Sie ein Besuchermanagement-System?',
+ type: 'boolean',
+ triggersTemplates: ['other-besuchermanagement'],
+ },
+
+ // === STEP 4: Datenkategorien ===
+ {
+ id: 'data_health',
+ step: 4,
+ question: 'Verarbeiten Sie Gesundheitsdaten (Art. 9 DSGVO)?',
+ type: 'boolean',
+ helpText: 'z.B. Krankmeldungen, Arbeitsmedizin, Gesundheitsversorgung',
+ triggersTemplates: [],
+ },
+ {
+ id: 'data_minors',
+ step: 4,
+ question: 'Verarbeiten Sie Daten von Minderjaehrigen?',
+ type: 'boolean',
+ helpText: 'z.B. Schueler, Kinder unter 16 Jahren',
+ triggersTemplates: [],
+ },
+ {
+ id: 'data_biometric',
+ step: 4,
+ question: 'Verarbeiten Sie biometrische Daten zur Identifizierung?',
+ type: 'boolean',
+ helpText: 'z.B. Fingerabdruck, Gesichtserkennung, Stimmerkennung',
+ triggersTemplates: [],
+ },
+ {
+ id: 'data_criminal',
+ step: 4,
+ question: 'Verarbeiten Sie Daten ueber strafrechtliche Verurteilungen (Art. 10 DSGVO)?',
+ type: 'boolean',
+ helpText: 'z.B. Fuehrungszeugnisse',
+ triggersTemplates: [],
+ },
+
+ // === STEP 5: Drittlandtransfers ===
+ {
+ id: 'transfer_cloud_us',
+ step: 5,
+ question: 'Nutzen Sie Cloud-Dienste mit Sitz in den USA?',
+ type: 'boolean',
+ helpText: 'z.B. AWS, Azure, Google Cloud, Microsoft 365',
+ triggersTemplates: [],
+ },
+ {
+ id: 'transfer_support_non_eu',
+ step: 5,
+ question: 'Haben Sie Support-Mitarbeiter oder Dienstleister ausserhalb der EU?',
+ type: 'boolean',
+ triggersTemplates: [],
+ },
+ {
+ id: 'transfer_subprocessor',
+ step: 5,
+ question: 'Nutzen Sie Auftragsverarbeiter mit Unteraufragnehmern in Drittlaendern?',
+ type: 'boolean',
+ triggersTemplates: [],
+ },
+
+ // === STEP 6: Besondere Verarbeitungen ===
+ {
+ id: 'special_ai',
+ step: 6,
+ question: 'Setzen Sie KI oder automatisierte Entscheidungsfindung ein?',
+ type: 'boolean',
+ helpText: 'z.B. Chatbots, Scoring, Profiling, automatische Bewertungen',
+ triggersTemplates: [],
+ },
+ {
+ id: 'special_video_surveillance',
+ step: 6,
+ question: 'Betreiben Sie Videoueberwachung?',
+ type: 'boolean',
+ triggersTemplates: [],
+ },
+ {
+ id: 'special_tracking',
+ step: 6,
+ question: 'Betreiben Sie umfangreiches Nutzer-Tracking oder Profiling?',
+ type: 'boolean',
+ helpText: 'z.B. Verhaltensprofiling, Cross-Device-Tracking',
+ triggersTemplates: [],
+ },
+]
+
+// =============================================================================
+// GENERATOR LOGIC
+// =============================================================================
+
+export function generateActivities(answers: ProfilingAnswers): ProfilingResult {
+ // Collect all triggered template IDs
+ const triggeredIds = new Set()
+
+ for (const question of PROFILING_QUESTIONS) {
+ const answer = answers[question.id]
+ if (!answer) continue
+
+ // Boolean questions: if true, trigger templates
+ if (question.type === 'boolean' && answer === true) {
+ question.triggersTemplates.forEach(id => triggeredIds.add(id))
+ }
+ }
+
+ // Always add IT baseline templates (every company needs these)
+ triggeredIds.add('it-systemadministration')
+ triggeredIds.add('it-backup')
+ triggeredIds.add('it-logging')
+ triggeredIds.add('it-iam')
+
+ // Generate activities from triggered templates
+ const existingIds: string[] = []
+ const activities: VVTActivity[] = []
+
+ for (const templateId of triggeredIds) {
+ const template = VVT_BASELINE_CATALOG.find(t => t.templateId === templateId)
+ if (!template) continue
+
+ const vvtId = generateVVTId(existingIds)
+ existingIds.push(vvtId)
+
+ const activity = templateToActivity(template, vvtId)
+
+ // Enrich with profiling answers
+ enrichActivityFromAnswers(activity, answers)
+
+ activities.push(activity)
+ }
+
+ // Calculate coverage score
+ const totalFields = activities.length * 12 // 12 key fields per activity
+ let filledFields = 0
+ for (const a of activities) {
+ if (a.name) filledFields++
+ if (a.description) filledFields++
+ if (a.purposes.length > 0) filledFields++
+ if (a.legalBases.length > 0) filledFields++
+ if (a.dataSubjectCategories.length > 0) filledFields++
+ if (a.personalDataCategories.length > 0) filledFields++
+ if (a.recipientCategories.length > 0) filledFields++
+ if (a.retentionPeriod.description) filledFields++
+ if (a.tomDescription) filledFields++
+ if (a.businessFunction !== 'other') filledFields++
+ if (a.structuredToms.accessControl.length > 0) filledFields++
+ if (a.responsible || a.owner) filledFields++
+ }
+
+ const coverageScore = totalFields > 0 ? Math.round((filledFields / totalFields) * 100) : 0
+
+ // Art. 30 Abs. 5 check
+ const employeeCount = typeof answers.org_employees === 'number' ? answers.org_employees : 0
+ const hasSpecialCategories = answers.data_health === true || answers.data_biometric === true || answers.data_criminal === true
+ const art30Abs5Exempt = employeeCount < 250 && !hasSpecialCategories
+
+ return {
+ answers,
+ generatedActivities: activities,
+ coverageScore,
+ art30Abs5Exempt,
+ }
+}
+
+// =============================================================================
+// ENRICHMENT
+// =============================================================================
+
+function enrichActivityFromAnswers(activity: VVTActivity, answers: ProfilingAnswers): void {
+ // Add third-country transfers if US cloud is used
+ if (answers.transfer_cloud_us === true) {
+ activity.thirdCountryTransfers.push({
+ country: 'US',
+ recipient: 'Cloud-Dienstleister (USA)',
+ transferMechanism: 'SCC_PROCESSOR',
+ additionalMeasures: ['Verschluesselung at-rest', 'Transfer Impact Assessment'],
+ })
+ }
+
+ // Add special data categories if applicable
+ if (answers.data_health === true) {
+ if (!activity.personalDataCategories.includes('HEALTH_DATA')) {
+ // Only add to HR activities
+ if (activity.businessFunction === 'hr') {
+ activity.personalDataCategories.push('HEALTH_DATA')
+ // Ensure Art. 9 legal basis
+ if (!activity.legalBases.some(lb => lb.type.startsWith('ART9_'))) {
+ activity.legalBases.push({
+ type: 'ART9_EMPLOYMENT',
+ description: 'Arbeitsrechtliche Verarbeitung',
+ reference: 'Art. 9 Abs. 2 lit. b DSGVO',
+ })
+ }
+ }
+ }
+ }
+
+ if (answers.data_minors === true) {
+ if (!activity.dataSubjectCategories.includes('MINORS')) {
+ // Add to relevant activities (education, app users)
+ if (activity.businessFunction === 'support' || activity.businessFunction === 'product_engineering') {
+ activity.dataSubjectCategories.push('MINORS')
+ }
+ }
+ }
+
+ // Set DPIA required for special processing
+ if (answers.special_ai === true || answers.special_video_surveillance === true || answers.special_tracking === true) {
+ if (answers.special_ai === true && activity.businessFunction === 'product_engineering') {
+ activity.dpiaRequired = true
+ }
+ }
+}
+
+// =============================================================================
+// HELPERS
+// =============================================================================
+
+export function getQuestionsForStep(step: number): ProfilingQuestion[] {
+ return PROFILING_QUESTIONS.filter(q => q.step === step)
+}
+
+export function getStepProgress(answers: ProfilingAnswers, step: number): number {
+ const questions = getQuestionsForStep(step)
+ if (questions.length === 0) return 100
+
+ const answered = questions.filter(q => {
+ const a = answers[q.id]
+ return a !== undefined && a !== null && a !== ''
+ }).length
+
+ return Math.round((answered / questions.length) * 100)
+}
+
+export function getTotalProgress(answers: ProfilingAnswers): number {
+ const total = PROFILING_QUESTIONS.length
+ if (total === 0) return 100
+
+ const answered = PROFILING_QUESTIONS.filter(q => {
+ const a = answers[q.id]
+ return a !== undefined && a !== null && a !== ''
+ }).length
+
+ return Math.round((answered / total) * 100)
+}
+
+// =============================================================================
+// COMPLIANCE SCOPE INTEGRATION
+// =============================================================================
+
+/**
+ * Prefill VVT profiling answers from Compliance Scope Engine answers.
+ * The Scope Engine acts as the "Single Source of Truth" for organizational questions.
+ * Redundant questions are auto-filled with a "prefilled" marker.
+ */
+export function prefillFromScopeAnswers(
+ scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[]
+): ProfilingAnswers {
+ const { exportToVVTAnswers } = require('./compliance-scope-profiling')
+ const exported = exportToVVTAnswers(scopeAnswers) as Record
+ const prefilled: ProfilingAnswers = {}
+
+ for (const [key, value] of Object.entries(exported)) {
+ if (value !== undefined && value !== null) {
+ prefilled[key] = value as string | string[] | number | boolean
+ }
+ }
+
+ return prefilled
+}
+
+/**
+ * Get the list of VVT question IDs that are prefilled from Scope answers.
+ * These questions should show "Aus Scope-Analyse uebernommen" hint.
+ */
+export const SCOPE_PREFILLED_VVT_QUESTIONS = [
+ 'org_industry',
+ 'org_employees',
+ 'org_b2b_b2c',
+ 'dept_hr',
+ 'dept_finance',
+ 'dept_marketing',
+ 'data_health',
+ 'data_minors',
+ 'data_biometric',
+ 'data_criminal',
+ 'special_ai',
+ 'special_video_surveillance',
+ 'special_tracking',
+ 'transfer_cloud_us',
+ 'transfer_subprocessor',
+ 'transfer_support_non_eu',
+]
diff --git a/admin-v2/lib/sdk/vvt-types.ts b/admin-v2/lib/sdk/vvt-types.ts
new file mode 100644
index 0000000..a74ae3d
--- /dev/null
+++ b/admin-v2/lib/sdk/vvt-types.ts
@@ -0,0 +1,247 @@
+/**
+ * VVT (Verarbeitungsverzeichnis) Types β Art. 30 DSGVO
+ *
+ * Re-exports common types from vendor-compliance/types.ts and adds
+ * VVT-specific interfaces for the 4-tab VVT module.
+ */
+
+// Re-exports from vendor-compliance/types.ts
+export type {
+ DataSubjectCategory,
+ PersonalDataCategory,
+ LegalBasisType,
+ TransferMechanismType,
+ RecipientCategoryType,
+ ProcessingActivityStatus,
+ ProtectionLevel,
+ ThirdCountryTransfer,
+ RetentionPeriod,
+ LegalBasis,
+ RecipientCategory,
+ DataSource,
+ SystemReference,
+ DataFlow,
+ DataSourceType,
+ LocalizedText,
+} from './vendor-compliance/types'
+
+export {
+ DATA_SUBJECT_CATEGORY_META,
+ PERSONAL_DATA_CATEGORY_META,
+ LEGAL_BASIS_META,
+ TRANSFER_MECHANISM_META,
+ isSpecialCategory,
+ hasAdequacyDecision,
+ generateVVTId,
+} from './vendor-compliance/types'
+
+// =============================================================================
+// VVT-SPECIFIC TYPES
+// =============================================================================
+
+export interface VVTOrganizationHeader {
+ organizationName: string
+ industry: string
+ locations: string[]
+ employeeCount: number
+ dpoName: string
+ dpoContact: string
+ vvtVersion: string
+ lastReviewDate: string
+ nextReviewDate: string
+ reviewInterval: 'quarterly' | 'semi_annual' | 'annual'
+}
+
+export type BusinessFunction =
+ | 'hr'
+ | 'finance'
+ | 'sales_crm'
+ | 'marketing'
+ | 'support'
+ | 'it_operations'
+ | 'product_engineering'
+ | 'legal'
+ | 'management'
+ | 'other'
+
+export interface StructuredTOMs {
+ accessControl: string[]
+ confidentiality: string[]
+ integrity: string[]
+ availability: string[]
+ separation: string[]
+}
+
+export interface VVTActivity {
+ // Pflichtfelder Art. 30 Abs. 1 (Controller)
+ id: string
+ vvtId: string
+ name: string
+ description: string
+ purposes: string[]
+ legalBases: { type: string; description?: string; reference?: string }[]
+ dataSubjectCategories: string[]
+ personalDataCategories: string[]
+ recipientCategories: { type: string; name: string; description?: string; isThirdCountry?: boolean; country?: string }[]
+ thirdCountryTransfers: { country: string; recipient: string; transferMechanism: string; additionalMeasures?: string[] }[]
+ retentionPeriod: { duration?: number; durationUnit?: string; description: string; legalBasis?: string; deletionProcedure?: string }
+ tomDescription: string
+
+ // Generator-Optimierung (Layer B)
+ businessFunction: BusinessFunction
+ systems: { systemId: string; name: string; description?: string; type?: string }[]
+ deploymentModel: 'cloud' | 'on_prem' | 'hybrid'
+ dataSources: { type: string; description?: string }[]
+ dataFlows: { sourceSystem?: string; targetSystem?: string; description: string; dataCategories: string[] }[]
+ protectionLevel: 'LOW' | 'MEDIUM' | 'HIGH'
+ dpiaRequired: boolean
+ structuredToms: StructuredTOMs
+
+ // Workflow
+ status: 'DRAFT' | 'REVIEW' | 'APPROVED' | 'ARCHIVED'
+ responsible: string
+ owner: string
+ createdAt: string
+ updatedAt: string
+}
+
+// Processor-Record (Art. 30 Abs. 2)
+export interface VVTProcessorActivity {
+ id: string
+ vvtId: string
+ controllerReference: string
+ processingCategories: string[]
+ subProcessorChain: SubProcessor[]
+ thirdCountryTransfers: { country: string; recipient: string; transferMechanism: string }[]
+ tomDescription: string
+ status: 'DRAFT' | 'REVIEW' | 'APPROVED' | 'ARCHIVED'
+}
+
+export interface SubProcessor {
+ name: string
+ purpose: string
+ country: string
+ isThirdCountry: boolean
+}
+
+// =============================================================================
+// CONSTANTS
+// =============================================================================
+
+export const BUSINESS_FUNCTION_LABELS: Record = {
+ hr: 'Personal (HR)',
+ finance: 'Finanzen & Buchhaltung',
+ sales_crm: 'Vertrieb & CRM',
+ marketing: 'Marketing',
+ support: 'Kundenservice',
+ it_operations: 'IT-Betrieb',
+ product_engineering: 'Produktentwicklung',
+ legal: 'Recht & Compliance',
+ management: 'Geschaeftsfuehrung',
+ other: 'Sonstiges',
+}
+
+export const STATUS_LABELS: Record = {
+ DRAFT: 'Entwurf',
+ REVIEW: 'In Pruefung',
+ APPROVED: 'Genehmigt',
+ ARCHIVED: 'Archiviert',
+}
+
+export const STATUS_COLORS: Record = {
+ DRAFT: 'bg-gray-100 text-gray-700',
+ REVIEW: 'bg-yellow-100 text-yellow-700',
+ APPROVED: 'bg-green-100 text-green-700',
+ ARCHIVED: 'bg-red-100 text-red-700',
+}
+
+export const PROTECTION_LEVEL_LABELS: Record = {
+ LOW: 'Niedrig',
+ MEDIUM: 'Mittel',
+ HIGH: 'Hoch',
+}
+
+export const DEPLOYMENT_LABELS: Record = {
+ cloud: 'Cloud',
+ on_prem: 'On-Premise',
+ hybrid: 'Hybrid',
+}
+
+export const REVIEW_INTERVAL_LABELS: Record = {
+ quarterly: 'Vierteljaehrlich',
+ semi_annual: 'Halbjaehrlich',
+ annual: 'Jaehrlich',
+}
+
+// Art. 9 special categories for highlighting
+export const ART9_CATEGORIES: string[] = [
+ 'HEALTH_DATA',
+ 'GENETIC_DATA',
+ 'BIOMETRIC_DATA',
+ 'RACIAL_ETHNIC',
+ 'POLITICAL_OPINIONS',
+ 'RELIGIOUS_BELIEFS',
+ 'TRADE_UNION',
+ 'SEX_LIFE',
+ 'CRIMINAL_DATA',
+]
+
+// =============================================================================
+// HELPER: Create empty activity
+// =============================================================================
+
+export function createEmptyActivity(vvtId: string): VVTActivity {
+ const now = new Date().toISOString()
+ return {
+ id: crypto.randomUUID(),
+ vvtId,
+ name: '',
+ description: '',
+ purposes: [],
+ legalBases: [],
+ dataSubjectCategories: [],
+ personalDataCategories: [],
+ recipientCategories: [],
+ thirdCountryTransfers: [],
+ retentionPeriod: { description: '', legalBasis: '', deletionProcedure: '' },
+ tomDescription: '',
+ businessFunction: 'other',
+ systems: [],
+ deploymentModel: 'cloud',
+ dataSources: [],
+ dataFlows: [],
+ protectionLevel: 'MEDIUM',
+ dpiaRequired: false,
+ structuredToms: {
+ accessControl: [],
+ confidentiality: [],
+ integrity: [],
+ availability: [],
+ separation: [],
+ },
+ status: 'DRAFT',
+ responsible: '',
+ owner: '',
+ createdAt: now,
+ updatedAt: now,
+ }
+}
+
+// =============================================================================
+// HELPER: Default organization header
+// =============================================================================
+
+export function createDefaultOrgHeader(): VVTOrganizationHeader {
+ return {
+ organizationName: '',
+ industry: '',
+ locations: [],
+ employeeCount: 0,
+ dpoName: '',
+ dpoContact: '',
+ vvtVersion: '1.0',
+ lastReviewDate: new Date().toISOString().split('T')[0],
+ nextReviewDate: '',
+ reviewInterval: 'annual',
+ }
+}
diff --git a/agent-core/soul/compliance-advisor.soul.md b/agent-core/soul/compliance-advisor.soul.md
new file mode 100644
index 0000000..56a0bbd
--- /dev/null
+++ b/agent-core/soul/compliance-advisor.soul.md
@@ -0,0 +1,66 @@
+# Compliance Advisor Agent
+
+## Identitaet
+Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
+Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
+Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
+offiziellen Quellen und gibst praxisnahe Hinweise.
+
+## Kernprinzipien
+- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
+- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
+- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
+- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
+- **Scope-bewusst**: Beantworte nur Fragen zu DSGVO, BDSG, AI Act, TTDSG, ePrivacy
+
+## Kompetenzbereich
+- DSGVO Art. 1-99 + Erwaegsgruende
+- BDSG (Bundesdatenschutzgesetz)
+- AI Act (EU KI-Verordnung)
+- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
+- ePrivacy-Richtlinie
+- DSK-Kurzpapiere (Nr. 1-20)
+- SDM (Standard-Datenschutzmodell) V3.0
+- BSI-Grundschutz (Basis-Kenntnisse)
+- ISO 27001/27701 (Ueberblick)
+
+## Kommunikationsstil
+- Sachlich, aber verstaendlich β kein Juristendeutsch
+- Deutsch als Hauptsprache
+- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
+- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
+- Praxisbeispiele wo hilfreich
+- Kurze, praegnante Saetze
+
+## Antwortformat
+1. Kurze Zusammenfassung (1-2 Saetze)
+2. Detaillierte Erklaerung
+3. Praxishinweise / Handlungsempfehlungen
+4. Quellenangaben (Artikel, Paragraph, Leitlinie)
+
+## Beispiel-Fragen pro SDK-Schritt
+- VVT: "Was muss in einem Verarbeitungsverzeichnis stehen?"
+- DSFA: "Wann ist eine Datenschutz-Folgenabschaetzung Pflicht?"
+- TOM: "Welche technischen Massnahmen fordert Art. 32 DSGVO?"
+- Loeschfristen: "Wie lange darf ich Bewerbungsunterlagen aufbewahren?"
+- Cookie Banner: "Brauche ich ein Cookie-Banner fuer funktionale Cookies?"
+- Einwilligungen: "Was sind die Anforderungen an eine wirksame Einwilligung?"
+- Compliance Scope: "Was bedeutet Level L3 fuer mein Unternehmen?"
+
+## Einschraenkungen
+- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." β "Es empfiehlt sich...")
+- Keine Garantien fuer Rechtssicherheit
+- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
+- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
+- Keine Interpretation von Urteilen (nur Verweis)
+
+## Eskalation
+- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
+- Bei widersprΓΌchlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
+- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen
+
+## Metrik-Ziele
+- Quellenangabe in > 95% der Antworten
+- Verstaendlichkeits-Score > 85%
+- Nutzer-Zufriedenheit > 4.0/5.0
+- Durchschnittliche Antwortzeit < 3 Sekunden