refactor(admin): split 8 oversized lib/ files into focused modules under 500 LOC

Split these files that exceeded the 500-line hard cap:
- privacy-policy.ts (965 LOC) -> sections + renderers
- academy/api.ts (787 LOC) -> courses + mock-data
- whistleblower/api.ts (755 LOC) -> operations + mock-data
- vvt-profiling.ts (659 LOC) -> data + logic
- cookie-banner.ts (595 LOC) -> config + embed
- dsr/types.ts (581 LOC) -> core + api types
- tom-generator/rules-engine.ts (560 LOC) -> evaluator + gap-analysis
- datapoint-helpers.ts (548 LOC) -> generators + validators

Each original file becomes a barrel re-export for backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-10 21:05:59 +02:00
parent be4d58009a
commit 528abc86ab
24 changed files with 4471 additions and 5402 deletions

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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<Course[]> {
const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
`${ACADEMY_API_BASE}/courses`
)
return mapCoursesFromBackend(res.courses || [])
}
export async function fetchCourse(id: string): Promise<Course> {
const res = await fetchWithTimeout<{ course: BackendCourse }>(
`${ACADEMY_API_BASE}/courses/${id}`
)
return mapCourseFromBackend(res.course)
}
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/courses`,
{ method: 'POST', body: JSON.stringify(request) }
)
}
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/courses/${id}`,
{ method: 'PUT', body: JSON.stringify(update) }
)
}
export async function deleteCourse(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/courses/${id}`,
{ method: 'DELETE' }
)
}
// =============================================================================
// ENROLLMENTS
// =============================================================================
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
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<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments`,
{ method: 'POST', body: JSON.stringify(request) }
)
}
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
{ method: 'PUT', body: JSON.stringify(update) }
)
}
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
{ method: 'POST' }
)
}
export async function deleteEnrollment(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/enrollments/${id}`,
{ method: 'DELETE' }
)
}
export async function updateEnrollment(id: string, data: { deadline?: string }): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${id}`,
{ method: 'PUT', body: JSON.stringify(data) }
)
}
// =============================================================================
// CERTIFICATES
// =============================================================================
export async function fetchCertificate(id: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/certificates/${id}`
)
}
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
{ method: 'POST' }
)
}
export async function fetchCertificates(): Promise<Certificate[]> {
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<SubmitQuizResponse> {
return fetchWithTimeout<SubmitQuizResponse>(
`${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<AcademyStatistics> {
const res = await fetchWithTimeout<{
total_courses: number
total_enrollments: number
completion_rate: number
overdue_count: number
avg_completion_days: number
by_category?: Record<string, number>
by_status?: Record<string, number>
}>(`${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<CourseCategory, number>,
byStatus: (res.by_status || {}) as Record<EnrollmentStatus, number>,
}
}
// =============================================================================
// 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`
)
}

View File

@@ -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,
}
}
}

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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<Course[]> {
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<Course> {
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<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/courses`,
{
method: 'POST',
body: JSON.stringify(request)
}
)
}
/**
* Kurs aktualisieren
*/
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/courses/${id}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Kurs loeschen
*/
export async function deleteCourse(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/courses/${id}`,
{
method: 'DELETE'
}
)
}
// =============================================================================
// ENROLLMENTS
// =============================================================================
/**
* Einschreibungen abrufen (optional gefiltert nach Kurs-ID)
*/
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
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<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments`,
{
method: 'POST',
body: JSON.stringify(request)
}
)
}
/**
* Fortschritt einer Einschreibung aktualisieren
*/
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Einschreibung als abgeschlossen markieren
*/
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
{
method: 'POST'
}
)
}
// =============================================================================
// CERTIFICATES
// =============================================================================
/**
* Zertifikat abrufen
*/
export async function fetchCertificate(id: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/certificates/${id}`
)
}
/**
* Zertifikat generieren nach erfolgreichem Kursabschluss
*/
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
{
method: 'POST'
}
)
}
/**
* Alle Zertifikate abrufen
*/
export async function fetchCertificates(): Promise<Certificate[]> {
const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>(
`${ACADEMY_API_BASE}/certificates`
)
return res.certificates || []
}
/**
* Einschreibung loeschen
*/
export async function deleteEnrollment(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/enrollments/${id}`,
{ method: 'DELETE' }
)
}
/**
* Einschreibung aktualisieren (z.B. Deadline)
*/
export async function updateEnrollment(id: string, data: { deadline?: string }): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${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<SubmitQuizResponse> {
return fetchWithTimeout<SubmitQuizResponse>(
`${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<AcademyStatistics> {
const res = await fetchWithTimeout<{
total_courses: number
total_enrollments: number
completion_rate: number
overdue_count: number
avg_completion_days: number
by_category?: Record<string, number>
by_status?: Record<string, number>
}>(`${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<CourseCategory, number>,
byStatus: (res.by_status || {}) as Record<EnrollmentStatus, number>,
}
}
// =============================================================================
// 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'

View File

@@ -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<RetentionPeriod, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.retentionPeriod
if (!acc[key]) acc[key] = []
acc[key].push(dp)
return acc
}, {} as Record<RetentionPeriod, DataPoint[]>)
}
export function groupByCategory(
dataPoints: DataPoint[]
): Record<DataPointCategory, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.category
if (!acc[key]) acc[key] = []
acc[key].push(dp)
return acc
}, {} as Record<DataPointCategory, DataPoint[]>)
}
// =============================================================================
// 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<string>()
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<LegalBasis>()
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<string>()
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<string>()
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<RiskLevel, number> = { 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)
}
}

View File

@@ -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<RetentionPeriod, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.retentionPeriod
if (!acc[key]) {
acc[key] = []
}
acc[key].push(dp)
return acc
}, {} as Record<RetentionPeriod, DataPoint[]>)
}
/**
* 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<DataPointCategory, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.category
if (!acc[key]) {
acc[key] = []
}
acc[key].push(dp)
return acc
}, {} as Record<DataPointCategory, DataPoint[]>)
}
/**
* 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<string>()
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<LegalBasis>()
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<string>()
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<string>()
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<RiskLevel, number> = {
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'

View File

@@ -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
}

View File

@@ -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<DSRStatus, number>
byType: Record<DSRType, number>
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]
}

View File

@@ -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<DSRType, DSRTypeInfo> = {
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<DSRStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
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<DSRErasureChecklistItem, 'checked' | 'applies' | 'notes'>[] = [
{ 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
}

View File

@@ -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<DSRType, DSRTypeInfo> = {
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<DSRStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
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<DSRErasureChecklistItem, 'checked' | 'applies' | 'notes'>[] = [
{
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<DSRStatus, number>
byType: Record<DSRType, number>
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'

View File

@@ -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<string, string> = {
'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<CookieBannerTexts>,
customStyling?: Partial<CookieBannerStyling>
): 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(),
}
}

View File

@@ -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 = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
return { html, css, js, scriptTag }
}
// =============================================================================
// CSS GENERATION
// =============================================================================
function generateCSS(styling: CookieBannerStyling): string {
const positionStyles: Record<string, string> = {
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 `
<div class="cookie-banner-category" data-category="${cat.id}">
<div class="cookie-banner-category-info">
<div class="cookie-banner-category-name">${cat.name.de}</div>
<div class="cookie-banner-category-desc">${cat.description.de}</div>
</div>
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
data-category="${cat.id}"
data-required="${isRequired}"></div>
</div>
`
})
.join('')
return `
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
<div class="cookie-banner-description">${config.texts.description.de}</div>
<div class="cookie-banner-buttons">
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
${config.texts.rejectAll.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
${config.texts.customize.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
${config.texts.acceptAll.de}
</button>
</div>
<div class="cookie-banner-details" id="cookieBannerDetails">
${categoriesHTML}
<div class="cookie-banner-buttons" style="margin-top: 16px;">
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
${config.texts.save.de}
</button>
</div>
</div>
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
${config.texts.privacyPolicyLink.de}
</a>
</div>
`.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()
}

View File

@@ -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<string, string> = {
'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<CookieBannerTexts>,
customStyling?: Partial<CookieBannerStyling>
): 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 = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
return {
html,
css,
js,
scriptTag,
}
}
/**
* Generiert das CSS fuer den Cookie Banner
*/
function generateCSS(styling: CookieBannerStyling): string {
const positionStyles: Record<string, string> = {
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 `
<div class="cookie-banner-category" data-category="${cat.id}">
<div class="cookie-banner-category-info">
<div class="cookie-banner-category-name">${cat.name.de}</div>
<div class="cookie-banner-category-desc">${cat.description.de}</div>
</div>
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
data-category="${cat.id}"
data-required="${isRequired}"></div>
</div>
`
})
.join('')
return `
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
<div class="cookie-banner-description">${config.texts.description.de}</div>
<div class="cookie-banner-buttons">
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
${config.texts.rejectAll.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
${config.texts.customize.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
${config.texts.acceptAll.de}
</button>
</div>
<div class="cookie-banner-details" id="cookieBannerDetails">
${categoriesHTML}
<div class="cookie-banner-buttons" style="margin-top: 16px;">
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
${config.texts.save.de}
</button>
</div>
</div>
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
${config.texts.privacyPolicyLink.de}
</a>
</div>
`.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'

View File

@@ -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, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/### (.+)/g, '<h3>$1</h3>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
return `
<section id="${section.id}">
<h2>${t(section.title, language)}</h2>
<p>${content}</p>
</section>
`
})
.join('\n')
return `<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
color: #333;
}
h1 { font-size: 2rem; margin-bottom: 2rem; }
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
p { margin: 1rem 0; }
ul, ol { margin: 1rem 0; padding-left: 2rem; }
li { margin: 0.5rem 0; }
strong { font-weight: 600; }
</style>
</head>
<body>
<h1>${title}</h1>
${sectionsHTML}
</body>
</html>`
}
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}`
}

View File

@@ -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<DataPointCategory, DataPoint[]> {
const grouped = new Map<DataPointCategory, DataPoint[]>()
for (const dp of dataPoints) {
const existing = grouped.get(dp.category) || []
grouped.set(dp.category, [...existing, dp])
}
return grouped
}
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
const grouped = new Map<LegalBasis, DataPoint[]>()
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<string>()
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<string, DataPoint[]>()
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<string, DataPoint[]>()
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,
}
}

View File

@@ -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<DataPointCategory, DataPoint[]> {
const grouped = new Map<DataPointCategory, DataPoint[]>()
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<LegalBasis, DataPoint[]> {
const grouped = new Map<LegalBasis, DataPoint[]>()
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<string>()
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<string, DataPoint[]>()
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<string, DataPoint[]>()
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, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/### (.+)/g, '<h3>$1</h3>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
return `
<section id="${section.id}">
<h2>${t(section.title, language)}</h2>
<p>${content}</p>
</section>
`
})
.join('\n')
return `<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
color: #333;
}
h1 { font-size: 2rem; margin-bottom: 2rem; }
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
p { margin: 1rem 0; }
ul, ol { margin: 1rem 0; padding-left: 2rem; }
li { margin: 0.5rem 0; }
strong { font-weight: 600; }
</style>
</head>
<body>
<h1>${title}</h1>
${sectionsHTML}
</body>
</html>`
}
/**
* 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'

View File

@@ -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)
}

View File

@@ -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<string, unknown>)[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<string, string> = {
'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<string, { required: number; recommended: number }>
} {
const results = this.evaluateControls(context)
const stats = {
total: results.length,
required: 0,
recommended: 0,
optional: 0,
notApplicable: 0,
byCategory: new Map<string, { required: number; recommended: number }>(),
}
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'

View File

@@ -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<string, unknown>)[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<string, string> = {
'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<string, { required: number; recommended: number }>
} {
const results = this.evaluateControls(context)
const stats = {
total: results.length, required: 0, recommended: 0, optional: 0, notApplicable: 0,
byCategory: new Map<string, { required: number; recommended: number }>(),
}
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()
}
}

View File

@@ -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<string, DepartmentDataConfig> = {
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 },
]
},
}

View File

@@ -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<string>()
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<string, unknown>
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',
]

View File

@@ -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<string, DepartmentDataConfig> = {
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<string>()
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<string, unknown>
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'

View File

@@ -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<ReportStatus, number> = {
new: 0, acknowledged: 0, under_review: 0, investigation: 0,
measures_taken: 0, closed: 0, rejected: 0
}
const byCategory: Record<ReportCategory, number> = {
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
}
}

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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<ReportListResponse> {
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<ReportListResponse>(url)
}
export async function fetchReport(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
)
}
export async function updateReport(id: string, update: ReportUpdateRequest): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
{ method: 'PUT', body: JSON.stringify(update) }
)
}
export async function deleteReport(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${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<WhistleblowerReport> {
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<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
{ method: 'POST' }
)
}
export async function startInvestigation(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`,
{ method: 'POST' }
)
}
export async function addMeasure(
id: string,
measure: Omit<WhistleblowerMeasure, 'id' | 'reportId' | 'completedAt'>
): Promise<WhistleblowerMeasure> {
return fetchWithTimeout<WhistleblowerMeasure>(
`${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<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${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<AnonymousMessage> {
return fetchWithTimeout<AnonymousMessage>(
`${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<AnonymousMessage[]> {
return fetchWithTimeout<AnonymousMessage[]>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`
)
}
// =============================================================================
// ATTACHMENTS
// =============================================================================
export async function uploadAttachment(
reportId: string,
file: File
): Promise<FileAttachment> {
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<void> {
await fetchWithTimeout<void>(
`${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
{ method: 'DELETE' }
)
}
// =============================================================================
// STATISTICS
// =============================================================================
export async function fetchWhistleblowerStatistics(): Promise<WhistleblowerStatistics> {
return fetchWithTimeout<WhistleblowerStatistics>(
`${WB_API_BASE}/api/v1/admin/whistleblower/statistics`
)
}

View File

@@ -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<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
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<ReportListResponse> {
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<ReportListResponse>(url)
}
/**
* Einzelne Meldung abrufen (Admin)
*/
export async function fetchReport(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
)
}
/**
* Meldung aktualisieren (Status, Prioritaet, Kategorie, Zuweisung)
*/
export async function updateReport(id: string, update: ReportUpdateRequest): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${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<void> {
await fetchWithTimeout<void>(
`${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<WhistleblowerReport> {
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<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
{
method: 'POST'
}
)
}
/**
* Untersuchung starten
*/
export async function startInvestigation(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${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<WhistleblowerMeasure, 'id' | 'reportId' | 'completedAt'>
): Promise<WhistleblowerMeasure> {
return fetchWithTimeout<WhistleblowerMeasure>(
`${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<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${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<AnonymousMessage> {
return fetchWithTimeout<AnonymousMessage>(
`${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<AnonymousMessage[]> {
return fetchWithTimeout<AnonymousMessage[]>(
`${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<FileAttachment> {
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<void> {
await fetchWithTimeout<void>(
`${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
{
method: 'DELETE'
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Statistiken fuer das Whistleblower-Dashboard abrufen
*/
export async function fetchWhistleblowerStatistics(): Promise<WhistleblowerStatistics> {
return fetchWithTimeout<WhistleblowerStatistics>(
`${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<ReportStatus, number> = {
new: 0,
acknowledged: 0,
under_review: 0,
investigation: 0,
measures_taken: 0,
closed: 0,
rejected: 0
}
const byCategory: Record<ReportCategory, number> = {
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'