diff --git a/admin-compliance/lib/sdk/academy/api-courses.ts b/admin-compliance/lib/sdk/academy/api-courses.ts
new file mode 100644
index 0000000..d83653a
--- /dev/null
+++ b/admin-compliance/lib/sdk/academy/api-courses.ts
@@ -0,0 +1,385 @@
+/**
+ * Academy API Client — Course, Enrollment, Certificate, Quiz, Statistics, Generation
+ *
+ * API client for the Compliance E-Learning Academy module
+ * Connects to the ai-compliance-sdk backend via Next.js proxy
+ */
+
+import type {
+ Course,
+ CourseCategory,
+ CourseCreateRequest,
+ CourseUpdateRequest,
+ GenerateCourseRequest,
+ Enrollment,
+ EnrollmentStatus,
+ EnrollmentListResponse,
+ EnrollUserRequest,
+ UpdateProgressRequest,
+ Certificate,
+ AcademyStatistics,
+ LessonType,
+ SubmitQuizRequest,
+ SubmitQuizResponse,
+} from './types'
+
+// =============================================================================
+// CONFIGURATION
+// =============================================================================
+
+const ACADEMY_API_BASE = '/api/sdk/v1/academy'
+const API_TIMEOUT = 30000
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+function getTenantId(): string {
+ if (typeof window !== 'undefined') {
+ return localStorage.getItem('bp_tenant_id') || 'default-tenant'
+ }
+ return 'default-tenant'
+}
+
+function getAuthHeaders(): HeadersInit {
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': getTenantId()
+ }
+
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('authToken')
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`
+ }
+ const userId = localStorage.getItem('bp_user_id')
+ if (userId) {
+ headers['X-User-ID'] = userId
+ }
+ }
+
+ return headers
+}
+
+async function fetchWithTimeout(
+ url: string,
+ options: RequestInit = {},
+ timeout: number = API_TIMEOUT
+): Promise {
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ signal: controller.signal,
+ headers: {
+ ...getAuthHeaders(),
+ ...options.headers
+ }
+ })
+
+ if (!response.ok) {
+ const errorBody = await response.text()
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`
+ try {
+ const errorJson = JSON.parse(errorBody)
+ errorMessage = errorJson.error || errorJson.message || errorMessage
+ } catch {
+ // Keep the HTTP status message
+ }
+ throw new Error(errorMessage)
+ }
+
+ const contentType = response.headers.get('content-type')
+ if (contentType && contentType.includes('application/json')) {
+ return response.json()
+ }
+
+ return {} as T
+ } finally {
+ clearTimeout(timeoutId)
+ }
+}
+
+// =============================================================================
+// BACKEND TYPES
+// =============================================================================
+
+interface BackendCourse {
+ id: string
+ title: string
+ description: string
+ category: CourseCategory
+ duration_minutes: number
+ required_for_roles: string[]
+ is_active: boolean
+ passing_score?: number
+ status?: string
+ lessons?: BackendLesson[]
+ created_at: string
+ updated_at: string
+}
+
+interface BackendQuizQuestion {
+ id: string
+ question: string
+ options: string[]
+ correct_index: number
+ explanation: string
+}
+
+interface BackendLesson {
+ id: string
+ course_id: string
+ title: string
+ description?: string
+ lesson_type: LessonType
+ content_url?: string
+ duration_minutes: number
+ order_index: number
+ quiz_questions?: BackendQuizQuestion[]
+}
+
+function mapCourseFromBackend(bc: BackendCourse): Course {
+ return {
+ id: bc.id,
+ title: bc.title,
+ description: bc.description || '',
+ category: bc.category,
+ durationMinutes: bc.duration_minutes || 0,
+ passingScore: bc.passing_score ?? 70,
+ isActive: bc.is_active ?? true,
+ status: (bc.status as 'draft' | 'published') ?? 'draft',
+ requiredForRoles: bc.required_for_roles || [],
+ lessons: (bc.lessons || []).map(l => ({
+ id: l.id,
+ courseId: l.course_id,
+ title: l.title,
+ type: l.lesson_type,
+ contentMarkdown: l.content_url || '',
+ durationMinutes: l.duration_minutes || 0,
+ order: l.order_index,
+ quizQuestions: (l.quiz_questions || []).map(q => ({
+ id: q.id || `q-${Math.random().toString(36).slice(2)}`,
+ lessonId: l.id,
+ question: q.question,
+ options: q.options,
+ correctOptionIndex: q.correct_index,
+ explanation: q.explanation,
+ })),
+ })),
+ createdAt: bc.created_at,
+ updatedAt: bc.updated_at,
+ }
+}
+
+function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
+ return courses.map(mapCourseFromBackend)
+}
+
+// =============================================================================
+// COURSE CRUD
+// =============================================================================
+
+export async function fetchCourses(): Promise {
+ const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
+ `${ACADEMY_API_BASE}/courses`
+ )
+ return mapCoursesFromBackend(res.courses || [])
+}
+
+export async function fetchCourse(id: string): Promise {
+ const res = await fetchWithTimeout<{ course: BackendCourse }>(
+ `${ACADEMY_API_BASE}/courses/${id}`
+ )
+ return mapCourseFromBackend(res.course)
+}
+
+export async function createCourse(request: CourseCreateRequest): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/courses`,
+ { method: 'POST', body: JSON.stringify(request) }
+ )
+}
+
+export async function updateCourse(id: string, update: CourseUpdateRequest): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/courses/${id}`,
+ { method: 'PUT', body: JSON.stringify(update) }
+ )
+}
+
+export async function deleteCourse(id: string): Promise {
+ await fetchWithTimeout(
+ `${ACADEMY_API_BASE}/courses/${id}`,
+ { method: 'DELETE' }
+ )
+}
+
+// =============================================================================
+// ENROLLMENTS
+// =============================================================================
+
+export async function fetchEnrollments(courseId?: string): Promise {
+ const params = new URLSearchParams()
+ if (courseId) {
+ params.set('course_id', courseId)
+ }
+ const queryString = params.toString()
+ const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}`
+
+ const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url)
+ return res.enrollments || []
+}
+
+export async function enrollUser(request: EnrollUserRequest): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments`,
+ { method: 'POST', body: JSON.stringify(request) }
+ )
+}
+
+export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
+ { method: 'PUT', body: JSON.stringify(update) }
+ )
+}
+
+export async function completeEnrollment(enrollmentId: string): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
+ { method: 'POST' }
+ )
+}
+
+export async function deleteEnrollment(id: string): Promise {
+ await fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${id}`,
+ { method: 'DELETE' }
+ )
+}
+
+export async function updateEnrollment(id: string, data: { deadline?: string }): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${id}`,
+ { method: 'PUT', body: JSON.stringify(data) }
+ )
+}
+
+// =============================================================================
+// CERTIFICATES
+// =============================================================================
+
+export async function fetchCertificate(id: string): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/certificates/${id}`
+ )
+}
+
+export async function generateCertificate(enrollmentId: string): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
+ { method: 'POST' }
+ )
+}
+
+export async function fetchCertificates(): Promise {
+ const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>(
+ `${ACADEMY_API_BASE}/certificates`
+ )
+ return res.certificates || []
+}
+
+// =============================================================================
+// QUIZ
+// =============================================================================
+
+export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`,
+ { method: 'POST', body: JSON.stringify(answers) }
+ )
+}
+
+export async function updateLesson(lessonId: string, update: {
+ title?: string
+ description?: string
+ content_url?: string
+ duration_minutes?: number
+ quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }>
+}): Promise<{ lesson: any }> {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/lessons/${lessonId}`,
+ { method: 'PUT', body: JSON.stringify(update) }
+ )
+}
+
+// =============================================================================
+// STATISTICS
+// =============================================================================
+
+export async function fetchAcademyStatistics(): Promise {
+ const res = await fetchWithTimeout<{
+ total_courses: number
+ total_enrollments: number
+ completion_rate: number
+ overdue_count: number
+ avg_completion_days: number
+ by_category?: Record
+ by_status?: Record
+ }>(`${ACADEMY_API_BASE}/stats`)
+
+ return {
+ totalCourses: res.total_courses || 0,
+ totalEnrollments: res.total_enrollments || 0,
+ completionRate: res.completion_rate || 0,
+ overdueCount: res.overdue_count || 0,
+ byCategory: (res.by_category || {}) as Record,
+ byStatus: (res.by_status || {}) as Record,
+ }
+}
+
+// =============================================================================
+// COURSE GENERATION
+// =============================================================================
+
+export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> {
+ return fetchWithTimeout<{ course: Course }>(
+ `${ACADEMY_API_BASE}/courses/generate`,
+ {
+ method: 'POST',
+ body: JSON.stringify({ module_id: request.moduleId || request.title })
+ },
+ 120000
+ )
+}
+
+export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/courses/generate-all`,
+ { method: 'POST' },
+ 300000
+ )
+}
+
+export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> {
+ return fetchWithTimeout<{ status: string; jobId?: string }>(
+ `${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`,
+ { method: 'POST' },
+ 300000
+ )
+}
+
+export async function getVideoStatus(courseId: string): Promise<{
+ status: string
+ total: number
+ completed: number
+ failed: number
+ videos: Array<{ lessonId: string; status: string; url?: string }>
+}> {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/courses/${courseId}/video-status`
+ )
+}
diff --git a/admin-compliance/lib/sdk/academy/api-mock-data.ts b/admin-compliance/lib/sdk/academy/api-mock-data.ts
new file mode 100644
index 0000000..d1b4108
--- /dev/null
+++ b/admin-compliance/lib/sdk/academy/api-mock-data.ts
@@ -0,0 +1,157 @@
+/**
+ * Academy API — Mock Data & SDK Proxy
+ *
+ * Fallback mock data for development and SDK proxy function
+ */
+
+import type {
+ Course,
+ CourseCategory,
+ Enrollment,
+ EnrollmentStatus,
+ AcademyStatistics,
+} from './types'
+import { isEnrollmentOverdue } from './types'
+import {
+ fetchCourses,
+ fetchEnrollments,
+ fetchAcademyStatistics,
+} from './api-courses'
+
+// =============================================================================
+// SDK PROXY FUNCTION
+// =============================================================================
+
+export async function fetchSDKAcademyList(): Promise<{
+ courses: Course[]
+ enrollments: Enrollment[]
+ statistics: AcademyStatistics
+}> {
+ try {
+ const [courses, enrollments, statistics] = await Promise.all([
+ fetchCourses(),
+ fetchEnrollments(),
+ fetchAcademyStatistics()
+ ])
+
+ return { courses, enrollments, statistics }
+ } catch (error) {
+ console.error('Failed to load Academy data from backend, using mock data:', error)
+
+ const courses = createMockCourses()
+ const enrollments = createMockEnrollments()
+ const statistics = createMockStatistics(courses, enrollments)
+
+ return { courses, enrollments, statistics }
+ }
+}
+
+// =============================================================================
+// MOCK DATA
+// =============================================================================
+
+export function createMockCourses(): Course[] {
+ const now = new Date()
+
+ return [
+ {
+ id: 'course-001',
+ title: 'DSGVO-Grundlagen fuer Mitarbeiter',
+ description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.',
+ category: 'dsgvo_basics',
+ durationMinutes: 90,
+ passingScore: 80,
+ isActive: true,
+ status: 'published',
+ requiredForRoles: ['all'],
+ createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
+ updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
+ lessons: [
+ { id: 'lesson-001-01', courseId: 'course-001', order: 1, title: 'Was ist die DSGVO?', type: 'text', contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...', durationMinutes: 15 },
+ { id: 'lesson-001-02', courseId: 'course-001', order: 2, title: 'Die 7 Grundsaetze der DSGVO', type: 'video', contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.', durationMinutes: 20, videoUrl: '/videos/dsgvo-grundsaetze.mp4' },
+ { id: 'lesson-001-03', courseId: 'course-001', order: 3, title: 'Betroffenenrechte (Art. 15-21)', type: 'text', contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.', durationMinutes: 20 },
+ { id: 'lesson-001-04', courseId: 'course-001', order: 4, title: 'Personenbezogene Daten im Arbeitsalltag', type: 'video', contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.', durationMinutes: 15, videoUrl: '/videos/dsgvo-praxis.mp4' },
+ { id: 'lesson-001-05', courseId: 'course-001', order: 5, title: 'Wissenstest: DSGVO-Grundlagen', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.', durationMinutes: 20 },
+ ]
+ },
+ {
+ id: 'course-002',
+ title: 'IT-Sicherheit & Cybersecurity Awareness',
+ description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
+ category: 'it_security',
+ durationMinutes: 60,
+ passingScore: 75,
+ isActive: true,
+ status: 'published',
+ requiredForRoles: ['all'],
+ createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
+ updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
+ lessons: [
+ { id: 'lesson-002-01', courseId: 'course-002', order: 1, title: 'Phishing erkennen und vermeiden', type: 'video', contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?', durationMinutes: 15, videoUrl: '/videos/phishing-awareness.mp4' },
+ { id: 'lesson-002-02', courseId: 'course-002', order: 2, title: 'Sichere Passwoerter und MFA', type: 'text', contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.', durationMinutes: 15 },
+ { id: 'lesson-002-03', courseId: 'course-002', order: 3, title: 'Social Engineering und Manipulation', type: 'text', contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.', durationMinutes: 15 },
+ { id: 'lesson-002-04', courseId: 'course-002', order: 4, title: 'Wissenstest: IT-Sicherheit', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.', durationMinutes: 15 },
+ ]
+ },
+ {
+ id: 'course-003',
+ title: 'AI Literacy - Sicherer Umgang mit KI',
+ description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.',
+ category: 'ai_literacy',
+ durationMinutes: 75,
+ passingScore: 70,
+ isActive: true,
+ status: 'draft',
+ requiredForRoles: ['admin', 'data_protection_officer'],
+ createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
+ updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
+ lessons: [
+ { id: 'lesson-003-01', courseId: 'course-003', order: 1, title: 'Was ist Kuenstliche Intelligenz?', type: 'text', contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.', durationMinutes: 15 },
+ { id: 'lesson-003-02', courseId: 'course-003', order: 2, title: 'Der EU AI Act - Was bedeutet er fuer uns?', type: 'video', contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.', durationMinutes: 20, videoUrl: '/videos/eu-ai-act.mp4' },
+ { id: 'lesson-003-03', courseId: 'course-003', order: 3, title: 'KI-Werkzeuge sicher nutzen', type: 'text', contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.', durationMinutes: 20 },
+ { id: 'lesson-003-04', courseId: 'course-003', order: 4, title: 'Wissenstest: AI Literacy', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.', durationMinutes: 20 },
+ ]
+ }
+ ]
+}
+
+export function createMockEnrollments(): Enrollment[] {
+ const now = new Date()
+
+ return [
+ { id: 'enr-001', courseId: 'course-001', userId: 'user-001', userName: 'Maria Fischer', userEmail: 'maria.fischer@example.de', status: 'in_progress', progress: 40, startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'enr-002', courseId: 'course-002', userId: 'user-002', userName: 'Stefan Mueller', userEmail: 'stefan.mueller@example.de', status: 'completed', progress: 100, startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(), certificateId: 'cert-001', deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'enr-003', courseId: 'course-001', userId: 'user-003', userName: 'Laura Schneider', userEmail: 'laura.schneider@example.de', status: 'not_started', progress: 0, startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'enr-004', courseId: 'course-003', userId: 'user-004', userName: 'Thomas Wagner', userEmail: 'thomas.wagner@example.de', status: 'expired', progress: 25, startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'enr-005', courseId: 'course-002', userId: 'user-005', userName: 'Julia Becker', userEmail: 'julia.becker@example.de', status: 'in_progress', progress: 50, startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() },
+ ]
+}
+
+export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics {
+ const c = courses || createMockCourses()
+ const e = enrollments || createMockEnrollments()
+
+ const completedCount = e.filter(en => en.status === 'completed').length
+ const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0
+ const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length
+
+ return {
+ totalCourses: c.length,
+ totalEnrollments: e.length,
+ completionRate,
+ overdueCount,
+ byCategory: {
+ dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length,
+ it_security: c.filter(co => co.category === 'it_security').length,
+ ai_literacy: c.filter(co => co.category === 'ai_literacy').length,
+ whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length,
+ custom: c.filter(co => co.category === 'custom').length,
+ },
+ byStatus: {
+ not_started: e.filter(en => en.status === 'not_started').length,
+ in_progress: e.filter(en => en.status === 'in_progress').length,
+ completed: e.filter(en => en.status === 'completed').length,
+ expired: e.filter(en => en.status === 'expired').length,
+ }
+ }
+}
diff --git a/admin-compliance/lib/sdk/academy/api.ts b/admin-compliance/lib/sdk/academy/api.ts
index c272939..4f5d7b6 100644
--- a/admin-compliance/lib/sdk/academy/api.ts
+++ b/admin-compliance/lib/sdk/academy/api.ts
@@ -1,787 +1,38 @@
/**
- * Academy API Client
+ * Academy API Client — barrel re-export
*
- * API client for the Compliance E-Learning Academy module
- * Connects to the ai-compliance-sdk backend via Next.js proxy
+ * Split into:
+ * - api-courses.ts (CRUD, enrollments, certificates, quiz, stats, generation)
+ * - api-mock-data.ts (mock data + SDK proxy)
*/
-import type {
- Course,
- CourseCategory,
- CourseCreateRequest,
- CourseUpdateRequest,
- GenerateCourseRequest,
- Enrollment,
- EnrollmentStatus,
- EnrollmentListResponse,
- EnrollUserRequest,
- UpdateProgressRequest,
- Certificate,
- AcademyStatistics,
- LessonType,
- SubmitQuizRequest,
- SubmitQuizResponse,
-} from './types'
-import { isEnrollmentOverdue } from './types'
+export {
+ fetchCourses,
+ fetchCourse,
+ createCourse,
+ updateCourse,
+ deleteCourse,
+ fetchEnrollments,
+ enrollUser,
+ updateProgress,
+ completeEnrollment,
+ deleteEnrollment,
+ updateEnrollment,
+ fetchCertificate,
+ generateCertificate,
+ fetchCertificates,
+ submitQuiz,
+ updateLesson,
+ fetchAcademyStatistics,
+ generateCourse,
+ generateAllCourses,
+ generateVideos,
+ getVideoStatus,
+} from './api-courses'
-// =============================================================================
-// CONFIGURATION
-// =============================================================================
-
-const ACADEMY_API_BASE = '/api/sdk/v1/academy'
-const API_TIMEOUT = 30000 // 30 seconds
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-function getTenantId(): string {
- if (typeof window !== 'undefined') {
- return localStorage.getItem('bp_tenant_id') || 'default-tenant'
- }
- return 'default-tenant'
-}
-
-function getAuthHeaders(): HeadersInit {
- const headers: HeadersInit = {
- 'Content-Type': 'application/json',
- 'X-Tenant-ID': getTenantId()
- }
-
- if (typeof window !== 'undefined') {
- const token = localStorage.getItem('authToken')
- if (token) {
- headers['Authorization'] = `Bearer ${token}`
- }
- const userId = localStorage.getItem('bp_user_id')
- if (userId) {
- headers['X-User-ID'] = userId
- }
- }
-
- return headers
-}
-
-async function fetchWithTimeout(
- url: string,
- options: RequestInit = {},
- timeout: number = API_TIMEOUT
-): Promise {
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), timeout)
-
- try {
- const response = await fetch(url, {
- ...options,
- signal: controller.signal,
- headers: {
- ...getAuthHeaders(),
- ...options.headers
- }
- })
-
- if (!response.ok) {
- const errorBody = await response.text()
- let errorMessage = `HTTP ${response.status}: ${response.statusText}`
- try {
- const errorJson = JSON.parse(errorBody)
- errorMessage = errorJson.error || errorJson.message || errorMessage
- } catch {
- // Keep the HTTP status message
- }
- throw new Error(errorMessage)
- }
-
- // Handle empty responses
- const contentType = response.headers.get('content-type')
- if (contentType && contentType.includes('application/json')) {
- return response.json()
- }
-
- return {} as T
- } finally {
- clearTimeout(timeoutId)
- }
-}
-
-// =============================================================================
-// COURSE CRUD
-// =============================================================================
-
-/**
- * Alle Kurse abrufen
- */
-export async function fetchCourses(): Promise {
- const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
- `${ACADEMY_API_BASE}/courses`
- )
- return mapCoursesFromBackend(res.courses || [])
-}
-
-/**
- * Einzelnen Kurs abrufen
- */
-export async function fetchCourse(id: string): Promise {
- const res = await fetchWithTimeout<{ course: BackendCourse }>(
- `${ACADEMY_API_BASE}/courses/${id}`
- )
- return mapCourseFromBackend(res.course)
-}
-
-// Backend returns snake_case, frontend uses camelCase
-interface BackendCourse {
- id: string
- title: string
- description: string
- category: CourseCategory
- duration_minutes: number
- required_for_roles: string[]
- is_active: boolean
- passing_score?: number
- status?: string
- lessons?: BackendLesson[]
- created_at: string
- updated_at: string
-}
-
-interface BackendQuizQuestion {
- id: string
- question: string
- options: string[]
- correct_index: number
- explanation: string
-}
-
-interface BackendLesson {
- id: string
- course_id: string
- title: string
- description?: string
- lesson_type: LessonType
- content_url?: string
- duration_minutes: number
- order_index: number
- quiz_questions?: BackendQuizQuestion[]
-}
-
-function mapCourseFromBackend(bc: BackendCourse): Course {
- return {
- id: bc.id,
- title: bc.title,
- description: bc.description || '',
- category: bc.category,
- durationMinutes: bc.duration_minutes || 0,
- passingScore: bc.passing_score ?? 70,
- isActive: bc.is_active ?? true,
- status: (bc.status as 'draft' | 'published') ?? 'draft',
- requiredForRoles: bc.required_for_roles || [],
- lessons: (bc.lessons || []).map(l => ({
- id: l.id,
- courseId: l.course_id,
- title: l.title,
- type: l.lesson_type,
- contentMarkdown: l.content_url || '',
- durationMinutes: l.duration_minutes || 0,
- order: l.order_index,
- quizQuestions: (l.quiz_questions || []).map(q => ({
- id: q.id || `q-${Math.random().toString(36).slice(2)}`,
- lessonId: l.id,
- question: q.question,
- options: q.options,
- correctOptionIndex: q.correct_index,
- explanation: q.explanation,
- })),
- })),
- createdAt: bc.created_at,
- updatedAt: bc.updated_at,
- }
-}
-
-function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
- return courses.map(mapCourseFromBackend)
-}
-
-/**
- * Neuen Kurs erstellen
- */
-export async function createCourse(request: CourseCreateRequest): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/courses`,
- {
- method: 'POST',
- body: JSON.stringify(request)
- }
- )
-}
-
-/**
- * Kurs aktualisieren
- */
-export async function updateCourse(id: string, update: CourseUpdateRequest): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/courses/${id}`,
- {
- method: 'PUT',
- body: JSON.stringify(update)
- }
- )
-}
-
-/**
- * Kurs loeschen
- */
-export async function deleteCourse(id: string): Promise {
- await fetchWithTimeout(
- `${ACADEMY_API_BASE}/courses/${id}`,
- {
- method: 'DELETE'
- }
- )
-}
-
-// =============================================================================
-// ENROLLMENTS
-// =============================================================================
-
-/**
- * Einschreibungen abrufen (optional gefiltert nach Kurs-ID)
- */
-export async function fetchEnrollments(courseId?: string): Promise {
- const params = new URLSearchParams()
- if (courseId) {
- params.set('course_id', courseId)
- }
- const queryString = params.toString()
- const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}`
-
- const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url)
- return res.enrollments || []
-}
-
-/**
- * Benutzer in einen Kurs einschreiben
- */
-export async function enrollUser(request: EnrollUserRequest): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/enrollments`,
- {
- method: 'POST',
- body: JSON.stringify(request)
- }
- )
-}
-
-/**
- * Fortschritt einer Einschreibung aktualisieren
- */
-export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
- {
- method: 'PUT',
- body: JSON.stringify(update)
- }
- )
-}
-
-/**
- * Einschreibung als abgeschlossen markieren
- */
-export async function completeEnrollment(enrollmentId: string): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
- {
- method: 'POST'
- }
- )
-}
-
-// =============================================================================
-// CERTIFICATES
-// =============================================================================
-
-/**
- * Zertifikat abrufen
- */
-export async function fetchCertificate(id: string): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/certificates/${id}`
- )
-}
-
-/**
- * Zertifikat generieren nach erfolgreichem Kursabschluss
- */
-export async function generateCertificate(enrollmentId: string): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
- {
- method: 'POST'
- }
- )
-}
-
-/**
- * Alle Zertifikate abrufen
- */
-export async function fetchCertificates(): Promise {
- const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>(
- `${ACADEMY_API_BASE}/certificates`
- )
- return res.certificates || []
-}
-
-/**
- * Einschreibung loeschen
- */
-export async function deleteEnrollment(id: string): Promise {
- await fetchWithTimeout(
- `${ACADEMY_API_BASE}/enrollments/${id}`,
- { method: 'DELETE' }
- )
-}
-
-/**
- * Einschreibung aktualisieren (z.B. Deadline)
- */
-export async function updateEnrollment(id: string, data: { deadline?: string }): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/enrollments/${id}`,
- {
- method: 'PUT',
- body: JSON.stringify(data),
- }
- )
-}
-
-// =============================================================================
-// QUIZ
-// =============================================================================
-
-/**
- * Quiz-Antworten einreichen und auswerten (ohne Enrollment)
- */
-export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`,
- {
- method: 'POST',
- body: JSON.stringify(answers)
- }
- )
-}
-
-/**
- * Lektion aktualisieren (Content, Titel, Quiz-Fragen)
- */
-export async function updateLesson(lessonId: string, update: {
- title?: string
- description?: string
- content_url?: string
- duration_minutes?: number
- quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }>
-}): Promise<{ lesson: any }> {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/lessons/${lessonId}`,
- {
- method: 'PUT',
- body: JSON.stringify(update)
- }
- )
-}
-
-// =============================================================================
-// STATISTICS
-// =============================================================================
-
-/**
- * Academy-Statistiken abrufen
- */
-export async function fetchAcademyStatistics(): Promise {
- const res = await fetchWithTimeout<{
- total_courses: number
- total_enrollments: number
- completion_rate: number
- overdue_count: number
- avg_completion_days: number
- by_category?: Record
- by_status?: Record
- }>(`${ACADEMY_API_BASE}/stats`)
-
- return {
- totalCourses: res.total_courses || 0,
- totalEnrollments: res.total_enrollments || 0,
- completionRate: res.completion_rate || 0,
- overdueCount: res.overdue_count || 0,
- byCategory: (res.by_category || {}) as Record,
- byStatus: (res.by_status || {}) as Record,
- }
-}
-
-// =============================================================================
-// COURSE GENERATION (via Training Engine)
-// =============================================================================
-
-/**
- * Academy-Kurs aus einem Training-Modul generieren
- */
-export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> {
- return fetchWithTimeout<{ course: Course }>(
- `${ACADEMY_API_BASE}/courses/generate`,
- {
- method: 'POST',
- body: JSON.stringify({ module_id: request.moduleId || request.title })
- },
- 120000 // 2 min timeout
- )
-}
-
-/**
- * Alle Academy-Kurse aus Training-Modulen generieren
- */
-export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/courses/generate-all`,
- { method: 'POST' },
- 300000 // 5 min timeout
- )
-}
-
-/**
- * Videos fuer alle Lektionen eines Kurses generieren
- */
-export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> {
- return fetchWithTimeout<{ status: string; jobId?: string }>(
- `${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`,
- {
- method: 'POST'
- },
- 300000 // 5 min timeout for video generation
- )
-}
-
-/**
- * Video-Generierungsstatus abrufen
- */
-export async function getVideoStatus(courseId: string): Promise<{
- status: string
- total: number
- completed: number
- failed: number
- videos: Array<{ lessonId: string; status: string; url?: string }>
-}> {
- return fetchWithTimeout(
- `${ACADEMY_API_BASE}/courses/${courseId}/video-status`
- )
-}
-
-// =============================================================================
-// SDK PROXY FUNCTION (wraps fetchCourses + fetchAcademyStatistics)
-// =============================================================================
-
-/**
- * Kurse und Statistiken laden - mit Fallback auf Mock-Daten
- */
-export async function fetchSDKAcademyList(): Promise<{
- courses: Course[]
- enrollments: Enrollment[]
- statistics: AcademyStatistics
-}> {
- try {
- const [courses, enrollments, statistics] = await Promise.all([
- fetchCourses(),
- fetchEnrollments(),
- fetchAcademyStatistics()
- ])
-
- return { courses, enrollments, statistics }
- } catch (error) {
- console.error('Failed to load Academy data from backend, using mock data:', error)
-
- // Fallback to mock data
- const courses = createMockCourses()
- const enrollments = createMockEnrollments()
- const statistics = createMockStatistics(courses, enrollments)
-
- return { courses, enrollments, statistics }
- }
-}
-
-// =============================================================================
-// MOCK DATA (Fallback / Demo)
-// =============================================================================
-
-/**
- * Demo-Kurse mit deutschen Titeln erstellen
- */
-export function createMockCourses(): Course[] {
- const now = new Date()
-
- return [
- {
- id: 'course-001',
- title: 'DSGVO-Grundlagen fuer Mitarbeiter',
- description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.',
- category: 'dsgvo_basics',
- durationMinutes: 90,
- passingScore: 80,
- isActive: true,
- status: 'published',
- requiredForRoles: ['all'],
- createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
- updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
- lessons: [
- {
- id: 'lesson-001-01',
- courseId: 'course-001',
- order: 1,
- title: 'Was ist die DSGVO?',
- type: 'text',
- contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...',
- durationMinutes: 15
- },
- {
- id: 'lesson-001-02',
- courseId: 'course-001',
- order: 2,
- title: 'Die 7 Grundsaetze der DSGVO',
- type: 'video',
- contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.',
- durationMinutes: 20,
- videoUrl: '/videos/dsgvo-grundsaetze.mp4'
- },
- {
- id: 'lesson-001-03',
- courseId: 'course-001',
- order: 3,
- title: 'Betroffenenrechte (Art. 15-21)',
- type: 'text',
- contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.',
- durationMinutes: 20
- },
- {
- id: 'lesson-001-04',
- courseId: 'course-001',
- order: 4,
- title: 'Personenbezogene Daten im Arbeitsalltag',
- type: 'video',
- contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.',
- durationMinutes: 15,
- videoUrl: '/videos/dsgvo-praxis.mp4'
- },
- {
- id: 'lesson-001-05',
- courseId: 'course-001',
- order: 5,
- title: 'Wissenstest: DSGVO-Grundlagen',
- type: 'quiz',
- contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.',
- durationMinutes: 20
- }
- ]
- },
- {
- id: 'course-002',
- title: 'IT-Sicherheit & Cybersecurity Awareness',
- description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
- category: 'it_security',
- durationMinutes: 60,
- passingScore: 75,
- isActive: true,
- status: 'published',
- requiredForRoles: ['all'],
- createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
- updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
- lessons: [
- {
- id: 'lesson-002-01',
- courseId: 'course-002',
- order: 1,
- title: 'Phishing erkennen und vermeiden',
- type: 'video',
- contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?',
- durationMinutes: 15,
- videoUrl: '/videos/phishing-awareness.mp4'
- },
- {
- id: 'lesson-002-02',
- courseId: 'course-002',
- order: 2,
- title: 'Sichere Passwoerter und MFA',
- type: 'text',
- contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.',
- durationMinutes: 15
- },
- {
- id: 'lesson-002-03',
- courseId: 'course-002',
- order: 3,
- title: 'Social Engineering und Manipulation',
- type: 'text',
- contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.',
- durationMinutes: 15
- },
- {
- id: 'lesson-002-04',
- courseId: 'course-002',
- order: 4,
- title: 'Wissenstest: IT-Sicherheit',
- type: 'quiz',
- contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.',
- durationMinutes: 15
- }
- ]
- },
- {
- id: 'course-003',
- title: 'AI Literacy - Sicherer Umgang mit KI',
- description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.',
- category: 'ai_literacy',
- durationMinutes: 75,
- passingScore: 70,
- isActive: true,
- status: 'draft',
- requiredForRoles: ['admin', 'data_protection_officer'],
- createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
- updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
- lessons: [
- {
- id: 'lesson-003-01',
- courseId: 'course-003',
- order: 1,
- title: 'Was ist Kuenstliche Intelligenz?',
- type: 'text',
- contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.',
- durationMinutes: 15
- },
- {
- id: 'lesson-003-02',
- courseId: 'course-003',
- order: 2,
- title: 'Der EU AI Act - Was bedeutet er fuer uns?',
- type: 'video',
- contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.',
- durationMinutes: 20,
- videoUrl: '/videos/eu-ai-act.mp4'
- },
- {
- id: 'lesson-003-03',
- courseId: 'course-003',
- order: 3,
- title: 'KI-Werkzeuge sicher nutzen',
- type: 'text',
- contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.',
- durationMinutes: 20
- },
- {
- id: 'lesson-003-04',
- courseId: 'course-003',
- order: 4,
- title: 'Wissenstest: AI Literacy',
- type: 'quiz',
- contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.',
- durationMinutes: 20
- }
- ]
- }
- ]
-}
-
-/**
- * Demo-Einschreibungen erstellen
- */
-export function createMockEnrollments(): Enrollment[] {
- const now = new Date()
-
- return [
- {
- id: 'enr-001',
- courseId: 'course-001',
- userId: 'user-001',
- userName: 'Maria Fischer',
- userEmail: 'maria.fischer@example.de',
- status: 'in_progress',
- progress: 40,
- startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
- deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'enr-002',
- courseId: 'course-002',
- userId: 'user-002',
- userName: 'Stefan Mueller',
- userEmail: 'stefan.mueller@example.de',
- status: 'completed',
- progress: 100,
- startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
- completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(),
- certificateId: 'cert-001',
- deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'enr-003',
- courseId: 'course-001',
- userId: 'user-003',
- userName: 'Laura Schneider',
- userEmail: 'laura.schneider@example.de',
- status: 'not_started',
- progress: 0,
- startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
- deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'enr-004',
- courseId: 'course-003',
- userId: 'user-004',
- userName: 'Thomas Wagner',
- userEmail: 'thomas.wagner@example.de',
- status: 'expired',
- progress: 25,
- startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
- deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'enr-005',
- courseId: 'course-002',
- userId: 'user-005',
- userName: 'Julia Becker',
- userEmail: 'julia.becker@example.de',
- status: 'in_progress',
- progress: 50,
- startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
- deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
- }
- ]
-}
-
-/**
- * Demo-Statistiken aus Kursen und Einschreibungen berechnen
- */
-export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics {
- const c = courses || createMockCourses()
- const e = enrollments || createMockEnrollments()
-
- const completedCount = e.filter(en => en.status === 'completed').length
- const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0
- const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length
-
- return {
- totalCourses: c.length,
- totalEnrollments: e.length,
- completionRate,
- overdueCount,
- byCategory: {
- dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length,
- it_security: c.filter(co => co.category === 'it_security').length,
- ai_literacy: c.filter(co => co.category === 'ai_literacy').length,
- whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length,
- custom: c.filter(co => co.category === 'custom').length,
- },
- byStatus: {
- not_started: e.filter(en => en.status === 'not_started').length,
- in_progress: e.filter(en => en.status === 'in_progress').length,
- completed: e.filter(en => en.status === 'completed').length,
- expired: e.filter(en => en.status === 'expired').length,
- }
- }
-}
+export {
+ fetchSDKAcademyList,
+ createMockCourses,
+ createMockEnrollments,
+ createMockStatistics,
+} from './api-mock-data'
diff --git a/admin-compliance/lib/sdk/document-generator/datapoint-generators.ts b/admin-compliance/lib/sdk/document-generator/datapoint-generators.ts
new file mode 100644
index 0000000..fb1c1fd
--- /dev/null
+++ b/admin-compliance/lib/sdk/document-generator/datapoint-generators.ts
@@ -0,0 +1,265 @@
+/**
+ * Datapoint Helpers — Generation Functions
+ *
+ * Functions that generate DSGVO-compliant text blocks from data points
+ * for the document generator.
+ */
+
+import {
+ DataPoint,
+ DataPointCategory,
+ LegalBasis,
+ RetentionPeriod,
+ RiskLevel,
+ CATEGORY_METADATA,
+ LEGAL_BASIS_INFO,
+ RETENTION_PERIOD_INFO,
+ RISK_LEVEL_STYLING,
+ LocalizedText,
+ SupportedLanguage
+} from '@/lib/sdk/einwilligungen/types'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export type Language = SupportedLanguage
+
+export interface DataPointPlaceholders {
+ '[DATENPUNKTE_COUNT]': string
+ '[DATENPUNKTE_LIST]': string
+ '[DATENPUNKTE_TABLE]': string
+ '[VERARBEITUNGSZWECKE]': string
+ '[RECHTSGRUNDLAGEN]': string
+ '[SPEICHERFRISTEN]': string
+ '[EMPFAENGER]': string
+ '[BESONDERE_KATEGORIEN]': string
+ '[DRITTLAND_TRANSFERS]': string
+ '[RISIKO_ZUSAMMENFASSUNG]': string
+}
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+function getText(text: LocalizedText, lang: Language): string {
+ return text[lang] || text.de
+}
+
+export function groupByRetention(
+ dataPoints: DataPoint[]
+): Record {
+ return dataPoints.reduce((acc, dp) => {
+ const key = dp.retentionPeriod
+ if (!acc[key]) acc[key] = []
+ acc[key].push(dp)
+ return acc
+ }, {} as Record)
+}
+
+export function groupByCategory(
+ dataPoints: DataPoint[]
+): Record {
+ return dataPoints.reduce((acc, dp) => {
+ const key = dp.category
+ if (!acc[key]) acc[key] = []
+ acc[key].push(dp)
+ return acc
+ }, {} as Record)
+}
+
+// =============================================================================
+// GENERATOR FUNCTIONS
+// =============================================================================
+
+export function generateDataPointsTable(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ if (dataPoints.length === 0) {
+ return lang === 'de' ? '*Keine Datenpunkte ausgewaehlt.*' : '*No data points selected.*'
+ }
+
+ const header = lang === 'de'
+ ? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
+ : '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
+ const separator = '|------------|-----------|-------|-----------------|---------------|'
+
+ const rows = dataPoints.map(dp => {
+ const category = CATEGORY_METADATA[dp.category]
+ const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
+ const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
+
+ const name = getText(dp.name, lang)
+ const categoryName = getText(category.name, lang)
+ const purpose = getText(dp.purpose, lang)
+ const legalBasisName = getText(legalBasis.name, lang)
+ const retentionLabel = getText(retention.label, lang)
+
+ const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
+
+ return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
+ }).join('\n')
+
+ return `${header}\n${separator}\n${rows}`
+}
+
+export function generateSpecialCategorySection(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ const special = dataPoints.filter(dp => dp.isSpecialCategory)
+ if (special.length === 0) return ''
+
+ if (lang === 'de') {
+ const items = special.map(dp =>
+ `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
+ ).join('\n')
+
+ return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
+
+Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
+
+${items}
+
+Die Verarbeitung erfolgt auf Grundlage Ihrer ausdruecklichen Einwilligung gemaess Art. 9 Abs. 2 lit. a DSGVO. Sie koennen Ihre Einwilligung jederzeit mit Wirkung fuer die Zukunft widerrufen.`
+ } else {
+ const items = special.map(dp =>
+ `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
+ ).join('\n')
+
+ return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
+
+We process the following special categories of personal data:
+
+${items}
+
+Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
+ }
+}
+
+export function generatePurposesList(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ const purposes = new Set()
+ dataPoints.forEach(dp => purposes.add(getText(dp.purpose, lang)))
+ return [...purposes].join(', ')
+}
+
+export function generateLegalBasisList(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ const bases = new Set()
+ dataPoints.forEach(dp => bases.add(dp.legalBasis))
+
+ return [...bases].map(basis => {
+ const info = LEGAL_BASIS_INFO[basis]
+ return `${info.article} (${getText(info.name, lang)})`
+ }).join(', ')
+}
+
+export function generateRetentionList(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ const grouped = groupByRetention(dataPoints)
+ const entries: string[] = []
+
+ for (const [period, points] of Object.entries(grouped)) {
+ const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
+ const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
+ entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
+ }
+
+ return entries.join('; ')
+}
+
+export function generateRecipientsList(dataPoints: DataPoint[]): string {
+ const recipients = new Set()
+ dataPoints.forEach(dp => {
+ dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
+ })
+ if (recipients.size === 0) return ''
+ return [...recipients].join(', ')
+}
+
+export function generateThirdCountrySection(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
+
+ const thirdCountryPoints = dataPoints.filter(dp =>
+ dp.thirdPartyRecipients?.some(r =>
+ thirdCountryIndicators.some(indicator =>
+ r.toLowerCase().includes(indicator.toLowerCase())
+ )
+ )
+ )
+
+ if (thirdCountryPoints.length === 0) return ''
+
+ const recipients = new Set()
+ thirdCountryPoints.forEach(dp => {
+ dp.thirdPartyRecipients?.forEach(r => {
+ if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
+ recipients.add(r)
+ }
+ })
+ })
+
+ if (lang === 'de') {
+ return `## Uebermittlung in Drittlaender
+
+Wir uebermitteln personenbezogene Daten an folgende Empfaenger in Drittlaendern (ausserhalb der EU/des EWR):
+
+${[...recipients].map(r => `- ${r}`).join('\n')}
+
+Die Uebermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
+ } else {
+ return `## Transfers to Third Countries
+
+We transfer personal data to the following recipients in third countries (outside the EU/EEA):
+
+${[...recipients].map(r => `- ${r}`).join('\n')}
+
+The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
+ }
+}
+
+export function generateRiskSummary(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): string {
+ const riskCounts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0 }
+ dataPoints.forEach(dp => riskCounts[dp.riskLevel]++)
+
+ const parts = Object.entries(riskCounts)
+ .filter(([, count]) => count > 0)
+ .map(([level, count]) => {
+ const styling = RISK_LEVEL_STYLING[level as RiskLevel]
+ return `${count} ${getText(styling.label, lang).toLowerCase()}`
+ })
+
+ return parts.join(', ')
+}
+
+export function generateAllPlaceholders(
+ dataPoints: DataPoint[],
+ lang: Language = 'de'
+): DataPointPlaceholders {
+ return {
+ '[DATENPUNKTE_COUNT]': String(dataPoints.length),
+ '[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
+ '[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
+ '[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
+ '[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
+ '[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
+ '[EMPFAENGER]': generateRecipientsList(dataPoints),
+ '[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
+ '[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
+ '[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
+ }
+}
diff --git a/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts b/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts
index 7994e9f..dddbba2 100644
--- a/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts
+++ b/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts
@@ -1,548 +1,37 @@
/**
- * Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
- * in den Dokumentengenerator.
+ * Datapoint Helpers — barrel re-export
*
- * Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf
- * den vom Benutzer ausgewählten Datenpunkten.
+ * Split into:
+ * - datapoint-generators.ts (text generation functions)
+ * - datapoint-validators.ts (document validation checks)
*/
-import {
- DataPoint,
- DataPointCategory,
- LegalBasis,
- RetentionPeriod,
- RiskLevel,
- CATEGORY_METADATA,
- LEGAL_BASIS_INFO,
- RETENTION_PERIOD_INFO,
- RISK_LEVEL_STYLING,
- LocalizedText,
- SupportedLanguage
-} from '@/lib/sdk/einwilligungen/types'
-
-// =============================================================================
-// TYPES
-// =============================================================================
-
-/**
- * Sprach-Option für alle Helper-Funktionen
- */
-export type Language = SupportedLanguage
-
-/**
- * Generierte Platzhalter-Map für den Dokumentengenerator
- */
-export interface DataPointPlaceholders {
- '[DATENPUNKTE_COUNT]': string
- '[DATENPUNKTE_LIST]': string
- '[DATENPUNKTE_TABLE]': string
- '[VERARBEITUNGSZWECKE]': string
- '[RECHTSGRUNDLAGEN]': string
- '[SPEICHERFRISTEN]': string
- '[EMPFAENGER]': string
- '[BESONDERE_KATEGORIEN]': string
- '[DRITTLAND_TRANSFERS]': string
- '[RISIKO_ZUSAMMENFASSUNG]': string
-}
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-/**
- * Extrahiert Text aus LocalizedText basierend auf Sprache
- */
-function getText(text: LocalizedText, lang: Language): string {
- return text[lang] || text.de
-}
-
-/**
- * Generiert eine Markdown-Tabelle der Datenpunkte
- *
- * @param dataPoints - Liste der ausgewählten Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Markdown-Tabelle als String
- */
-export function generateDataPointsTable(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- if (dataPoints.length === 0) {
- return lang === 'de'
- ? '*Keine Datenpunkte ausgewählt.*'
- : '*No data points selected.*'
- }
-
- const header = lang === 'de'
- ? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
- : '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
- const separator = '|------------|-----------|-------|-----------------|---------------|'
-
- const rows = dataPoints.map(dp => {
- const category = CATEGORY_METADATA[dp.category]
- const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
- const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
-
- const name = getText(dp.name, lang)
- const categoryName = getText(category.name, lang)
- const purpose = getText(dp.purpose, lang)
- const legalBasisName = getText(legalBasis.name, lang)
- const retentionLabel = getText(retention.label, lang)
-
- // Truncate long texts for table readability
- const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
-
- return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
- }).join('\n')
-
- return `${header}\n${separator}\n${rows}`
-}
-
-/**
- * Gruppiert Datenpunkte nach Speicherfrist
- *
- * @param dataPoints - Liste der Datenpunkte
- * @returns Record mit Speicherfrist als Key und Datenpunkten als Value
- */
-export function groupByRetention(
- dataPoints: DataPoint[]
-): Record {
- return dataPoints.reduce((acc, dp) => {
- const key = dp.retentionPeriod
- if (!acc[key]) {
- acc[key] = []
- }
- acc[key].push(dp)
- return acc
- }, {} as Record)
-}
-
-/**
- * Gruppiert Datenpunkte nach Kategorie
- *
- * @param dataPoints - Liste der Datenpunkte
- * @returns Record mit Kategorie als Key und Datenpunkten als Value
- */
-export function groupByCategory(
- dataPoints: DataPoint[]
-): Record {
- return dataPoints.reduce((acc, dp) => {
- const key = dp.category
- if (!acc[key]) {
- acc[key] = []
- }
- acc[key].push(dp)
- return acc
- }, {} as Record)
-}
-
-/**
- * Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO)
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten)
- */
-export function generateSpecialCategorySection(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- const special = dataPoints.filter(dp => dp.isSpecialCategory)
-
- if (special.length === 0) {
- return ''
- }
-
- if (lang === 'de') {
- const items = special.map(dp =>
- `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
- ).join('\n')
-
- return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
-
-Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
-
-${items}
-
-Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.`
- } else {
- const items = special.map(dp =>
- `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
- ).join('\n')
-
- return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
-
-We process the following special categories of personal data:
-
-${items}
-
-Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
- }
-}
-
-/**
- * Generiert Liste aller eindeutigen Verarbeitungszwecke
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Kommaseparierte Liste der Zwecke
- */
-export function generatePurposesList(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- const purposes = new Set()
-
- dataPoints.forEach(dp => {
- purposes.add(getText(dp.purpose, lang))
- })
-
- return [...purposes].join(', ')
-}
-
-/**
- * Generiert Liste aller verwendeten Rechtsgrundlagen
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Formatierte Liste der Rechtsgrundlagen
- */
-export function generateLegalBasisList(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- const bases = new Set()
-
- dataPoints.forEach(dp => {
- bases.add(dp.legalBasis)
- })
-
- return [...bases].map(basis => {
- const info = LEGAL_BASIS_INFO[basis]
- return `${info.article} (${getText(info.name, lang)})`
- }).join(', ')
-}
-
-/**
- * Generiert Liste aller Speicherfristen gruppiert
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien
- */
-export function generateRetentionList(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- const grouped = groupByRetention(dataPoints)
- const entries: string[] = []
-
- for (const [period, points] of Object.entries(grouped)) {
- const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
- const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
-
- entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
- }
-
- return entries.join('; ')
-}
-
-/**
- * Generiert Liste aller Empfänger/Drittparteien
- *
- * @param dataPoints - Liste der Datenpunkte
- * @returns Kommaseparierte Liste der Empfänger
- */
-export function generateRecipientsList(dataPoints: DataPoint[]): string {
- const recipients = new Set()
-
- dataPoints.forEach(dp => {
- dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
- })
-
- if (recipients.size === 0) {
- return ''
- }
-
- return [...recipients].join(', ')
-}
-
-/**
- * Generiert Abschnitt für Drittland-Übermittlungen
- *
- * @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true
- * @param lang - Sprache für die Ausgabe
- * @returns Markdown-Abschnitt als String
- */
-export function generateThirdCountrySection(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- // Note: We assume dataPoints have been filtered for thirdCountryTransfer
- // The actual flag would need to be added to the DataPoint interface
- // For now, we check if any thirdPartyRecipients suggest third country
- const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
-
- const thirdCountryPoints = dataPoints.filter(dp =>
- dp.thirdPartyRecipients?.some(r =>
- thirdCountryIndicators.some(indicator =>
- r.toLowerCase().includes(indicator.toLowerCase())
- )
- )
- )
-
- if (thirdCountryPoints.length === 0) {
- return ''
- }
-
- const recipients = new Set()
- thirdCountryPoints.forEach(dp => {
- dp.thirdPartyRecipients?.forEach(r => {
- if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
- recipients.add(r)
- }
- })
- })
-
- if (lang === 'de') {
- return `## Übermittlung in Drittländer
-
-Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR):
-
-${[...recipients].map(r => `- ${r}`).join('\n')}
-
-Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
- } else {
- return `## Transfers to Third Countries
-
-We transfer personal data to the following recipients in third countries (outside the EU/EEA):
-
-${[...recipients].map(r => `- ${r}`).join('\n')}
-
-The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
- }
-}
-
-/**
- * Generiert Risiko-Zusammenfassung
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Formatierte Risiko-Zusammenfassung
- */
-export function generateRiskSummary(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): string {
- const riskCounts: Record = {
- LOW: 0,
- MEDIUM: 0,
- HIGH: 0
- }
-
- dataPoints.forEach(dp => {
- riskCounts[dp.riskLevel]++
- })
-
- const parts = Object.entries(riskCounts)
- .filter(([, count]) => count > 0)
- .map(([level, count]) => {
- const styling = RISK_LEVEL_STYLING[level as RiskLevel]
- return `${count} ${getText(styling.label, lang).toLowerCase()}`
- })
-
- return parts.join(', ')
-}
-
-/**
- * Generiert alle Platzhalter für den Dokumentengenerator
- *
- * @param dataPoints - Liste der ausgewählten Datenpunkte
- * @param lang - Sprache für die Ausgabe
- * @returns Objekt mit allen Platzhaltern
- */
-export function generateAllPlaceholders(
- dataPoints: DataPoint[],
- lang: Language = 'de'
-): DataPointPlaceholders {
- return {
- '[DATENPUNKTE_COUNT]': String(dataPoints.length),
- '[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
- '[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
- '[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
- '[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
- '[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
- '[EMPFAENGER]': generateRecipientsList(dataPoints),
- '[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
- '[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
- '[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
- }
-}
-
-// =============================================================================
-// VALIDATION HELPERS
-// =============================================================================
-
-/**
- * Validierungswarnung für den Dokumentengenerator
- */
-export interface ValidationWarning {
- type: 'error' | 'warning' | 'info'
- code: string
- message: string
- suggestion: string
- affectedDataPoints?: DataPoint[]
-}
-
-/**
- * Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param documentContent - Der generierte Dokumentinhalt
- * @param lang - Sprache
- * @returns ValidationWarning oder null
- */
-export function checkSpecialCategoriesWarning(
- dataPoints: DataPoint[],
- documentContent: string,
- lang: Language = 'de'
-): ValidationWarning | null {
- const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
-
- if (specialCategories.length === 0) {
- return null
- }
-
- const hasSection = lang === 'de'
- ? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
- : documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
-
- if (!hasSection) {
- return {
- type: 'error',
- code: 'MISSING_ART9_SECTION',
- message: lang === 'de'
- ? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.`
- : `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
- suggestion: lang === 'de'
- ? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
- : 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
- affectedDataPoints: specialCategories
- }
- }
-
- return null
-}
-
-/**
- * Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param documentContent - Der generierte Dokumentinhalt
- * @param lang - Sprache
- * @returns ValidationWarning oder null
- */
-export function checkThirdCountryWarning(
- dataPoints: DataPoint[],
- documentContent: string,
- lang: Language = 'de'
-): ValidationWarning | null {
- const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
-
- const thirdCountryPoints = dataPoints.filter(dp =>
- dp.thirdPartyRecipients?.some(r =>
- thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
- )
- )
-
- if (thirdCountryPoints.length === 0) {
- return null
- }
-
- const hasSCCMention = lang === 'de'
- ? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
- : documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
-
- if (!hasSCCMention) {
- return {
- type: 'warning',
- code: 'MISSING_SCC_SECTION',
- message: lang === 'de'
- ? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.`
- : `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
- suggestion: lang === 'de'
- ? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
- : 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
- affectedDataPoints: thirdCountryPoints
- }
- }
-
- return null
-}
-
-/**
- * Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param documentContent - Der generierte Dokumentinhalt
- * @param lang - Sprache
- * @returns ValidationWarning oder null
- */
-export function checkExplicitConsentWarning(
- dataPoints: DataPoint[],
- documentContent: string,
- lang: Language = 'de'
-): ValidationWarning | null {
- const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
-
- if (explicitConsentPoints.length === 0) {
- return null
- }
-
- const hasConsentSection = lang === 'de'
- ? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
- : documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
-
- if (!hasConsentSection) {
- return {
- type: 'warning',
- code: 'MISSING_CONSENT_SECTION',
- message: lang === 'de'
- ? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
- : `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
- suggestion: lang === 'de'
- ? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
- : 'Add a section about the right to withdraw consent.',
- affectedDataPoints: explicitConsentPoints
- }
- }
-
- return null
-}
-
-/**
- * Führt alle Validierungsprüfungen durch
- *
- * @param dataPoints - Liste der Datenpunkte
- * @param documentContent - Der generierte Dokumentinhalt
- * @param lang - Sprache
- * @returns Array aller Warnungen
- */
-export function validateDocument(
- dataPoints: DataPoint[],
- documentContent: string,
- lang: Language = 'de'
-): ValidationWarning[] {
- const warnings: ValidationWarning[] = []
-
- const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
- if (specialCatWarning) warnings.push(specialCatWarning)
-
- const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
- if (thirdCountryWarning) warnings.push(thirdCountryWarning)
-
- const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
- if (consentWarning) warnings.push(consentWarning)
-
- return warnings
-}
+export type {
+ Language,
+ DataPointPlaceholders,
+} from './datapoint-generators'
+
+export {
+ generateDataPointsTable,
+ groupByRetention,
+ groupByCategory,
+ generateSpecialCategorySection,
+ generatePurposesList,
+ generateLegalBasisList,
+ generateRetentionList,
+ generateRecipientsList,
+ generateThirdCountrySection,
+ generateRiskSummary,
+ generateAllPlaceholders,
+} from './datapoint-generators'
+
+export type {
+ ValidationWarning,
+} from './datapoint-validators'
+
+export {
+ checkSpecialCategoriesWarning,
+ checkThirdCountryWarning,
+ checkExplicitConsentWarning,
+ validateDocument,
+} from './datapoint-validators'
diff --git a/admin-compliance/lib/sdk/document-generator/datapoint-validators.ts b/admin-compliance/lib/sdk/document-generator/datapoint-validators.ts
new file mode 100644
index 0000000..c8443c0
--- /dev/null
+++ b/admin-compliance/lib/sdk/document-generator/datapoint-validators.ts
@@ -0,0 +1,144 @@
+/**
+ * Datapoint Helpers — Validation Functions
+ *
+ * Document validation checks for DSGVO compliance.
+ */
+
+import {
+ DataPoint,
+ LocalizedText,
+ SupportedLanguage,
+} from '@/lib/sdk/einwilligungen/types'
+
+import type { Language } from './datapoint-generators'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export interface ValidationWarning {
+ type: 'error' | 'warning' | 'info'
+ code: string
+ message: string
+ suggestion: string
+ affectedDataPoints?: DataPoint[]
+}
+
+// =============================================================================
+// VALIDATION FUNCTIONS
+// =============================================================================
+
+export function checkSpecialCategoriesWarning(
+ dataPoints: DataPoint[],
+ documentContent: string,
+ lang: Language = 'de'
+): ValidationWarning | null {
+ const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
+
+ if (specialCategories.length === 0) return null
+
+ const hasSection = lang === 'de'
+ ? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
+ : documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
+
+ if (!hasSection) {
+ return {
+ type: 'error',
+ code: 'MISSING_ART9_SECTION',
+ message: lang === 'de'
+ ? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewaehlt, aber kein entsprechender Abschnitt im Dokument gefunden.`
+ : `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
+ suggestion: lang === 'de'
+ ? 'Fuegen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
+ : 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
+ affectedDataPoints: specialCategories
+ }
+ }
+
+ return null
+}
+
+export function checkThirdCountryWarning(
+ dataPoints: DataPoint[],
+ documentContent: string,
+ lang: Language = 'de'
+): ValidationWarning | null {
+ const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
+
+ const thirdCountryPoints = dataPoints.filter(dp =>
+ dp.thirdPartyRecipients?.some(r =>
+ thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
+ )
+ )
+
+ if (thirdCountryPoints.length === 0) return null
+
+ const hasSCCMention = lang === 'de'
+ ? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
+ : documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
+
+ if (!hasSCCMention) {
+ return {
+ type: 'warning',
+ code: 'MISSING_SCC_SECTION',
+ message: lang === 'de'
+ ? `Drittland-Uebermittlung fuer ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwaehnt.`
+ : `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
+ suggestion: lang === 'de'
+ ? 'Erwaegen Sie die Aufnahme eines Abschnitts zu Drittland-Uebermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
+ : 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
+ affectedDataPoints: thirdCountryPoints
+ }
+ }
+
+ return null
+}
+
+export function checkExplicitConsentWarning(
+ dataPoints: DataPoint[],
+ documentContent: string,
+ lang: Language = 'de'
+): ValidationWarning | null {
+ const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
+
+ if (explicitConsentPoints.length === 0) return null
+
+ const hasConsentSection = lang === 'de'
+ ? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
+ : documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
+
+ if (!hasConsentSection) {
+ return {
+ type: 'warning',
+ code: 'MISSING_CONSENT_SECTION',
+ message: lang === 'de'
+ ? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrueckliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
+ : `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
+ suggestion: lang === 'de'
+ ? 'Fuegen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
+ : 'Add a section about the right to withdraw consent.',
+ affectedDataPoints: explicitConsentPoints
+ }
+ }
+
+ return null
+}
+
+export function validateDocument(
+ dataPoints: DataPoint[],
+ documentContent: string,
+ lang: Language = 'de'
+): ValidationWarning[] {
+ const warnings: ValidationWarning[] = []
+
+ const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
+ if (specialCatWarning) warnings.push(specialCatWarning)
+
+ const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
+ if (thirdCountryWarning) warnings.push(thirdCountryWarning)
+
+ const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
+ if (consentWarning) warnings.push(consentWarning)
+
+ return warnings
+}
diff --git a/admin-compliance/lib/sdk/dsr/types-api.ts b/admin-compliance/lib/sdk/dsr/types-api.ts
new file mode 100644
index 0000000..0a30868
--- /dev/null
+++ b/admin-compliance/lib/sdk/dsr/types-api.ts
@@ -0,0 +1,243 @@
+/**
+ * DSR Types — API Types, Communication, Audit, Templates, Statistics & Helpers
+ */
+
+import type {
+ DSRType,
+ DSRStatus,
+ DSRPriority,
+ DSRSource,
+ DSRRequester,
+ DSRAssignment,
+ DSRRequest,
+ DSRDataExport,
+ IdentityVerificationMethod,
+ CommunicationType,
+ CommunicationChannel,
+ DSRTypeInfo,
+} from './types-core'
+
+export { DSR_TYPE_INFO } from './types-core'
+
+// =============================================================================
+// COMMUNICATION
+// =============================================================================
+
+export interface DSRCommunication {
+ id: string
+ dsrId: string
+ type: CommunicationType
+ channel: CommunicationChannel
+ subject?: string
+ content: string
+ templateUsed?: string
+ attachments?: {
+ name: string
+ url: string
+ size: number
+ type: string
+ }[]
+ sentAt?: string
+ sentBy?: string
+ receivedAt?: string
+ createdAt: string
+ createdBy: string
+}
+
+// =============================================================================
+// AUDIT LOG
+// =============================================================================
+
+export interface DSRAuditEntry {
+ id: string
+ dsrId: string
+ action: string
+ previousValue?: string
+ newValue?: string
+ performedBy: string
+ performedAt: string
+ notes?: string
+}
+
+// =============================================================================
+// EMAIL TEMPLATES
+// =============================================================================
+
+export interface DSREmailTemplate {
+ id: string
+ name: string
+ subject: string
+ body: string
+ type: DSRType | 'general'
+ stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion'
+ language: 'de' | 'en'
+ variables: string[]
+}
+
+export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [
+ {
+ id: 'intake_confirmation',
+ name: 'Eingangsbestaetigung',
+ subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}',
+ body: `Sehr geehrte(r) {{requesterName}},
+
+wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}.
+
+Referenznummer: {{referenceNumber}}
+Art der Anfrage: {{requestType}}
+
+Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten.
+
+Mit freundlichen Gruessen
+{{senderName}}
+Datenschutzbeauftragter`,
+ type: 'general',
+ stage: 'intake',
+ language: 'de',
+ variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName']
+ },
+ {
+ id: 'identity_request',
+ name: 'Identitaetsanfrage',
+ subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}',
+ body: `Sehr geehrte(r) {{requesterName}},
+
+um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet.
+
+Bitte senden Sie uns eines der folgenden Dokumente:
+- Kopie Ihres Personalausweises (Vorder- und Rueckseite)
+- Kopie Ihres Reisepasses
+
+Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht.
+
+Mit freundlichen Gruessen
+{{senderName}}
+Datenschutzbeauftragter`,
+ type: 'general',
+ stage: 'identity_request',
+ language: 'de',
+ variables: ['requesterName', 'referenceNumber', 'senderName']
+ }
+]
+
+// =============================================================================
+// API TYPES
+// =============================================================================
+
+export interface DSRFilters {
+ status?: DSRStatus | DSRStatus[]
+ type?: DSRType | DSRType[]
+ priority?: DSRPriority
+ assignedTo?: string
+ overdue?: boolean
+ search?: string
+ dateFrom?: string
+ dateTo?: string
+}
+
+export interface DSRListResponse {
+ requests: DSRRequest[]
+ total: number
+ page: number
+ pageSize: number
+}
+
+export interface DSRCreateRequest {
+ type: DSRType
+ requester: DSRRequester
+ source: DSRSource
+ sourceDetails?: string
+ requestText?: string
+ priority?: DSRPriority
+}
+
+export interface DSRUpdateRequest {
+ status?: DSRStatus
+ priority?: DSRPriority
+ notes?: string
+ internalNotes?: string
+ assignment?: DSRAssignment
+}
+
+export interface DSRVerifyIdentityRequest {
+ method: IdentityVerificationMethod
+ notes?: string
+ documentRef?: string
+}
+
+export interface DSRCompleteRequest {
+ completionNotes?: string
+ dataExport?: DSRDataExport
+}
+
+export interface DSRRejectRequest {
+ reason: string
+ legalBasis?: string
+}
+
+export interface DSRExtendDeadlineRequest {
+ extensionMonths: 1 | 2
+ reason: string
+}
+
+export interface DSRSendCommunicationRequest {
+ type: CommunicationType
+ channel: CommunicationChannel
+ subject?: string
+ content: string
+ templateId?: string
+}
+
+// =============================================================================
+// STATISTICS
+// =============================================================================
+
+export interface DSRStatistics {
+ total: number
+ byStatus: Record
+ byType: Record
+ overdue: number
+ dueThisWeek: number
+ averageProcessingDays: number
+ completedThisMonth: number
+}
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+export function getDaysRemaining(deadline: string): number {
+ const deadlineDate = new Date(deadline)
+ const now = new Date()
+ const diff = deadlineDate.getTime() - now.getTime()
+ return Math.ceil(diff / (1000 * 60 * 60 * 24))
+}
+
+export function isOverdue(request: DSRRequest): boolean {
+ if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
+ return false
+ }
+ return getDaysRemaining(request.deadline.currentDeadline) < 0
+}
+
+export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean {
+ if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
+ return false
+ }
+ const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
+ return daysRemaining >= 0 && daysRemaining <= thresholdDays
+}
+
+export function generateReferenceNumber(year: number, sequence: number): string {
+ return `DSR-${year}-${String(sequence).padStart(6, '0')}`
+}
+
+export function getTypeInfo(type: DSRType): DSRTypeInfo {
+ const { DSR_TYPE_INFO } = require('./types-core')
+ return DSR_TYPE_INFO[type]
+}
+
+export function getStatusInfo(status: DSRStatus) {
+ const { DSR_STATUS_INFO } = require('./types-core')
+ return DSR_STATUS_INFO[status]
+}
diff --git a/admin-compliance/lib/sdk/dsr/types-core.ts b/admin-compliance/lib/sdk/dsr/types-core.ts
new file mode 100644
index 0000000..6c20701
--- /dev/null
+++ b/admin-compliance/lib/sdk/dsr/types-core.ts
@@ -0,0 +1,235 @@
+/**
+ * DSR (Data Subject Request) Types — Core Types & Constants
+ *
+ * Enums, constants, metadata, and main interfaces for GDPR Art. 15-21
+ */
+
+// =============================================================================
+// ENUMS & CONSTANTS
+// =============================================================================
+
+export type DSRType =
+ | 'access' // Art. 15
+ | 'rectification' // Art. 16
+ | 'erasure' // Art. 17
+ | 'restriction' // Art. 18
+ | 'portability' // Art. 20
+ | 'objection' // Art. 21
+
+export type DSRStatus =
+ | 'intake'
+ | 'identity_verification'
+ | 'processing'
+ | 'completed'
+ | 'rejected'
+ | 'cancelled'
+
+export type DSRPriority = 'low' | 'normal' | 'high' | 'critical'
+
+export type DSRSource =
+ | 'web_form' | 'email' | 'letter' | 'phone' | 'in_person' | 'other'
+
+export type IdentityVerificationMethod =
+ | 'id_document' | 'email' | 'phone' | 'postal' | 'existing_account' | 'other'
+
+export type CommunicationType = 'incoming' | 'outgoing' | 'internal'
+
+export type CommunicationChannel =
+ | 'email' | 'letter' | 'phone' | 'portal' | 'internal_note'
+
+// =============================================================================
+// DSR TYPE METADATA
+// =============================================================================
+
+export interface DSRTypeInfo {
+ type: DSRType
+ article: string
+ label: string
+ labelShort: string
+ description: string
+ defaultDeadlineDays: number
+ maxExtensionMonths: number
+ color: string
+ bgColor: string
+ processDocument?: string
+}
+
+export const DSR_TYPE_INFO: Record = {
+ access: {
+ type: 'access', article: 'Art. 15', label: 'Auskunftsrecht', labelShort: 'Auskunft',
+ description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten',
+ defaultDeadlineDays: 30, maxExtensionMonths: 2,
+ color: 'text-blue-700', bgColor: 'bg-blue-100',
+ processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf'
+ },
+ rectification: {
+ type: 'rectification', article: 'Art. 16', label: 'Berichtigungsrecht', labelShort: 'Berichtigung',
+ description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
+ defaultDeadlineDays: 14, maxExtensionMonths: 2,
+ color: 'text-yellow-700', bgColor: 'bg-yellow-100',
+ processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf'
+ },
+ erasure: {
+ type: 'erasure', article: 'Art. 17', label: 'Loeschungsrecht', labelShort: 'Loeschung',
+ description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")',
+ defaultDeadlineDays: 14, maxExtensionMonths: 2,
+ color: 'text-red-700', bgColor: 'bg-red-100',
+ processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf'
+ },
+ restriction: {
+ type: 'restriction', article: 'Art. 18', label: 'Einschraenkungsrecht', labelShort: 'Einschraenkung',
+ description: 'Recht auf Einschraenkung der Verarbeitung',
+ defaultDeadlineDays: 14, maxExtensionMonths: 2,
+ color: 'text-orange-700', bgColor: 'bg-orange-100',
+ processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf'
+ },
+ portability: {
+ type: 'portability', article: 'Art. 20', label: 'Datenuebertragbarkeit', labelShort: 'Uebertragung',
+ description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format',
+ defaultDeadlineDays: 30, maxExtensionMonths: 2,
+ color: 'text-purple-700', bgColor: 'bg-purple-100',
+ processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf'
+ },
+ objection: {
+ type: 'objection', article: 'Art. 21', label: 'Widerspruchsrecht', labelShort: 'Widerspruch',
+ description: 'Recht auf Widerspruch gegen die Verarbeitung',
+ defaultDeadlineDays: 30, maxExtensionMonths: 0,
+ color: 'text-gray-700', bgColor: 'bg-gray-100'
+ }
+}
+
+export const DSR_STATUS_INFO: Record = {
+ intake: { label: 'Eingang', color: 'text-blue-700', bgColor: 'bg-blue-100', borderColor: 'border-blue-200' },
+ identity_verification: { label: 'ID-Pruefung', color: 'text-yellow-700', bgColor: 'bg-yellow-100', borderColor: 'border-yellow-200' },
+ processing: { label: 'In Bearbeitung', color: 'text-purple-700', bgColor: 'bg-purple-100', borderColor: 'border-purple-200' },
+ completed: { label: 'Abgeschlossen', color: 'text-green-700', bgColor: 'bg-green-100', borderColor: 'border-green-200' },
+ rejected: { label: 'Abgelehnt', color: 'text-red-700', bgColor: 'bg-red-100', borderColor: 'border-red-200' },
+ cancelled: { label: 'Storniert', color: 'text-gray-700', bgColor: 'bg-gray-100', borderColor: 'border-gray-200' },
+}
+
+// =============================================================================
+// MAIN INTERFACES
+// =============================================================================
+
+export interface DSRRequester {
+ name: string
+ email: string
+ phone?: string
+ address?: string
+ customerId?: string
+}
+
+export interface DSRIdentityVerification {
+ verified: boolean
+ method?: IdentityVerificationMethod
+ verifiedAt?: string
+ verifiedBy?: string
+ notes?: string
+ documentRef?: string
+}
+
+export interface DSRAssignment {
+ assignedTo: string | null
+ assignedAt?: string
+ assignedBy?: string
+}
+
+export interface DSRDeadline {
+ originalDeadline: string
+ currentDeadline: string
+ extended: boolean
+ extensionReason?: string
+ extensionApprovedBy?: string
+ extensionApprovedAt?: string
+}
+
+export interface DSRRequest {
+ id: string
+ referenceNumber: string
+ type: DSRType
+ status: DSRStatus
+ priority: DSRPriority
+ requester: DSRRequester
+ source: DSRSource
+ sourceDetails?: string
+ requestText?: string
+ receivedAt: string
+ deadline: DSRDeadline
+ completedAt?: string
+ identityVerification: DSRIdentityVerification
+ assignment: DSRAssignment
+ notes?: string
+ internalNotes?: string
+ erasureChecklist?: DSRErasureChecklist
+ dataExport?: DSRDataExport
+ rectificationDetails?: DSRRectificationDetails
+ objectionDetails?: DSRObjectionDetails
+ createdAt: string
+ createdBy: string
+ updatedAt: string
+ updatedBy?: string
+ tenantId: string
+}
+
+// =============================================================================
+// TYPE-SPECIFIC INTERFACES
+// =============================================================================
+
+export interface DSRErasureChecklistItem {
+ id: string
+ article: string
+ label: string
+ description: string
+ checked: boolean
+ applies: boolean
+ notes?: string
+}
+
+export interface DSRErasureChecklist {
+ items: DSRErasureChecklistItem[]
+ canProceedWithErasure: boolean
+ reviewedBy?: string
+ reviewedAt?: string
+}
+
+export const ERASURE_EXCEPTIONS: Omit[] = [
+ { id: 'art17_3_a', article: '17(3)(a)', label: 'Meinungs- und Informationsfreiheit', description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information' },
+ { id: 'art17_3_b', article: '17(3)(b)', label: 'Rechtliche Verpflichtung', description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)' },
+ { id: 'art17_3_c', article: '17(3)(c)', label: 'Oeffentliches Interesse', description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit' },
+ { id: 'art17_3_d', article: '17(3)(d)', label: 'Archivzwecke', description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik' },
+ { id: 'art17_3_e', article: '17(3)(e)', label: 'Rechtsansprueche', description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen' },
+]
+
+export interface DSRDataExport {
+ format: 'json' | 'csv' | 'xml' | 'pdf'
+ generatedAt?: string
+ generatedBy?: string
+ fileUrl?: string
+ fileName?: string
+ fileSize?: number
+ includesThirdPartyData: boolean
+ anonymizedFields?: string[]
+ transferMethod?: 'download' | 'email' | 'third_party'
+ transferRecipient?: string
+}
+
+export interface DSRRectificationDetails {
+ fieldsToCorrect: {
+ field: string
+ currentValue: string
+ requestedValue: string
+ corrected: boolean
+ correctedAt?: string
+ correctedBy?: string
+ }[]
+}
+
+export interface DSRObjectionDetails {
+ processingPurpose: string
+ legalBasis: string
+ objectionGrounds: string
+ decision: 'accepted' | 'rejected' | 'pending'
+ decisionReason?: string
+ decisionBy?: string
+ decisionAt?: string
+}
diff --git a/admin-compliance/lib/sdk/dsr/types.ts b/admin-compliance/lib/sdk/dsr/types.ts
index 71feee3..eb4d433 100644
--- a/admin-compliance/lib/sdk/dsr/types.ts
+++ b/admin-compliance/lib/sdk/dsr/types.ts
@@ -1,581 +1,60 @@
/**
- * DSR (Data Subject Request) Types
+ * DSR (Data Subject Request) Types — barrel re-export
*
- * TypeScript definitions for GDPR Art. 15-21 Data Subject Requests
- * Based on the Go Consent Service backend API structure
+ * Split into:
+ * - types-core.ts (enums, constants, metadata, main interfaces)
+ * - types-api.ts (API types, communication, audit, templates, helpers)
*/
-// =============================================================================
-// ENUMS & CONSTANTS
-// =============================================================================
-
-export type DSRType =
- | 'access' // Art. 15 - Auskunftsrecht
- | 'rectification' // Art. 16 - Berichtigungsrecht
- | 'erasure' // Art. 17 - Loeschungsrecht
- | 'restriction' // Art. 18 - Einschraenkungsrecht
- | 'portability' // Art. 20 - Datenuebertragbarkeit
- | 'objection' // Art. 21 - Widerspruchsrecht
-
-export type DSRStatus =
- | 'intake' // Eingang - Anfrage dokumentiert
- | 'identity_verification' // Identitaetspruefung
- | 'processing' // In Bearbeitung
- | 'completed' // Abgeschlossen
- | 'rejected' // Abgelehnt
- | 'cancelled' // Storniert
-
-export type DSRPriority = 'low' | 'normal' | 'high' | 'critical'
-
-export type DSRSource =
- | 'web_form' // Kontaktformular/Portal
- | 'email' // E-Mail
- | 'letter' // Brief
- | 'phone' // Telefon
- | 'in_person' // Persoenlich
- | 'other' // Sonstiges
-
-export type IdentityVerificationMethod =
- | 'id_document' // Ausweiskopie
- | 'email' // E-Mail-Bestaetigung
- | 'phone' // Telefonische Bestaetigung
- | 'postal' // Postalische Bestaetigung
- | 'existing_account' // Bestehendes Kundenkonto
- | 'other' // Sonstiges
-
-export type CommunicationType =
- | 'incoming' // Eingehend (vom Betroffenen)
- | 'outgoing' // Ausgehend (an Betroffenen)
- | 'internal' // Intern (Notizen)
-
-export type CommunicationChannel =
- | 'email'
- | 'letter'
- | 'phone'
- | 'portal'
- | 'internal_note'
-
-// =============================================================================
-// DSR TYPE METADATA
-// =============================================================================
-
-export interface DSRTypeInfo {
- type: DSRType
- article: string
- label: string
- labelShort: string
- description: string
- defaultDeadlineDays: number
- maxExtensionMonths: number
- color: string
- bgColor: string
- processDocument?: string // Reference to process document
-}
-
-export const DSR_TYPE_INFO: Record = {
- access: {
- type: 'access',
- article: 'Art. 15',
- label: 'Auskunftsrecht',
- labelShort: 'Auskunft',
- description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten',
- defaultDeadlineDays: 30,
- maxExtensionMonths: 2,
- color: 'text-blue-700',
- bgColor: 'bg-blue-100',
- processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf'
- },
- rectification: {
- type: 'rectification',
- article: 'Art. 16',
- label: 'Berichtigungsrecht',
- labelShort: 'Berichtigung',
- description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
- defaultDeadlineDays: 14,
- maxExtensionMonths: 2,
- color: 'text-yellow-700',
- bgColor: 'bg-yellow-100',
- processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf'
- },
- erasure: {
- type: 'erasure',
- article: 'Art. 17',
- label: 'Loeschungsrecht',
- labelShort: 'Loeschung',
- description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")',
- defaultDeadlineDays: 14,
- maxExtensionMonths: 2,
- color: 'text-red-700',
- bgColor: 'bg-red-100',
- processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf'
- },
- restriction: {
- type: 'restriction',
- article: 'Art. 18',
- label: 'Einschraenkungsrecht',
- labelShort: 'Einschraenkung',
- description: 'Recht auf Einschraenkung der Verarbeitung',
- defaultDeadlineDays: 14,
- maxExtensionMonths: 2,
- color: 'text-orange-700',
- bgColor: 'bg-orange-100',
- processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf'
- },
- portability: {
- type: 'portability',
- article: 'Art. 20',
- label: 'Datenuebertragbarkeit',
- labelShort: 'Uebertragung',
- description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format',
- defaultDeadlineDays: 30,
- maxExtensionMonths: 2,
- color: 'text-purple-700',
- bgColor: 'bg-purple-100',
- processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf'
- },
- objection: {
- type: 'objection',
- article: 'Art. 21',
- label: 'Widerspruchsrecht',
- labelShort: 'Widerspruch',
- description: 'Recht auf Widerspruch gegen die Verarbeitung',
- defaultDeadlineDays: 30,
- maxExtensionMonths: 0, // No extension allowed for objections
- color: 'text-gray-700',
- bgColor: 'bg-gray-100'
- }
-}
-
-export const DSR_STATUS_INFO: Record = {
- intake: {
- label: 'Eingang',
- color: 'text-blue-700',
- bgColor: 'bg-blue-100',
- borderColor: 'border-blue-200'
- },
- identity_verification: {
- label: 'ID-Pruefung',
- color: 'text-yellow-700',
- bgColor: 'bg-yellow-100',
- borderColor: 'border-yellow-200'
- },
- processing: {
- label: 'In Bearbeitung',
- color: 'text-purple-700',
- bgColor: 'bg-purple-100',
- borderColor: 'border-purple-200'
- },
- completed: {
- label: 'Abgeschlossen',
- color: 'text-green-700',
- bgColor: 'bg-green-100',
- borderColor: 'border-green-200'
- },
- rejected: {
- label: 'Abgelehnt',
- color: 'text-red-700',
- bgColor: 'bg-red-100',
- borderColor: 'border-red-200'
- },
- cancelled: {
- label: 'Storniert',
- color: 'text-gray-700',
- bgColor: 'bg-gray-100',
- borderColor: 'border-gray-200'
- }
-}
-
-// =============================================================================
-// MAIN INTERFACES
-// =============================================================================
-
-export interface DSRRequester {
- name: string
- email: string
- phone?: string
- address?: string
- customerId?: string // If existing customer
-}
-
-export interface DSRIdentityVerification {
- verified: boolean
- method?: IdentityVerificationMethod
- verifiedAt?: string
- verifiedBy?: string
- notes?: string
- documentRef?: string // Reference to uploaded ID document
-}
-
-export interface DSRAssignment {
- assignedTo: string | null
- assignedAt?: string
- assignedBy?: string
-}
-
-export interface DSRDeadline {
- originalDeadline: string
- currentDeadline: string
- extended: boolean
- extensionReason?: string
- extensionApprovedBy?: string
- extensionApprovedAt?: string
-}
-
-export interface DSRRequest {
- id: string
- referenceNumber: string // e.g., "DSR-2025-000042"
- type: DSRType
- status: DSRStatus
- priority: DSRPriority
-
- // Requester info
- requester: DSRRequester
-
- // Request details
- source: DSRSource
- sourceDetails?: string // e.g., "Kontaktformular auf website.de"
- requestText?: string // Original request text
-
- // Dates
- receivedAt: string
- deadline: DSRDeadline
- completedAt?: string
-
- // Verification
- identityVerification: DSRIdentityVerification
-
- // Assignment
- assignment: DSRAssignment
-
- // Processing
- notes?: string
- internalNotes?: string
-
- // Type-specific data
- erasureChecklist?: DSRErasureChecklist // For Art. 17
- dataExport?: DSRDataExport // For Art. 15, 20
- rectificationDetails?: DSRRectificationDetails // For Art. 16
- objectionDetails?: DSRObjectionDetails // For Art. 21
-
- // Audit
- createdAt: string
- createdBy: string
- updatedAt: string
- updatedBy?: string
-
- // Metadata
- tenantId: string
-}
-
-// =============================================================================
-// TYPE-SPECIFIC INTERFACES
-// =============================================================================
-
-// Art. 17(3) Erasure Exceptions Checklist
-export interface DSRErasureChecklistItem {
- id: string
- article: string // e.g., "17(3)(a)"
- label: string
- description: string
- checked: boolean
- applies: boolean
- notes?: string
-}
-
-export interface DSRErasureChecklist {
- items: DSRErasureChecklistItem[]
- canProceedWithErasure: boolean
- reviewedBy?: string
- reviewedAt?: string
-}
-
-export const ERASURE_EXCEPTIONS: Omit[] = [
- {
- id: 'art17_3_a',
- article: '17(3)(a)',
- label: 'Meinungs- und Informationsfreiheit',
- description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information'
- },
- {
- id: 'art17_3_b',
- article: '17(3)(b)',
- label: 'Rechtliche Verpflichtung',
- description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)'
- },
- {
- id: 'art17_3_c',
- article: '17(3)(c)',
- label: 'Oeffentliches Interesse',
- description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit'
- },
- {
- id: 'art17_3_d',
- article: '17(3)(d)',
- label: 'Archivzwecke',
- description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik'
- },
- {
- id: 'art17_3_e',
- article: '17(3)(e)',
- label: 'Rechtsansprueche',
- description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen'
- }
-]
-
-// Data Export for Art. 15, 20
-export interface DSRDataExport {
- format: 'json' | 'csv' | 'xml' | 'pdf'
- generatedAt?: string
- generatedBy?: string
- fileUrl?: string
- fileName?: string
- fileSize?: number
- includesThirdPartyData: boolean
- anonymizedFields?: string[]
- transferMethod?: 'download' | 'email' | 'third_party' // For Art. 20 transfer
- transferRecipient?: string // For Art. 20 transfer to another controller
-}
-
-// Rectification Details for Art. 16
-export interface DSRRectificationDetails {
- fieldsToCorrect: {
- field: string
- currentValue: string
- requestedValue: string
- corrected: boolean
- correctedAt?: string
- correctedBy?: string
- }[]
-}
-
-// Objection Details for Art. 21
-export interface DSRObjectionDetails {
- processingPurpose: string
- legalBasis: string
- objectionGrounds: string
- decision: 'accepted' | 'rejected' | 'pending'
- decisionReason?: string
- decisionBy?: string
- decisionAt?: string
-}
-
-// =============================================================================
-// COMMUNICATION
-// =============================================================================
-
-export interface DSRCommunication {
- id: string
- dsrId: string
- type: CommunicationType
- channel: CommunicationChannel
- subject?: string
- content: string
- templateUsed?: string // Reference to email template
- attachments?: {
- name: string
- url: string
- size: number
- type: string
- }[]
- sentAt?: string
- sentBy?: string
- receivedAt?: string
- createdAt: string
- createdBy: string
-}
-
-// =============================================================================
-// AUDIT LOG
-// =============================================================================
-
-export interface DSRAuditEntry {
- id: string
- dsrId: string
- action: string // e.g., "status_changed", "identity_verified", "assigned"
- previousValue?: string
- newValue?: string
- performedBy: string
- performedAt: string
- notes?: string
-}
-
-// =============================================================================
-// EMAIL TEMPLATES
-// =============================================================================
-
-export interface DSREmailTemplate {
- id: string
- name: string
- subject: string
- body: string
- type: DSRType | 'general'
- stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion'
- language: 'de' | 'en'
- variables: string[] // e.g., ["requesterName", "referenceNumber", "deadline"]
-}
-
-export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [
- {
- id: 'intake_confirmation',
- name: 'Eingangsbestaetigung',
- subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}',
- body: `Sehr geehrte(r) {{requesterName}},
-
-wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}.
-
-Referenznummer: {{referenceNumber}}
-Art der Anfrage: {{requestType}}
-
-Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten.
-
-Mit freundlichen Gruessen
-{{senderName}}
-Datenschutzbeauftragter`,
- type: 'general',
- stage: 'intake',
- language: 'de',
- variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName']
- },
- {
- id: 'identity_request',
- name: 'Identitaetsanfrage',
- subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}',
- body: `Sehr geehrte(r) {{requesterName}},
-
-um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet.
-
-Bitte senden Sie uns eines der folgenden Dokumente:
-- Kopie Ihres Personalausweises (Vorder- und Rueckseite)
-- Kopie Ihres Reisepasses
-
-Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht.
-
-Mit freundlichen Gruessen
-{{senderName}}
-Datenschutzbeauftragter`,
- type: 'general',
- stage: 'identity_request',
- language: 'de',
- variables: ['requesterName', 'referenceNumber', 'senderName']
- }
-]
-
-// =============================================================================
-// API TYPES
-// =============================================================================
-
-export interface DSRFilters {
- status?: DSRStatus | DSRStatus[]
- type?: DSRType | DSRType[]
- priority?: DSRPriority
- assignedTo?: string
- overdue?: boolean
- search?: string
- dateFrom?: string
- dateTo?: string
-}
-
-export interface DSRListResponse {
- requests: DSRRequest[]
- total: number
- page: number
- pageSize: number
-}
-
-export interface DSRCreateRequest {
- type: DSRType
- requester: DSRRequester
- source: DSRSource
- sourceDetails?: string
- requestText?: string
- priority?: DSRPriority
-}
-
-export interface DSRUpdateRequest {
- status?: DSRStatus
- priority?: DSRPriority
- notes?: string
- internalNotes?: string
- assignment?: DSRAssignment
-}
-
-export interface DSRVerifyIdentityRequest {
- method: IdentityVerificationMethod
- notes?: string
- documentRef?: string
-}
-
-export interface DSRCompleteRequest {
- completionNotes?: string
- dataExport?: DSRDataExport
-}
-
-export interface DSRRejectRequest {
- reason: string
- legalBasis?: string // e.g., Art. 17(3) exception
-}
-
-export interface DSRExtendDeadlineRequest {
- extensionMonths: 1 | 2
- reason: string
-}
-
-export interface DSRSendCommunicationRequest {
- type: CommunicationType
- channel: CommunicationChannel
- subject?: string
- content: string
- templateId?: string
-}
-
-// =============================================================================
-// STATISTICS
-// =============================================================================
-
-export interface DSRStatistics {
- total: number
- byStatus: Record
- byType: Record
- overdue: number
- dueThisWeek: number
- averageProcessingDays: number
- completedThisMonth: number
-}
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-export function getDaysRemaining(deadline: string): number {
- const deadlineDate = new Date(deadline)
- const now = new Date()
- const diff = deadlineDate.getTime() - now.getTime()
- return Math.ceil(diff / (1000 * 60 * 60 * 24))
-}
-
-export function isOverdue(request: DSRRequest): boolean {
- if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
- return false
- }
- return getDaysRemaining(request.deadline.currentDeadline) < 0
-}
-
-export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean {
- if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
- return false
- }
- const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
- return daysRemaining >= 0 && daysRemaining <= thresholdDays
-}
-
-export function generateReferenceNumber(year: number, sequence: number): string {
- return `DSR-${year}-${String(sequence).padStart(6, '0')}`
-}
-
-export function getTypeInfo(type: DSRType): DSRTypeInfo {
- return DSR_TYPE_INFO[type]
-}
-
-export function getStatusInfo(status: DSRStatus) {
- return DSR_STATUS_INFO[status]
-}
+export type {
+ DSRType,
+ DSRStatus,
+ DSRPriority,
+ DSRSource,
+ IdentityVerificationMethod,
+ CommunicationType,
+ CommunicationChannel,
+ DSRTypeInfo,
+ DSRRequester,
+ DSRIdentityVerification,
+ DSRAssignment,
+ DSRDeadline,
+ DSRRequest,
+ DSRErasureChecklistItem,
+ DSRErasureChecklist,
+ DSRDataExport,
+ DSRRectificationDetails,
+ DSRObjectionDetails,
+} from './types-core'
+
+export {
+ DSR_TYPE_INFO,
+ DSR_STATUS_INFO,
+ ERASURE_EXCEPTIONS,
+} from './types-core'
+
+export type {
+ DSRCommunication,
+ DSRAuditEntry,
+ DSREmailTemplate,
+ DSRFilters,
+ DSRListResponse,
+ DSRCreateRequest,
+ DSRUpdateRequest,
+ DSRVerifyIdentityRequest,
+ DSRCompleteRequest,
+ DSRRejectRequest,
+ DSRExtendDeadlineRequest,
+ DSRSendCommunicationRequest,
+ DSRStatistics,
+} from './types-api'
+
+export {
+ DSR_EMAIL_TEMPLATES,
+ getDaysRemaining,
+ isOverdue,
+ isUrgent,
+ generateReferenceNumber,
+ getTypeInfo,
+ getStatusInfo,
+} from './types-api'
diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts
new file mode 100644
index 0000000..3e31731
--- /dev/null
+++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts
@@ -0,0 +1,119 @@
+/**
+ * Cookie Banner — Configuration & Category Generation
+ *
+ * Default texts, styling, and category generation from data points.
+ */
+
+import {
+ DataPoint,
+ CookieBannerCategory,
+ CookieBannerConfig,
+ CookieBannerStyling,
+ CookieBannerTexts,
+ CookieInfo,
+ LocalizedText,
+ SupportedLanguage,
+} from '../types'
+import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+function t(text: LocalizedText, language: SupportedLanguage): string {
+ return text[language]
+}
+
+// =============================================================================
+// DEFAULT CONFIGURATION
+// =============================================================================
+
+export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
+ title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
+ description: {
+ de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
+ en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
+ },
+ acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
+ rejectAll: { de: 'Nur notwendige', en: 'Essential Only' },
+ customize: { de: 'Einstellungen', en: 'Customize' },
+ save: { de: 'Auswahl speichern', en: 'Save Selection' },
+ privacyPolicyLink: {
+ de: 'Mehr in unserer Datenschutzerklaerung',
+ en: 'More in our Privacy Policy',
+ },
+}
+
+export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
+ position: 'BOTTOM',
+ theme: 'LIGHT',
+ primaryColor: '#6366f1',
+ secondaryColor: '#f1f5f9',
+ textColor: '#1e293b',
+ backgroundColor: '#ffffff',
+ borderRadius: 12,
+ maxWidth: 480,
+}
+
+// =============================================================================
+// GENERATOR FUNCTIONS
+// =============================================================================
+
+function getExpiryFromRetention(retention: string): string {
+ const mapping: Record = {
+ '24_HOURS': '24 Stunden / 24 hours',
+ '30_DAYS': '30 Tage / 30 days',
+ '90_DAYS': '90 Tage / 90 days',
+ '12_MONTHS': '1 Jahr / 1 year',
+ '24_MONTHS': '2 Jahre / 2 years',
+ '36_MONTHS': '3 Jahre / 3 years',
+ 'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
+ 'UNTIL_PURPOSE_FULFILLED': 'Session',
+ 'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
+ }
+ return mapping[retention] || 'Session'
+}
+
+export function generateCookieCategories(
+ dataPoints: DataPoint[]
+): CookieBannerCategory[] {
+ const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
+
+ return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
+ const categoryDataPoints = cookieDataPoints.filter(
+ (dp) => dp.cookieCategory === defaultCat.id
+ )
+
+ const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
+ name: dp.code,
+ provider: 'First Party',
+ purpose: dp.purpose,
+ expiry: getExpiryFromRetention(dp.retentionPeriod),
+ type: 'FIRST_PARTY',
+ }))
+
+ return {
+ ...defaultCat,
+ dataPointIds: categoryDataPoints.map((dp) => dp.id),
+ cookies,
+ }
+ }).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
+}
+
+export function generateCookieBannerConfig(
+ tenantId: string,
+ dataPoints: DataPoint[],
+ customTexts?: Partial,
+ customStyling?: Partial
+): CookieBannerConfig {
+ const categories = generateCookieCategories(dataPoints)
+
+ return {
+ id: `cookie-banner-${tenantId}`,
+ tenantId,
+ categories,
+ styling: { ...DEFAULT_COOKIE_BANNER_STYLING, ...customStyling },
+ texts: { ...DEFAULT_COOKIE_BANNER_TEXTS, ...customTexts },
+ updatedAt: new Date(),
+ }
+}
diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts
new file mode 100644
index 0000000..6b553d7
--- /dev/null
+++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts
@@ -0,0 +1,418 @@
+/**
+ * Cookie Banner — Embed Code Generation (CSS, HTML, JS)
+ *
+ * Generates the embeddable cookie banner code from configuration.
+ */
+
+import {
+ CookieBannerConfig,
+ CookieBannerStyling,
+ CookieBannerEmbedCode,
+ LocalizedText,
+ SupportedLanguage,
+} from '../types'
+
+// =============================================================================
+// MAIN EXPORT
+// =============================================================================
+
+export function generateEmbedCode(
+ config: CookieBannerConfig,
+ privacyPolicyUrl: string = '/datenschutz'
+): CookieBannerEmbedCode {
+ const css = generateCSS(config.styling)
+ const html = generateHTML(config, privacyPolicyUrl)
+ const js = generateJS(config)
+
+ const scriptTag = ``
+
+ return { html, css, js, scriptTag }
+}
+
+// =============================================================================
+// CSS GENERATION
+// =============================================================================
+
+function generateCSS(styling: CookieBannerStyling): string {
+ const positionStyles: Record = {
+ BOTTOM: 'bottom: 0; left: 0; right: 0;',
+ TOP: 'top: 0; left: 0; right: 0;',
+ CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
+ }
+
+ const isDark = styling.theme === 'DARK'
+ const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
+ const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
+ const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
+
+ return `
+/* Cookie Banner Styles */
+.cookie-banner-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 9998;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.cookie-banner-overlay.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+.cookie-banner {
+ position: fixed;
+ ${positionStyles[styling.position]}
+ z-index: 9999;
+ background: ${bgColor};
+ color: ${textColor};
+ border-radius: ${styling.borderRadius || 12}px;
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
+ padding: 24px;
+ max-width: ${styling.maxWidth}px;
+ margin: ${styling.position === 'CENTER' ? '0' : '16px'};
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ transform: translateY(100%);
+ opacity: 0;
+ transition: all 0.3s ease;
+}
+
+.cookie-banner.active {
+ transform: translateY(0);
+ opacity: 1;
+}
+
+.cookie-banner-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 12px;
+}
+
+.cookie-banner-description {
+ font-size: 14px;
+ line-height: 1.5;
+ margin-bottom: 16px;
+ opacity: 0.8;
+}
+
+.cookie-banner-buttons {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.cookie-banner-btn {
+ flex: 1;
+ min-width: 120px;
+ padding: 12px 20px;
+ border-radius: ${(styling.borderRadius || 12) / 2}px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border: none;
+}
+
+.cookie-banner-btn-primary {
+ background: ${styling.primaryColor};
+ color: white;
+}
+
+.cookie-banner-btn-primary:hover {
+ filter: brightness(1.1);
+}
+
+.cookie-banner-btn-secondary {
+ background: ${styling.secondaryColor || borderColor};
+ color: ${textColor};
+}
+
+.cookie-banner-btn-secondary:hover {
+ filter: brightness(0.95);
+}
+
+.cookie-banner-link {
+ display: block;
+ margin-top: 16px;
+ font-size: 12px;
+ color: ${styling.primaryColor};
+ text-decoration: none;
+}
+
+.cookie-banner-link:hover {
+ text-decoration: underline;
+}
+
+/* Category Details */
+.cookie-banner-details {
+ margin-top: 16px;
+ border-top: 1px solid ${borderColor};
+ padding-top: 16px;
+ display: none;
+}
+
+.cookie-banner-details.active {
+ display: block;
+}
+
+.cookie-banner-category {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid ${borderColor};
+}
+
+.cookie-banner-category:last-child {
+ border-bottom: none;
+}
+
+.cookie-banner-category-info {
+ flex: 1;
+}
+
+.cookie-banner-category-name {
+ font-weight: 500;
+ font-size: 14px;
+}
+
+.cookie-banner-category-desc {
+ font-size: 12px;
+ opacity: 0.7;
+ margin-top: 4px;
+}
+
+.cookie-banner-toggle {
+ position: relative;
+ width: 48px;
+ height: 28px;
+ background: ${borderColor};
+ border-radius: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.cookie-banner-toggle.active {
+ background: ${styling.primaryColor};
+}
+
+.cookie-banner-toggle.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.cookie-banner-toggle::after {
+ content: '';
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ width: 20px;
+ height: 20px;
+ background: white;
+ border-radius: 50%;
+ transition: all 0.2s ease;
+}
+
+.cookie-banner-toggle.active::after {
+ left: 24px;
+}
+
+@media (max-width: 640px) {
+ .cookie-banner {
+ margin: 0;
+ border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
+ max-width: 100%;
+ }
+
+ .cookie-banner-buttons {
+ flex-direction: column;
+ }
+
+ .cookie-banner-btn {
+ width: 100%;
+ }
+}
+`.trim()
+}
+
+// =============================================================================
+// HTML GENERATION
+// =============================================================================
+
+function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
+ const categoriesHTML = config.categories
+ .map((cat) => {
+ const isRequired = cat.isRequired
+ return `
+
+
+
${cat.name.de}
+
${cat.description.de}
+
+
+
+ `
+ })
+ .join('')
+
+ return `
+
+
+
${config.texts.title.de}
+
${config.texts.description.de}
+
+
+
+
+
+
+
+
+ ${categoriesHTML}
+
+
+
+
+
+
+ ${config.texts.privacyPolicyLink.de}
+
+
+`.trim()
+}
+
+// =============================================================================
+// JS GENERATION
+// =============================================================================
+
+function generateJS(config: CookieBannerConfig): string {
+ const categoryIds = config.categories.map((c) => c.id)
+ const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
+
+ return `
+(function() {
+ 'use strict';
+
+ const COOKIE_NAME = 'cookie_consent';
+ const COOKIE_EXPIRY_DAYS = 365;
+ const CATEGORIES = ${JSON.stringify(categoryIds)};
+ const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
+
+ function getConsent() {
+ const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
+ if (!cookie) return null;
+ try {
+ return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
+ } catch {
+ return null;
+ }
+ }
+
+ function saveConsent(consent) {
+ const date = new Date();
+ date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
+ document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
+ ';expires=' + date.toUTCString() +
+ ';path=/;SameSite=Lax';
+ window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
+ }
+
+ function hasConsent(category) {
+ const consent = getConsent();
+ if (!consent) return REQUIRED_CATEGORIES.includes(category);
+ return consent[category] === true;
+ }
+
+ function initBanner() {
+ const banner = document.getElementById('cookieBanner');
+ const overlay = document.getElementById('cookieBannerOverlay');
+ const details = document.getElementById('cookieBannerDetails');
+
+ if (!banner) return;
+
+ const consent = getConsent();
+ if (consent) return;
+
+ setTimeout(() => {
+ banner.classList.add('active');
+ overlay.classList.add('active');
+ }, 500);
+
+ document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
+ const consent = {};
+ CATEGORIES.forEach(cat => consent[cat] = true);
+ saveConsent(consent);
+ closeBanner();
+ });
+
+ document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
+ const consent = {};
+ CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
+ saveConsent(consent);
+ closeBanner();
+ });
+
+ document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
+ details.classList.toggle('active');
+ });
+
+ document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
+ const consent = {};
+ CATEGORIES.forEach(cat => {
+ const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
+ consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
+ });
+ saveConsent(consent);
+ closeBanner();
+ });
+
+ document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
+ if (toggle.dataset.required === 'true') return;
+ toggle.addEventListener('click', () => {
+ toggle.classList.toggle('active');
+ });
+ });
+
+ overlay?.addEventListener('click', () => {
+ // Don't close - user must make a choice
+ });
+ }
+
+ function closeBanner() {
+ const banner = document.getElementById('cookieBanner');
+ const overlay = document.getElementById('cookieBannerOverlay');
+ banner?.classList.remove('active');
+ overlay?.classList.remove('active');
+ }
+
+ window.CookieConsent = {
+ getConsent,
+ saveConsent,
+ hasConsent,
+ show: () => {
+ document.getElementById('cookieBanner')?.classList.add('active');
+ document.getElementById('cookieBannerOverlay')?.classList.add('active');
+ },
+ hide: closeBanner
+ };
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initBanner);
+ } else {
+ initBanner();
+ }
+})();
+`.trim()
+}
diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts
index 95626cf..7fb2058 100644
--- a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts
+++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts
@@ -1,595 +1,18 @@
/**
- * Cookie Banner Generator
+ * Cookie Banner Generator — barrel re-export
*
- * Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog.
- * Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet.
+ * Split into:
+ * - cookie-banner-config.ts (defaults, category generation, config builder)
+ * - cookie-banner-embed.ts (CSS, HTML, JS embed code generation)
*/
-import {
- DataPoint,
- CookieCategory,
- CookieBannerCategory,
- CookieBannerConfig,
- CookieBannerStyling,
- CookieBannerTexts,
- CookieBannerEmbedCode,
- CookieInfo,
- LocalizedText,
- SupportedLanguage,
-} from '../types'
-import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-/**
- * Holt den lokalisierten Text
- */
-function t(text: LocalizedText, language: SupportedLanguage): string {
- return text[language]
-}
-
-// =============================================================================
-// COOKIE BANNER CONFIGURATION
-// =============================================================================
-
-/**
- * Standard Cookie Banner Texte
- */
-export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
- title: {
- de: 'Cookie-Einstellungen',
- en: 'Cookie Settings',
- },
- description: {
- de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
- en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
- },
- acceptAll: {
- de: 'Alle akzeptieren',
- en: 'Accept All',
- },
- rejectAll: {
- de: 'Nur notwendige',
- en: 'Essential Only',
- },
- customize: {
- de: 'Einstellungen',
- en: 'Customize',
- },
- save: {
- de: 'Auswahl speichern',
- en: 'Save Selection',
- },
- privacyPolicyLink: {
- de: 'Mehr in unserer Datenschutzerklaerung',
- en: 'More in our Privacy Policy',
- },
-}
-
-/**
- * Standard Styling fuer Cookie Banner
- */
-export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
- position: 'BOTTOM',
- theme: 'LIGHT',
- primaryColor: '#6366f1', // Indigo
- secondaryColor: '#f1f5f9', // Slate-100
- textColor: '#1e293b', // Slate-800
- backgroundColor: '#ffffff',
- borderRadius: 12,
- maxWidth: 480,
-}
-
-// =============================================================================
-// GENERATOR FUNCTIONS
-// =============================================================================
-
-/**
- * Generiert Cookie-Banner Kategorien aus Datenpunkten
- */
-export function generateCookieCategories(
- dataPoints: DataPoint[]
-): CookieBannerCategory[] {
- // Filtere nur Datenpunkte mit Cookie-Kategorie
- const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
-
- // Erstelle die Kategorien basierend auf den Defaults
- return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
- // Filtere die Datenpunkte fuer diese Kategorie
- const categoryDataPoints = cookieDataPoints.filter(
- (dp) => dp.cookieCategory === defaultCat.id
- )
-
- // Erstelle Cookie-Infos aus den Datenpunkten
- const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
- name: dp.code,
- provider: 'First Party',
- purpose: dp.purpose,
- expiry: getExpiryFromRetention(dp.retentionPeriod),
- type: 'FIRST_PARTY',
- }))
-
- return {
- ...defaultCat,
- dataPointIds: categoryDataPoints.map((dp) => dp.id),
- cookies,
- }
- }).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
-}
-
-/**
- * Konvertiert Retention Period zu Cookie-Expiry String
- */
-function getExpiryFromRetention(retention: string): string {
- const mapping: Record = {
- '24_HOURS': '24 Stunden / 24 hours',
- '30_DAYS': '30 Tage / 30 days',
- '90_DAYS': '90 Tage / 90 days',
- '12_MONTHS': '1 Jahr / 1 year',
- '24_MONTHS': '2 Jahre / 2 years',
- '36_MONTHS': '3 Jahre / 3 years',
- 'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
- 'UNTIL_PURPOSE_FULFILLED': 'Session',
- 'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
- }
- return mapping[retention] || 'Session'
-}
-
-/**
- * Generiert die vollstaendige Cookie Banner Konfiguration
- */
-export function generateCookieBannerConfig(
- tenantId: string,
- dataPoints: DataPoint[],
- customTexts?: Partial,
- customStyling?: Partial
-): CookieBannerConfig {
- const categories = generateCookieCategories(dataPoints)
-
- return {
- id: `cookie-banner-${tenantId}`,
- tenantId,
- categories,
- styling: {
- ...DEFAULT_COOKIE_BANNER_STYLING,
- ...customStyling,
- },
- texts: {
- ...DEFAULT_COOKIE_BANNER_TEXTS,
- ...customTexts,
- },
- updatedAt: new Date(),
- }
-}
-
-// =============================================================================
-// EMBED CODE GENERATION
-// =============================================================================
-
-/**
- * Generiert den Embed-Code fuer den Cookie Banner
- */
-export function generateEmbedCode(
- config: CookieBannerConfig,
- privacyPolicyUrl: string = '/datenschutz'
-): CookieBannerEmbedCode {
- const css = generateCSS(config.styling)
- const html = generateHTML(config, privacyPolicyUrl)
- const js = generateJS(config)
-
- const scriptTag = ``
-
- return {
- html,
- css,
- js,
- scriptTag,
- }
-}
-
-/**
- * Generiert das CSS fuer den Cookie Banner
- */
-function generateCSS(styling: CookieBannerStyling): string {
- const positionStyles: Record = {
- BOTTOM: 'bottom: 0; left: 0; right: 0;',
- TOP: 'top: 0; left: 0; right: 0;',
- CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
- }
-
- const isDark = styling.theme === 'DARK'
- const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
- const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
- const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
-
- return `
-/* Cookie Banner Styles */
-.cookie-banner-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.4);
- z-index: 9998;
- opacity: 0;
- visibility: hidden;
- transition: all 0.3s ease;
-}
-
-.cookie-banner-overlay.active {
- opacity: 1;
- visibility: visible;
-}
-
-.cookie-banner {
- position: fixed;
- ${positionStyles[styling.position]}
- z-index: 9999;
- background: ${bgColor};
- color: ${textColor};
- border-radius: ${styling.borderRadius || 12}px;
- box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
- padding: 24px;
- max-width: ${styling.maxWidth}px;
- margin: ${styling.position === 'CENTER' ? '0' : '16px'};
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- transform: translateY(100%);
- opacity: 0;
- transition: all 0.3s ease;
-}
-
-.cookie-banner.active {
- transform: translateY(0);
- opacity: 1;
-}
-
-.cookie-banner-title {
- font-size: 18px;
- font-weight: 600;
- margin-bottom: 12px;
-}
-
-.cookie-banner-description {
- font-size: 14px;
- line-height: 1.5;
- margin-bottom: 16px;
- opacity: 0.8;
-}
-
-.cookie-banner-buttons {
- display: flex;
- gap: 12px;
- flex-wrap: wrap;
-}
-
-.cookie-banner-btn {
- flex: 1;
- min-width: 120px;
- padding: 12px 20px;
- border-radius: ${(styling.borderRadius || 12) / 2}px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s ease;
- border: none;
-}
-
-.cookie-banner-btn-primary {
- background: ${styling.primaryColor};
- color: white;
-}
-
-.cookie-banner-btn-primary:hover {
- filter: brightness(1.1);
-}
-
-.cookie-banner-btn-secondary {
- background: ${styling.secondaryColor || borderColor};
- color: ${textColor};
-}
-
-.cookie-banner-btn-secondary:hover {
- filter: brightness(0.95);
-}
-
-.cookie-banner-link {
- display: block;
- margin-top: 16px;
- font-size: 12px;
- color: ${styling.primaryColor};
- text-decoration: none;
-}
-
-.cookie-banner-link:hover {
- text-decoration: underline;
-}
-
-/* Category Details */
-.cookie-banner-details {
- margin-top: 16px;
- border-top: 1px solid ${borderColor};
- padding-top: 16px;
- display: none;
-}
-
-.cookie-banner-details.active {
- display: block;
-}
-
-.cookie-banner-category {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 0;
- border-bottom: 1px solid ${borderColor};
-}
-
-.cookie-banner-category:last-child {
- border-bottom: none;
-}
-
-.cookie-banner-category-info {
- flex: 1;
-}
-
-.cookie-banner-category-name {
- font-weight: 500;
- font-size: 14px;
-}
-
-.cookie-banner-category-desc {
- font-size: 12px;
- opacity: 0.7;
- margin-top: 4px;
-}
-
-.cookie-banner-toggle {
- position: relative;
- width: 48px;
- height: 28px;
- background: ${borderColor};
- border-radius: 14px;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.cookie-banner-toggle.active {
- background: ${styling.primaryColor};
-}
-
-.cookie-banner-toggle.disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.cookie-banner-toggle::after {
- content: '';
- position: absolute;
- top: 4px;
- left: 4px;
- width: 20px;
- height: 20px;
- background: white;
- border-radius: 50%;
- transition: all 0.2s ease;
-}
-
-.cookie-banner-toggle.active::after {
- left: 24px;
-}
-
-@media (max-width: 640px) {
- .cookie-banner {
- margin: 0;
- border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
- max-width: 100%;
- }
-
- .cookie-banner-buttons {
- flex-direction: column;
- }
-
- .cookie-banner-btn {
- width: 100%;
- }
-}
-`.trim()
-}
-
-/**
- * Generiert das HTML fuer den Cookie Banner
- */
-function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
- const categoriesHTML = config.categories
- .map((cat) => {
- const isRequired = cat.isRequired
- return `
-
-
-
${cat.name.de}
-
${cat.description.de}
-
-
-
- `
- })
- .join('')
-
- return `
-
-
-
${config.texts.title.de}
-
${config.texts.description.de}
-
-
-
-
-
-
-
-
- ${categoriesHTML}
-
-
-
-
-
-
- ${config.texts.privacyPolicyLink.de}
-
-
-`.trim()
-}
-
-/**
- * Generiert das JavaScript fuer den Cookie Banner
- */
-function generateJS(config: CookieBannerConfig): string {
- const categoryIds = config.categories.map((c) => c.id)
- const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
-
- return `
-(function() {
- 'use strict';
-
- const COOKIE_NAME = 'cookie_consent';
- const COOKIE_EXPIRY_DAYS = 365;
- const CATEGORIES = ${JSON.stringify(categoryIds)};
- const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
-
- // Get consent from cookie
- function getConsent() {
- const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
- if (!cookie) return null;
- try {
- return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
- } catch {
- return null;
- }
- }
-
- // Save consent to cookie
- function saveConsent(consent) {
- const date = new Date();
- date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
- document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
- ';expires=' + date.toUTCString() +
- ';path=/;SameSite=Lax';
-
- // Dispatch event
- window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
- }
-
- // Check if category is consented
- function hasConsent(category) {
- const consent = getConsent();
- if (!consent) return REQUIRED_CATEGORIES.includes(category);
- return consent[category] === true;
- }
-
- // Initialize banner
- function initBanner() {
- const banner = document.getElementById('cookieBanner');
- const overlay = document.getElementById('cookieBannerOverlay');
- const details = document.getElementById('cookieBannerDetails');
-
- if (!banner) return;
-
- const consent = getConsent();
- if (consent) {
- // User has already consented
- return;
- }
-
- // Show banner
- setTimeout(() => {
- banner.classList.add('active');
- overlay.classList.add('active');
- }, 500);
-
- // Accept all
- document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
- const consent = {};
- CATEGORIES.forEach(cat => consent[cat] = true);
- saveConsent(consent);
- closeBanner();
- });
-
- // Reject all (only essential)
- document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
- const consent = {};
- CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
- saveConsent(consent);
- closeBanner();
- });
-
- // Customize
- document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
- details.classList.toggle('active');
- });
-
- // Save selection
- document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
- const consent = {};
- CATEGORIES.forEach(cat => {
- const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
- consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
- });
- saveConsent(consent);
- closeBanner();
- });
-
- // Toggle handlers
- document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
- if (toggle.dataset.required === 'true') return;
- toggle.addEventListener('click', () => {
- toggle.classList.toggle('active');
- });
- });
-
- // Close on overlay click
- overlay?.addEventListener('click', () => {
- // Don't close - user must make a choice
- });
- }
-
- function closeBanner() {
- const banner = document.getElementById('cookieBanner');
- const overlay = document.getElementById('cookieBannerOverlay');
- banner?.classList.remove('active');
- overlay?.classList.remove('active');
- }
-
- // Expose API
- window.CookieConsent = {
- getConsent,
- saveConsent,
- hasConsent,
- show: () => {
- document.getElementById('cookieBanner')?.classList.add('active');
- document.getElementById('cookieBannerOverlay')?.classList.add('active');
- },
- hide: closeBanner
- };
-
- // Initialize on DOM ready
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initBanner);
- } else {
- initBanner();
- }
-})();
-`.trim()
-}
-
-// Note: All exports are defined inline with 'export const' and 'export function'
+export {
+ DEFAULT_COOKIE_BANNER_TEXTS,
+ DEFAULT_COOKIE_BANNER_STYLING,
+ generateCookieCategories,
+ generateCookieBannerConfig,
+} from './cookie-banner-config'
+
+export {
+ generateEmbedCode,
+} from './cookie-banner-embed'
diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts
new file mode 100644
index 0000000..9c7b1f3
--- /dev/null
+++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts
@@ -0,0 +1,322 @@
+/**
+ * Privacy Policy Renderers & Main Generator
+ *
+ * Cookies section, changes section, rendering (HTML/Markdown),
+ * and the main generatePrivacyPolicy entry point.
+ */
+
+import {
+ DataPoint,
+ CompanyInfo,
+ PrivacyPolicySection,
+ GeneratedPrivacyPolicy,
+ SupportedLanguage,
+ ExportFormat,
+ LocalizedText,
+} from '../types'
+import { RETENTION_MATRIX } from '../catalog/loader'
+
+import {
+ formatDate,
+ generateControllerSection,
+ generateDataCollectionSection,
+ generatePurposesSection,
+ generateLegalBasisSection,
+ generateRecipientsSection,
+ generateRetentionSection,
+ generateSpecialCategoriesSection,
+ generateRightsSection,
+} from './privacy-policy-sections'
+
+// =============================================================================
+// HELPER
+// =============================================================================
+
+function t(text: LocalizedText, language: SupportedLanguage): string {
+ return text[language]
+}
+
+// =============================================================================
+// SECTION GENERATORS (cookies + changes)
+// =============================================================================
+
+export function generateCookiesSection(
+ dataPoints: DataPoint[],
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '8. Cookies und aehnliche Technologien',
+ en: '8. Cookies and Similar Technologies',
+ }
+
+ const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
+
+ if (cookieDataPoints.length === 0) {
+ const content: LocalizedText = {
+ de: 'Wir verwenden auf dieser Website keine Cookies.',
+ en: 'We do not use cookies on this website.',
+ }
+ return {
+ id: 'cookies',
+ order: 8,
+ title,
+ content,
+ dataPointIds: [],
+ isRequired: false,
+ isGenerated: false,
+ }
+ }
+
+ const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
+ const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
+ const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
+ const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
+
+ const sections: string[] = []
+
+ if (essential.length > 0) {
+ const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
+ sections.push(
+ language === 'de'
+ ? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
+ : `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
+ )
+ }
+
+ if (performance.length > 0) {
+ const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
+ sections.push(
+ language === 'de'
+ ? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
+ : `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
+ )
+ }
+
+ if (personalization.length > 0) {
+ const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
+ sections.push(
+ language === 'de'
+ ? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
+ : `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
+ )
+ }
+
+ if (externalMedia.length > 0) {
+ const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
+ sections.push(
+ language === 'de'
+ ? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
+ : `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
+ )
+ }
+
+ const intro: LocalizedText = {
+ de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
+ en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
+ }
+
+ const content: LocalizedText = {
+ de: `${intro.de}\n\n${sections.join('\n\n')}`,
+ en: `${intro.en}\n\n${sections.join('\n\n')}`,
+ }
+
+ return {
+ id: 'cookies',
+ order: 8,
+ title,
+ content,
+ dataPointIds: cookieDataPoints.map((dp) => dp.id),
+ isRequired: true,
+ isGenerated: true,
+ }
+}
+
+export function generateChangesSection(
+ version: string,
+ date: Date,
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '9. Aenderungen dieser Datenschutzerklaerung',
+ en: '9. Changes to this Privacy Policy',
+ }
+
+ const formattedDate = formatDate(date, language)
+
+ const content: LocalizedText = {
+ de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
+
+Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
+
+Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
+ en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
+
+We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
+
+The new privacy policy will then apply for your next visit.`,
+ }
+
+ return {
+ id: 'changes',
+ order: 9,
+ title,
+ content,
+ dataPointIds: [],
+ isRequired: true,
+ isGenerated: false,
+ }
+}
+
+// =============================================================================
+// MAIN GENERATOR FUNCTIONS
+// =============================================================================
+
+export function generatePrivacyPolicySections(
+ dataPoints: DataPoint[],
+ companyInfo: CompanyInfo,
+ language: SupportedLanguage,
+ version: string = '1.0.0'
+): PrivacyPolicySection[] {
+ const now = new Date()
+
+ const sections: PrivacyPolicySection[] = [
+ generateControllerSection(companyInfo, language),
+ generateDataCollectionSection(dataPoints, language),
+ generatePurposesSection(dataPoints, language),
+ generateLegalBasisSection(dataPoints, language),
+ generateRecipientsSection(dataPoints, language),
+ generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
+ ]
+
+ const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
+ if (specialCategoriesSection) {
+ sections.push(specialCategoriesSection)
+ }
+
+ sections.push(
+ generateRightsSection(language),
+ generateCookiesSection(dataPoints, language),
+ generateChangesSection(version, now, language)
+ )
+
+ sections.forEach((section, index) => {
+ section.order = index + 1
+ const titleDe = section.title.de
+ const titleEn = section.title.en
+ if (titleDe.match(/^\d+[a-z]?\./)) {
+ section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
+ }
+ if (titleEn.match(/^\d+[a-z]?\./)) {
+ section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
+ }
+ })
+
+ return sections
+}
+
+export function generatePrivacyPolicy(
+ tenantId: string,
+ dataPoints: DataPoint[],
+ companyInfo: CompanyInfo,
+ language: SupportedLanguage,
+ format: ExportFormat = 'HTML'
+): GeneratedPrivacyPolicy {
+ const version = '1.0.0'
+ const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
+ const content = renderPrivacyPolicy(sections, language, format)
+
+ return {
+ id: `privacy-policy-${tenantId}-${Date.now()}`,
+ tenantId,
+ language,
+ sections,
+ companyInfo,
+ generatedAt: new Date(),
+ version,
+ format,
+ content,
+ }
+}
+
+// =============================================================================
+// RENDERERS
+// =============================================================================
+
+function renderPrivacyPolicy(
+ sections: PrivacyPolicySection[],
+ language: SupportedLanguage,
+ format: ExportFormat
+): string {
+ switch (format) {
+ case 'HTML':
+ return renderAsHTML(sections, language)
+ case 'MARKDOWN':
+ return renderAsMarkdown(sections, language)
+ default:
+ return renderAsMarkdown(sections, language)
+ }
+}
+
+export function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
+ const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
+
+ const sectionsHTML = sections
+ .map((section) => {
+ const content = t(section.content, language)
+ .replace(/\n\n/g, '
')
+ .replace(/\n/g, '
')
+ .replace(/### (.+)/g, '
$1
')
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ .replace(/- (.+)(?:
|$)/g, '$1')
+
+ return `
+
+ ${t(section.title, language)}
+ ${content}
+
+ `
+ })
+ .join('\n')
+
+ return `
+
+
+
+
+ ${title}
+
+
+
+ ${title}
+ ${sectionsHTML}
+
+`
+}
+
+export function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
+ const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
+
+ const sectionsMarkdown = sections
+ .map((section) => {
+ return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
+ })
+ .join('\n\n---\n\n')
+
+ return `# ${title}\n\n${sectionsMarkdown}`
+}
diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts
new file mode 100644
index 0000000..689dda2
--- /dev/null
+++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts
@@ -0,0 +1,559 @@
+/**
+ * Privacy Policy Section Generators
+ *
+ * Generiert die 9 Abschnitte der Datenschutzerklaerung (DSI)
+ * aus dem Datenpunktkatalog.
+ */
+
+import {
+ DataPoint,
+ DataPointCategory,
+ CompanyInfo,
+ PrivacyPolicySection,
+ SupportedLanguage,
+ LocalizedText,
+ RetentionMatrixEntry,
+ LegalBasis,
+ CATEGORY_METADATA,
+ LEGAL_BASIS_INFO,
+ RETENTION_PERIOD_INFO,
+} from '../types'
+
+// =============================================================================
+// KONSTANTEN
+// =============================================================================
+
+const ALL_CATEGORIES: DataPointCategory[] = [
+ 'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT',
+ 'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION',
+ 'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA',
+ 'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA',
+ 'AI_DATA', 'SECURITY',
+]
+
+const ALL_LEGAL_BASES: LegalBasis[] = [
+ 'CONTRACT', 'CONSENT', 'EXPLICIT_CONSENT', 'LEGITIMATE_INTEREST',
+ 'LEGAL_OBLIGATION', 'VITAL_INTERESTS', 'PUBLIC_INTEREST',
+]
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+function t(text: LocalizedText, language: SupportedLanguage): string {
+ return text[language]
+}
+
+function groupByCategory(dataPoints: DataPoint[]): Map {
+ const grouped = new Map()
+ for (const dp of dataPoints) {
+ const existing = grouped.get(dp.category) || []
+ grouped.set(dp.category, [...existing, dp])
+ }
+ return grouped
+}
+
+function groupByLegalBasis(dataPoints: DataPoint[]): Map {
+ const grouped = new Map()
+ for (const dp of dataPoints) {
+ const existing = grouped.get(dp.legalBasis) || []
+ grouped.set(dp.legalBasis, [...existing, dp])
+ }
+ return grouped
+}
+
+function extractThirdParties(dataPoints: DataPoint[]): string[] {
+ const thirdParties = new Set()
+ for (const dp of dataPoints) {
+ for (const recipient of dp.thirdPartyRecipients) {
+ thirdParties.add(recipient)
+ }
+ }
+ return Array.from(thirdParties).sort()
+}
+
+export function formatDate(date: Date, language: SupportedLanguage): string {
+ return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+}
+
+// =============================================================================
+// SECTION GENERATORS
+// =============================================================================
+
+export function generateControllerSection(
+ companyInfo: CompanyInfo,
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '1. Verantwortlicher',
+ en: '1. Data Controller',
+ }
+
+ const dpoSection = companyInfo.dpoName
+ ? language === 'de'
+ ? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
+ : `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
+ : ''
+
+ const content: LocalizedText = {
+ de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
+
+**${companyInfo.name}**
+${companyInfo.address}
+${companyInfo.postalCode} ${companyInfo.city}
+${companyInfo.country}
+
+E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
+ en: `The controller responsible for data processing on this website is:
+
+**${companyInfo.name}**
+${companyInfo.address}
+${companyInfo.postalCode} ${companyInfo.city}
+${companyInfo.country}
+
+Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
+ }
+
+ return {
+ id: 'controller',
+ order: 1,
+ title,
+ content,
+ dataPointIds: [],
+ isRequired: true,
+ isGenerated: false,
+ }
+}
+
+export function generateDataCollectionSection(
+ dataPoints: DataPoint[],
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '2. Erhobene personenbezogene Daten',
+ en: '2. Personal Data We Collect',
+ }
+
+ const grouped = groupByCategory(dataPoints)
+ const sections: string[] = []
+ const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
+
+ for (const category of ALL_CATEGORIES) {
+ const categoryData = grouped.get(category)
+ if (!categoryData || categoryData.length === 0) continue
+
+ const categoryMeta = CATEGORY_METADATA[category]
+ if (!categoryMeta) continue
+
+ const categoryTitle = t(categoryMeta.name, language)
+
+ let categoryNote = ''
+ if (category === 'HEALTH_DATA') {
+ categoryNote = language === 'de'
+ ? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
+ : `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
+ } else if (category === 'EMPLOYEE_DATA') {
+ categoryNote = language === 'de'
+ ? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
+ : `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
+ } else if (category === 'AI_DATA') {
+ categoryNote = language === 'de'
+ ? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
+ : `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
+ }
+
+ const dataList = categoryData
+ .map((dp) => {
+ const specialTag = dp.isSpecialCategory
+ ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
+ : ''
+ return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
+ })
+ .join('\n')
+
+ sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
+ }
+
+ const intro: LocalizedText = {
+ de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
+ en: 'We collect and process the following personal data:',
+ }
+
+ const specialCategoryNote: LocalizedText = hasSpecialCategoryData
+ ? {
+ de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
+ en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
+ }
+ : { de: '', en: '' }
+
+ const content: LocalizedText = {
+ de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
+ en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
+ }
+
+ return {
+ id: 'data-collection',
+ order: 2,
+ title,
+ content,
+ dataPointIds: dataPoints.map((dp) => dp.id),
+ isRequired: true,
+ isGenerated: true,
+ }
+}
+
+export function generatePurposesSection(
+ dataPoints: DataPoint[],
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '3. Zwecke der Datenverarbeitung',
+ en: '3. Purposes of Data Processing',
+ }
+
+ const purposes = new Map()
+ for (const dp of dataPoints) {
+ const purpose = t(dp.purpose, language)
+ const existing = purposes.get(purpose) || []
+ purposes.set(purpose, [...existing, dp])
+ }
+
+ const purposeList = Array.from(purposes.entries())
+ .map(([purpose, dps]) => {
+ const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
+ return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
+ })
+ .join('\n\n')
+
+ const content: LocalizedText = {
+ de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
+ en: `We process your personal data for the following purposes:\n\n${purposeList}`,
+ }
+
+ return {
+ id: 'purposes',
+ order: 3,
+ title,
+ content,
+ dataPointIds: dataPoints.map((dp) => dp.id),
+ isRequired: true,
+ isGenerated: true,
+ }
+}
+
+export function generateLegalBasisSection(
+ dataPoints: DataPoint[],
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '4. Rechtsgrundlagen der Verarbeitung',
+ en: '4. Legal Basis for Processing',
+ }
+
+ const grouped = groupByLegalBasis(dataPoints)
+ const sections: string[] = []
+
+ for (const basis of ALL_LEGAL_BASES) {
+ const basisData = grouped.get(basis)
+ if (!basisData || basisData.length === 0) continue
+
+ const basisInfo = LEGAL_BASIS_INFO[basis]
+ if (!basisInfo) continue
+
+ const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
+ const basisDesc = t(basisInfo.description, language)
+
+ let additionalWarning = ''
+ if (basis === 'EXPLICIT_CONSENT') {
+ additionalWarning = language === 'de'
+ ? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
+ : `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
+ }
+
+ const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
+ const dataList = basisData
+ .map((dp) => {
+ const specialTag = dp.isSpecialCategory
+ ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
+ : ''
+ return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
+ })
+ .join('\n')
+
+ sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
+ }
+
+ const content: LocalizedText = {
+ de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
+ en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
+ }
+
+ return {
+ id: 'legal-basis',
+ order: 4,
+ title,
+ content,
+ dataPointIds: dataPoints.map((dp) => dp.id),
+ isRequired: true,
+ isGenerated: true,
+ }
+}
+
+export function generateRecipientsSection(
+ dataPoints: DataPoint[],
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '5. Empfaenger und Datenweitergabe',
+ en: '5. Recipients and Data Sharing',
+ }
+
+ const thirdParties = extractThirdParties(dataPoints)
+
+ if (thirdParties.length === 0) {
+ const content: LocalizedText = {
+ de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
+ en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
+ }
+ return {
+ id: 'recipients',
+ order: 5,
+ title,
+ content,
+ dataPointIds: [],
+ isRequired: true,
+ isGenerated: false,
+ }
+ }
+
+ const recipientDetails = new Map()
+ for (const dp of dataPoints) {
+ for (const recipient of dp.thirdPartyRecipients) {
+ const existing = recipientDetails.get(recipient) || []
+ recipientDetails.set(recipient, [...existing, dp])
+ }
+ }
+
+ const recipientList = Array.from(recipientDetails.entries())
+ .map(([recipient, dps]) => {
+ const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
+ return `- **${recipient}**: ${dataNames}`
+ })
+ .join('\n')
+
+ const content: LocalizedText = {
+ de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
+ en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
+ }
+
+ return {
+ id: 'recipients',
+ order: 5,
+ title,
+ content,
+ dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
+ isRequired: true,
+ isGenerated: true,
+ }
+}
+
+export function generateRetentionSection(
+ dataPoints: DataPoint[],
+ retentionMatrix: RetentionMatrixEntry[],
+ language: SupportedLanguage
+): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '6. Speicherdauer',
+ en: '6. Data Retention',
+ }
+
+ const grouped = groupByCategory(dataPoints)
+ const sections: string[] = []
+
+ for (const entry of retentionMatrix) {
+ const categoryData = grouped.get(entry.category)
+ if (!categoryData || categoryData.length === 0) continue
+
+ const categoryName = t(entry.categoryName, language)
+ const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
+
+ const dataRetention = categoryData
+ .map((dp) => {
+ const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
+ return `- ${t(dp.name, language)}: ${period}`
+ })
+ .join('\n')
+
+ sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
+ }
+
+ const content: LocalizedText = {
+ de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
+ en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
+ }
+
+ return {
+ id: 'retention',
+ order: 6,
+ title,
+ content,
+ dataPointIds: dataPoints.map((dp) => dp.id),
+ isRequired: true,
+ isGenerated: true,
+ }
+}
+
+export function generateSpecialCategoriesSection(
+ dataPoints: DataPoint[],
+ language: SupportedLanguage
+): PrivacyPolicySection | null {
+ const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
+
+ if (specialCategoryDataPoints.length === 0) {
+ return null
+ }
+
+ const title: LocalizedText = {
+ de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
+ en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
+ }
+
+ const dataList = specialCategoryDataPoints
+ .map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
+ .join('\n')
+
+ const content: LocalizedText = {
+ de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
+
+${dataList}
+
+### Ihre ausdrueckliche Einwilligung
+
+Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
+
+### Ihre Rechte bei Art. 9 Daten
+
+- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
+- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
+- Bei Widerruf werden Ihre Daten unverzueglich geloescht
+- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
+
+### Besondere Schutzmassnahmen
+
+Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
+- Ende-zu-Ende-Verschluesselung
+- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
+- Audit-Logging aller Zugriffe
+- Regelmaessige Datenschutz-Folgenabschaetzungen`,
+ en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
+
+${dataList}
+
+### Your Explicit Consent
+
+Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
+
+### Your Rights Regarding Art. 9 Data
+
+- You can **withdraw your consent at any time**
+- Withdrawal does not affect the lawfulness of previous processing
+- Upon withdrawal, your data will be deleted immediately
+- You have the right to **access, rectification, and erasure**
+
+### Special Protection Measures
+
+For this sensitive data, we have implemented special technical and organizational measures:
+- End-to-end encryption
+- Strict access control (need-to-know principle)
+- Audit logging of all access
+- Regular data protection impact assessments`,
+ }
+
+ return {
+ id: 'special-categories',
+ order: 6.5,
+ title,
+ content,
+ dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
+ isRequired: false,
+ isGenerated: true,
+ }
+}
+
+export function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
+ const title: LocalizedText = {
+ de: '7. Ihre Rechte als betroffene Person',
+ en: '7. Your Rights as a Data Subject',
+ }
+
+ const content: LocalizedText = {
+ de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
+
+### Auskunftsrecht (Art. 15 DSGVO)
+Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
+
+### Recht auf Berichtigung (Art. 16 DSGVO)
+Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
+
+### Recht auf Loeschung (Art. 17 DSGVO)
+Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
+
+### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
+Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
+
+### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
+Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
+
+### Widerspruchsrecht (Art. 21 DSGVO)
+Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
+
+### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
+Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
+
+### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
+Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
+
+**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
+ en: `You have the following rights regarding your personal data:
+
+### Right of Access (Art. 15 GDPR)
+You have the right to request information about the personal data we process about you.
+
+### Right to Rectification (Art. 16 GDPR)
+You have the right to request the correction of inaccurate data or the completion of incomplete data.
+
+### Right to Erasure (Art. 17 GDPR)
+You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
+
+### Right to Restriction of Processing (Art. 18 GDPR)
+You have the right to request the restriction of processing of your data.
+
+### Right to Data Portability (Art. 20 GDPR)
+You have the right to receive your data in a structured, commonly used, and machine-readable format.
+
+### Right to Object (Art. 21 GDPR)
+You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
+
+### Right to Withdraw Consent (Art. 7(3) GDPR)
+You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
+
+### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
+You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
+
+**To exercise your rights, please contact us using the contact details provided above.**`,
+ }
+
+ return {
+ id: 'rights',
+ order: 7,
+ title,
+ content,
+ dataPointIds: [],
+ isRequired: true,
+ isGenerated: false,
+ }
+}
diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts
index 83fbeb8..0e859ec 100644
--- a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts
+++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts
@@ -1,954 +1,11 @@
/**
- * Privacy Policy Generator
+ * Privacy Policy Generator — barrel re-export
*
- * Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog.
- * Die DSI wird aus 9 Abschnitten generiert:
- *
- * 1. Verantwortlicher (companyInfo)
- * 2. Erhobene Daten (dataPoints nach Kategorie)
- * 3. Verarbeitungszwecke (dataPoints.purpose)
- * 4. Rechtsgrundlagen (dataPoints.legalBasis)
- * 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients)
- * 6. Speicherdauer (retentionMatrix)
- * 7. Betroffenenrechte (statischer Text + Links)
- * 8. Cookies (cookieCategory-basiert)
- * 9. Aenderungen (statischer Text + Versionierung)
+ * Split into:
+ * - privacy-policy-sections.ts (section generators 1-7)
+ * - privacy-policy-renderers.ts (sections 8-9, renderers, main generator)
*/
-import {
- DataPoint,
- DataPointCategory,
- CompanyInfo,
- PrivacyPolicySection,
- GeneratedPrivacyPolicy,
- SupportedLanguage,
- ExportFormat,
- LocalizedText,
- RetentionMatrixEntry,
- LegalBasis,
- CATEGORY_METADATA,
- LEGAL_BASIS_INFO,
- RETENTION_PERIOD_INFO,
- ARTICLE_9_WARNING,
-} from '../types'
-import { RETENTION_MATRIX } from '../catalog/loader'
-
-// =============================================================================
-// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge
-// =============================================================================
-
-const ALL_CATEGORIES: DataPointCategory[] = [
- 'MASTER_DATA', // A
- 'CONTACT_DATA', // B
- 'AUTHENTICATION', // C
- 'CONSENT', // D
- 'COMMUNICATION', // E
- 'PAYMENT', // F
- 'USAGE_DATA', // G
- 'LOCATION', // H
- 'DEVICE_DATA', // I
- 'MARKETING', // J
- 'ANALYTICS', // K
- 'SOCIAL_MEDIA', // L
- 'HEALTH_DATA', // M - Art. 9 DSGVO
- 'EMPLOYEE_DATA', // N - BDSG § 26
- 'CONTRACT_DATA', // O
- 'LOG_DATA', // P
- 'AI_DATA', // Q - AI Act
- 'SECURITY', // R
-]
-
-// Alle Rechtsgrundlagen in der richtigen Reihenfolge
-const ALL_LEGAL_BASES: LegalBasis[] = [
- 'CONTRACT',
- 'CONSENT',
- 'EXPLICIT_CONSENT',
- 'LEGITIMATE_INTEREST',
- 'LEGAL_OBLIGATION',
- 'VITAL_INTERESTS',
- 'PUBLIC_INTEREST',
-]
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-/**
- * Holt den lokalisierten Text
- */
-function t(text: LocalizedText, language: SupportedLanguage): string {
- return text[language]
-}
-
-/**
- * Gruppiert Datenpunkte nach Kategorie
- */
-function groupByCategory(dataPoints: DataPoint[]): Map {
- const grouped = new Map()
- for (const dp of dataPoints) {
- const existing = grouped.get(dp.category) || []
- grouped.set(dp.category, [...existing, dp])
- }
- return grouped
-}
-
-/**
- * Gruppiert Datenpunkte nach Rechtsgrundlage
- */
-function groupByLegalBasis(dataPoints: DataPoint[]): Map {
- const grouped = new Map()
- for (const dp of dataPoints) {
- const existing = grouped.get(dp.legalBasis) || []
- grouped.set(dp.legalBasis, [...existing, dp])
- }
- return grouped
-}
-
-/**
- * Extrahiert alle einzigartigen Drittanbieter
- */
-function extractThirdParties(dataPoints: DataPoint[]): string[] {
- const thirdParties = new Set()
- for (const dp of dataPoints) {
- for (const recipient of dp.thirdPartyRecipients) {
- thirdParties.add(recipient)
- }
- }
- return Array.from(thirdParties).sort()
-}
-
-/**
- * Formatiert ein Datum fuer die Anzeige
- */
-function formatDate(date: Date, language: SupportedLanguage): string {
- return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })
-}
-
-// =============================================================================
-// SECTION GENERATORS
-// =============================================================================
-
-/**
- * Abschnitt 1: Verantwortlicher
- */
-function generateControllerSection(
- companyInfo: CompanyInfo,
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '1. Verantwortlicher',
- en: '1. Data Controller',
- }
-
- const dpoSection = companyInfo.dpoName
- ? language === 'de'
- ? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
- : `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
- : ''
-
- const content: LocalizedText = {
- de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
-
-**${companyInfo.name}**
-${companyInfo.address}
-${companyInfo.postalCode} ${companyInfo.city}
-${companyInfo.country}
-
-E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
- en: `The controller responsible for data processing on this website is:
-
-**${companyInfo.name}**
-${companyInfo.address}
-${companyInfo.postalCode} ${companyInfo.city}
-${companyInfo.country}
-
-Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
- }
-
- return {
- id: 'controller',
- order: 1,
- title,
- content,
- dataPointIds: [],
- isRequired: true,
- isGenerated: false,
- }
-}
-
-/**
- * Abschnitt 2: Erhobene Daten (18 Kategorien)
- */
-function generateDataCollectionSection(
- dataPoints: DataPoint[],
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '2. Erhobene personenbezogene Daten',
- en: '2. Personal Data We Collect',
- }
-
- const grouped = groupByCategory(dataPoints)
- const sections: string[] = []
-
- // Prüfe ob Art. 9 Daten enthalten sind
- const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
-
- for (const category of ALL_CATEGORIES) {
- const categoryData = grouped.get(category)
- if (!categoryData || categoryData.length === 0) continue
-
- const categoryMeta = CATEGORY_METADATA[category]
- if (!categoryMeta) continue
-
- const categoryTitle = t(categoryMeta.name, language)
-
- // Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten)
- let categoryNote = ''
- if (category === 'HEALTH_DATA') {
- categoryNote = language === 'de'
- ? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
- : `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
- } else if (category === 'EMPLOYEE_DATA') {
- categoryNote = language === 'de'
- ? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
- : `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
- } else if (category === 'AI_DATA') {
- categoryNote = language === 'de'
- ? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
- : `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
- }
-
- const dataList = categoryData
- .map((dp) => {
- const specialTag = dp.isSpecialCategory
- ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
- : ''
- return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
- })
- .join('\n')
-
- sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
- }
-
- const intro: LocalizedText = {
- de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
- en: 'We collect and process the following personal data:',
- }
-
- // Zusätzlicher Hinweis für Art. 9 Daten
- const specialCategoryNote: LocalizedText = hasSpecialCategoryData
- ? {
- de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
- en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
- }
- : { de: '', en: '' }
-
- const content: LocalizedText = {
- de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
- en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
- }
-
- return {
- id: 'data-collection',
- order: 2,
- title,
- content,
- dataPointIds: dataPoints.map((dp) => dp.id),
- isRequired: true,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 3: Verarbeitungszwecke
- */
-function generatePurposesSection(
- dataPoints: DataPoint[],
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '3. Zwecke der Datenverarbeitung',
- en: '3. Purposes of Data Processing',
- }
-
- // Gruppiere nach Zweck (unique purposes)
- const purposes = new Map()
- for (const dp of dataPoints) {
- const purpose = t(dp.purpose, language)
- const existing = purposes.get(purpose) || []
- purposes.set(purpose, [...existing, dp])
- }
-
- const purposeList = Array.from(purposes.entries())
- .map(([purpose, dps]) => {
- const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
- return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
- })
- .join('\n\n')
-
- const content: LocalizedText = {
- de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
- en: `We process your personal data for the following purposes:\n\n${purposeList}`,
- }
-
- return {
- id: 'purposes',
- order: 3,
- title,
- content,
- dataPointIds: dataPoints.map((dp) => dp.id),
- isRequired: true,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen)
- */
-function generateLegalBasisSection(
- dataPoints: DataPoint[],
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '4. Rechtsgrundlagen der Verarbeitung',
- en: '4. Legal Basis for Processing',
- }
-
- const grouped = groupByLegalBasis(dataPoints)
- const sections: string[] = []
-
- // Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge
- for (const basis of ALL_LEGAL_BASES) {
- const basisData = grouped.get(basis)
- if (!basisData || basisData.length === 0) continue
-
- const basisInfo = LEGAL_BASIS_INFO[basis]
- if (!basisInfo) continue
-
- const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
- const basisDesc = t(basisInfo.description, language)
-
- // Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen
- let additionalWarning = ''
- if (basis === 'EXPLICIT_CONSENT') {
- additionalWarning = language === 'de'
- ? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
- : `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
- }
-
- const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
- const dataList = basisData
- .map((dp) => {
- const specialTag = dp.isSpecialCategory
- ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
- : ''
- return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
- })
- .join('\n')
-
- sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
- }
-
- const content: LocalizedText = {
- de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
- en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
- }
-
- return {
- id: 'legal-basis',
- order: 4,
- title,
- content,
- dataPointIds: dataPoints.map((dp) => dp.id),
- isRequired: true,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 5: Empfaenger / Dritte
- */
-function generateRecipientsSection(
- dataPoints: DataPoint[],
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '5. Empfaenger und Datenweitergabe',
- en: '5. Recipients and Data Sharing',
- }
-
- const thirdParties = extractThirdParties(dataPoints)
-
- if (thirdParties.length === 0) {
- const content: LocalizedText = {
- de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
- en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
- }
- return {
- id: 'recipients',
- order: 5,
- title,
- content,
- dataPointIds: [],
- isRequired: true,
- isGenerated: false,
- }
- }
-
- // Gruppiere nach Drittanbieter
- const recipientDetails = new Map()
- for (const dp of dataPoints) {
- for (const recipient of dp.thirdPartyRecipients) {
- const existing = recipientDetails.get(recipient) || []
- recipientDetails.set(recipient, [...existing, dp])
- }
- }
-
- const recipientList = Array.from(recipientDetails.entries())
- .map(([recipient, dps]) => {
- const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
- return `- **${recipient}**: ${dataNames}`
- })
- .join('\n')
-
- const content: LocalizedText = {
- de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
- en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
- }
-
- return {
- id: 'recipients',
- order: 5,
- title,
- content,
- dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
- isRequired: true,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 6: Speicherdauer
- */
-function generateRetentionSection(
- dataPoints: DataPoint[],
- retentionMatrix: RetentionMatrixEntry[],
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '6. Speicherdauer',
- en: '6. Data Retention',
- }
-
- const grouped = groupByCategory(dataPoints)
- const sections: string[] = []
-
- for (const entry of retentionMatrix) {
- const categoryData = grouped.get(entry.category)
- if (!categoryData || categoryData.length === 0) continue
-
- const categoryName = t(entry.categoryName, language)
- const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
-
- const dataRetention = categoryData
- .map((dp) => {
- const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
- return `- ${t(dp.name, language)}: ${period}`
- })
- .join('\n')
-
- sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
- }
-
- const content: LocalizedText = {
- de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
- en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
- }
-
- return {
- id: 'retention',
- order: 6,
- title,
- content,
- dataPointIds: dataPoints.map((dp) => dp.id),
- isRequired: true,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO)
- * Wird nur generiert, wenn Art. 9 Daten vorhanden sind
- */
-function generateSpecialCategoriesSection(
- dataPoints: DataPoint[],
- language: SupportedLanguage
-): PrivacyPolicySection | null {
- // Filtere Art. 9 Datenpunkte
- const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
-
- if (specialCategoryDataPoints.length === 0) {
- return null
- }
-
- const title: LocalizedText = {
- de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
- en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
- }
-
- const dataList = specialCategoryDataPoints
- .map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
- .join('\n')
-
- const content: LocalizedText = {
- de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
-
-${dataList}
-
-### Ihre ausdrueckliche Einwilligung
-
-Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
-
-### Ihre Rechte bei Art. 9 Daten
-
-- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
-- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
-- Bei Widerruf werden Ihre Daten unverzueglich geloescht
-- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
-
-### Besondere Schutzmassnahmen
-
-Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
-- Ende-zu-Ende-Verschluesselung
-- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
-- Audit-Logging aller Zugriffe
-- Regelmaessige Datenschutz-Folgenabschaetzungen`,
- en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
-
-${dataList}
-
-### Your Explicit Consent
-
-Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
-
-### Your Rights Regarding Art. 9 Data
-
-- You can **withdraw your consent at any time**
-- Withdrawal does not affect the lawfulness of previous processing
-- Upon withdrawal, your data will be deleted immediately
-- You have the right to **access, rectification, and erasure**
-
-### Special Protection Measures
-
-For this sensitive data, we have implemented special technical and organizational measures:
-- End-to-end encryption
-- Strict access control (need-to-know principle)
-- Audit logging of all access
-- Regular data protection impact assessments`,
- }
-
- return {
- id: 'special-categories',
- order: 6.5, // Zwischen Speicherdauer (6) und Rechte (7)
- title,
- content,
- dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
- isRequired: false,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 7: Betroffenenrechte
- */
-function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '7. Ihre Rechte als betroffene Person',
- en: '7. Your Rights as a Data Subject',
- }
-
- const content: LocalizedText = {
- de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
-
-### Auskunftsrecht (Art. 15 DSGVO)
-Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
-
-### Recht auf Berichtigung (Art. 16 DSGVO)
-Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
-
-### Recht auf Loeschung (Art. 17 DSGVO)
-Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
-
-### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
-Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
-
-### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
-Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
-
-### Widerspruchsrecht (Art. 21 DSGVO)
-Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
-
-### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
-Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
-
-### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
-Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
-
-**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
- en: `You have the following rights regarding your personal data:
-
-### Right of Access (Art. 15 GDPR)
-You have the right to request information about the personal data we process about you.
-
-### Right to Rectification (Art. 16 GDPR)
-You have the right to request the correction of inaccurate data or the completion of incomplete data.
-
-### Right to Erasure (Art. 17 GDPR)
-You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
-
-### Right to Restriction of Processing (Art. 18 GDPR)
-You have the right to request the restriction of processing of your data.
-
-### Right to Data Portability (Art. 20 GDPR)
-You have the right to receive your data in a structured, commonly used, and machine-readable format.
-
-### Right to Object (Art. 21 GDPR)
-You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
-
-### Right to Withdraw Consent (Art. 7(3) GDPR)
-You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
-
-### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
-You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
-
-**To exercise your rights, please contact us using the contact details provided above.**`,
- }
-
- return {
- id: 'rights',
- order: 7,
- title,
- content,
- dataPointIds: [],
- isRequired: true,
- isGenerated: false,
- }
-}
-
-/**
- * Abschnitt 8: Cookies
- */
-function generateCookiesSection(
- dataPoints: DataPoint[],
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '8. Cookies und aehnliche Technologien',
- en: '8. Cookies and Similar Technologies',
- }
-
- // Filtere Datenpunkte mit Cookie-Kategorie
- const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
-
- if (cookieDataPoints.length === 0) {
- const content: LocalizedText = {
- de: 'Wir verwenden auf dieser Website keine Cookies.',
- en: 'We do not use cookies on this website.',
- }
- return {
- id: 'cookies',
- order: 8,
- title,
- content,
- dataPointIds: [],
- isRequired: false,
- isGenerated: false,
- }
- }
-
- // Gruppiere nach Cookie-Kategorie
- const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
- const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
- const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
- const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
-
- const sections: string[] = []
-
- if (essential.length > 0) {
- const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
- sections.push(
- language === 'de'
- ? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
- : `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
- )
- }
-
- if (performance.length > 0) {
- const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
- sections.push(
- language === 'de'
- ? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
- : `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
- )
- }
-
- if (personalization.length > 0) {
- const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
- sections.push(
- language === 'de'
- ? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
- : `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
- )
- }
-
- if (externalMedia.length > 0) {
- const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
- sections.push(
- language === 'de'
- ? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
- : `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
- )
- }
-
- const intro: LocalizedText = {
- de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
- en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
- }
-
- const content: LocalizedText = {
- de: `${intro.de}\n\n${sections.join('\n\n')}`,
- en: `${intro.en}\n\n${sections.join('\n\n')}`,
- }
-
- return {
- id: 'cookies',
- order: 8,
- title,
- content,
- dataPointIds: cookieDataPoints.map((dp) => dp.id),
- isRequired: true,
- isGenerated: true,
- }
-}
-
-/**
- * Abschnitt 9: Aenderungen
- */
-function generateChangesSection(
- version: string,
- date: Date,
- language: SupportedLanguage
-): PrivacyPolicySection {
- const title: LocalizedText = {
- de: '9. Aenderungen dieser Datenschutzerklaerung',
- en: '9. Changes to this Privacy Policy',
- }
-
- const formattedDate = formatDate(date, language)
-
- const content: LocalizedText = {
- de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
-
-Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
-
-Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
- en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
-
-We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
-
-The new privacy policy will then apply for your next visit.`,
- }
-
- return {
- id: 'changes',
- order: 9,
- title,
- content,
- dataPointIds: [],
- isRequired: true,
- isGenerated: false,
- }
-}
-
-// =============================================================================
-// MAIN GENERATOR FUNCTIONS
-// =============================================================================
-
-/**
- * Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9)
- */
-export function generatePrivacyPolicySections(
- dataPoints: DataPoint[],
- companyInfo: CompanyInfo,
- language: SupportedLanguage,
- version: string = '1.0.0'
-): PrivacyPolicySection[] {
- const now = new Date()
-
- const sections: PrivacyPolicySection[] = [
- generateControllerSection(companyInfo, language),
- generateDataCollectionSection(dataPoints, language),
- generatePurposesSection(dataPoints, language),
- generateLegalBasisSection(dataPoints, language),
- generateRecipientsSection(dataPoints, language),
- generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
- ]
-
- // Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden
- const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
- if (specialCategoriesSection) {
- sections.push(specialCategoriesSection)
- }
-
- sections.push(
- generateRightsSection(language),
- generateCookiesSection(dataPoints, language),
- generateChangesSection(version, now, language)
- )
-
- // Abschnittsnummern neu vergeben
- sections.forEach((section, index) => {
- section.order = index + 1
- // Titel-Nummer aktualisieren
- const titleDe = section.title.de
- const titleEn = section.title.en
- if (titleDe.match(/^\d+[a-z]?\./)) {
- section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
- }
- if (titleEn.match(/^\d+[a-z]?\./)) {
- section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
- }
- })
-
- return sections
-}
-
-/**
- * Generiert die vollstaendige Privacy Policy
- */
-export function generatePrivacyPolicy(
- tenantId: string,
- dataPoints: DataPoint[],
- companyInfo: CompanyInfo,
- language: SupportedLanguage,
- format: ExportFormat = 'HTML'
-): GeneratedPrivacyPolicy {
- const version = '1.0.0'
- const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
-
- // Generiere den Inhalt
- const content = renderPrivacyPolicy(sections, language, format)
-
- return {
- id: `privacy-policy-${tenantId}-${Date.now()}`,
- tenantId,
- language,
- sections,
- companyInfo,
- generatedAt: new Date(),
- version,
- format,
- content,
- }
-}
-
-/**
- * Rendert die Privacy Policy im gewuenschten Format
- */
-function renderPrivacyPolicy(
- sections: PrivacyPolicySection[],
- language: SupportedLanguage,
- format: ExportFormat
-): string {
- switch (format) {
- case 'HTML':
- return renderAsHTML(sections, language)
- case 'MARKDOWN':
- return renderAsMarkdown(sections, language)
- default:
- return renderAsMarkdown(sections, language)
- }
-}
-
-/**
- * Rendert als HTML
- */
-function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
- const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
-
- const sectionsHTML = sections
- .map((section) => {
- const content = t(section.content, language)
- .replace(/\n\n/g, '')
- .replace(/\n/g, '
')
- .replace(/### (.+)/g, '
$1
')
- .replace(/\*\*(.+?)\*\*/g, '$1')
- .replace(/- (.+)(?:
|$)/g, '$1')
-
- return `
-
- ${t(section.title, language)}
- ${content}
-
- `
- })
- .join('\n')
-
- return `
-
-
-
-
- ${title}
-
-
-
- ${title}
- ${sectionsHTML}
-
-`
-}
-
-/**
- * Rendert als Markdown
- */
-function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
- const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
-
- const sectionsMarkdown = sections
- .map((section) => {
- return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
- })
- .join('\n\n---\n\n')
-
- return `# ${title}\n\n${sectionsMarkdown}`
-}
-
-// =============================================================================
-// EXPORTS
-// =============================================================================
-
export {
generateControllerSection,
generateDataCollectionSection,
@@ -958,8 +15,13 @@ export {
generateRetentionSection,
generateSpecialCategoriesSection,
generateRightsSection,
+} from './privacy-policy-sections'
+
+export {
generateCookiesSection,
generateChangesSection,
+ generatePrivacyPolicySections,
+ generatePrivacyPolicy,
renderAsHTML,
renderAsMarkdown,
-}
+} from './privacy-policy-renderers'
diff --git a/admin-compliance/lib/sdk/tom-generator/gap-analysis.ts b/admin-compliance/lib/sdk/tom-generator/gap-analysis.ts
new file mode 100644
index 0000000..c27f6a1
--- /dev/null
+++ b/admin-compliance/lib/sdk/tom-generator/gap-analysis.ts
@@ -0,0 +1,171 @@
+/**
+ * TOM Rules Engine — Gap Analysis & Helper Functions
+ *
+ * Singleton instance, convenience functions, and gap analysis logic.
+ */
+
+import {
+ DerivedTOM,
+ EvidenceDocument,
+ GapAnalysisResult,
+ MissingControl,
+ PartialControl,
+ MissingEvidence,
+ RulesEngineResult,
+ RulesEngineEvaluationContext,
+} from './types'
+import { getControlById } from './controls/loader'
+import { TOMRulesEngine } from './rules-evaluator'
+
+// =============================================================================
+// SINGLETON INSTANCE
+// =============================================================================
+
+let rulesEngineInstance: TOMRulesEngine | null = null
+
+export function getTOMRulesEngine(): TOMRulesEngine {
+ if (!rulesEngineInstance) {
+ rulesEngineInstance = new TOMRulesEngine()
+ }
+ return rulesEngineInstance
+}
+
+// =============================================================================
+// GAP ANALYSIS
+// =============================================================================
+
+export function performGapAnalysis(
+ derivedTOMs: DerivedTOM[],
+ documents: EvidenceDocument[]
+): GapAnalysisResult {
+ const missingControls: MissingControl[] = []
+ const partialControls: PartialControl[] = []
+ const missingEvidence: MissingEvidence[] = []
+ const recommendations: string[] = []
+
+ let totalScore = 0
+ let totalWeight = 0
+
+ const applicableTOMs = derivedTOMs.filter(
+ (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
+ )
+
+ for (const tom of applicableTOMs) {
+ const control = getControlById(tom.controlId)
+ if (!control) continue
+
+ const weight = tom.applicability === 'REQUIRED' ? 3 : 1
+ totalWeight += weight
+
+ if (tom.implementationStatus === 'NOT_IMPLEMENTED') {
+ missingControls.push({
+ controlId: tom.controlId,
+ reason: `${control.name.de} ist nicht implementiert`,
+ priority: control.priority,
+ })
+ } else if (tom.implementationStatus === 'PARTIAL') {
+ partialControls.push({
+ controlId: tom.controlId,
+ missingAspects: tom.evidenceGaps,
+ })
+ totalScore += weight * 0.5
+ } else {
+ totalScore += weight
+ }
+
+ const linkedEvidenceIds = tom.linkedEvidence
+ const requiredEvidence = control.evidenceRequirements
+ const providedEvidence = documents.filter((doc) =>
+ linkedEvidenceIds.includes(doc.id)
+ )
+
+ if (providedEvidence.length < requiredEvidence.length) {
+ const missing = requiredEvidence.filter(
+ (req) =>
+ !providedEvidence.some(
+ (doc) =>
+ doc.documentType === 'POLICY' ||
+ doc.documentType === 'CERTIFICATE' ||
+ doc.originalName.toLowerCase().includes(req.toLowerCase())
+ )
+ )
+
+ if (missing.length > 0) {
+ missingEvidence.push({
+ controlId: tom.controlId,
+ requiredEvidence: missing,
+ })
+ }
+ }
+ }
+
+ const overallScore =
+ totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0
+
+ if (missingControls.length > 0) {
+ const criticalMissing = missingControls.filter((mc) => mc.priority === 'CRITICAL')
+ if (criticalMissing.length > 0) {
+ recommendations.push(
+ `${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.`
+ )
+ }
+ }
+
+ if (partialControls.length > 0) {
+ recommendations.push(
+ `${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollstaendigen Sie die Implementierung.`
+ )
+ }
+
+ if (missingEvidence.length > 0) {
+ recommendations.push(
+ `Fuer ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.`
+ )
+ }
+
+ if (overallScore >= 80) {
+ recommendations.push(
+ 'Ihr TOM-Compliance-Score ist gut. Fuehren Sie regelmaessige Ueberpruefungen durch.'
+ )
+ } else if (overallScore >= 50) {
+ recommendations.push(
+ 'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Luecken.'
+ )
+ } else {
+ recommendations.push(
+ 'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Ueberarbeitung der Massnahmen wird empfohlen.'
+ )
+ }
+
+ return {
+ overallScore,
+ missingControls,
+ partialControls,
+ missingEvidence,
+ recommendations,
+ generatedAt: new Date(),
+ }
+}
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+export function evaluateControlsForContext(
+ context: RulesEngineEvaluationContext
+): RulesEngineResult[] {
+ return getTOMRulesEngine().evaluateControls(context)
+}
+
+export function deriveTOMsForContext(
+ context: RulesEngineEvaluationContext
+): DerivedTOM[] {
+ return getTOMRulesEngine().deriveAllTOMs(context)
+}
+
+export function performQuickGapAnalysis(
+ derivedTOMs: DerivedTOM[],
+ documents: EvidenceDocument[]
+): GapAnalysisResult {
+ return performGapAnalysis(derivedTOMs, documents)
+}
diff --git a/admin-compliance/lib/sdk/tom-generator/rules-engine.ts b/admin-compliance/lib/sdk/tom-generator/rules-engine.ts
index b1eb99b..05a47ba 100644
--- a/admin-compliance/lib/sdk/tom-generator/rules-engine.ts
+++ b/admin-compliance/lib/sdk/tom-generator/rules-engine.ts
@@ -1,560 +1,17 @@
-// =============================================================================
-// TOM Rules Engine
-// Evaluates control applicability based on company context
-// =============================================================================
-
-import {
- ControlLibraryEntry,
- ApplicabilityCondition,
- ControlApplicability,
- RulesEngineResult,
- RulesEngineEvaluationContext,
- DerivedTOM,
- EvidenceDocument,
- GapAnalysisResult,
- MissingControl,
- PartialControl,
- MissingEvidence,
- ConditionOperator,
-} from './types'
-import { getAllControls, getControlById } from './controls/loader'
-
-// =============================================================================
-// RULES ENGINE CLASS
-// =============================================================================
-
-export class TOMRulesEngine {
- private controls: ControlLibraryEntry[]
-
- constructor() {
- this.controls = getAllControls()
- }
-
- /**
- * Evaluate all controls against the current context
- */
- evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] {
- return this.controls.map((control) => this.evaluateControl(control, context))
- }
-
- /**
- * Evaluate a single control against the context
- */
- evaluateControl(
- control: ControlLibraryEntry,
- context: RulesEngineEvaluationContext
- ): RulesEngineResult {
- // Sort conditions by priority (highest first)
- const sortedConditions = [...control.applicabilityConditions].sort(
- (a, b) => b.priority - a.priority
- )
-
- // Evaluate conditions in priority order
- for (const condition of sortedConditions) {
- const matches = this.evaluateCondition(condition, context)
- if (matches) {
- return {
- controlId: control.id,
- applicability: condition.result,
- reason: this.formatConditionReason(condition, context),
- matchedCondition: condition,
- }
- }
- }
-
- // No condition matched, use default applicability
- return {
- controlId: control.id,
- applicability: control.defaultApplicability,
- reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfüllt)',
- }
- }
-
- /**
- * Evaluate a single condition
- */
- private evaluateCondition(
- condition: ApplicabilityCondition,
- context: RulesEngineEvaluationContext
- ): boolean {
- const value = this.getFieldValue(condition.field, context)
-
- if (value === undefined || value === null) {
- return false
- }
-
- return this.evaluateOperator(condition.operator, value, condition.value)
- }
-
- /**
- * Get a nested field value from the context
- */
- private getFieldValue(
- fieldPath: string,
- context: RulesEngineEvaluationContext
- ): unknown {
- const parts = fieldPath.split('.')
- let current: unknown = context
-
- for (const part of parts) {
- if (current === null || current === undefined) {
- return undefined
- }
- if (typeof current === 'object') {
- current = (current as Record)[part]
- } else {
- return undefined
- }
- }
-
- return current
- }
-
- /**
- * Evaluate an operator with given values
- */
- private evaluateOperator(
- operator: ConditionOperator,
- actualValue: unknown,
- expectedValue: unknown
- ): boolean {
- switch (operator) {
- case 'EQUALS':
- return actualValue === expectedValue
-
- case 'NOT_EQUALS':
- return actualValue !== expectedValue
-
- case 'CONTAINS':
- if (Array.isArray(actualValue)) {
- return actualValue.includes(expectedValue)
- }
- if (typeof actualValue === 'string' && typeof expectedValue === 'string') {
- return actualValue.includes(expectedValue)
- }
- return false
-
- case 'GREATER_THAN':
- if (typeof actualValue === 'number' && typeof expectedValue === 'number') {
- return actualValue > expectedValue
- }
- return false
-
- case 'IN':
- if (Array.isArray(expectedValue)) {
- return expectedValue.includes(actualValue)
- }
- return false
-
- default:
- return false
- }
- }
-
- /**
- * Format a human-readable reason for the condition match
- */
- private formatConditionReason(
- condition: ApplicabilityCondition,
- context: RulesEngineEvaluationContext
- ): string {
- const fieldValue = this.getFieldValue(condition.field, context)
- const fieldLabel = this.getFieldLabel(condition.field)
-
- switch (condition.operator) {
- case 'EQUALS':
- return `${fieldLabel} ist "${this.formatValue(fieldValue)}"`
-
- case 'NOT_EQUALS':
- return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"`
-
- case 'CONTAINS':
- return `${fieldLabel} enthält "${this.formatValue(condition.value)}"`
-
- case 'GREATER_THAN':
- return `${fieldLabel} ist größer als ${this.formatValue(condition.value)}`
-
- case 'IN':
- return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]`
-
- default:
- return `Bedingung erfüllt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}`
- }
- }
-
- /**
- * Get a human-readable label for a field path
- */
- private getFieldLabel(fieldPath: string): string {
- const labels: Record = {
- 'companyProfile.role': 'Unternehmensrolle',
- 'companyProfile.size': 'Unternehmensgröße',
- 'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien',
- 'dataProfile.processesMinors': 'Verarbeitung von Minderjährigen-Daten',
- 'dataProfile.dataVolume': 'Datenvolumen',
- 'dataProfile.thirdCountryTransfers': 'Drittlandübermittlungen',
- 'architectureProfile.hostingModel': 'Hosting-Modell',
- 'architectureProfile.hostingLocation': 'Hosting-Standort',
- 'architectureProfile.multiTenancy': 'Mandantentrennung',
- 'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter',
- 'architectureProfile.encryptionAtRest': 'Verschlüsselung ruhender Daten',
- 'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung',
- 'securityProfile.hasSSO': 'Single Sign-On',
- 'securityProfile.hasPAM': 'Privileged Access Management',
- 'riskProfile.protectionLevel': 'Schutzbedarf',
- 'riskProfile.dsfaRequired': 'DSFA erforderlich',
- 'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit',
- 'riskProfile.ciaAssessment.integrity': 'Integrität',
- 'riskProfile.ciaAssessment.availability': 'Verfügbarkeit',
- }
-
- return labels[fieldPath] || fieldPath
- }
-
- /**
- * Format a value for display
- */
- private formatValue(value: unknown): string {
- if (value === true) return 'Ja'
- if (value === false) return 'Nein'
- if (value === null || value === undefined) return 'nicht gesetzt'
- if (Array.isArray(value)) return value.join(', ')
- return String(value)
- }
-
- /**
- * Derive all TOMs based on the current context
- */
- deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
- const results = this.evaluateControls(context)
-
- return results.map((result) => {
- const control = getControlById(result.controlId)
- if (!control) {
- throw new Error(`Control not found: ${result.controlId}`)
- }
-
- return {
- id: `derived-${result.controlId}`,
- controlId: result.controlId,
- name: control.name.de,
- description: control.description.de,
- applicability: result.applicability,
- applicabilityReason: result.reason,
- implementationStatus: 'NOT_IMPLEMENTED',
- responsiblePerson: null,
- responsibleDepartment: null,
- implementationDate: null,
- reviewDate: null,
- linkedEvidence: [],
- evidenceGaps: [...control.evidenceRequirements],
- aiGeneratedDescription: null,
- aiRecommendations: [],
- }
- })
- }
-
- /**
- * Get only required and recommended TOMs
- */
- getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
- const allTOMs = this.deriveAllTOMs(context)
- return allTOMs.filter(
- (tom) =>
- tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
- )
- }
-
- /**
- * Get only required TOMs
- */
- getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
- const allTOMs = this.deriveAllTOMs(context)
- return allTOMs.filter((tom) => tom.applicability === 'REQUIRED')
- }
-
- /**
- * Perform gap analysis on derived TOMs and evidence
- */
- performGapAnalysis(
- derivedTOMs: DerivedTOM[],
- documents: EvidenceDocument[]
- ): GapAnalysisResult {
- const missingControls: MissingControl[] = []
- const partialControls: PartialControl[] = []
- const missingEvidence: MissingEvidence[] = []
- const recommendations: string[] = []
-
- let totalScore = 0
- let totalWeight = 0
-
- // Analyze each required/recommended TOM
- const applicableTOMs = derivedTOMs.filter(
- (tom) =>
- tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
- )
-
- for (const tom of applicableTOMs) {
- const control = getControlById(tom.controlId)
- if (!control) continue
-
- const weight = tom.applicability === 'REQUIRED' ? 3 : 1
- totalWeight += weight
-
- // Check implementation status
- if (tom.implementationStatus === 'NOT_IMPLEMENTED') {
- missingControls.push({
- controlId: tom.controlId,
- reason: `${control.name.de} ist nicht implementiert`,
- priority: control.priority,
- })
- // Score: 0 for not implemented
- } else if (tom.implementationStatus === 'PARTIAL') {
- partialControls.push({
- controlId: tom.controlId,
- missingAspects: tom.evidenceGaps,
- })
- // Score: 50% for partial
- totalScore += weight * 0.5
- } else {
- // Fully implemented
- totalScore += weight
- }
-
- // Check evidence
- const linkedEvidenceIds = tom.linkedEvidence
- const requiredEvidence = control.evidenceRequirements
- const providedEvidence = documents.filter((doc) =>
- linkedEvidenceIds.includes(doc.id)
- )
-
- if (providedEvidence.length < requiredEvidence.length) {
- const missing = requiredEvidence.filter(
- (req) =>
- !providedEvidence.some(
- (doc) =>
- doc.documentType === 'POLICY' ||
- doc.documentType === 'CERTIFICATE' ||
- doc.originalName.toLowerCase().includes(req.toLowerCase())
- )
- )
-
- if (missing.length > 0) {
- missingEvidence.push({
- controlId: tom.controlId,
- requiredEvidence: missing,
- })
- }
- }
- }
-
- // Calculate overall score as percentage
- const overallScore =
- totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0
-
- // Generate recommendations
- if (missingControls.length > 0) {
- const criticalMissing = missingControls.filter(
- (mc) => mc.priority === 'CRITICAL'
- )
- if (criticalMissing.length > 0) {
- recommendations.push(
- `${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.`
- )
- }
- }
-
- if (partialControls.length > 0) {
- recommendations.push(
- `${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollständigen Sie die Implementierung.`
- )
- }
-
- if (missingEvidence.length > 0) {
- recommendations.push(
- `Für ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.`
- )
- }
-
- if (overallScore >= 80) {
- recommendations.push(
- 'Ihr TOM-Compliance-Score ist gut. Führen Sie regelmäßige Überprüfungen durch.'
- )
- } else if (overallScore >= 50) {
- recommendations.push(
- 'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Lücken.'
- )
- } else {
- recommendations.push(
- 'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Überarbeitung der Maßnahmen wird empfohlen.'
- )
- }
-
- return {
- overallScore,
- missingControls,
- partialControls,
- missingEvidence,
- recommendations,
- generatedAt: new Date(),
- }
- }
-
- /**
- * Get controls by applicability level
- */
- getControlsByApplicability(
- context: RulesEngineEvaluationContext,
- applicability: ControlApplicability
- ): ControlLibraryEntry[] {
- const results = this.evaluateControls(context)
- return results
- .filter((r) => r.applicability === applicability)
- .map((r) => getControlById(r.controlId))
- .filter((c): c is ControlLibraryEntry => c !== undefined)
- }
-
- /**
- * Get summary statistics for the evaluation
- */
- getSummaryStatistics(context: RulesEngineEvaluationContext): {
- total: number
- required: number
- recommended: number
- optional: number
- notApplicable: number
- byCategory: Map
- } {
- const results = this.evaluateControls(context)
-
- const stats = {
- total: results.length,
- required: 0,
- recommended: 0,
- optional: 0,
- notApplicable: 0,
- byCategory: new Map(),
- }
-
- for (const result of results) {
- switch (result.applicability) {
- case 'REQUIRED':
- stats.required++
- break
- case 'RECOMMENDED':
- stats.recommended++
- break
- case 'OPTIONAL':
- stats.optional++
- break
- case 'NOT_APPLICABLE':
- stats.notApplicable++
- break
- }
-
- // Count by category
- const control = getControlById(result.controlId)
- if (control) {
- const category = control.category
- const existing = stats.byCategory.get(category) || {
- required: 0,
- recommended: 0,
- }
-
- if (result.applicability === 'REQUIRED') {
- existing.required++
- } else if (result.applicability === 'RECOMMENDED') {
- existing.recommended++
- }
-
- stats.byCategory.set(category, existing)
- }
- }
-
- return stats
- }
-
- /**
- * Check if a specific control is applicable
- */
- isControlApplicable(
- controlId: string,
- context: RulesEngineEvaluationContext
- ): boolean {
- const control = getControlById(controlId)
- if (!control) return false
-
- const result = this.evaluateControl(control, context)
- return (
- result.applicability === 'REQUIRED' ||
- result.applicability === 'RECOMMENDED'
- )
- }
-
- /**
- * Get all controls that match a specific tag
- */
- getControlsByTagWithApplicability(
- tag: string,
- context: RulesEngineEvaluationContext
- ): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> {
- return this.controls
- .filter((control) => control.tags.includes(tag))
- .map((control) => ({
- control,
- result: this.evaluateControl(control, context),
- }))
- }
-
- /**
- * Reload controls (useful if the control library is updated)
- */
- reloadControls(): void {
- this.controls = getAllControls()
- }
-}
-
-// =============================================================================
-// SINGLETON INSTANCE
-// =============================================================================
-
-let rulesEngineInstance: TOMRulesEngine | null = null
-
-export function getTOMRulesEngine(): TOMRulesEngine {
- if (!rulesEngineInstance) {
- rulesEngineInstance = new TOMRulesEngine()
- }
- return rulesEngineInstance
-}
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
/**
- * Quick evaluation of controls for a context
+ * TOM Rules Engine — barrel re-export
+ *
+ * Split into:
+ * - rules-evaluator.ts (TOMRulesEngine class with condition evaluation)
+ * - gap-analysis.ts (gap analysis, singleton, helper functions)
*/
-export function evaluateControlsForContext(
- context: RulesEngineEvaluationContext
-): RulesEngineResult[] {
- return getTOMRulesEngine().evaluateControls(context)
-}
-/**
- * Quick derivation of TOMs for a context
- */
-export function deriveTOMsForContext(
- context: RulesEngineEvaluationContext
-): DerivedTOM[] {
- return getTOMRulesEngine().deriveAllTOMs(context)
-}
+export { TOMRulesEngine } from './rules-evaluator'
-/**
- * Quick gap analysis
- */
-export function performQuickGapAnalysis(
- derivedTOMs: DerivedTOM[],
- documents: EvidenceDocument[]
-): GapAnalysisResult {
- return getTOMRulesEngine().performGapAnalysis(derivedTOMs, documents)
-}
+export {
+ getTOMRulesEngine,
+ performGapAnalysis,
+ evaluateControlsForContext,
+ deriveTOMsForContext,
+ performQuickGapAnalysis,
+} from './gap-analysis'
diff --git a/admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts b/admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts
new file mode 100644
index 0000000..265b0c1
--- /dev/null
+++ b/admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts
@@ -0,0 +1,276 @@
+/**
+ * TOM Rules Engine — Control Evaluation
+ *
+ * Evaluates control applicability based on company context.
+ * Core engine class with condition evaluation and TOM derivation.
+ */
+
+import {
+ ControlLibraryEntry,
+ ApplicabilityCondition,
+ ControlApplicability,
+ RulesEngineResult,
+ RulesEngineEvaluationContext,
+ DerivedTOM,
+ EvidenceDocument,
+ GapAnalysisResult,
+ ConditionOperator,
+} from './types'
+import { getAllControls, getControlById } from './controls/loader'
+
+// =============================================================================
+// RULES ENGINE CLASS — Evaluation Methods
+// =============================================================================
+
+export class TOMRulesEngine {
+ private controls: ControlLibraryEntry[]
+
+ constructor() {
+ this.controls = getAllControls()
+ }
+
+ evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] {
+ return this.controls.map((control) => this.evaluateControl(control, context))
+ }
+
+ evaluateControl(
+ control: ControlLibraryEntry,
+ context: RulesEngineEvaluationContext
+ ): RulesEngineResult {
+ const sortedConditions = [...control.applicabilityConditions].sort(
+ (a, b) => b.priority - a.priority
+ )
+
+ for (const condition of sortedConditions) {
+ const matches = this.evaluateCondition(condition, context)
+ if (matches) {
+ return {
+ controlId: control.id,
+ applicability: condition.result,
+ reason: this.formatConditionReason(condition, context),
+ matchedCondition: condition,
+ }
+ }
+ }
+
+ return {
+ controlId: control.id,
+ applicability: control.defaultApplicability,
+ reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfuellt)',
+ }
+ }
+
+ private evaluateCondition(
+ condition: ApplicabilityCondition,
+ context: RulesEngineEvaluationContext
+ ): boolean {
+ const value = this.getFieldValue(condition.field, context)
+ if (value === undefined || value === null) return false
+ return this.evaluateOperator(condition.operator, value, condition.value)
+ }
+
+ private getFieldValue(fieldPath: string, context: RulesEngineEvaluationContext): unknown {
+ const parts = fieldPath.split('.')
+ let current: unknown = context
+
+ for (const part of parts) {
+ if (current === null || current === undefined) return undefined
+ if (typeof current === 'object') {
+ current = (current as Record)[part]
+ } else {
+ return undefined
+ }
+ }
+
+ return current
+ }
+
+ private evaluateOperator(
+ operator: ConditionOperator,
+ actualValue: unknown,
+ expectedValue: unknown
+ ): boolean {
+ switch (operator) {
+ case 'EQUALS':
+ return actualValue === expectedValue
+ case 'NOT_EQUALS':
+ return actualValue !== expectedValue
+ case 'CONTAINS':
+ if (Array.isArray(actualValue)) return actualValue.includes(expectedValue)
+ if (typeof actualValue === 'string' && typeof expectedValue === 'string')
+ return actualValue.includes(expectedValue)
+ return false
+ case 'GREATER_THAN':
+ if (typeof actualValue === 'number' && typeof expectedValue === 'number')
+ return actualValue > expectedValue
+ return false
+ case 'IN':
+ if (Array.isArray(expectedValue)) return expectedValue.includes(actualValue)
+ return false
+ default:
+ return false
+ }
+ }
+
+ private formatConditionReason(
+ condition: ApplicabilityCondition,
+ context: RulesEngineEvaluationContext
+ ): string {
+ const fieldValue = this.getFieldValue(condition.field, context)
+ const fieldLabel = this.getFieldLabel(condition.field)
+
+ switch (condition.operator) {
+ case 'EQUALS':
+ return `${fieldLabel} ist "${this.formatValue(fieldValue)}"`
+ case 'NOT_EQUALS':
+ return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"`
+ case 'CONTAINS':
+ return `${fieldLabel} enthaelt "${this.formatValue(condition.value)}"`
+ case 'GREATER_THAN':
+ return `${fieldLabel} ist groesser als ${this.formatValue(condition.value)}`
+ case 'IN':
+ return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]`
+ default:
+ return `Bedingung erfuellt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}`
+ }
+ }
+
+ private getFieldLabel(fieldPath: string): string {
+ const labels: Record = {
+ 'companyProfile.role': 'Unternehmensrolle',
+ 'companyProfile.size': 'Unternehmensgroesse',
+ 'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien',
+ 'dataProfile.processesMinors': 'Verarbeitung von Minderjaehrigen-Daten',
+ 'dataProfile.dataVolume': 'Datenvolumen',
+ 'dataProfile.thirdCountryTransfers': 'Drittlanduebermittlungen',
+ 'architectureProfile.hostingModel': 'Hosting-Modell',
+ 'architectureProfile.hostingLocation': 'Hosting-Standort',
+ 'architectureProfile.multiTenancy': 'Mandantentrennung',
+ 'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter',
+ 'architectureProfile.encryptionAtRest': 'Verschluesselung ruhender Daten',
+ 'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung',
+ 'securityProfile.hasSSO': 'Single Sign-On',
+ 'securityProfile.hasPAM': 'Privileged Access Management',
+ 'riskProfile.protectionLevel': 'Schutzbedarf',
+ 'riskProfile.dsfaRequired': 'DSFA erforderlich',
+ 'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit',
+ 'riskProfile.ciaAssessment.integrity': 'Integritaet',
+ 'riskProfile.ciaAssessment.availability': 'Verfuegbarkeit',
+ }
+ return labels[fieldPath] || fieldPath
+ }
+
+ private formatValue(value: unknown): string {
+ if (value === true) return 'Ja'
+ if (value === false) return 'Nein'
+ if (value === null || value === undefined) return 'nicht gesetzt'
+ if (Array.isArray(value)) return value.join(', ')
+ return String(value)
+ }
+
+ deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
+ const results = this.evaluateControls(context)
+
+ return results.map((result) => {
+ const control = getControlById(result.controlId)
+ if (!control) throw new Error(`Control not found: ${result.controlId}`)
+
+ return {
+ id: `derived-${result.controlId}`,
+ controlId: result.controlId,
+ name: control.name.de,
+ description: control.description.de,
+ applicability: result.applicability,
+ applicabilityReason: result.reason,
+ implementationStatus: 'NOT_IMPLEMENTED',
+ responsiblePerson: null,
+ responsibleDepartment: null,
+ implementationDate: null,
+ reviewDate: null,
+ linkedEvidence: [],
+ evidenceGaps: [...control.evidenceRequirements],
+ aiGeneratedDescription: null,
+ aiRecommendations: [],
+ }
+ })
+ }
+
+ getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
+ return this.deriveAllTOMs(context).filter(
+ (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
+ )
+ }
+
+ getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
+ return this.deriveAllTOMs(context).filter((tom) => tom.applicability === 'REQUIRED')
+ }
+
+ getControlsByApplicability(
+ context: RulesEngineEvaluationContext,
+ applicability: ControlApplicability
+ ): ControlLibraryEntry[] {
+ return this.evaluateControls(context)
+ .filter((r) => r.applicability === applicability)
+ .map((r) => getControlById(r.controlId))
+ .filter((c): c is ControlLibraryEntry => c !== undefined)
+ }
+
+ getSummaryStatistics(context: RulesEngineEvaluationContext): {
+ total: number; required: number; recommended: number; optional: number; notApplicable: number
+ byCategory: Map
+ } {
+ const results = this.evaluateControls(context)
+ const stats = {
+ total: results.length, required: 0, recommended: 0, optional: 0, notApplicable: 0,
+ byCategory: new Map(),
+ }
+
+ for (const result of results) {
+ switch (result.applicability) {
+ case 'REQUIRED': stats.required++; break
+ case 'RECOMMENDED': stats.recommended++; break
+ case 'OPTIONAL': stats.optional++; break
+ case 'NOT_APPLICABLE': stats.notApplicable++; break
+ }
+
+ const control = getControlById(result.controlId)
+ if (control) {
+ const existing = stats.byCategory.get(control.category) || { required: 0, recommended: 0 }
+ if (result.applicability === 'REQUIRED') existing.required++
+ else if (result.applicability === 'RECOMMENDED') existing.recommended++
+ stats.byCategory.set(control.category, existing)
+ }
+ }
+
+ return stats
+ }
+
+ isControlApplicable(controlId: string, context: RulesEngineEvaluationContext): boolean {
+ const control = getControlById(controlId)
+ if (!control) return false
+ const result = this.evaluateControl(control, context)
+ return result.applicability === 'REQUIRED' || result.applicability === 'RECOMMENDED'
+ }
+
+ getControlsByTagWithApplicability(
+ tag: string,
+ context: RulesEngineEvaluationContext
+ ): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> {
+ return this.controls
+ .filter((control) => control.tags.includes(tag))
+ .map((control) => ({ control, result: this.evaluateControl(control, context) }))
+ }
+
+ performGapAnalysis(
+ derivedTOMs: DerivedTOM[],
+ documents: EvidenceDocument[]
+ ): GapAnalysisResult {
+ // Delegate to standalone function to keep this class focused on evaluation
+ const { performGapAnalysis: doGapAnalysis } = require('./gap-analysis')
+ return doGapAnalysis(derivedTOMs, documents)
+ }
+
+ reloadControls(): void {
+ this.controls = getAllControls()
+ }
+}
diff --git a/admin-compliance/lib/sdk/vvt-profiling-data.ts b/admin-compliance/lib/sdk/vvt-profiling-data.ts
new file mode 100644
index 0000000..fca78a1
--- /dev/null
+++ b/admin-compliance/lib/sdk/vvt-profiling-data.ts
@@ -0,0 +1,286 @@
+/**
+ * VVT Profiling — Questions, Steps & Department Data Categories
+ *
+ * Static data for the profiling questionnaire (~25 questions in 6 steps).
+ */
+
+// =============================================================================
+// 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[]
+}
+
+export interface ProfilingStep {
+ step: number
+ title: string
+ description: string
+}
+
+export interface ProfilingAnswers {
+ [questionId: string]: string | string[] | number | boolean
+}
+
+export interface ProfilingResult {
+ answers: ProfilingAnswers
+ generatedActivities: import('./vvt-types').VVTActivity[]
+ coverageScore: number
+ art30Abs5Exempt: boolean
+}
+
+export interface DepartmentCategory {
+ id: string
+ label: string
+ info: string
+ isArt9?: boolean
+ isTypical?: boolean
+}
+
+export interface DepartmentDataConfig {
+ label: string
+ icon: string
+ categories: DepartmentCategory[]
+}
+
+// =============================================================================
+// 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: [] },
+]
+
+// =============================================================================
+// DEPARTMENT DATA CATEGORIES
+// =============================================================================
+
+export const DEPARTMENT_DATA_CATEGORIES: Record = {
+ dept_hr: {
+ label: 'Personal (HR)', icon: '\u{1F465}',
+ categories: [
+ { id: 'NAME', label: 'Stammdaten', info: 'Vor-/Nachname, Titel, Geschlecht, Geburtsdatum', isTypical: true },
+ { id: 'ADDRESS', label: 'Adressdaten', info: 'Wohn-/Melde-/Lieferadresse, Telefon, E-Mail', isTypical: true },
+ { id: 'SOCIAL_SECURITY', label: 'Sozialversicherungsnr.', info: 'SV-Nummer fuer Meldungen an DRV, Krankenkasse', isTypical: true },
+ { id: 'TAX_ID', label: 'Steuer-ID', info: 'Steueridentifikationsnummer, Steuerklasse, Freibetraege', isTypical: true },
+ { id: 'BANK_ACCOUNT', label: 'Bankverbindung', info: 'IBAN, BIC fuer Gehaltsueberweisungen', isTypical: true },
+ { id: 'SALARY_DATA', label: 'Gehaltsdaten', info: 'Bruttogehalt, Zulagen, Praemien, VWL, Abzuege', isTypical: true },
+ { id: 'EMPLOYMENT_DATA', label: 'Beschaeftigungsdaten', info: 'Vertrag, Eintrittsdatum, Abteilung, Position, Arbeitszeitmodell', isTypical: true },
+ { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Krankheitstage (AU-Bescheinigungen), BEM-Daten, Schwerbehinderung', isArt9: true },
+ { id: 'RELIGIOUS_BELIEFS', label: 'Religionszugehoerigkeit', info: 'Konfession fuer Kirchensteuer-Abfuehrung', isArt9: true },
+ { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Zertifikate, Weiterbildungen, Schulungsnachweise' },
+ { id: 'PHOTO_VIDEO', label: 'Mitarbeiterfotos', info: 'Passbilder fuer Ausweise, Intranet-Profilbilder' },
+ ]
+ },
+ dept_recruiting: {
+ label: 'Recruiting / Bewerbermanagement', icon: '\u{1F4CB}',
+ categories: [
+ { id: 'NAME', label: 'Bewerberstammdaten', info: 'Name, Anschrift, Kontaktdaten der Bewerber', isTypical: true },
+ { id: 'APPLICATION_DATA', label: 'Bewerbungsunterlagen', info: 'Lebenslauf, Anschreiben, Zeugnisse, Zertifikate', isTypical: true },
+ { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Berufserfahrung, Sprachkenntnisse', isTypical: true },
+ { id: 'ASSESSMENT_DATA', label: 'Bewertungsdaten', info: 'Interviewnotizen, Assessment-Ergebnisse, Eignungstests' },
+ { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Schwerbehinderung (freiwillige Angabe), Eignungsuntersuchung', isArt9: true },
+ { id: 'PHOTO_VIDEO', label: 'Bewerbungsfotos', info: 'Bewerbungsfoto (freiwillig), Video-Interview-Aufnahmen' },
+ ]
+ },
+ dept_finance: {
+ label: 'Finanzen & Buchhaltung', icon: '\u{1F4B0}',
+ categories: [
+ { id: 'NAME', label: 'Kunden-/Lieferantenstammdaten', info: 'Firmenname, Ansprechpartner, Kontaktdaten', isTypical: true },
+ { id: 'ADDRESS', label: 'Rechnungsadressen', info: 'Rechnungs-/Lieferadressen, USt-IdNr.', isTypical: true },
+ { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC, SEPA-Mandate, Zahlungsbedingungen', isTypical: true },
+ { id: 'TAX_ID', label: 'Steuer-IDs', info: 'Steuernummer, USt-IdNr., Steueridentifikationsnr.', isTypical: true },
+ { id: 'INVOICE_DATA', label: 'Rechnungsdaten', info: 'Rechnungen, Gutschriften, Mahnungen, Zahlungshistorie', isTypical: true },
+ { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertragskonditionen, Laufzeiten, Kuendigungsfristen' },
+ ]
+ },
+ dept_sales: {
+ label: 'Vertrieb & CRM', icon: '\u{1F91D}',
+ categories: [
+ { id: 'NAME', label: 'Kontaktdaten', info: 'Name, E-Mail, Telefon, Position der Ansprechpartner', isTypical: true },
+ { id: 'ADDRESS', label: 'Firmenadresse', info: 'Firmenanschrift, Standorte', isTypical: true },
+ { id: 'CRM_DATA', label: 'CRM-Daten', info: 'Lead-Status, Opportunities, Sales-Pipeline, Umsatzhistorie', isTypical: true },
+ { id: 'COMMUNICATION_DATA', label: 'Kommunikation', info: 'E-Mail-Verlauf, Gespraechsnotizen, Meeting-Protokolle', isTypical: true },
+ { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Angebote, Bestellungen, Rahmenvertraege' },
+ { id: 'PREFERENCE_DATA', label: 'Praeferenzen', info: 'Produktinteressen, Kaufhistorie, Kundensegmentierung' },
+ ]
+ },
+ dept_marketing: {
+ label: 'Marketing', icon: '\u{1F4E2}',
+ categories: [
+ { id: 'EMAIL', label: 'E-Mail-Adressen', info: 'Newsletter-Abonnenten, Kampagnen-Empfaenger', isTypical: true },
+ { id: 'TRACKING_DATA', label: 'Tracking-/Analytics-Daten', info: 'IP-Adressen, Cookies, Seitenaufrufe, Klickpfade', isTypical: true },
+ { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Cookie-Consent, Newsletter-Opt-in, Widerrufe', isTypical: true },
+ { id: 'SOCIAL_MEDIA_DATA', label: 'Social-Media-Daten', info: 'Follower-Interaktionen, Kommentare, Reichweitendaten' },
+ { id: 'PREFERENCE_DATA', label: 'Interessenprofil', info: 'Produktinteressen, Segmentierung, A/B-Test-Zuordnungen' },
+ { id: 'PHOTO_VIDEO', label: 'Bild-/Videomaterial', info: 'Kundenfotos (Testimonials), Event-Aufnahmen, UGC' },
+ ]
+ },
+ dept_support: {
+ label: 'Kundenservice / Support', icon: '\u{1F3A7}',
+ categories: [
+ { id: 'NAME', label: 'Kundenstammdaten', info: 'Name, E-Mail, Telefon, Kundennummer', isTypical: true },
+ { id: 'TICKET_DATA', label: 'Ticket-/Anfragedaten', info: 'Ticketnummer, Betreff, Beschreibung, Status, Prioritaet', isTypical: true },
+ { id: 'COMMUNICATION_DATA', label: 'Kommunikationsverlauf', info: 'E-Mails, Chat-Protokolle, Anrufnotizen', isTypical: true },
+ { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Produktversion, Lizenz, SLA-Status', isTypical: true },
+ { id: 'TECHNICAL_DATA', label: 'Technische Daten', info: 'Systeminfos, Logdateien, Screenshots bei Fehlermeldungen' },
+ ]
+ },
+ dept_it: {
+ label: 'IT / Administration', icon: '\u{1F4BB}',
+ categories: [
+ { id: 'USER_ACCOUNTS', label: 'Benutzerkonten', info: 'Benutzernamen, Passwort-Hashes, Rollen, Berechtigungen', isTypical: true },
+ { id: 'LOG_DATA', label: 'Log-/Protokolldaten', info: 'System-Logs, Zugriffsprotokolle, Fehlerprotokolle, IP-Adressen', isTypical: true },
+ { id: 'DEVICE_DATA', label: 'Geraetedaten', info: 'Inventar, Seriennummern, MAC-Adressen, zugewiesene Geraete', isTypical: true },
+ { id: 'NETWORK_DATA', label: 'Netzwerkdaten', info: 'IP-Adressen, VPN-Verbindungen, Firewall-Logs', isTypical: true },
+ { id: 'EMAIL_DATA', label: 'E-Mail-/Kommunikation', info: 'E-Mail-Konten, Verteiler, Archivierung', isTypical: true },
+ { id: 'BACKUP_DATA', label: 'Backup-Daten', info: 'Sicherungskopien mit personenbezogenen Inhalten' },
+ { id: 'MONITORING_DATA', label: 'Monitoring-Daten', info: 'Systemueberwachung, Performance-Metriken mit Nutzerbezug' },
+ ]
+ },
+ dept_recht: {
+ label: 'Recht / Compliance', icon: '\u{2696}\u{FE0F}',
+ categories: [
+ { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertraege, NDAs, AVVs, Rahmenvereinbarungen', isTypical: true },
+ { id: 'NAME', label: 'Ansprechpartner', info: 'Namen, Kontaktdaten von Vertragspartnern und Anwaelten', isTypical: true },
+ { id: 'COMPLIANCE_DATA', label: 'Compliance-Daten', info: 'Datenschutzanfragen, Meldungen, Audit-Ergebnisse', isTypical: true },
+ { id: 'INCIDENT_DATA', label: 'Vorfallsdaten', info: 'Datenschutzvorfaelle, Beschwerden, Meldungen an Aufsichtsbehoerden' },
+ { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Consent-Nachweise, Widerrufe, Opt-in/Opt-out-Protokolle' },
+ { id: 'CRIMINAL_DATA', label: 'Strafrechtliche Daten', info: 'Fuehrungszeugnisse, Compliance-Pruefungen (Art. 10 DSGVO)', isArt9: true },
+ ]
+ },
+ dept_produktion: {
+ label: 'Produktion / Fertigung', icon: '\u{1F3ED}',
+ categories: [
+ { id: 'EMPLOYMENT_DATA', label: 'Schichtplaene', info: 'Schichtzuordnung, Arbeitszeiten, Anwesenheitslisten', isTypical: true },
+ { id: 'NAME', label: 'Mitarbeiterstammdaten', info: 'Name, Personalnummer, Qualifikation, Maschinenberechtigungen', isTypical: true },
+ { id: 'HEALTH_DATA', label: 'Arbeitsschutzdaten', info: 'Arbeitsmedizinische Vorsorge, Unfallmeldungen, Gefahrstoff-Expositionen', isArt9: true },
+ { id: 'ACCESS_DATA', label: 'Zugangsdaten', info: 'Zutrittskontrolle, Badge-Protokolle, Bereichsberechtigungen', isTypical: true },
+ { id: 'QUALITY_DATA', label: 'Qualitaetsdaten', info: 'Pruefprotokolle mit Pruefernamen, Fehlerberichte' },
+ { id: 'PHOTO_VIDEO', label: 'Bild-/Videodaten', info: 'Kameraueberwachung in Produktionsbereichen' },
+ ]
+ },
+ dept_logistik: {
+ label: 'Logistik / Versand', icon: '\u{1F69A}',
+ categories: [
+ { id: 'NAME', label: 'Empfaengerdaten', info: 'Name, Lieferadresse, Telefon fuer Zustellung', isTypical: true },
+ { id: 'ADDRESS', label: 'Versandadressen', info: 'Liefer-/Abholadressen, Paketshop-Zuordnung', isTypical: true },
+ { id: 'TRACKING_DATA', label: 'Sendungsverfolgung', info: 'Tracking-Nummern, Zustellstatus, Lieferzeitfenster', isTypical: true },
+ { id: 'DRIVER_DATA', label: 'Fahrerdaten', info: 'Fahrerlaubnis, Touren, GPS-Standortdaten', isTypical: true },
+ { id: 'CUSTOMS_DATA', label: 'Zolldaten', info: 'Zollerklaerungen, EORI-Nummern bei internationalem Versand' },
+ ]
+ },
+ dept_einkauf: {
+ label: 'Einkauf / Beschaffung', icon: '\u{1F6D2}',
+ categories: [
+ { id: 'NAME', label: 'Lieferantenkontakte', info: 'Ansprechpartner, E-Mail, Telefon der Lieferanten', isTypical: true },
+ { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Rahmenvertraege, Bestellungen, Konditionen, Laufzeiten', isTypical: true },
+ { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC der Lieferanten fuer Zahlungsabwicklung', isTypical: true },
+ { id: 'TAX_ID', label: 'Steuer-IDs', info: 'USt-IdNr., Steuernummer der Lieferanten', isTypical: true },
+ { id: 'COMPLIANCE_DATA', label: 'Lieferantenbewertung', info: 'Qualitaetsbewertungen, Audit-Ergebnisse, Zertifizierungen' },
+ ]
+ },
+ dept_facility: {
+ label: 'Facility Management', icon: '\u{1F3E2}',
+ categories: [
+ { id: 'ACCESS_DATA', label: 'Zutrittsdaten', info: 'Schluesselausgaben, Badge-Protokolle, Zutrittslisten', isTypical: true },
+ { id: 'NAME', label: 'Dienstleisterkontakte', info: 'Reinigung, Wartung, Sicherheitsdienst — Namen und Kontaktdaten', isTypical: true },
+ { id: 'PHOTO_VIDEO', label: 'Videoueberwachung', info: 'Kameraaufnahmen in/an Gebaeuden, Parkplaetzen', isTypical: true },
+ { id: 'VISITOR_DATA', label: 'Besucherdaten', info: 'Name, Firma, Besuchsgrund, Ein-/Austrittszeiten', isTypical: true },
+ { id: 'HEALTH_DATA', label: 'Gesundheits-/Sicherheitsdaten', info: 'Unfallmeldungen, Evakuierungslisten, Ersthelfer-Register', isArt9: true },
+ ]
+ },
+}
diff --git a/admin-compliance/lib/sdk/vvt-profiling-logic.ts b/admin-compliance/lib/sdk/vvt-profiling-logic.ts
new file mode 100644
index 0000000..a16618a
--- /dev/null
+++ b/admin-compliance/lib/sdk/vvt-profiling-logic.ts
@@ -0,0 +1,187 @@
+/**
+ * VVT Profiling — Generator Logic, Enrichment & Helpers
+ *
+ * Generates baseline VVT activities from profiling answers.
+ */
+
+import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog'
+import { generateVVTId } from './vvt-types'
+import type { VVTActivity } from './vvt-types'
+import {
+ PROFILING_QUESTIONS,
+ type ProfilingAnswers,
+ type ProfilingResult,
+} from './vvt-profiling-data'
+
+// =============================================================================
+// GENERATOR LOGIC
+// =============================================================================
+
+export function generateActivities(answers: ProfilingAnswers): ProfilingResult {
+ const triggeredIds = new Set()
+
+ for (const question of PROFILING_QUESTIONS) {
+ const answer = answers[question.id]
+ if (!answer) continue
+
+ if (question.type === 'boolean' && answer === true) {
+ question.triggersTemplates.forEach(id => triggeredIds.add(id))
+ }
+ }
+
+ // Always add IT baseline templates
+ triggeredIds.add('it-systemadministration')
+ triggeredIds.add('it-backup')
+ triggeredIds.add('it-logging')
+ triggeredIds.add('it-iam')
+
+ 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)
+ enrichActivityFromAnswers(activity, answers)
+ activities.push(activity)
+ }
+
+ // Calculate coverage score
+ const totalFields = activities.length * 12
+ 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
+
+ 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 {
+ 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'],
+ })
+ }
+
+ if (answers.data_health === true) {
+ if (!activity.personalDataCategories.includes('HEALTH_DATA')) {
+ if (activity.businessFunction === 'hr') {
+ activity.personalDataCategories.push('HEALTH_DATA')
+ 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')) {
+ if (activity.businessFunction === 'support' || activity.businessFunction === 'product_engineering') {
+ activity.dataSubjectCategories.push('MINORS')
+ }
+ }
+ }
+
+ 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) {
+ 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
+// =============================================================================
+
+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
+}
+
+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-compliance/lib/sdk/vvt-profiling.ts b/admin-compliance/lib/sdk/vvt-profiling.ts
index 294e211..1e26bcb 100644
--- a/admin-compliance/lib/sdk/vvt-profiling.ts
+++ b/admin-compliance/lib/sdk/vvt-profiling.ts
@@ -1,659 +1,31 @@
/**
- * VVT Profiling — Generator-Fragebogen
+ * VVT Profiling — barrel re-export
*
- * ~25 Fragen in 6 Schritten, die auf Basis der Antworten
- * Baseline-Verarbeitungstaetigkeiten generieren.
+ * Split into:
+ * - vvt-profiling-data.ts (types, steps, questions, department categories)
+ * - vvt-profiling-logic.ts (generator, enrichment, helpers, scope integration)
*/
-import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog'
-import { generateVVTId } from './vvt-types'
-import type { VVTActivity, BusinessFunction } from './vvt-types'
+export type {
+ ProfilingQuestion,
+ ProfilingStep,
+ ProfilingAnswers,
+ ProfilingResult,
+ DepartmentCategory,
+ DepartmentDataConfig,
+} from './vvt-profiling-data'
-// =============================================================================
-// TYPES
-// =============================================================================
+export {
+ PROFILING_STEPS,
+ PROFILING_QUESTIONS,
+ DEPARTMENT_DATA_CATEGORIES,
+} from './vvt-profiling-data'
-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: [],
- },
-]
-
-// =============================================================================
-// DEPARTMENT DATA CATEGORIES (Aufklappbare Kacheln Step 2)
-// =============================================================================
-
-export interface DepartmentCategory {
- id: string
- label: string
- info: string
- isArt9?: boolean
- isTypical?: boolean
-}
-
-export interface DepartmentDataConfig {
- label: string
- icon: string
- categories: DepartmentCategory[]
-}
-
-export const DEPARTMENT_DATA_CATEGORIES: Record = {
- dept_hr: {
- label: 'Personal (HR)',
- icon: '👥',
- categories: [
- { id: 'NAME', label: 'Stammdaten', info: 'Vor-/Nachname, Titel, Geschlecht, Geburtsdatum', isTypical: true },
- { id: 'ADDRESS', label: 'Adressdaten', info: 'Wohn-/Melde-/Lieferadresse, Telefon, E-Mail', isTypical: true },
- { id: 'SOCIAL_SECURITY', label: 'Sozialversicherungsnr.', info: 'SV-Nummer fuer Meldungen an DRV, Krankenkasse', isTypical: true },
- { id: 'TAX_ID', label: 'Steuer-ID', info: 'Steueridentifikationsnummer, Steuerklasse, Freibetraege', isTypical: true },
- { id: 'BANK_ACCOUNT', label: 'Bankverbindung', info: 'IBAN, BIC fuer Gehaltsueberweisungen', isTypical: true },
- { id: 'SALARY_DATA', label: 'Gehaltsdaten', info: 'Bruttogehalt, Zulagen, Praemien, VWL, Abzuege', isTypical: true },
- { id: 'EMPLOYMENT_DATA', label: 'Beschaeftigungsdaten', info: 'Vertrag, Eintrittsdatum, Abteilung, Position, Arbeitszeitmodell', isTypical: true },
- { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Krankheitstage (AU-Bescheinigungen), BEM-Daten, Schwerbehinderung', isArt9: true },
- { id: 'RELIGIOUS_BELIEFS', label: 'Religionszugehoerigkeit', info: 'Konfession fuer Kirchensteuer-Abfuehrung', isArt9: true },
- { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Zertifikate, Weiterbildungen, Schulungsnachweise' },
- { id: 'PHOTO_VIDEO', label: 'Mitarbeiterfotos', info: 'Passbilder fuer Ausweise, Intranet-Profilbilder' },
- ]
- },
- dept_recruiting: {
- label: 'Recruiting / Bewerbermanagement',
- icon: '📋',
- categories: [
- { id: 'NAME', label: 'Bewerberstammdaten', info: 'Name, Anschrift, Kontaktdaten der Bewerber', isTypical: true },
- { id: 'APPLICATION_DATA', label: 'Bewerbungsunterlagen', info: 'Lebenslauf, Anschreiben, Zeugnisse, Zertifikate', isTypical: true },
- { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Berufserfahrung, Sprachkenntnisse', isTypical: true },
- { id: 'ASSESSMENT_DATA', label: 'Bewertungsdaten', info: 'Interviewnotizen, Assessment-Ergebnisse, Eignungstests' },
- { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Schwerbehinderung (freiwillige Angabe), Eignungsuntersuchung', isArt9: true },
- { id: 'PHOTO_VIDEO', label: 'Bewerbungsfotos', info: 'Bewerbungsfoto (freiwillig), Video-Interview-Aufnahmen' },
- ]
- },
- dept_finance: {
- label: 'Finanzen & Buchhaltung',
- icon: '💰',
- categories: [
- { id: 'NAME', label: 'Kunden-/Lieferantenstammdaten', info: 'Firmenname, Ansprechpartner, Kontaktdaten', isTypical: true },
- { id: 'ADDRESS', label: 'Rechnungsadressen', info: 'Rechnungs-/Lieferadressen, USt-IdNr.', isTypical: true },
- { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC, SEPA-Mandate, Zahlungsbedingungen', isTypical: true },
- { id: 'TAX_ID', label: 'Steuer-IDs', info: 'Steuernummer, USt-IdNr., Steueridentifikationsnr.', isTypical: true },
- { id: 'INVOICE_DATA', label: 'Rechnungsdaten', info: 'Rechnungen, Gutschriften, Mahnungen, Zahlungshistorie', isTypical: true },
- { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertragskonditionen, Laufzeiten, Kuendigungsfristen' },
- ]
- },
- dept_sales: {
- label: 'Vertrieb & CRM',
- icon: '🤝',
- categories: [
- { id: 'NAME', label: 'Kontaktdaten', info: 'Name, E-Mail, Telefon, Position der Ansprechpartner', isTypical: true },
- { id: 'ADDRESS', label: 'Firmenadresse', info: 'Firmenanschrift, Standorte', isTypical: true },
- { id: 'CRM_DATA', label: 'CRM-Daten', info: 'Lead-Status, Opportunities, Sales-Pipeline, Umsatzhistorie', isTypical: true },
- { id: 'COMMUNICATION_DATA', label: 'Kommunikation', info: 'E-Mail-Verlauf, Gespraechsnotizen, Meeting-Protokolle', isTypical: true },
- { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Angebote, Bestellungen, Rahmenvertraege' },
- { id: 'PREFERENCE_DATA', label: 'Praeferenzen', info: 'Produktinteressen, Kaufhistorie, Kundensegmentierung' },
- ]
- },
- dept_marketing: {
- label: 'Marketing',
- icon: '📢',
- categories: [
- { id: 'EMAIL', label: 'E-Mail-Adressen', info: 'Newsletter-Abonnenten, Kampagnen-Empfaenger', isTypical: true },
- { id: 'TRACKING_DATA', label: 'Tracking-/Analytics-Daten', info: 'IP-Adressen, Cookies, Seitenaufrufe, Klickpfade', isTypical: true },
- { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Cookie-Consent, Newsletter-Opt-in, Widerrufe', isTypical: true },
- { id: 'SOCIAL_MEDIA_DATA', label: 'Social-Media-Daten', info: 'Follower-Interaktionen, Kommentare, Reichweitendaten' },
- { id: 'PREFERENCE_DATA', label: 'Interessenprofil', info: 'Produktinteressen, Segmentierung, A/B-Test-Zuordnungen' },
- { id: 'PHOTO_VIDEO', label: 'Bild-/Videomaterial', info: 'Kundenfotos (Testimonials), Event-Aufnahmen, UGC' },
- ]
- },
- dept_support: {
- label: 'Kundenservice / Support',
- icon: '🎧',
- categories: [
- { id: 'NAME', label: 'Kundenstammdaten', info: 'Name, E-Mail, Telefon, Kundennummer', isTypical: true },
- { id: 'TICKET_DATA', label: 'Ticket-/Anfragedaten', info: 'Ticketnummer, Betreff, Beschreibung, Status, Prioritaet', isTypical: true },
- { id: 'COMMUNICATION_DATA', label: 'Kommunikationsverlauf', info: 'E-Mails, Chat-Protokolle, Anrufnotizen', isTypical: true },
- { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Produktversion, Lizenz, SLA-Status', isTypical: true },
- { id: 'TECHNICAL_DATA', label: 'Technische Daten', info: 'Systeminfos, Logdateien, Screenshots bei Fehlermeldungen' },
- ]
- },
- dept_it: {
- label: 'IT / Administration',
- icon: '💻',
- categories: [
- { id: 'USER_ACCOUNTS', label: 'Benutzerkonten', info: 'Benutzernamen, Passwort-Hashes, Rollen, Berechtigungen', isTypical: true },
- { id: 'LOG_DATA', label: 'Log-/Protokolldaten', info: 'System-Logs, Zugriffsprotokolle, Fehlerprotokolle, IP-Adressen', isTypical: true },
- { id: 'DEVICE_DATA', label: 'Geraetedaten', info: 'Inventar, Seriennummern, MAC-Adressen, zugewiesene Geraete', isTypical: true },
- { id: 'NETWORK_DATA', label: 'Netzwerkdaten', info: 'IP-Adressen, VPN-Verbindungen, Firewall-Logs', isTypical: true },
- { id: 'EMAIL_DATA', label: 'E-Mail-/Kommunikation', info: 'E-Mail-Konten, Verteiler, Archivierung', isTypical: true },
- { id: 'BACKUP_DATA', label: 'Backup-Daten', info: 'Sicherungskopien mit personenbezogenen Inhalten' },
- { id: 'MONITORING_DATA', label: 'Monitoring-Daten', info: 'Systemueberwachung, Performance-Metriken mit Nutzerbezug' },
- ]
- },
- dept_recht: {
- label: 'Recht / Compliance',
- icon: '⚖️',
- categories: [
- { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertraege, NDAs, AVVs, Rahmenvereinbarungen', isTypical: true },
- { id: 'NAME', label: 'Ansprechpartner', info: 'Namen, Kontaktdaten von Vertragspartnern und Anwaelten', isTypical: true },
- { id: 'COMPLIANCE_DATA', label: 'Compliance-Daten', info: 'Datenschutzanfragen, Meldungen, Audit-Ergebnisse', isTypical: true },
- { id: 'INCIDENT_DATA', label: 'Vorfallsdaten', info: 'Datenschutzvorfaelle, Beschwerden, Meldungen an Aufsichtsbehoerden' },
- { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Consent-Nachweise, Widerrufe, Opt-in/Opt-out-Protokolle' },
- { id: 'CRIMINAL_DATA', label: 'Strafrechtliche Daten', info: 'Fuehrungszeugnisse, Compliance-Pruefungen (Art. 10 DSGVO)', isArt9: true },
- ]
- },
- dept_produktion: {
- label: 'Produktion / Fertigung',
- icon: '🏭',
- categories: [
- { id: 'EMPLOYMENT_DATA', label: 'Schichtplaene', info: 'Schichtzuordnung, Arbeitszeiten, Anwesenheitslisten', isTypical: true },
- { id: 'NAME', label: 'Mitarbeiterstammdaten', info: 'Name, Personalnummer, Qualifikation, Maschinenberechtigungen', isTypical: true },
- { id: 'HEALTH_DATA', label: 'Arbeitsschutzdaten', info: 'Arbeitsmedizinische Vorsorge, Unfallmeldungen, Gefahrstoff-Expositionen', isArt9: true },
- { id: 'ACCESS_DATA', label: 'Zugangsdaten', info: 'Zutrittskontrolle, Badge-Protokolle, Bereichsberechtigungen', isTypical: true },
- { id: 'QUALITY_DATA', label: 'Qualitaetsdaten', info: 'Pruefprotokolle mit Pruefernamen, Fehlerberichte' },
- { id: 'PHOTO_VIDEO', label: 'Bild-/Videodaten', info: 'Kameraueberwachung in Produktionsbereichen' },
- ]
- },
- dept_logistik: {
- label: 'Logistik / Versand',
- icon: '🚚',
- categories: [
- { id: 'NAME', label: 'Empfaengerdaten', info: 'Name, Lieferadresse, Telefon fuer Zustellung', isTypical: true },
- { id: 'ADDRESS', label: 'Versandadressen', info: 'Liefer-/Abholadressen, Paketshop-Zuordnung', isTypical: true },
- { id: 'TRACKING_DATA', label: 'Sendungsverfolgung', info: 'Tracking-Nummern, Zustellstatus, Lieferzeitfenster', isTypical: true },
- { id: 'DRIVER_DATA', label: 'Fahrerdaten', info: 'Fahrerlaubnis, Touren, GPS-Standortdaten', isTypical: true },
- { id: 'CUSTOMS_DATA', label: 'Zolldaten', info: 'Zollerklaerungen, EORI-Nummern bei internationalem Versand' },
- ]
- },
- dept_einkauf: {
- label: 'Einkauf / Beschaffung',
- icon: '🛒',
- categories: [
- { id: 'NAME', label: 'Lieferantenkontakte', info: 'Ansprechpartner, E-Mail, Telefon der Lieferanten', isTypical: true },
- { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Rahmenvertraege, Bestellungen, Konditionen, Laufzeiten', isTypical: true },
- { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC der Lieferanten fuer Zahlungsabwicklung', isTypical: true },
- { id: 'TAX_ID', label: 'Steuer-IDs', info: 'USt-IdNr., Steuernummer der Lieferanten', isTypical: true },
- { id: 'COMPLIANCE_DATA', label: 'Lieferantenbewertung', info: 'Qualitaetsbewertungen, Audit-Ergebnisse, Zertifizierungen' },
- ]
- },
- dept_facility: {
- label: 'Facility Management',
- icon: '🏢',
- categories: [
- { id: 'ACCESS_DATA', label: 'Zutrittsdaten', info: 'Schluesselausgaben, Badge-Protokolle, Zutrittslisten', isTypical: true },
- { id: 'NAME', label: 'Dienstleisterkontakte', info: 'Reinigung, Wartung, Sicherheitsdienst — Namen und Kontaktdaten', isTypical: true },
- { id: 'PHOTO_VIDEO', label: 'Videoueberwachung', info: 'Kameraaufnahmen in/an Gebaeuden, Parkplaetzen', isTypical: true },
- { id: 'VISITOR_DATA', label: 'Besucherdaten', info: 'Name, Firma, Besuchsgrund, Ein-/Austrittszeiten', isTypical: true },
- { id: 'HEALTH_DATA', label: 'Gesundheits-/Sicherheitsdaten', info: 'Unfallmeldungen, Evakuierungslisten, Ersthelfer-Register', isArt9: true },
- ]
- },
-}
-
-// =============================================================================
-// 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',
-]
+export {
+ generateActivities,
+ getQuestionsForStep,
+ getStepProgress,
+ getTotalProgress,
+ prefillFromScopeAnswers,
+ SCOPE_PREFILLED_VVT_QUESTIONS,
+} from './vvt-profiling-logic'
diff --git a/admin-compliance/lib/sdk/whistleblower/api-mock-data.ts b/admin-compliance/lib/sdk/whistleblower/api-mock-data.ts
new file mode 100644
index 0000000..d2cfccb
--- /dev/null
+++ b/admin-compliance/lib/sdk/whistleblower/api-mock-data.ts
@@ -0,0 +1,187 @@
+/**
+ * Whistleblower API — Mock Data & SDK Proxy
+ *
+ * Fallback mock data for development and SDK proxy function
+ */
+
+import {
+ WhistleblowerReport,
+ WhistleblowerStatistics,
+ ReportCategory,
+ ReportStatus,
+ generateAccessKey,
+} from './types'
+import {
+ fetchReports,
+ fetchWhistleblowerStatistics,
+} from './api-operations'
+
+// =============================================================================
+// SDK PROXY FUNCTION
+// =============================================================================
+
+export async function fetchSDKWhistleblowerList(): Promise<{
+ reports: WhistleblowerReport[]
+ statistics: WhistleblowerStatistics
+}> {
+ try {
+ const [reportsResponse, statsResponse] = await Promise.all([
+ fetchReports(),
+ fetchWhistleblowerStatistics()
+ ])
+ return {
+ reports: reportsResponse.reports,
+ statistics: statsResponse
+ }
+ } catch (error) {
+ console.error('Failed to load Whistleblower data from API, using mock data:', error)
+ const reports = createMockReports()
+ const statistics = createMockStatistics()
+ return { reports, statistics }
+ }
+}
+
+// =============================================================================
+// MOCK DATA
+// =============================================================================
+
+export function createMockReports(): WhistleblowerReport[] {
+ const now = new Date()
+
+ function calcDeadlines(receivedAt: Date): { ack: string; fb: string } {
+ const ack = new Date(receivedAt)
+ ack.setDate(ack.getDate() + 7)
+ const fb = new Date(receivedAt)
+ fb.setMonth(fb.getMonth() + 3)
+ return { ack: ack.toISOString(), fb: fb.toISOString() }
+ }
+
+ const received1 = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
+ const deadlines1 = calcDeadlines(received1)
+ const received2 = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
+ const deadlines2 = calcDeadlines(received2)
+ const received3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
+ const deadlines3 = calcDeadlines(received3)
+ const received4 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
+ const deadlines4 = calcDeadlines(received4)
+
+ return [
+ {
+ id: 'wb-001', referenceNumber: 'WB-2026-000001', accessKey: generateAccessKey(),
+ category: 'corruption', status: 'new', priority: 'high',
+ title: 'Unregelmaessigkeiten bei Auftragsvergabe',
+ description: 'Bei der Vergabe des IT-Rahmenvertrags im November wurden offenbar Angebote eines bestimmten Anbieters bevorzugt. Der zustaendige Abteilungsleiter hat private Verbindungen zum Geschaeftsfuehrer des Anbieters.',
+ isAnonymous: true, receivedAt: received1.toISOString(),
+ deadlineAcknowledgment: deadlines1.ack, deadlineFeedback: deadlines1.fb,
+ measures: [], messages: [], attachments: [],
+ auditTrail: [{ id: 'audit-001', action: 'report_created', description: 'Meldung ueber Online-Meldeformular eingegangen', performedBy: 'system', performedAt: received1.toISOString() }]
+ },
+ {
+ id: 'wb-002', referenceNumber: 'WB-2026-000002', accessKey: generateAccessKey(),
+ category: 'data_protection', status: 'under_review', priority: 'normal',
+ title: 'Unerlaubte Weitergabe von Kundendaten',
+ description: 'Ein Mitarbeiter der Vertriebsabteilung gibt regelmaessig Kundenlisten an externe Dienstleister weiter, ohne dass eine Auftragsverarbeitungsvereinbarung vorliegt.',
+ isAnonymous: false, reporterName: 'Maria Schmidt', reporterEmail: 'maria.schmidt@example.de',
+ assignedTo: 'DSB Mueller', receivedAt: received2.toISOString(),
+ acknowledgedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
+ deadlineAcknowledgment: deadlines2.ack, deadlineFeedback: deadlines2.fb,
+ measures: [],
+ messages: [
+ { id: 'msg-001', reportId: 'wb-002', senderRole: 'ombudsperson', message: 'Vielen Dank fuer Ihre Meldung. Koennen Sie uns mitteilen, welche Dienstleister konkret betroffen sind?', createdAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), isRead: true },
+ { id: 'msg-002', reportId: 'wb-002', senderRole: 'reporter', message: 'Es handelt sich um die Firma DataServ GmbH und MarketPro AG. Die Listen werden per unverschluesselter E-Mail versendet.', createdAt: new Date(received2.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), isRead: true },
+ ],
+ attachments: [{ id: 'att-001', fileName: 'email_screenshot_vertrieb.png', fileSize: 245000, mimeType: 'image/png', uploadedAt: received2.toISOString(), uploadedBy: 'reporter' }],
+ auditTrail: [
+ { id: 'audit-002', action: 'report_created', description: 'Meldung per E-Mail eingegangen', performedBy: 'system', performedAt: received2.toISOString() },
+ { id: 'audit-003', action: 'acknowledged', description: 'Eingangsbestaetigung an Hinweisgeber versendet', performedBy: 'DSB Mueller', performedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'audit-004', action: 'status_changed', description: 'Status geaendert: Bestaetigt -> In Pruefung', performedBy: 'DSB Mueller', performedAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() },
+ ]
+ },
+ {
+ id: 'wb-003', referenceNumber: 'WB-2026-000003', accessKey: generateAccessKey(),
+ category: 'product_safety', status: 'investigation', priority: 'critical',
+ title: 'Fehlende Sicherheitspruefungen bei Produktfreigabe',
+ description: 'In der Fertigung werden seit Wochen Produkte ohne die vorgeschriebenen Sicherheitspruefungen freigegeben. Pruefprotokolle werden nachtraeglich erstellt, ohne dass tatsaechliche Pruefungen stattfinden.',
+ isAnonymous: true, assignedTo: 'Qualitaetsbeauftragter Weber',
+ receivedAt: received3.toISOString(),
+ acknowledgedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
+ deadlineAcknowledgment: deadlines3.ack, deadlineFeedback: deadlines3.fb,
+ measures: [
+ { id: 'msr-001', reportId: 'wb-003', title: 'Sofortiger Produktionsstopp fuer betroffene Charge', description: 'Produktion der betroffenen Produktlinie stoppen bis Pruefverfahren sichergestellt ist', status: 'completed', responsible: 'Fertigungsleitung', dueDate: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(received3.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'msr-002', reportId: 'wb-003', title: 'Externe Pruefung der Pruefprotokolle', description: 'Unabhaengige Pruefstelle mit der Revision aller Pruefprotokolle der letzten 6 Monate beauftragen', status: 'in_progress', responsible: 'Qualitaetsmanagement', dueDate: new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString() },
+ ],
+ messages: [],
+ attachments: [{ id: 'att-002', fileName: 'pruefprotokoll_vergleich.pdf', fileSize: 890000, mimeType: 'application/pdf', uploadedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), uploadedBy: 'ombudsperson' }],
+ auditTrail: [
+ { id: 'audit-005', action: 'report_created', description: 'Meldung ueber Online-Meldeformular eingegangen', performedBy: 'system', performedAt: received3.toISOString() },
+ { id: 'audit-006', action: 'acknowledged', description: 'Eingangsbestaetigung versendet', performedBy: 'Qualitaetsbeauftragter Weber', performedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'audit-007', action: 'investigation_started', description: 'Formelle Untersuchung eingeleitet', performedBy: 'Qualitaetsbeauftragter Weber', performedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() },
+ ]
+ },
+ {
+ id: 'wb-004', referenceNumber: 'WB-2026-000004', accessKey: generateAccessKey(),
+ category: 'fraud', status: 'closed', priority: 'high',
+ title: 'Gefaelschte Reisekostenabrechnungen',
+ description: 'Ein leitender Mitarbeiter reicht seit ueber einem Jahr gefaelschte Reisekostenabrechnungen ein. Hotelrechnungen werden manipuliert, Taxiquittungen erfunden.',
+ isAnonymous: false, reporterName: 'Thomas Klein', reporterEmail: 'thomas.klein@example.de', reporterPhone: '+49 170 9876543',
+ assignedTo: 'Compliance-Abteilung', receivedAt: received4.toISOString(),
+ acknowledgedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
+ deadlineAcknowledgment: deadlines4.ack, deadlineFeedback: deadlines4.fb,
+ closedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
+ measures: [
+ { id: 'msr-003', reportId: 'wb-004', title: 'Interne Revision der Reisekosten', description: 'Pruefung aller Reisekostenabrechnungen des betroffenen Mitarbeiters der letzten 24 Monate', status: 'completed', responsible: 'Interne Revision', dueDate: new Date(received4.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(received4.getTime() + 25 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'msr-004', reportId: 'wb-004', title: 'Arbeitsrechtliche Konsequenzen', description: 'Einleitung arbeitsrechtlicher Schritte nach Bestaetigung des Betrugs', status: 'completed', responsible: 'Personalabteilung', dueDate: new Date(received4.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(received4.getTime() + 55 * 24 * 60 * 60 * 1000).toISOString() },
+ ],
+ messages: [],
+ attachments: [{ id: 'att-003', fileName: 'vergleich_originalrechnung_einreichung.pdf', fileSize: 567000, mimeType: 'application/pdf', uploadedAt: received4.toISOString(), uploadedBy: 'reporter' }],
+ auditTrail: [
+ { id: 'audit-008', action: 'report_created', description: 'Meldung per Brief eingegangen', performedBy: 'system', performedAt: received4.toISOString() },
+ { id: 'audit-009', action: 'acknowledged', description: 'Eingangsbestaetigung versendet', performedBy: 'Compliance-Abteilung', performedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() },
+ { id: 'audit-010', action: 'closed', description: 'Fall abgeschlossen - Betrug bestaetigt, arbeitsrechtliche Massnahmen eingeleitet', performedBy: 'Compliance-Abteilung', performedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString() },
+ ]
+ }
+ ]
+}
+
+export function createMockStatistics(): WhistleblowerStatistics {
+ const reports = createMockReports()
+ const now = new Date()
+
+ const byStatus: Record = {
+ new: 0, acknowledged: 0, under_review: 0, investigation: 0,
+ measures_taken: 0, closed: 0, rejected: 0
+ }
+
+ const byCategory: Record = {
+ corruption: 0, fraud: 0, data_protection: 0, discrimination: 0,
+ environment: 0, competition: 0, product_safety: 0, tax_evasion: 0, other: 0
+ }
+
+ reports.forEach(r => {
+ byStatus[r.status]++
+ byCategory[r.category]++
+ })
+
+ const closedStatuses: ReportStatus[] = ['closed', 'rejected']
+
+ const overdueAcknowledgment = reports.filter(r => {
+ if (r.status !== 'new') return false
+ return now > new Date(r.deadlineAcknowledgment)
+ }).length
+
+ const overdueFeedback = reports.filter(r => {
+ if (closedStatuses.includes(r.status)) return false
+ return now > new Date(r.deadlineFeedback)
+ }).length
+
+ return {
+ totalReports: reports.length,
+ newReports: byStatus.new,
+ underReview: byStatus.under_review + byStatus.investigation,
+ closed: byStatus.closed + byStatus.rejected,
+ overdueAcknowledgment,
+ overdueFeedback,
+ byCategory,
+ byStatus
+ }
+}
diff --git a/admin-compliance/lib/sdk/whistleblower/api-operations.ts b/admin-compliance/lib/sdk/whistleblower/api-operations.ts
new file mode 100644
index 0000000..f5363a3
--- /dev/null
+++ b/admin-compliance/lib/sdk/whistleblower/api-operations.ts
@@ -0,0 +1,306 @@
+/**
+ * Whistleblower API Client — CRUD, Workflow, Messaging, Attachments, Statistics
+ *
+ * API client for Hinweisgeberschutzgesetz (HinSchG) compliant
+ * Whistleblower/Hinweisgebersystem management
+ */
+
+import {
+ WhistleblowerReport,
+ WhistleblowerStatistics,
+ ReportListResponse,
+ ReportFilters,
+ PublicReportSubmission,
+ ReportUpdateRequest,
+ AnonymousMessage,
+ WhistleblowerMeasure,
+ FileAttachment,
+} from './types'
+
+// =============================================================================
+// CONFIGURATION
+// =============================================================================
+
+const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
+const API_TIMEOUT = 30000
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+function getTenantId(): string {
+ if (typeof window !== 'undefined') {
+ return localStorage.getItem('bp_tenant_id') || 'default-tenant'
+ }
+ return 'default-tenant'
+}
+
+function getAuthHeaders(): HeadersInit {
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': getTenantId()
+ }
+
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('authToken')
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`
+ }
+ const userId = localStorage.getItem('bp_user_id')
+ if (userId) {
+ headers['X-User-ID'] = userId
+ }
+ }
+
+ return headers
+}
+
+async function fetchWithTimeout(
+ url: string,
+ options: RequestInit = {},
+ timeout: number = API_TIMEOUT
+): Promise {
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ signal: controller.signal,
+ headers: {
+ ...getAuthHeaders(),
+ ...options.headers
+ }
+ })
+
+ if (!response.ok) {
+ const errorBody = await response.text()
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`
+ try {
+ const errorJson = JSON.parse(errorBody)
+ errorMessage = errorJson.error || errorJson.message || errorMessage
+ } catch {
+ // Keep the HTTP status message
+ }
+ throw new Error(errorMessage)
+ }
+
+ const contentType = response.headers.get('content-type')
+ if (contentType && contentType.includes('application/json')) {
+ return response.json()
+ }
+
+ return {} as T
+ } finally {
+ clearTimeout(timeoutId)
+ }
+}
+
+// =============================================================================
+// ADMIN CRUD - Reports
+// =============================================================================
+
+export async function fetchReports(filters?: ReportFilters): Promise {
+ const params = new URLSearchParams()
+
+ if (filters) {
+ if (filters.status) {
+ const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
+ statuses.forEach(s => params.append('status', s))
+ }
+ if (filters.category) {
+ const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
+ categories.forEach(c => params.append('category', c))
+ }
+ if (filters.priority) params.set('priority', filters.priority)
+ if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
+ if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous))
+ if (filters.search) params.set('search', filters.search)
+ if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
+ if (filters.dateTo) params.set('dateTo', filters.dateTo)
+ }
+
+ const queryString = params.toString()
+ const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}`
+
+ return fetchWithTimeout(url)
+}
+
+export async function fetchReport(id: string): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
+ )
+}
+
+export async function updateReport(id: string, update: ReportUpdateRequest): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
+ { method: 'PUT', body: JSON.stringify(update) }
+ )
+}
+
+export async function deleteReport(id: string): Promise {
+ await fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
+ { method: 'DELETE' }
+ )
+}
+
+// =============================================================================
+// PUBLIC ENDPOINTS
+// =============================================================================
+
+export async function submitPublicReport(
+ data: PublicReportSubmission
+): Promise<{ report: WhistleblowerReport; accessKey: string }> {
+ const response = await fetch(
+ `${WB_API_BASE}/api/v1/public/whistleblower/submit`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ }
+ )
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+ }
+
+ return response.json()
+}
+
+export async function fetchReportByAccessKey(
+ accessKey: string
+): Promise {
+ const response = await fetch(
+ `${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`,
+ { method: 'GET', headers: { 'Content-Type': 'application/json' } }
+ )
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+ }
+
+ return response.json()
+}
+
+// =============================================================================
+// WORKFLOW ACTIONS
+// =============================================================================
+
+export async function acknowledgeReport(id: string): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
+ { method: 'POST' }
+ )
+}
+
+export async function startInvestigation(id: string): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`,
+ { method: 'POST' }
+ )
+}
+
+export async function addMeasure(
+ id: string,
+ measure: Omit
+): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`,
+ { method: 'POST', body: JSON.stringify(measure) }
+ )
+}
+
+export async function closeReport(
+ id: string,
+ resolution: { reason: string; notes: string }
+): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`,
+ { method: 'POST', body: JSON.stringify(resolution) }
+ )
+}
+
+// =============================================================================
+// ANONYMOUS MESSAGING
+// =============================================================================
+
+export async function sendMessage(
+ reportId: string,
+ message: string,
+ role: 'reporter' | 'ombudsperson'
+): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`,
+ { method: 'POST', body: JSON.stringify({ senderRole: role, message }) }
+ )
+}
+
+export async function fetchMessages(reportId: string): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`
+ )
+}
+
+// =============================================================================
+// ATTACHMENTS
+// =============================================================================
+
+export async function uploadAttachment(
+ reportId: string,
+ file: File
+): Promise {
+ const formData = new FormData()
+ formData.append('file', file)
+
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), 60000)
+
+ try {
+ const headers: HeadersInit = {
+ 'X-Tenant-ID': getTenantId()
+ }
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('authToken')
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`
+ }
+ }
+
+ const response = await fetch(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`,
+ {
+ method: 'POST',
+ headers,
+ body: formData,
+ signal: controller.signal
+ }
+ )
+
+ if (!response.ok) {
+ throw new Error(`Upload fehlgeschlagen: ${response.statusText}`)
+ }
+
+ return response.json()
+ } finally {
+ clearTimeout(timeoutId)
+ }
+}
+
+export async function deleteAttachment(id: string): Promise {
+ await fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
+ { method: 'DELETE' }
+ )
+}
+
+// =============================================================================
+// STATISTICS
+// =============================================================================
+
+export async function fetchWhistleblowerStatistics(): Promise {
+ return fetchWithTimeout(
+ `${WB_API_BASE}/api/v1/admin/whistleblower/statistics`
+ )
+}
diff --git a/admin-compliance/lib/sdk/whistleblower/api.ts b/admin-compliance/lib/sdk/whistleblower/api.ts
index 0e07909..09a545a 100644
--- a/admin-compliance/lib/sdk/whistleblower/api.ts
+++ b/admin-compliance/lib/sdk/whistleblower/api.ts
@@ -1,755 +1,31 @@
/**
- * Whistleblower System API Client
+ * Whistleblower System API Client — barrel re-export
*
- * API client for Hinweisgeberschutzgesetz (HinSchG) compliant
- * Whistleblower/Hinweisgebersystem management
- * Connects to the ai-compliance-sdk backend
+ * Split into:
+ * - api-operations.ts (CRUD, workflow, messaging, attachments, statistics)
+ * - api-mock-data.ts (mock data + SDK proxy)
*/
-import {
- WhistleblowerReport,
- WhistleblowerStatistics,
- ReportListResponse,
- ReportFilters,
- PublicReportSubmission,
- ReportUpdateRequest,
- MessageSendRequest,
- AnonymousMessage,
- WhistleblowerMeasure,
- FileAttachment,
- ReportCategory,
- ReportStatus,
- ReportPriority,
- generateAccessKey
-} from './types'
-
-// =============================================================================
-// CONFIGURATION
-// =============================================================================
-
-const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
-const API_TIMEOUT = 30000 // 30 seconds
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-function getTenantId(): string {
- if (typeof window !== 'undefined') {
- return localStorage.getItem('bp_tenant_id') || 'default-tenant'
- }
- return 'default-tenant'
-}
-
-function getAuthHeaders(): HeadersInit {
- const headers: HeadersInit = {
- 'Content-Type': 'application/json',
- 'X-Tenant-ID': getTenantId()
- }
-
- if (typeof window !== 'undefined') {
- const token = localStorage.getItem('authToken')
- if (token) {
- headers['Authorization'] = `Bearer ${token}`
- }
- const userId = localStorage.getItem('bp_user_id')
- if (userId) {
- headers['X-User-ID'] = userId
- }
- }
-
- return headers
-}
-
-async function fetchWithTimeout(
- url: string,
- options: RequestInit = {},
- timeout: number = API_TIMEOUT
-): Promise {
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), timeout)
-
- try {
- const response = await fetch(url, {
- ...options,
- signal: controller.signal,
- headers: {
- ...getAuthHeaders(),
- ...options.headers
- }
- })
-
- if (!response.ok) {
- const errorBody = await response.text()
- let errorMessage = `HTTP ${response.status}: ${response.statusText}`
- try {
- const errorJson = JSON.parse(errorBody)
- errorMessage = errorJson.error || errorJson.message || errorMessage
- } catch {
- // Keep the HTTP status message
- }
- throw new Error(errorMessage)
- }
-
- // Handle empty responses
- const contentType = response.headers.get('content-type')
- if (contentType && contentType.includes('application/json')) {
- return response.json()
- }
-
- return {} as T
- } finally {
- clearTimeout(timeoutId)
- }
-}
-
-// =============================================================================
-// ADMIN CRUD - Reports
-// =============================================================================
-
-/**
- * Alle Meldungen abrufen (Admin)
- */
-export async function fetchReports(filters?: ReportFilters): Promise {
- const params = new URLSearchParams()
-
- if (filters) {
- if (filters.status) {
- const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
- statuses.forEach(s => params.append('status', s))
- }
- if (filters.category) {
- const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
- categories.forEach(c => params.append('category', c))
- }
- if (filters.priority) params.set('priority', filters.priority)
- if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
- if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous))
- if (filters.search) params.set('search', filters.search)
- if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
- if (filters.dateTo) params.set('dateTo', filters.dateTo)
- }
-
- const queryString = params.toString()
- const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}`
-
- return fetchWithTimeout(url)
-}
-
-/**
- * Einzelne Meldung abrufen (Admin)
- */
-export async function fetchReport(id: string): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
- )
-}
-
-/**
- * Meldung aktualisieren (Status, Prioritaet, Kategorie, Zuweisung)
- */
-export async function updateReport(id: string, update: ReportUpdateRequest): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
- {
- method: 'PUT',
- body: JSON.stringify(update)
- }
- )
-}
-
-/**
- * Meldung loeschen (soft delete)
- */
-export async function deleteReport(id: string): Promise {
- await fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
- {
- method: 'DELETE'
- }
- )
-}
-
-// =============================================================================
-// PUBLIC ENDPOINTS - Kein Auth erforderlich
-// =============================================================================
-
-/**
- * Neue Meldung einreichen (oeffentlich, keine Auth)
- */
-export async function submitPublicReport(
- data: PublicReportSubmission
-): Promise<{ report: WhistleblowerReport; accessKey: string }> {
- const response = await fetch(
- `${WB_API_BASE}/api/v1/public/whistleblower/submit`,
- {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- }
- )
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
-
- return response.json()
-}
-
-/**
- * Meldung ueber Zugangscode abrufen (oeffentlich, keine Auth)
- */
-export async function fetchReportByAccessKey(
- accessKey: string
-): Promise {
- const response = await fetch(
- `${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`,
- {
- method: 'GET',
- headers: { 'Content-Type': 'application/json' }
- }
- )
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
-
- return response.json()
-}
-
-// =============================================================================
-// WORKFLOW ACTIONS
-// =============================================================================
-
-/**
- * Eingangsbestaetigung versenden (HinSchG ss 17 Abs. 1)
- */
-export async function acknowledgeReport(id: string): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
- {
- method: 'POST'
- }
- )
-}
-
-/**
- * Untersuchung starten
- */
-export async function startInvestigation(id: string): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`,
- {
- method: 'POST'
- }
- )
-}
-
-/**
- * Massnahme zu einer Meldung hinzufuegen
- */
-export async function addMeasure(
- id: string,
- measure: Omit
-): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`,
- {
- method: 'POST',
- body: JSON.stringify(measure)
- }
- )
-}
-
-/**
- * Meldung abschliessen mit Begruendung
- */
-export async function closeReport(
- id: string,
- resolution: { reason: string; notes: string }
-): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`,
- {
- method: 'POST',
- body: JSON.stringify(resolution)
- }
- )
-}
-
-// =============================================================================
-// ANONYMOUS MESSAGING
-// =============================================================================
-
-/**
- * Nachricht im anonymen Kanal senden
- */
-export async function sendMessage(
- reportId: string,
- message: string,
- role: 'reporter' | 'ombudsperson'
-): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`,
- {
- method: 'POST',
- body: JSON.stringify({ senderRole: role, message })
- }
- )
-}
-
-/**
- * Nachrichten fuer eine Meldung abrufen
- */
-export async function fetchMessages(reportId: string): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`
- )
-}
-
-// =============================================================================
-// ATTACHMENTS
-// =============================================================================
-
-/**
- * Anhang zu einer Meldung hochladen
- */
-export async function uploadAttachment(
- reportId: string,
- file: File
-): Promise {
- const formData = new FormData()
- formData.append('file', file)
-
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), 60000) // 60s for uploads
-
- try {
- const headers: HeadersInit = {
- 'X-Tenant-ID': getTenantId()
- }
- if (typeof window !== 'undefined') {
- const token = localStorage.getItem('authToken')
- if (token) {
- headers['Authorization'] = `Bearer ${token}`
- }
- }
-
- const response = await fetch(
- `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`,
- {
- method: 'POST',
- headers,
- body: formData,
- signal: controller.signal
- }
- )
-
- if (!response.ok) {
- throw new Error(`Upload fehlgeschlagen: ${response.statusText}`)
- }
-
- return response.json()
- } finally {
- clearTimeout(timeoutId)
- }
-}
-
-/**
- * Anhang loeschen
- */
-export async function deleteAttachment(id: string): Promise {
- await fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
- {
- method: 'DELETE'
- }
- )
-}
-
-// =============================================================================
-// STATISTICS
-// =============================================================================
-
-/**
- * Statistiken fuer das Whistleblower-Dashboard abrufen
- */
-export async function fetchWhistleblowerStatistics(): Promise {
- return fetchWithTimeout(
- `${WB_API_BASE}/api/v1/admin/whistleblower/statistics`
- )
-}
-
-// =============================================================================
-// SDK PROXY FUNCTION (via Next.js proxy)
-// =============================================================================
-
-/**
- * Fetch Whistleblower-Daten via SDK Proxy mit Fallback auf Mock-Daten
- */
-export async function fetchSDKWhistleblowerList(): Promise<{
- reports: WhistleblowerReport[]
- statistics: WhistleblowerStatistics
-}> {
- try {
- const [reportsResponse, statsResponse] = await Promise.all([
- fetchReports(),
- fetchWhistleblowerStatistics()
- ])
- return {
- reports: reportsResponse.reports,
- statistics: statsResponse
- }
- } catch (error) {
- console.error('Failed to load Whistleblower data from API, using mock data:', error)
- // Fallback to mock data
- const reports = createMockReports()
- const statistics = createMockStatistics()
- return { reports, statistics }
- }
-}
-
-// =============================================================================
-// MOCK DATA (Demo/Entwicklung)
-// =============================================================================
-
-/**
- * Erstellt Demo-Meldungen fuer Entwicklung und Praesentationen
- */
-export function createMockReports(): WhistleblowerReport[] {
- const now = new Date()
-
- // Helper: Berechne Fristen
- function calcDeadlines(receivedAt: Date): { ack: string; fb: string } {
- const ack = new Date(receivedAt)
- ack.setDate(ack.getDate() + 7)
- const fb = new Date(receivedAt)
- fb.setMonth(fb.getMonth() + 3)
- return { ack: ack.toISOString(), fb: fb.toISOString() }
- }
-
- const received1 = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
- const deadlines1 = calcDeadlines(received1)
-
- const received2 = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
- const deadlines2 = calcDeadlines(received2)
-
- const received3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
- const deadlines3 = calcDeadlines(received3)
-
- const received4 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
- const deadlines4 = calcDeadlines(received4)
-
- return [
- // Report 1: Neu
- {
- id: 'wb-001',
- referenceNumber: 'WB-2026-000001',
- accessKey: generateAccessKey(),
- category: 'corruption',
- status: 'new',
- priority: 'high',
- title: 'Unregelmaessigkeiten bei Auftragsvergabe',
- description: 'Bei der Vergabe des IT-Rahmenvertrags im November wurden offenbar Angebote eines bestimmten Anbieters bevorzugt. Der zustaendige Abteilungsleiter hat private Verbindungen zum Geschaeftsfuehrer des Anbieters.',
- isAnonymous: true,
- receivedAt: received1.toISOString(),
- deadlineAcknowledgment: deadlines1.ack,
- deadlineFeedback: deadlines1.fb,
- measures: [],
- messages: [],
- attachments: [],
- auditTrail: [
- {
- id: 'audit-001',
- action: 'report_created',
- description: 'Meldung ueber Online-Meldeformular eingegangen',
- performedBy: 'system',
- performedAt: received1.toISOString()
- }
- ]
- },
-
- // Report 2: In Pruefung (under_review)
- {
- id: 'wb-002',
- referenceNumber: 'WB-2026-000002',
- accessKey: generateAccessKey(),
- category: 'data_protection',
- status: 'under_review',
- priority: 'normal',
- title: 'Unerlaubte Weitergabe von Kundendaten',
- description: 'Ein Mitarbeiter der Vertriebsabteilung gibt regelmaessig Kundenlisten an externe Dienstleister weiter, ohne dass eine Auftragsverarbeitungsvereinbarung vorliegt.',
- isAnonymous: false,
- reporterName: 'Maria Schmidt',
- reporterEmail: 'maria.schmidt@example.de',
- assignedTo: 'DSB Mueller',
- receivedAt: received2.toISOString(),
- acknowledgedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
- deadlineAcknowledgment: deadlines2.ack,
- deadlineFeedback: deadlines2.fb,
- measures: [],
- messages: [
- {
- id: 'msg-001',
- reportId: 'wb-002',
- senderRole: 'ombudsperson',
- message: 'Vielen Dank fuer Ihre Meldung. Koennen Sie uns mitteilen, welche Dienstleister konkret betroffen sind?',
- createdAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
- isRead: true
- },
- {
- id: 'msg-002',
- reportId: 'wb-002',
- senderRole: 'reporter',
- message: 'Es handelt sich um die Firma DataServ GmbH und MarketPro AG. Die Listen werden per unverschluesselter E-Mail versendet.',
- createdAt: new Date(received2.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
- isRead: true
- }
- ],
- attachments: [
- {
- id: 'att-001',
- fileName: 'email_screenshot_vertrieb.png',
- fileSize: 245000,
- mimeType: 'image/png',
- uploadedAt: received2.toISOString(),
- uploadedBy: 'reporter'
- }
- ],
- auditTrail: [
- {
- id: 'audit-002',
- action: 'report_created',
- description: 'Meldung per E-Mail eingegangen',
- performedBy: 'system',
- performedAt: received2.toISOString()
- },
- {
- id: 'audit-003',
- action: 'acknowledged',
- description: 'Eingangsbestaetigung an Hinweisgeber versendet',
- performedBy: 'DSB Mueller',
- performedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'audit-004',
- action: 'status_changed',
- description: 'Status geaendert: Bestaetigt -> In Pruefung',
- performedBy: 'DSB Mueller',
- performedAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
- }
- ]
- },
-
- // Report 3: Untersuchung (investigation)
- {
- id: 'wb-003',
- referenceNumber: 'WB-2026-000003',
- accessKey: generateAccessKey(),
- category: 'product_safety',
- status: 'investigation',
- priority: 'critical',
- title: 'Fehlende Sicherheitspruefungen bei Produktfreigabe',
- description: 'In der Fertigung werden seit Wochen Produkte ohne die vorgeschriebenen Sicherheitspruefungen freigegeben. Pruefprotokolle werden nachtraeglich erstellt, ohne dass tatsaechliche Pruefungen stattfinden.',
- isAnonymous: true,
- assignedTo: 'Qualitaetsbeauftragter Weber',
- receivedAt: received3.toISOString(),
- acknowledgedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
- deadlineAcknowledgment: deadlines3.ack,
- deadlineFeedback: deadlines3.fb,
- measures: [
- {
- id: 'msr-001',
- reportId: 'wb-003',
- title: 'Sofortiger Produktionsstopp fuer betroffene Charge',
- description: 'Produktion der betroffenen Produktlinie stoppen bis Pruefverfahren sichergestellt ist',
- status: 'completed',
- responsible: 'Fertigungsleitung',
- dueDate: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
- completedAt: new Date(received3.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'msr-002',
- reportId: 'wb-003',
- title: 'Externe Pruefung der Pruefprotokolle',
- description: 'Unabhaengige Pruefstelle mit der Revision aller Pruefprotokolle der letzten 6 Monate beauftragen',
- status: 'in_progress',
- responsible: 'Qualitaetsmanagement',
- dueDate: new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString()
- }
- ],
- messages: [],
- attachments: [
- {
- id: 'att-002',
- fileName: 'pruefprotokoll_vergleich.pdf',
- fileSize: 890000,
- mimeType: 'application/pdf',
- uploadedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
- uploadedBy: 'ombudsperson'
- }
- ],
- auditTrail: [
- {
- id: 'audit-005',
- action: 'report_created',
- description: 'Meldung ueber Online-Meldeformular eingegangen',
- performedBy: 'system',
- performedAt: received3.toISOString()
- },
- {
- id: 'audit-006',
- action: 'acknowledged',
- description: 'Eingangsbestaetigung versendet',
- performedBy: 'Qualitaetsbeauftragter Weber',
- performedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'audit-007',
- action: 'investigation_started',
- description: 'Formelle Untersuchung eingeleitet',
- performedBy: 'Qualitaetsbeauftragter Weber',
- performedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
- }
- ]
- },
-
- // Report 4: Abgeschlossen (closed)
- {
- id: 'wb-004',
- referenceNumber: 'WB-2026-000004',
- accessKey: generateAccessKey(),
- category: 'fraud',
- status: 'closed',
- priority: 'high',
- title: 'Gefaelschte Reisekostenabrechnungen',
- description: 'Ein leitender Mitarbeiter reicht seit ueber einem Jahr gefaelschte Reisekostenabrechnungen ein. Hotelrechnungen werden manipuliert, Taxiquittungen erfunden.',
- isAnonymous: false,
- reporterName: 'Thomas Klein',
- reporterEmail: 'thomas.klein@example.de',
- reporterPhone: '+49 170 9876543',
- assignedTo: 'Compliance-Abteilung',
- receivedAt: received4.toISOString(),
- acknowledgedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
- deadlineAcknowledgment: deadlines4.ack,
- deadlineFeedback: deadlines4.fb,
- closedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
- measures: [
- {
- id: 'msr-003',
- reportId: 'wb-004',
- title: 'Interne Revision der Reisekosten',
- description: 'Pruefung aller Reisekostenabrechnungen des betroffenen Mitarbeiters der letzten 24 Monate',
- status: 'completed',
- responsible: 'Interne Revision',
- dueDate: new Date(received4.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
- completedAt: new Date(received4.getTime() + 25 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'msr-004',
- reportId: 'wb-004',
- title: 'Arbeitsrechtliche Konsequenzen',
- description: 'Einleitung arbeitsrechtlicher Schritte nach Bestaetigung des Betrugs',
- status: 'completed',
- responsible: 'Personalabteilung',
- dueDate: new Date(received4.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString(),
- completedAt: new Date(received4.getTime() + 55 * 24 * 60 * 60 * 1000).toISOString()
- }
- ],
- messages: [],
- attachments: [
- {
- id: 'att-003',
- fileName: 'vergleich_originalrechnung_einreichung.pdf',
- fileSize: 567000,
- mimeType: 'application/pdf',
- uploadedAt: received4.toISOString(),
- uploadedBy: 'reporter'
- }
- ],
- auditTrail: [
- {
- id: 'audit-008',
- action: 'report_created',
- description: 'Meldung per Brief eingegangen',
- performedBy: 'system',
- performedAt: received4.toISOString()
- },
- {
- id: 'audit-009',
- action: 'acknowledged',
- description: 'Eingangsbestaetigung versendet',
- performedBy: 'Compliance-Abteilung',
- performedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
- },
- {
- id: 'audit-010',
- action: 'closed',
- description: 'Fall abgeschlossen - Betrug bestaetigt, arbeitsrechtliche Massnahmen eingeleitet',
- performedBy: 'Compliance-Abteilung',
- performedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString()
- }
- ]
- }
- ]
-}
-
-/**
- * Berechnet Statistiken aus den Mock-Daten
- */
-export function createMockStatistics(): WhistleblowerStatistics {
- const reports = createMockReports()
- const now = new Date()
-
- const byStatus: Record = {
- new: 0,
- acknowledged: 0,
- under_review: 0,
- investigation: 0,
- measures_taken: 0,
- closed: 0,
- rejected: 0
- }
-
- const byCategory: Record = {
- corruption: 0,
- fraud: 0,
- data_protection: 0,
- discrimination: 0,
- environment: 0,
- competition: 0,
- product_safety: 0,
- tax_evasion: 0,
- other: 0
- }
-
- reports.forEach(r => {
- byStatus[r.status]++
- byCategory[r.category]++
- })
-
- const closedStatuses: ReportStatus[] = ['closed', 'rejected']
-
- // Pruefe ueberfaellige Eingangsbestaetigungen
- const overdueAcknowledgment = reports.filter(r => {
- if (r.status !== 'new') return false
- return now > new Date(r.deadlineAcknowledgment)
- }).length
-
- // Pruefe ueberfaellige Rueckmeldungen
- const overdueFeedback = reports.filter(r => {
- if (closedStatuses.includes(r.status)) return false
- return now > new Date(r.deadlineFeedback)
- }).length
-
- return {
- totalReports: reports.length,
- newReports: byStatus.new,
- underReview: byStatus.under_review + byStatus.investigation,
- closed: byStatus.closed + byStatus.rejected,
- overdueAcknowledgment,
- overdueFeedback,
- byCategory,
- byStatus
- }
-}
+export {
+ fetchReports,
+ fetchReport,
+ updateReport,
+ deleteReport,
+ submitPublicReport,
+ fetchReportByAccessKey,
+ acknowledgeReport,
+ startInvestigation,
+ addMeasure,
+ closeReport,
+ sendMessage,
+ fetchMessages,
+ uploadAttachment,
+ deleteAttachment,
+ fetchWhistleblowerStatistics,
+} from './api-operations'
+
+export {
+ fetchSDKWhistleblowerList,
+ createMockReports,
+ createMockStatistics,
+} from './api-mock-data'