From 528abc86ab1a7ecd828de1dcc6f3da64768d352b Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:05:59 +0200 Subject: [PATCH] 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) --- .../lib/sdk/academy/api-courses.ts | 385 +++++++ .../lib/sdk/academy/api-mock-data.ts | 157 +++ admin-compliance/lib/sdk/academy/api.ts | 815 +-------------- .../datapoint-generators.ts | 265 +++++ .../document-generator/datapoint-helpers.ts | 577 +---------- .../datapoint-validators.ts | 144 +++ admin-compliance/lib/sdk/dsr/types-api.ts | 243 +++++ admin-compliance/lib/sdk/dsr/types-core.ts | 235 +++++ admin-compliance/lib/sdk/dsr/types.ts | 633 +----------- .../generator/cookie-banner-config.ts | 119 +++ .../generator/cookie-banner-embed.ts | 418 ++++++++ .../einwilligungen/generator/cookie-banner.ts | 605 +---------- .../generator/privacy-policy-renderers.ts | 322 ++++++ .../generator/privacy-policy-sections.ts | 559 ++++++++++ .../generator/privacy-policy.ts | 958 +----------------- .../lib/sdk/tom-generator/gap-analysis.ts | 171 ++++ .../lib/sdk/tom-generator/rules-engine.ts | 569 +---------- .../lib/sdk/tom-generator/rules-evaluator.ts | 276 +++++ .../lib/sdk/vvt-profiling-data.ts | 286 ++++++ .../lib/sdk/vvt-profiling-logic.ts | 187 ++++ admin-compliance/lib/sdk/vvt-profiling.ts | 678 +------------ .../lib/sdk/whistleblower/api-mock-data.ts | 187 ++++ .../lib/sdk/whistleblower/api-operations.ts | 306 ++++++ admin-compliance/lib/sdk/whistleblower/api.ts | 778 +------------- 24 files changed, 4471 insertions(+), 5402 deletions(-) create mode 100644 admin-compliance/lib/sdk/academy/api-courses.ts create mode 100644 admin-compliance/lib/sdk/academy/api-mock-data.ts create mode 100644 admin-compliance/lib/sdk/document-generator/datapoint-generators.ts create mode 100644 admin-compliance/lib/sdk/document-generator/datapoint-validators.ts create mode 100644 admin-compliance/lib/sdk/dsr/types-api.ts create mode 100644 admin-compliance/lib/sdk/dsr/types-core.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/gap-analysis.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts create mode 100644 admin-compliance/lib/sdk/vvt-profiling-data.ts create mode 100644 admin-compliance/lib/sdk/vvt-profiling-logic.ts create mode 100644 admin-compliance/lib/sdk/whistleblower/api-mock-data.ts create mode 100644 admin-compliance/lib/sdk/whistleblower/api-operations.ts diff --git a/admin-compliance/lib/sdk/academy/api-courses.ts b/admin-compliance/lib/sdk/academy/api-courses.ts new file mode 100644 index 0000000..d83653a --- /dev/null +++ b/admin-compliance/lib/sdk/academy/api-courses.ts @@ -0,0 +1,385 @@ +/** + * Academy API Client — Course, Enrollment, Certificate, Quiz, Statistics, Generation + * + * API client for the Compliance E-Learning Academy module + * Connects to the ai-compliance-sdk backend via Next.js proxy + */ + +import type { + Course, + CourseCategory, + CourseCreateRequest, + CourseUpdateRequest, + GenerateCourseRequest, + Enrollment, + EnrollmentStatus, + EnrollmentListResponse, + EnrollUserRequest, + UpdateProgressRequest, + Certificate, + AcademyStatistics, + LessonType, + SubmitQuizRequest, + SubmitQuizResponse, +} from './types' + +// ============================================================================= +// CONFIGURATION +// ============================================================================= + +const ACADEMY_API_BASE = '/api/sdk/v1/academy' +const API_TIMEOUT = 30000 + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function getTenantId(): string { + if (typeof window !== 'undefined') { + return localStorage.getItem('bp_tenant_id') || 'default-tenant' + } + return 'default-tenant' +} + +function getAuthHeaders(): HeadersInit { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + 'X-Tenant-ID': getTenantId() + } + + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken') + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + const userId = localStorage.getItem('bp_user_id') + if (userId) { + headers['X-User-ID'] = userId + } + } + + return headers +} + +async function fetchWithTimeout( + url: string, + options: RequestInit = {}, + timeout: number = API_TIMEOUT +): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + ...getAuthHeaders(), + ...options.headers + } + }) + + if (!response.ok) { + const errorBody = await response.text() + let errorMessage = `HTTP ${response.status}: ${response.statusText}` + try { + const errorJson = JSON.parse(errorBody) + errorMessage = errorJson.error || errorJson.message || errorMessage + } catch { + // Keep the HTTP status message + } + throw new Error(errorMessage) + } + + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + return response.json() + } + + return {} as T + } finally { + clearTimeout(timeoutId) + } +} + +// ============================================================================= +// BACKEND TYPES +// ============================================================================= + +interface BackendCourse { + id: string + title: string + description: string + category: CourseCategory + duration_minutes: number + required_for_roles: string[] + is_active: boolean + passing_score?: number + status?: string + lessons?: BackendLesson[] + created_at: string + updated_at: string +} + +interface BackendQuizQuestion { + id: string + question: string + options: string[] + correct_index: number + explanation: string +} + +interface BackendLesson { + id: string + course_id: string + title: string + description?: string + lesson_type: LessonType + content_url?: string + duration_minutes: number + order_index: number + quiz_questions?: BackendQuizQuestion[] +} + +function mapCourseFromBackend(bc: BackendCourse): Course { + return { + id: bc.id, + title: bc.title, + description: bc.description || '', + category: bc.category, + durationMinutes: bc.duration_minutes || 0, + passingScore: bc.passing_score ?? 70, + isActive: bc.is_active ?? true, + status: (bc.status as 'draft' | 'published') ?? 'draft', + requiredForRoles: bc.required_for_roles || [], + lessons: (bc.lessons || []).map(l => ({ + id: l.id, + courseId: l.course_id, + title: l.title, + type: l.lesson_type, + contentMarkdown: l.content_url || '', + durationMinutes: l.duration_minutes || 0, + order: l.order_index, + quizQuestions: (l.quiz_questions || []).map(q => ({ + id: q.id || `q-${Math.random().toString(36).slice(2)}`, + lessonId: l.id, + question: q.question, + options: q.options, + correctOptionIndex: q.correct_index, + explanation: q.explanation, + })), + })), + createdAt: bc.created_at, + updatedAt: bc.updated_at, + } +} + +function mapCoursesFromBackend(courses: BackendCourse[]): Course[] { + return courses.map(mapCourseFromBackend) +} + +// ============================================================================= +// COURSE CRUD +// ============================================================================= + +export async function fetchCourses(): Promise { + const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>( + `${ACADEMY_API_BASE}/courses` + ) + return mapCoursesFromBackend(res.courses || []) +} + +export async function fetchCourse(id: string): Promise { + const res = await fetchWithTimeout<{ course: BackendCourse }>( + `${ACADEMY_API_BASE}/courses/${id}` + ) + return mapCourseFromBackend(res.course) +} + +export async function createCourse(request: CourseCreateRequest): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/courses`, + { method: 'POST', body: JSON.stringify(request) } + ) +} + +export async function updateCourse(id: string, update: CourseUpdateRequest): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/courses/${id}`, + { method: 'PUT', body: JSON.stringify(update) } + ) +} + +export async function deleteCourse(id: string): Promise { + await fetchWithTimeout( + `${ACADEMY_API_BASE}/courses/${id}`, + { method: 'DELETE' } + ) +} + +// ============================================================================= +// ENROLLMENTS +// ============================================================================= + +export async function fetchEnrollments(courseId?: string): Promise { + const params = new URLSearchParams() + if (courseId) { + params.set('course_id', courseId) + } + const queryString = params.toString() + const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}` + + const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url) + return res.enrollments || [] +} + +export async function enrollUser(request: EnrollUserRequest): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/enrollments`, + { method: 'POST', body: JSON.stringify(request) } + ) +} + +export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`, + { method: 'PUT', body: JSON.stringify(update) } + ) +} + +export async function completeEnrollment(enrollmentId: string): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`, + { method: 'POST' } + ) +} + +export async function deleteEnrollment(id: string): Promise { + await fetchWithTimeout( + `${ACADEMY_API_BASE}/enrollments/${id}`, + { method: 'DELETE' } + ) +} + +export async function updateEnrollment(id: string, data: { deadline?: string }): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/enrollments/${id}`, + { method: 'PUT', body: JSON.stringify(data) } + ) +} + +// ============================================================================= +// CERTIFICATES +// ============================================================================= + +export async function fetchCertificate(id: string): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/certificates/${id}` + ) +} + +export async function generateCertificate(enrollmentId: string): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`, + { method: 'POST' } + ) +} + +export async function fetchCertificates(): Promise { + const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>( + `${ACADEMY_API_BASE}/certificates` + ) + return res.certificates || [] +} + +// ============================================================================= +// QUIZ +// ============================================================================= + +export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`, + { method: 'POST', body: JSON.stringify(answers) } + ) +} + +export async function updateLesson(lessonId: string, update: { + title?: string + description?: string + content_url?: string + duration_minutes?: number + quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }> +}): Promise<{ lesson: any }> { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/lessons/${lessonId}`, + { method: 'PUT', body: JSON.stringify(update) } + ) +} + +// ============================================================================= +// STATISTICS +// ============================================================================= + +export async function fetchAcademyStatistics(): Promise { + const res = await fetchWithTimeout<{ + total_courses: number + total_enrollments: number + completion_rate: number + overdue_count: number + avg_completion_days: number + by_category?: Record + by_status?: Record + }>(`${ACADEMY_API_BASE}/stats`) + + return { + totalCourses: res.total_courses || 0, + totalEnrollments: res.total_enrollments || 0, + completionRate: res.completion_rate || 0, + overdueCount: res.overdue_count || 0, + byCategory: (res.by_category || {}) as Record, + byStatus: (res.by_status || {}) as Record, + } +} + +// ============================================================================= +// COURSE GENERATION +// ============================================================================= + +export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> { + return fetchWithTimeout<{ course: Course }>( + `${ACADEMY_API_BASE}/courses/generate`, + { + method: 'POST', + body: JSON.stringify({ module_id: request.moduleId || request.title }) + }, + 120000 + ) +} + +export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/courses/generate-all`, + { method: 'POST' }, + 300000 + ) +} + +export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> { + return fetchWithTimeout<{ status: string; jobId?: string }>( + `${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`, + { method: 'POST' }, + 300000 + ) +} + +export async function getVideoStatus(courseId: string): Promise<{ + status: string + total: number + completed: number + failed: number + videos: Array<{ lessonId: string; status: string; url?: string }> +}> { + return fetchWithTimeout( + `${ACADEMY_API_BASE}/courses/${courseId}/video-status` + ) +} diff --git a/admin-compliance/lib/sdk/academy/api-mock-data.ts b/admin-compliance/lib/sdk/academy/api-mock-data.ts new file mode 100644 index 0000000..d1b4108 --- /dev/null +++ b/admin-compliance/lib/sdk/academy/api-mock-data.ts @@ -0,0 +1,157 @@ +/** + * Academy API — Mock Data & SDK Proxy + * + * Fallback mock data for development and SDK proxy function + */ + +import type { + Course, + CourseCategory, + Enrollment, + EnrollmentStatus, + AcademyStatistics, +} from './types' +import { isEnrollmentOverdue } from './types' +import { + fetchCourses, + fetchEnrollments, + fetchAcademyStatistics, +} from './api-courses' + +// ============================================================================= +// SDK PROXY FUNCTION +// ============================================================================= + +export async function fetchSDKAcademyList(): Promise<{ + courses: Course[] + enrollments: Enrollment[] + statistics: AcademyStatistics +}> { + try { + const [courses, enrollments, statistics] = await Promise.all([ + fetchCourses(), + fetchEnrollments(), + fetchAcademyStatistics() + ]) + + return { courses, enrollments, statistics } + } catch (error) { + console.error('Failed to load Academy data from backend, using mock data:', error) + + const courses = createMockCourses() + const enrollments = createMockEnrollments() + const statistics = createMockStatistics(courses, enrollments) + + return { courses, enrollments, statistics } + } +} + +// ============================================================================= +// MOCK DATA +// ============================================================================= + +export function createMockCourses(): Course[] { + const now = new Date() + + return [ + { + id: 'course-001', + title: 'DSGVO-Grundlagen fuer Mitarbeiter', + description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.', + category: 'dsgvo_basics', + durationMinutes: 90, + passingScore: 80, + isActive: true, + status: 'published', + requiredForRoles: ['all'], + createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + lessons: [ + { id: 'lesson-001-01', courseId: 'course-001', order: 1, title: 'Was ist die DSGVO?', type: 'text', contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...', durationMinutes: 15 }, + { id: 'lesson-001-02', courseId: 'course-001', order: 2, title: 'Die 7 Grundsaetze der DSGVO', type: 'video', contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.', durationMinutes: 20, videoUrl: '/videos/dsgvo-grundsaetze.mp4' }, + { id: 'lesson-001-03', courseId: 'course-001', order: 3, title: 'Betroffenenrechte (Art. 15-21)', type: 'text', contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.', durationMinutes: 20 }, + { id: 'lesson-001-04', courseId: 'course-001', order: 4, title: 'Personenbezogene Daten im Arbeitsalltag', type: 'video', contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.', durationMinutes: 15, videoUrl: '/videos/dsgvo-praxis.mp4' }, + { id: 'lesson-001-05', courseId: 'course-001', order: 5, title: 'Wissenstest: DSGVO-Grundlagen', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.', durationMinutes: 20 }, + ] + }, + { + id: 'course-002', + title: 'IT-Sicherheit & Cybersecurity Awareness', + description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.', + category: 'it_security', + durationMinutes: 60, + passingScore: 75, + isActive: true, + status: 'published', + requiredForRoles: ['all'], + createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), + lessons: [ + { id: 'lesson-002-01', courseId: 'course-002', order: 1, title: 'Phishing erkennen und vermeiden', type: 'video', contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?', durationMinutes: 15, videoUrl: '/videos/phishing-awareness.mp4' }, + { id: 'lesson-002-02', courseId: 'course-002', order: 2, title: 'Sichere Passwoerter und MFA', type: 'text', contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.', durationMinutes: 15 }, + { id: 'lesson-002-03', courseId: 'course-002', order: 3, title: 'Social Engineering und Manipulation', type: 'text', contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.', durationMinutes: 15 }, + { id: 'lesson-002-04', courseId: 'course-002', order: 4, title: 'Wissenstest: IT-Sicherheit', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.', durationMinutes: 15 }, + ] + }, + { + id: 'course-003', + title: 'AI Literacy - Sicherer Umgang mit KI', + description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.', + category: 'ai_literacy', + durationMinutes: 75, + passingScore: 70, + isActive: true, + status: 'draft', + requiredForRoles: ['admin', 'data_protection_officer'], + createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), + lessons: [ + { id: 'lesson-003-01', courseId: 'course-003', order: 1, title: 'Was ist Kuenstliche Intelligenz?', type: 'text', contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.', durationMinutes: 15 }, + { id: 'lesson-003-02', courseId: 'course-003', order: 2, title: 'Der EU AI Act - Was bedeutet er fuer uns?', type: 'video', contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.', durationMinutes: 20, videoUrl: '/videos/eu-ai-act.mp4' }, + { id: 'lesson-003-03', courseId: 'course-003', order: 3, title: 'KI-Werkzeuge sicher nutzen', type: 'text', contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.', durationMinutes: 20 }, + { id: 'lesson-003-04', courseId: 'course-003', order: 4, title: 'Wissenstest: AI Literacy', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.', durationMinutes: 20 }, + ] + } + ] +} + +export function createMockEnrollments(): Enrollment[] { + const now = new Date() + + return [ + { id: 'enr-001', courseId: 'course-001', userId: 'user-001', userName: 'Maria Fischer', userEmail: 'maria.fischer@example.de', status: 'in_progress', progress: 40, startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'enr-002', courseId: 'course-002', userId: 'user-002', userName: 'Stefan Mueller', userEmail: 'stefan.mueller@example.de', status: 'completed', progress: 100, startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(), certificateId: 'cert-001', deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'enr-003', courseId: 'course-001', userId: 'user-003', userName: 'Laura Schneider', userEmail: 'laura.schneider@example.de', status: 'not_started', progress: 0, startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'enr-004', courseId: 'course-003', userId: 'user-004', userName: 'Thomas Wagner', userEmail: 'thomas.wagner@example.de', status: 'expired', progress: 25, startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'enr-005', courseId: 'course-002', userId: 'user-005', userName: 'Julia Becker', userEmail: 'julia.becker@example.de', status: 'in_progress', progress: 50, startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() }, + ] +} + +export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics { + const c = courses || createMockCourses() + const e = enrollments || createMockEnrollments() + + const completedCount = e.filter(en => en.status === 'completed').length + const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0 + const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length + + return { + totalCourses: c.length, + totalEnrollments: e.length, + completionRate, + overdueCount, + byCategory: { + dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length, + it_security: c.filter(co => co.category === 'it_security').length, + ai_literacy: c.filter(co => co.category === 'ai_literacy').length, + whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length, + custom: c.filter(co => co.category === 'custom').length, + }, + byStatus: { + not_started: e.filter(en => en.status === 'not_started').length, + in_progress: e.filter(en => en.status === 'in_progress').length, + completed: e.filter(en => en.status === 'completed').length, + expired: e.filter(en => en.status === 'expired').length, + } + } +} diff --git a/admin-compliance/lib/sdk/academy/api.ts b/admin-compliance/lib/sdk/academy/api.ts index c272939..4f5d7b6 100644 --- a/admin-compliance/lib/sdk/academy/api.ts +++ b/admin-compliance/lib/sdk/academy/api.ts @@ -1,787 +1,38 @@ /** - * Academy API Client + * Academy API Client — barrel re-export * - * API client for the Compliance E-Learning Academy module - * Connects to the ai-compliance-sdk backend via Next.js proxy + * Split into: + * - api-courses.ts (CRUD, enrollments, certificates, quiz, stats, generation) + * - api-mock-data.ts (mock data + SDK proxy) */ -import type { - Course, - CourseCategory, - CourseCreateRequest, - CourseUpdateRequest, - GenerateCourseRequest, - Enrollment, - EnrollmentStatus, - EnrollmentListResponse, - EnrollUserRequest, - UpdateProgressRequest, - Certificate, - AcademyStatistics, - LessonType, - SubmitQuizRequest, - SubmitQuizResponse, -} from './types' -import { isEnrollmentOverdue } from './types' +export { + fetchCourses, + fetchCourse, + createCourse, + updateCourse, + deleteCourse, + fetchEnrollments, + enrollUser, + updateProgress, + completeEnrollment, + deleteEnrollment, + updateEnrollment, + fetchCertificate, + generateCertificate, + fetchCertificates, + submitQuiz, + updateLesson, + fetchAcademyStatistics, + generateCourse, + generateAllCourses, + generateVideos, + getVideoStatus, +} from './api-courses' -// ============================================================================= -// CONFIGURATION -// ============================================================================= - -const ACADEMY_API_BASE = '/api/sdk/v1/academy' -const API_TIMEOUT = 30000 // 30 seconds - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -function getTenantId(): string { - if (typeof window !== 'undefined') { - return localStorage.getItem('bp_tenant_id') || 'default-tenant' - } - return 'default-tenant' -} - -function getAuthHeaders(): HeadersInit { - const headers: HeadersInit = { - 'Content-Type': 'application/json', - 'X-Tenant-ID': getTenantId() - } - - if (typeof window !== 'undefined') { - const token = localStorage.getItem('authToken') - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - const userId = localStorage.getItem('bp_user_id') - if (userId) { - headers['X-User-ID'] = userId - } - } - - return headers -} - -async function fetchWithTimeout( - url: string, - options: RequestInit = {}, - timeout: number = API_TIMEOUT -): Promise { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) - - try { - const response = await fetch(url, { - ...options, - signal: controller.signal, - headers: { - ...getAuthHeaders(), - ...options.headers - } - }) - - if (!response.ok) { - const errorBody = await response.text() - let errorMessage = `HTTP ${response.status}: ${response.statusText}` - try { - const errorJson = JSON.parse(errorBody) - errorMessage = errorJson.error || errorJson.message || errorMessage - } catch { - // Keep the HTTP status message - } - throw new Error(errorMessage) - } - - // Handle empty responses - const contentType = response.headers.get('content-type') - if (contentType && contentType.includes('application/json')) { - return response.json() - } - - return {} as T - } finally { - clearTimeout(timeoutId) - } -} - -// ============================================================================= -// COURSE CRUD -// ============================================================================= - -/** - * Alle Kurse abrufen - */ -export async function fetchCourses(): Promise { - const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>( - `${ACADEMY_API_BASE}/courses` - ) - return mapCoursesFromBackend(res.courses || []) -} - -/** - * Einzelnen Kurs abrufen - */ -export async function fetchCourse(id: string): Promise { - const res = await fetchWithTimeout<{ course: BackendCourse }>( - `${ACADEMY_API_BASE}/courses/${id}` - ) - return mapCourseFromBackend(res.course) -} - -// Backend returns snake_case, frontend uses camelCase -interface BackendCourse { - id: string - title: string - description: string - category: CourseCategory - duration_minutes: number - required_for_roles: string[] - is_active: boolean - passing_score?: number - status?: string - lessons?: BackendLesson[] - created_at: string - updated_at: string -} - -interface BackendQuizQuestion { - id: string - question: string - options: string[] - correct_index: number - explanation: string -} - -interface BackendLesson { - id: string - course_id: string - title: string - description?: string - lesson_type: LessonType - content_url?: string - duration_minutes: number - order_index: number - quiz_questions?: BackendQuizQuestion[] -} - -function mapCourseFromBackend(bc: BackendCourse): Course { - return { - id: bc.id, - title: bc.title, - description: bc.description || '', - category: bc.category, - durationMinutes: bc.duration_minutes || 0, - passingScore: bc.passing_score ?? 70, - isActive: bc.is_active ?? true, - status: (bc.status as 'draft' | 'published') ?? 'draft', - requiredForRoles: bc.required_for_roles || [], - lessons: (bc.lessons || []).map(l => ({ - id: l.id, - courseId: l.course_id, - title: l.title, - type: l.lesson_type, - contentMarkdown: l.content_url || '', - durationMinutes: l.duration_minutes || 0, - order: l.order_index, - quizQuestions: (l.quiz_questions || []).map(q => ({ - id: q.id || `q-${Math.random().toString(36).slice(2)}`, - lessonId: l.id, - question: q.question, - options: q.options, - correctOptionIndex: q.correct_index, - explanation: q.explanation, - })), - })), - createdAt: bc.created_at, - updatedAt: bc.updated_at, - } -} - -function mapCoursesFromBackend(courses: BackendCourse[]): Course[] { - return courses.map(mapCourseFromBackend) -} - -/** - * Neuen Kurs erstellen - */ -export async function createCourse(request: CourseCreateRequest): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/courses`, - { - method: 'POST', - body: JSON.stringify(request) - } - ) -} - -/** - * Kurs aktualisieren - */ -export async function updateCourse(id: string, update: CourseUpdateRequest): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/courses/${id}`, - { - method: 'PUT', - body: JSON.stringify(update) - } - ) -} - -/** - * Kurs loeschen - */ -export async function deleteCourse(id: string): Promise { - await fetchWithTimeout( - `${ACADEMY_API_BASE}/courses/${id}`, - { - method: 'DELETE' - } - ) -} - -// ============================================================================= -// ENROLLMENTS -// ============================================================================= - -/** - * Einschreibungen abrufen (optional gefiltert nach Kurs-ID) - */ -export async function fetchEnrollments(courseId?: string): Promise { - const params = new URLSearchParams() - if (courseId) { - params.set('course_id', courseId) - } - const queryString = params.toString() - const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}` - - const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url) - return res.enrollments || [] -} - -/** - * Benutzer in einen Kurs einschreiben - */ -export async function enrollUser(request: EnrollUserRequest): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/enrollments`, - { - method: 'POST', - body: JSON.stringify(request) - } - ) -} - -/** - * Fortschritt einer Einschreibung aktualisieren - */ -export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`, - { - method: 'PUT', - body: JSON.stringify(update) - } - ) -} - -/** - * Einschreibung als abgeschlossen markieren - */ -export async function completeEnrollment(enrollmentId: string): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`, - { - method: 'POST' - } - ) -} - -// ============================================================================= -// CERTIFICATES -// ============================================================================= - -/** - * Zertifikat abrufen - */ -export async function fetchCertificate(id: string): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/certificates/${id}` - ) -} - -/** - * Zertifikat generieren nach erfolgreichem Kursabschluss - */ -export async function generateCertificate(enrollmentId: string): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`, - { - method: 'POST' - } - ) -} - -/** - * Alle Zertifikate abrufen - */ -export async function fetchCertificates(): Promise { - const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>( - `${ACADEMY_API_BASE}/certificates` - ) - return res.certificates || [] -} - -/** - * Einschreibung loeschen - */ -export async function deleteEnrollment(id: string): Promise { - await fetchWithTimeout( - `${ACADEMY_API_BASE}/enrollments/${id}`, - { method: 'DELETE' } - ) -} - -/** - * Einschreibung aktualisieren (z.B. Deadline) - */ -export async function updateEnrollment(id: string, data: { deadline?: string }): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/enrollments/${id}`, - { - method: 'PUT', - body: JSON.stringify(data), - } - ) -} - -// ============================================================================= -// QUIZ -// ============================================================================= - -/** - * Quiz-Antworten einreichen und auswerten (ohne Enrollment) - */ -export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`, - { - method: 'POST', - body: JSON.stringify(answers) - } - ) -} - -/** - * Lektion aktualisieren (Content, Titel, Quiz-Fragen) - */ -export async function updateLesson(lessonId: string, update: { - title?: string - description?: string - content_url?: string - duration_minutes?: number - quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }> -}): Promise<{ lesson: any }> { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/lessons/${lessonId}`, - { - method: 'PUT', - body: JSON.stringify(update) - } - ) -} - -// ============================================================================= -// STATISTICS -// ============================================================================= - -/** - * Academy-Statistiken abrufen - */ -export async function fetchAcademyStatistics(): Promise { - const res = await fetchWithTimeout<{ - total_courses: number - total_enrollments: number - completion_rate: number - overdue_count: number - avg_completion_days: number - by_category?: Record - by_status?: Record - }>(`${ACADEMY_API_BASE}/stats`) - - return { - totalCourses: res.total_courses || 0, - totalEnrollments: res.total_enrollments || 0, - completionRate: res.completion_rate || 0, - overdueCount: res.overdue_count || 0, - byCategory: (res.by_category || {}) as Record, - byStatus: (res.by_status || {}) as Record, - } -} - -// ============================================================================= -// COURSE GENERATION (via Training Engine) -// ============================================================================= - -/** - * Academy-Kurs aus einem Training-Modul generieren - */ -export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> { - return fetchWithTimeout<{ course: Course }>( - `${ACADEMY_API_BASE}/courses/generate`, - { - method: 'POST', - body: JSON.stringify({ module_id: request.moduleId || request.title }) - }, - 120000 // 2 min timeout - ) -} - -/** - * Alle Academy-Kurse aus Training-Modulen generieren - */ -export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/courses/generate-all`, - { method: 'POST' }, - 300000 // 5 min timeout - ) -} - -/** - * Videos fuer alle Lektionen eines Kurses generieren - */ -export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> { - return fetchWithTimeout<{ status: string; jobId?: string }>( - `${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`, - { - method: 'POST' - }, - 300000 // 5 min timeout for video generation - ) -} - -/** - * Video-Generierungsstatus abrufen - */ -export async function getVideoStatus(courseId: string): Promise<{ - status: string - total: number - completed: number - failed: number - videos: Array<{ lessonId: string; status: string; url?: string }> -}> { - return fetchWithTimeout( - `${ACADEMY_API_BASE}/courses/${courseId}/video-status` - ) -} - -// ============================================================================= -// SDK PROXY FUNCTION (wraps fetchCourses + fetchAcademyStatistics) -// ============================================================================= - -/** - * Kurse und Statistiken laden - mit Fallback auf Mock-Daten - */ -export async function fetchSDKAcademyList(): Promise<{ - courses: Course[] - enrollments: Enrollment[] - statistics: AcademyStatistics -}> { - try { - const [courses, enrollments, statistics] = await Promise.all([ - fetchCourses(), - fetchEnrollments(), - fetchAcademyStatistics() - ]) - - return { courses, enrollments, statistics } - } catch (error) { - console.error('Failed to load Academy data from backend, using mock data:', error) - - // Fallback to mock data - const courses = createMockCourses() - const enrollments = createMockEnrollments() - const statistics = createMockStatistics(courses, enrollments) - - return { courses, enrollments, statistics } - } -} - -// ============================================================================= -// MOCK DATA (Fallback / Demo) -// ============================================================================= - -/** - * Demo-Kurse mit deutschen Titeln erstellen - */ -export function createMockCourses(): Course[] { - const now = new Date() - - return [ - { - id: 'course-001', - title: 'DSGVO-Grundlagen fuer Mitarbeiter', - description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.', - category: 'dsgvo_basics', - durationMinutes: 90, - passingScore: 80, - isActive: true, - status: 'published', - requiredForRoles: ['all'], - createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(), - updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), - lessons: [ - { - id: 'lesson-001-01', - courseId: 'course-001', - order: 1, - title: 'Was ist die DSGVO?', - type: 'text', - contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...', - durationMinutes: 15 - }, - { - id: 'lesson-001-02', - courseId: 'course-001', - order: 2, - title: 'Die 7 Grundsaetze der DSGVO', - type: 'video', - contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.', - durationMinutes: 20, - videoUrl: '/videos/dsgvo-grundsaetze.mp4' - }, - { - id: 'lesson-001-03', - courseId: 'course-001', - order: 3, - title: 'Betroffenenrechte (Art. 15-21)', - type: 'text', - contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.', - durationMinutes: 20 - }, - { - id: 'lesson-001-04', - courseId: 'course-001', - order: 4, - title: 'Personenbezogene Daten im Arbeitsalltag', - type: 'video', - contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.', - durationMinutes: 15, - videoUrl: '/videos/dsgvo-praxis.mp4' - }, - { - id: 'lesson-001-05', - courseId: 'course-001', - order: 5, - title: 'Wissenstest: DSGVO-Grundlagen', - type: 'quiz', - contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.', - durationMinutes: 20 - } - ] - }, - { - id: 'course-002', - title: 'IT-Sicherheit & Cybersecurity Awareness', - description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.', - category: 'it_security', - durationMinutes: 60, - passingScore: 75, - isActive: true, - status: 'published', - requiredForRoles: ['all'], - createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), - updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), - lessons: [ - { - id: 'lesson-002-01', - courseId: 'course-002', - order: 1, - title: 'Phishing erkennen und vermeiden', - type: 'video', - contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?', - durationMinutes: 15, - videoUrl: '/videos/phishing-awareness.mp4' - }, - { - id: 'lesson-002-02', - courseId: 'course-002', - order: 2, - title: 'Sichere Passwoerter und MFA', - type: 'text', - contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.', - durationMinutes: 15 - }, - { - id: 'lesson-002-03', - courseId: 'course-002', - order: 3, - title: 'Social Engineering und Manipulation', - type: 'text', - contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.', - durationMinutes: 15 - }, - { - id: 'lesson-002-04', - courseId: 'course-002', - order: 4, - title: 'Wissenstest: IT-Sicherheit', - type: 'quiz', - contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.', - durationMinutes: 15 - } - ] - }, - { - id: 'course-003', - title: 'AI Literacy - Sicherer Umgang mit KI', - description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.', - category: 'ai_literacy', - durationMinutes: 75, - passingScore: 70, - isActive: true, - status: 'draft', - requiredForRoles: ['admin', 'data_protection_officer'], - createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), - updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), - lessons: [ - { - id: 'lesson-003-01', - courseId: 'course-003', - order: 1, - title: 'Was ist Kuenstliche Intelligenz?', - type: 'text', - contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.', - durationMinutes: 15 - }, - { - id: 'lesson-003-02', - courseId: 'course-003', - order: 2, - title: 'Der EU AI Act - Was bedeutet er fuer uns?', - type: 'video', - contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.', - durationMinutes: 20, - videoUrl: '/videos/eu-ai-act.mp4' - }, - { - id: 'lesson-003-03', - courseId: 'course-003', - order: 3, - title: 'KI-Werkzeuge sicher nutzen', - type: 'text', - contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.', - durationMinutes: 20 - }, - { - id: 'lesson-003-04', - courseId: 'course-003', - order: 4, - title: 'Wissenstest: AI Literacy', - type: 'quiz', - contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.', - durationMinutes: 20 - } - ] - } - ] -} - -/** - * Demo-Einschreibungen erstellen - */ -export function createMockEnrollments(): Enrollment[] { - const now = new Date() - - return [ - { - id: 'enr-001', - courseId: 'course-001', - userId: 'user-001', - userName: 'Maria Fischer', - userEmail: 'maria.fischer@example.de', - status: 'in_progress', - progress: 40, - startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), - deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'enr-002', - courseId: 'course-002', - userId: 'user-002', - userName: 'Stefan Mueller', - userEmail: 'stefan.mueller@example.de', - status: 'completed', - progress: 100, - startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(), - certificateId: 'cert-001', - deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'enr-003', - courseId: 'course-001', - userId: 'user-003', - userName: 'Laura Schneider', - userEmail: 'laura.schneider@example.de', - status: 'not_started', - progress: 0, - startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), - deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'enr-004', - courseId: 'course-003', - userId: 'user-004', - userName: 'Thomas Wagner', - userEmail: 'thomas.wagner@example.de', - status: 'expired', - progress: 25, - startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), - deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'enr-005', - courseId: 'course-002', - userId: 'user-005', - userName: 'Julia Becker', - userEmail: 'julia.becker@example.de', - status: 'in_progress', - progress: 50, - startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), - deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() - } - ] -} - -/** - * Demo-Statistiken aus Kursen und Einschreibungen berechnen - */ -export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics { - const c = courses || createMockCourses() - const e = enrollments || createMockEnrollments() - - const completedCount = e.filter(en => en.status === 'completed').length - const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0 - const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length - - return { - totalCourses: c.length, - totalEnrollments: e.length, - completionRate, - overdueCount, - byCategory: { - dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length, - it_security: c.filter(co => co.category === 'it_security').length, - ai_literacy: c.filter(co => co.category === 'ai_literacy').length, - whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length, - custom: c.filter(co => co.category === 'custom').length, - }, - byStatus: { - not_started: e.filter(en => en.status === 'not_started').length, - in_progress: e.filter(en => en.status === 'in_progress').length, - completed: e.filter(en => en.status === 'completed').length, - expired: e.filter(en => en.status === 'expired').length, - } - } -} +export { + fetchSDKAcademyList, + createMockCourses, + createMockEnrollments, + createMockStatistics, +} from './api-mock-data' diff --git a/admin-compliance/lib/sdk/document-generator/datapoint-generators.ts b/admin-compliance/lib/sdk/document-generator/datapoint-generators.ts new file mode 100644 index 0000000..fb1c1fd --- /dev/null +++ b/admin-compliance/lib/sdk/document-generator/datapoint-generators.ts @@ -0,0 +1,265 @@ +/** + * Datapoint Helpers — Generation Functions + * + * Functions that generate DSGVO-compliant text blocks from data points + * for the document generator. + */ + +import { + DataPoint, + DataPointCategory, + LegalBasis, + RetentionPeriod, + RiskLevel, + CATEGORY_METADATA, + LEGAL_BASIS_INFO, + RETENTION_PERIOD_INFO, + RISK_LEVEL_STYLING, + LocalizedText, + SupportedLanguage +} from '@/lib/sdk/einwilligungen/types' + +// ============================================================================= +// TYPES +// ============================================================================= + +export type Language = SupportedLanguage + +export interface DataPointPlaceholders { + '[DATENPUNKTE_COUNT]': string + '[DATENPUNKTE_LIST]': string + '[DATENPUNKTE_TABLE]': string + '[VERARBEITUNGSZWECKE]': string + '[RECHTSGRUNDLAGEN]': string + '[SPEICHERFRISTEN]': string + '[EMPFAENGER]': string + '[BESONDERE_KATEGORIEN]': string + '[DRITTLAND_TRANSFERS]': string + '[RISIKO_ZUSAMMENFASSUNG]': string +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function getText(text: LocalizedText, lang: Language): string { + return text[lang] || text.de +} + +export function groupByRetention( + dataPoints: DataPoint[] +): Record { + return dataPoints.reduce((acc, dp) => { + const key = dp.retentionPeriod + if (!acc[key]) acc[key] = [] + acc[key].push(dp) + return acc + }, {} as Record) +} + +export function groupByCategory( + dataPoints: DataPoint[] +): Record { + return dataPoints.reduce((acc, dp) => { + const key = dp.category + if (!acc[key]) acc[key] = [] + acc[key].push(dp) + return acc + }, {} as Record) +} + +// ============================================================================= +// GENERATOR FUNCTIONS +// ============================================================================= + +export function generateDataPointsTable( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + if (dataPoints.length === 0) { + return lang === 'de' ? '*Keine Datenpunkte ausgewaehlt.*' : '*No data points selected.*' + } + + const header = lang === 'de' + ? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |' + : '| Data Point | Category | Purpose | Legal Basis | Retention Period |' + const separator = '|------------|-----------|-------|-----------------|---------------|' + + const rows = dataPoints.map(dp => { + const category = CATEGORY_METADATA[dp.category] + const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis] + const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod] + + const name = getText(dp.name, lang) + const categoryName = getText(category.name, lang) + const purpose = getText(dp.purpose, lang) + const legalBasisName = getText(legalBasis.name, lang) + const retentionLabel = getText(retention.label, lang) + + const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose + + return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |` + }).join('\n') + + return `${header}\n${separator}\n${rows}` +} + +export function generateSpecialCategorySection( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + const special = dataPoints.filter(dp => dp.isSpecialCategory) + if (special.length === 0) return '' + + if (lang === 'de') { + const items = special.map(dp => + `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}` + ).join('\n') + + return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) + +Wir verarbeiten folgende besondere Kategorien personenbezogener Daten: + +${items} + +Die Verarbeitung erfolgt auf Grundlage Ihrer ausdruecklichen Einwilligung gemaess Art. 9 Abs. 2 lit. a DSGVO. Sie koennen Ihre Einwilligung jederzeit mit Wirkung fuer die Zukunft widerrufen.` + } else { + const items = special.map(dp => + `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}` + ).join('\n') + + return `## Processing of Special Categories of Personal Data (Art. 9 GDPR) + +We process the following special categories of personal data: + +${items} + +Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.` + } +} + +export function generatePurposesList( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + const purposes = new Set() + dataPoints.forEach(dp => purposes.add(getText(dp.purpose, lang))) + return [...purposes].join(', ') +} + +export function generateLegalBasisList( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + const bases = new Set() + dataPoints.forEach(dp => bases.add(dp.legalBasis)) + + return [...bases].map(basis => { + const info = LEGAL_BASIS_INFO[basis] + return `${info.article} (${getText(info.name, lang)})` + }).join(', ') +} + +export function generateRetentionList( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + const grouped = groupByRetention(dataPoints) + const entries: string[] = [] + + for (const [period, points] of Object.entries(grouped)) { + const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod] + const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))] + entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`) + } + + return entries.join('; ') +} + +export function generateRecipientsList(dataPoints: DataPoint[]): string { + const recipients = new Set() + dataPoints.forEach(dp => { + dp.thirdPartyRecipients?.forEach(r => recipients.add(r)) + }) + if (recipients.size === 0) return '' + return [...recipients].join(', ') +} + +export function generateThirdCountrySection( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare'] + + const thirdCountryPoints = dataPoints.filter(dp => + dp.thirdPartyRecipients?.some(r => + thirdCountryIndicators.some(indicator => + r.toLowerCase().includes(indicator.toLowerCase()) + ) + ) + ) + + if (thirdCountryPoints.length === 0) return '' + + const recipients = new Set() + thirdCountryPoints.forEach(dp => { + dp.thirdPartyRecipients?.forEach(r => { + if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) { + recipients.add(r) + } + }) + }) + + if (lang === 'de') { + return `## Uebermittlung in Drittlaender + +Wir uebermitteln personenbezogene Daten an folgende Empfaenger in Drittlaendern (ausserhalb der EU/des EWR): + +${[...recipients].map(r => `- ${r}`).join('\n')} + +Die Uebermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).` + } else { + return `## Transfers to Third Countries + +We transfer personal data to the following recipients in third countries (outside the EU/EEA): + +${[...recipients].map(r => `- ${r}`).join('\n')} + +The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).` + } +} + +export function generateRiskSummary( + dataPoints: DataPoint[], + lang: Language = 'de' +): string { + const riskCounts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0 } + dataPoints.forEach(dp => riskCounts[dp.riskLevel]++) + + const parts = Object.entries(riskCounts) + .filter(([, count]) => count > 0) + .map(([level, count]) => { + const styling = RISK_LEVEL_STYLING[level as RiskLevel] + return `${count} ${getText(styling.label, lang).toLowerCase()}` + }) + + return parts.join(', ') +} + +export function generateAllPlaceholders( + dataPoints: DataPoint[], + lang: Language = 'de' +): DataPointPlaceholders { + return { + '[DATENPUNKTE_COUNT]': String(dataPoints.length), + '[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '), + '[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang), + '[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang), + '[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang), + '[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang), + '[EMPFAENGER]': generateRecipientsList(dataPoints), + '[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang), + '[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang), + '[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang) + } +} diff --git a/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts b/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts index 7994e9f..dddbba2 100644 --- a/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts +++ b/admin-compliance/lib/sdk/document-generator/datapoint-helpers.ts @@ -1,548 +1,37 @@ /** - * Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten - * in den Dokumentengenerator. + * Datapoint Helpers — barrel re-export * - * Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf - * den vom Benutzer ausgewählten Datenpunkten. + * Split into: + * - datapoint-generators.ts (text generation functions) + * - datapoint-validators.ts (document validation checks) */ -import { - DataPoint, - DataPointCategory, - LegalBasis, - RetentionPeriod, - RiskLevel, - CATEGORY_METADATA, - LEGAL_BASIS_INFO, - RETENTION_PERIOD_INFO, - RISK_LEVEL_STYLING, - LocalizedText, - SupportedLanguage -} from '@/lib/sdk/einwilligungen/types' - -// ============================================================================= -// TYPES -// ============================================================================= - -/** - * Sprach-Option für alle Helper-Funktionen - */ -export type Language = SupportedLanguage - -/** - * Generierte Platzhalter-Map für den Dokumentengenerator - */ -export interface DataPointPlaceholders { - '[DATENPUNKTE_COUNT]': string - '[DATENPUNKTE_LIST]': string - '[DATENPUNKTE_TABLE]': string - '[VERARBEITUNGSZWECKE]': string - '[RECHTSGRUNDLAGEN]': string - '[SPEICHERFRISTEN]': string - '[EMPFAENGER]': string - '[BESONDERE_KATEGORIEN]': string - '[DRITTLAND_TRANSFERS]': string - '[RISIKO_ZUSAMMENFASSUNG]': string -} - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -/** - * Extrahiert Text aus LocalizedText basierend auf Sprache - */ -function getText(text: LocalizedText, lang: Language): string { - return text[lang] || text.de -} - -/** - * Generiert eine Markdown-Tabelle der Datenpunkte - * - * @param dataPoints - Liste der ausgewählten Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Markdown-Tabelle als String - */ -export function generateDataPointsTable( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - if (dataPoints.length === 0) { - return lang === 'de' - ? '*Keine Datenpunkte ausgewählt.*' - : '*No data points selected.*' - } - - const header = lang === 'de' - ? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |' - : '| Data Point | Category | Purpose | Legal Basis | Retention Period |' - const separator = '|------------|-----------|-------|-----------------|---------------|' - - const rows = dataPoints.map(dp => { - const category = CATEGORY_METADATA[dp.category] - const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis] - const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod] - - const name = getText(dp.name, lang) - const categoryName = getText(category.name, lang) - const purpose = getText(dp.purpose, lang) - const legalBasisName = getText(legalBasis.name, lang) - const retentionLabel = getText(retention.label, lang) - - // Truncate long texts for table readability - const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose - - return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |` - }).join('\n') - - return `${header}\n${separator}\n${rows}` -} - -/** - * Gruppiert Datenpunkte nach Speicherfrist - * - * @param dataPoints - Liste der Datenpunkte - * @returns Record mit Speicherfrist als Key und Datenpunkten als Value - */ -export function groupByRetention( - dataPoints: DataPoint[] -): Record { - return dataPoints.reduce((acc, dp) => { - const key = dp.retentionPeriod - if (!acc[key]) { - acc[key] = [] - } - acc[key].push(dp) - return acc - }, {} as Record) -} - -/** - * Gruppiert Datenpunkte nach Kategorie - * - * @param dataPoints - Liste der Datenpunkte - * @returns Record mit Kategorie als Key und Datenpunkten als Value - */ -export function groupByCategory( - dataPoints: DataPoint[] -): Record { - return dataPoints.reduce((acc, dp) => { - const key = dp.category - if (!acc[key]) { - acc[key] = [] - } - acc[key].push(dp) - return acc - }, {} as Record) -} - -/** - * Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO) - * - * @param dataPoints - Liste der Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten) - */ -export function generateSpecialCategorySection( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - const special = dataPoints.filter(dp => dp.isSpecialCategory) - - if (special.length === 0) { - return '' - } - - if (lang === 'de') { - const items = special.map(dp => - `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}` - ).join('\n') - - return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) - -Wir verarbeiten folgende besondere Kategorien personenbezogener Daten: - -${items} - -Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.` - } else { - const items = special.map(dp => - `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}` - ).join('\n') - - return `## Processing of Special Categories of Personal Data (Art. 9 GDPR) - -We process the following special categories of personal data: - -${items} - -Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.` - } -} - -/** - * Generiert Liste aller eindeutigen Verarbeitungszwecke - * - * @param dataPoints - Liste der Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Kommaseparierte Liste der Zwecke - */ -export function generatePurposesList( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - const purposes = new Set() - - dataPoints.forEach(dp => { - purposes.add(getText(dp.purpose, lang)) - }) - - return [...purposes].join(', ') -} - -/** - * Generiert Liste aller verwendeten Rechtsgrundlagen - * - * @param dataPoints - Liste der Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Formatierte Liste der Rechtsgrundlagen - */ -export function generateLegalBasisList( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - const bases = new Set() - - dataPoints.forEach(dp => { - bases.add(dp.legalBasis) - }) - - return [...bases].map(basis => { - const info = LEGAL_BASIS_INFO[basis] - return `${info.article} (${getText(info.name, lang)})` - }).join(', ') -} - -/** - * Generiert Liste aller Speicherfristen gruppiert - * - * @param dataPoints - Liste der Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien - */ -export function generateRetentionList( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - const grouped = groupByRetention(dataPoints) - const entries: string[] = [] - - for (const [period, points] of Object.entries(grouped)) { - const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod] - const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))] - - entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`) - } - - return entries.join('; ') -} - -/** - * Generiert Liste aller Empfänger/Drittparteien - * - * @param dataPoints - Liste der Datenpunkte - * @returns Kommaseparierte Liste der Empfänger - */ -export function generateRecipientsList(dataPoints: DataPoint[]): string { - const recipients = new Set() - - dataPoints.forEach(dp => { - dp.thirdPartyRecipients?.forEach(r => recipients.add(r)) - }) - - if (recipients.size === 0) { - return '' - } - - return [...recipients].join(', ') -} - -/** - * Generiert Abschnitt für Drittland-Übermittlungen - * - * @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true - * @param lang - Sprache für die Ausgabe - * @returns Markdown-Abschnitt als String - */ -export function generateThirdCountrySection( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - // Note: We assume dataPoints have been filtered for thirdCountryTransfer - // The actual flag would need to be added to the DataPoint interface - // For now, we check if any thirdPartyRecipients suggest third country - const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare'] - - const thirdCountryPoints = dataPoints.filter(dp => - dp.thirdPartyRecipients?.some(r => - thirdCountryIndicators.some(indicator => - r.toLowerCase().includes(indicator.toLowerCase()) - ) - ) - ) - - if (thirdCountryPoints.length === 0) { - return '' - } - - const recipients = new Set() - thirdCountryPoints.forEach(dp => { - dp.thirdPartyRecipients?.forEach(r => { - if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) { - recipients.add(r) - } - }) - }) - - if (lang === 'de') { - return `## Übermittlung in Drittländer - -Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR): - -${[...recipients].map(r => `- ${r}`).join('\n')} - -Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).` - } else { - return `## Transfers to Third Countries - -We transfer personal data to the following recipients in third countries (outside the EU/EEA): - -${[...recipients].map(r => `- ${r}`).join('\n')} - -The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).` - } -} - -/** - * Generiert Risiko-Zusammenfassung - * - * @param dataPoints - Liste der Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Formatierte Risiko-Zusammenfassung - */ -export function generateRiskSummary( - dataPoints: DataPoint[], - lang: Language = 'de' -): string { - const riskCounts: Record = { - LOW: 0, - MEDIUM: 0, - HIGH: 0 - } - - dataPoints.forEach(dp => { - riskCounts[dp.riskLevel]++ - }) - - const parts = Object.entries(riskCounts) - .filter(([, count]) => count > 0) - .map(([level, count]) => { - const styling = RISK_LEVEL_STYLING[level as RiskLevel] - return `${count} ${getText(styling.label, lang).toLowerCase()}` - }) - - return parts.join(', ') -} - -/** - * Generiert alle Platzhalter für den Dokumentengenerator - * - * @param dataPoints - Liste der ausgewählten Datenpunkte - * @param lang - Sprache für die Ausgabe - * @returns Objekt mit allen Platzhaltern - */ -export function generateAllPlaceholders( - dataPoints: DataPoint[], - lang: Language = 'de' -): DataPointPlaceholders { - return { - '[DATENPUNKTE_COUNT]': String(dataPoints.length), - '[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '), - '[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang), - '[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang), - '[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang), - '[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang), - '[EMPFAENGER]': generateRecipientsList(dataPoints), - '[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang), - '[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang), - '[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang) - } -} - -// ============================================================================= -// VALIDATION HELPERS -// ============================================================================= - -/** - * Validierungswarnung für den Dokumentengenerator - */ -export interface ValidationWarning { - type: 'error' | 'warning' | 'info' - code: string - message: string - suggestion: string - affectedDataPoints?: DataPoint[] -} - -/** - * Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt - * - * @param dataPoints - Liste der Datenpunkte - * @param documentContent - Der generierte Dokumentinhalt - * @param lang - Sprache - * @returns ValidationWarning oder null - */ -export function checkSpecialCategoriesWarning( - dataPoints: DataPoint[], - documentContent: string, - lang: Language = 'de' -): ValidationWarning | null { - const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory) - - if (specialCategories.length === 0) { - return null - } - - const hasSection = lang === 'de' - ? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie') - : documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor') - - if (!hasSection) { - return { - type: 'error', - code: 'MISSING_ART9_SECTION', - message: lang === 'de' - ? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.` - : `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`, - suggestion: lang === 'de' - ? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.' - : 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.', - affectedDataPoints: specialCategories - } - } - - return null -} - -/** - * Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden - * - * @param dataPoints - Liste der Datenpunkte - * @param documentContent - Der generierte Dokumentinhalt - * @param lang - Sprache - * @returns ValidationWarning oder null - */ -export function checkThirdCountryWarning( - dataPoints: DataPoint[], - documentContent: string, - lang: Language = 'de' -): ValidationWarning | null { - const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US'] - - const thirdCountryPoints = dataPoints.filter(dp => - dp.thirdPartyRecipients?.some(r => - thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase())) - ) - ) - - if (thirdCountryPoints.length === 0) { - return null - } - - const hasSCCMention = lang === 'de' - ? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46') - : documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46') - - if (!hasSCCMention) { - return { - type: 'warning', - code: 'MISSING_SCC_SECTION', - message: lang === 'de' - ? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.` - : `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`, - suggestion: lang === 'de' - ? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.' - : 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.', - affectedDataPoints: thirdCountryPoints - } - } - - return null -} - -/** - * Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden - * - * @param dataPoints - Liste der Datenpunkte - * @param documentContent - Der generierte Dokumentinhalt - * @param lang - Sprache - * @returns ValidationWarning oder null - */ -export function checkExplicitConsentWarning( - dataPoints: DataPoint[], - documentContent: string, - lang: Language = 'de' -): ValidationWarning | null { - const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent) - - if (explicitConsentPoints.length === 0) { - return null - } - - const hasConsentSection = lang === 'de' - ? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7') - : documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7') - - if (!hasConsentSection) { - return { - type: 'warning', - code: 'MISSING_CONSENT_SECTION', - message: lang === 'de' - ? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.` - : `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`, - suggestion: lang === 'de' - ? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.' - : 'Add a section about the right to withdraw consent.', - affectedDataPoints: explicitConsentPoints - } - } - - return null -} - -/** - * Führt alle Validierungsprüfungen durch - * - * @param dataPoints - Liste der Datenpunkte - * @param documentContent - Der generierte Dokumentinhalt - * @param lang - Sprache - * @returns Array aller Warnungen - */ -export function validateDocument( - dataPoints: DataPoint[], - documentContent: string, - lang: Language = 'de' -): ValidationWarning[] { - const warnings: ValidationWarning[] = [] - - const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang) - if (specialCatWarning) warnings.push(specialCatWarning) - - const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang) - if (thirdCountryWarning) warnings.push(thirdCountryWarning) - - const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang) - if (consentWarning) warnings.push(consentWarning) - - return warnings -} +export type { + Language, + DataPointPlaceholders, +} from './datapoint-generators' + +export { + generateDataPointsTable, + groupByRetention, + groupByCategory, + generateSpecialCategorySection, + generatePurposesList, + generateLegalBasisList, + generateRetentionList, + generateRecipientsList, + generateThirdCountrySection, + generateRiskSummary, + generateAllPlaceholders, +} from './datapoint-generators' + +export type { + ValidationWarning, +} from './datapoint-validators' + +export { + checkSpecialCategoriesWarning, + checkThirdCountryWarning, + checkExplicitConsentWarning, + validateDocument, +} from './datapoint-validators' diff --git a/admin-compliance/lib/sdk/document-generator/datapoint-validators.ts b/admin-compliance/lib/sdk/document-generator/datapoint-validators.ts new file mode 100644 index 0000000..c8443c0 --- /dev/null +++ b/admin-compliance/lib/sdk/document-generator/datapoint-validators.ts @@ -0,0 +1,144 @@ +/** + * Datapoint Helpers — Validation Functions + * + * Document validation checks for DSGVO compliance. + */ + +import { + DataPoint, + LocalizedText, + SupportedLanguage, +} from '@/lib/sdk/einwilligungen/types' + +import type { Language } from './datapoint-generators' + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface ValidationWarning { + type: 'error' | 'warning' | 'info' + code: string + message: string + suggestion: string + affectedDataPoints?: DataPoint[] +} + +// ============================================================================= +// VALIDATION FUNCTIONS +// ============================================================================= + +export function checkSpecialCategoriesWarning( + dataPoints: DataPoint[], + documentContent: string, + lang: Language = 'de' +): ValidationWarning | null { + const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory) + + if (specialCategories.length === 0) return null + + const hasSection = lang === 'de' + ? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie') + : documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor') + + if (!hasSection) { + return { + type: 'error', + code: 'MISSING_ART9_SECTION', + message: lang === 'de' + ? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewaehlt, aber kein entsprechender Abschnitt im Dokument gefunden.` + : `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`, + suggestion: lang === 'de' + ? 'Fuegen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.' + : 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.', + affectedDataPoints: specialCategories + } + } + + return null +} + +export function checkThirdCountryWarning( + dataPoints: DataPoint[], + documentContent: string, + lang: Language = 'de' +): ValidationWarning | null { + const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US'] + + const thirdCountryPoints = dataPoints.filter(dp => + dp.thirdPartyRecipients?.some(r => + thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase())) + ) + ) + + if (thirdCountryPoints.length === 0) return null + + const hasSCCMention = lang === 'de' + ? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46') + : documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46') + + if (!hasSCCMention) { + return { + type: 'warning', + code: 'MISSING_SCC_SECTION', + message: lang === 'de' + ? `Drittland-Uebermittlung fuer ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwaehnt.` + : `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`, + suggestion: lang === 'de' + ? 'Erwaegen Sie die Aufnahme eines Abschnitts zu Drittland-Uebermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.' + : 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.', + affectedDataPoints: thirdCountryPoints + } + } + + return null +} + +export function checkExplicitConsentWarning( + dataPoints: DataPoint[], + documentContent: string, + lang: Language = 'de' +): ValidationWarning | null { + const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent) + + if (explicitConsentPoints.length === 0) return null + + const hasConsentSection = lang === 'de' + ? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7') + : documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7') + + if (!hasConsentSection) { + return { + type: 'warning', + code: 'MISSING_CONSENT_SECTION', + message: lang === 'de' + ? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrueckliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.` + : `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`, + suggestion: lang === 'de' + ? 'Fuegen Sie einen Abschnitt zum Widerrufsrecht hinzu.' + : 'Add a section about the right to withdraw consent.', + affectedDataPoints: explicitConsentPoints + } + } + + return null +} + +export function validateDocument( + dataPoints: DataPoint[], + documentContent: string, + lang: Language = 'de' +): ValidationWarning[] { + const warnings: ValidationWarning[] = [] + + const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang) + if (specialCatWarning) warnings.push(specialCatWarning) + + const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang) + if (thirdCountryWarning) warnings.push(thirdCountryWarning) + + const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang) + if (consentWarning) warnings.push(consentWarning) + + return warnings +} diff --git a/admin-compliance/lib/sdk/dsr/types-api.ts b/admin-compliance/lib/sdk/dsr/types-api.ts new file mode 100644 index 0000000..0a30868 --- /dev/null +++ b/admin-compliance/lib/sdk/dsr/types-api.ts @@ -0,0 +1,243 @@ +/** + * DSR Types — API Types, Communication, Audit, Templates, Statistics & Helpers + */ + +import type { + DSRType, + DSRStatus, + DSRPriority, + DSRSource, + DSRRequester, + DSRAssignment, + DSRRequest, + DSRDataExport, + IdentityVerificationMethod, + CommunicationType, + CommunicationChannel, + DSRTypeInfo, +} from './types-core' + +export { DSR_TYPE_INFO } from './types-core' + +// ============================================================================= +// COMMUNICATION +// ============================================================================= + +export interface DSRCommunication { + id: string + dsrId: string + type: CommunicationType + channel: CommunicationChannel + subject?: string + content: string + templateUsed?: string + attachments?: { + name: string + url: string + size: number + type: string + }[] + sentAt?: string + sentBy?: string + receivedAt?: string + createdAt: string + createdBy: string +} + +// ============================================================================= +// AUDIT LOG +// ============================================================================= + +export interface DSRAuditEntry { + id: string + dsrId: string + action: string + previousValue?: string + newValue?: string + performedBy: string + performedAt: string + notes?: string +} + +// ============================================================================= +// EMAIL TEMPLATES +// ============================================================================= + +export interface DSREmailTemplate { + id: string + name: string + subject: string + body: string + type: DSRType | 'general' + stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion' + language: 'de' | 'en' + variables: string[] +} + +export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [ + { + id: 'intake_confirmation', + name: 'Eingangsbestaetigung', + subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}', + body: `Sehr geehrte(r) {{requesterName}}, + +wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}. + +Referenznummer: {{referenceNumber}} +Art der Anfrage: {{requestType}} + +Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten. + +Mit freundlichen Gruessen +{{senderName}} +Datenschutzbeauftragter`, + type: 'general', + stage: 'intake', + language: 'de', + variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName'] + }, + { + id: 'identity_request', + name: 'Identitaetsanfrage', + subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}', + body: `Sehr geehrte(r) {{requesterName}}, + +um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet. + +Bitte senden Sie uns eines der folgenden Dokumente: +- Kopie Ihres Personalausweises (Vorder- und Rueckseite) +- Kopie Ihres Reisepasses + +Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht. + +Mit freundlichen Gruessen +{{senderName}} +Datenschutzbeauftragter`, + type: 'general', + stage: 'identity_request', + language: 'de', + variables: ['requesterName', 'referenceNumber', 'senderName'] + } +] + +// ============================================================================= +// API TYPES +// ============================================================================= + +export interface DSRFilters { + status?: DSRStatus | DSRStatus[] + type?: DSRType | DSRType[] + priority?: DSRPriority + assignedTo?: string + overdue?: boolean + search?: string + dateFrom?: string + dateTo?: string +} + +export interface DSRListResponse { + requests: DSRRequest[] + total: number + page: number + pageSize: number +} + +export interface DSRCreateRequest { + type: DSRType + requester: DSRRequester + source: DSRSource + sourceDetails?: string + requestText?: string + priority?: DSRPriority +} + +export interface DSRUpdateRequest { + status?: DSRStatus + priority?: DSRPriority + notes?: string + internalNotes?: string + assignment?: DSRAssignment +} + +export interface DSRVerifyIdentityRequest { + method: IdentityVerificationMethod + notes?: string + documentRef?: string +} + +export interface DSRCompleteRequest { + completionNotes?: string + dataExport?: DSRDataExport +} + +export interface DSRRejectRequest { + reason: string + legalBasis?: string +} + +export interface DSRExtendDeadlineRequest { + extensionMonths: 1 | 2 + reason: string +} + +export interface DSRSendCommunicationRequest { + type: CommunicationType + channel: CommunicationChannel + subject?: string + content: string + templateId?: string +} + +// ============================================================================= +// STATISTICS +// ============================================================================= + +export interface DSRStatistics { + total: number + byStatus: Record + byType: Record + overdue: number + dueThisWeek: number + averageProcessingDays: number + completedThisMonth: number +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +export function getDaysRemaining(deadline: string): number { + const deadlineDate = new Date(deadline) + const now = new Date() + const diff = deadlineDate.getTime() - now.getTime() + return Math.ceil(diff / (1000 * 60 * 60 * 24)) +} + +export function isOverdue(request: DSRRequest): boolean { + if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') { + return false + } + return getDaysRemaining(request.deadline.currentDeadline) < 0 +} + +export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean { + if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') { + return false + } + const daysRemaining = getDaysRemaining(request.deadline.currentDeadline) + return daysRemaining >= 0 && daysRemaining <= thresholdDays +} + +export function generateReferenceNumber(year: number, sequence: number): string { + return `DSR-${year}-${String(sequence).padStart(6, '0')}` +} + +export function getTypeInfo(type: DSRType): DSRTypeInfo { + const { DSR_TYPE_INFO } = require('./types-core') + return DSR_TYPE_INFO[type] +} + +export function getStatusInfo(status: DSRStatus) { + const { DSR_STATUS_INFO } = require('./types-core') + return DSR_STATUS_INFO[status] +} diff --git a/admin-compliance/lib/sdk/dsr/types-core.ts b/admin-compliance/lib/sdk/dsr/types-core.ts new file mode 100644 index 0000000..6c20701 --- /dev/null +++ b/admin-compliance/lib/sdk/dsr/types-core.ts @@ -0,0 +1,235 @@ +/** + * DSR (Data Subject Request) Types — Core Types & Constants + * + * Enums, constants, metadata, and main interfaces for GDPR Art. 15-21 + */ + +// ============================================================================= +// ENUMS & CONSTANTS +// ============================================================================= + +export type DSRType = + | 'access' // Art. 15 + | 'rectification' // Art. 16 + | 'erasure' // Art. 17 + | 'restriction' // Art. 18 + | 'portability' // Art. 20 + | 'objection' // Art. 21 + +export type DSRStatus = + | 'intake' + | 'identity_verification' + | 'processing' + | 'completed' + | 'rejected' + | 'cancelled' + +export type DSRPriority = 'low' | 'normal' | 'high' | 'critical' + +export type DSRSource = + | 'web_form' | 'email' | 'letter' | 'phone' | 'in_person' | 'other' + +export type IdentityVerificationMethod = + | 'id_document' | 'email' | 'phone' | 'postal' | 'existing_account' | 'other' + +export type CommunicationType = 'incoming' | 'outgoing' | 'internal' + +export type CommunicationChannel = + | 'email' | 'letter' | 'phone' | 'portal' | 'internal_note' + +// ============================================================================= +// DSR TYPE METADATA +// ============================================================================= + +export interface DSRTypeInfo { + type: DSRType + article: string + label: string + labelShort: string + description: string + defaultDeadlineDays: number + maxExtensionMonths: number + color: string + bgColor: string + processDocument?: string +} + +export const DSR_TYPE_INFO: Record = { + access: { + type: 'access', article: 'Art. 15', label: 'Auskunftsrecht', labelShort: 'Auskunft', + description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten', + defaultDeadlineDays: 30, maxExtensionMonths: 2, + color: 'text-blue-700', bgColor: 'bg-blue-100', + processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf' + }, + rectification: { + type: 'rectification', article: 'Art. 16', label: 'Berichtigungsrecht', labelShort: 'Berichtigung', + description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten', + defaultDeadlineDays: 14, maxExtensionMonths: 2, + color: 'text-yellow-700', bgColor: 'bg-yellow-100', + processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf' + }, + erasure: { + type: 'erasure', article: 'Art. 17', label: 'Loeschungsrecht', labelShort: 'Loeschung', + description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")', + defaultDeadlineDays: 14, maxExtensionMonths: 2, + color: 'text-red-700', bgColor: 'bg-red-100', + processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf' + }, + restriction: { + type: 'restriction', article: 'Art. 18', label: 'Einschraenkungsrecht', labelShort: 'Einschraenkung', + description: 'Recht auf Einschraenkung der Verarbeitung', + defaultDeadlineDays: 14, maxExtensionMonths: 2, + color: 'text-orange-700', bgColor: 'bg-orange-100', + processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf' + }, + portability: { + type: 'portability', article: 'Art. 20', label: 'Datenuebertragbarkeit', labelShort: 'Uebertragung', + description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format', + defaultDeadlineDays: 30, maxExtensionMonths: 2, + color: 'text-purple-700', bgColor: 'bg-purple-100', + processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf' + }, + objection: { + type: 'objection', article: 'Art. 21', label: 'Widerspruchsrecht', labelShort: 'Widerspruch', + description: 'Recht auf Widerspruch gegen die Verarbeitung', + defaultDeadlineDays: 30, maxExtensionMonths: 0, + color: 'text-gray-700', bgColor: 'bg-gray-100' + } +} + +export const DSR_STATUS_INFO: Record = { + intake: { label: 'Eingang', color: 'text-blue-700', bgColor: 'bg-blue-100', borderColor: 'border-blue-200' }, + identity_verification: { label: 'ID-Pruefung', color: 'text-yellow-700', bgColor: 'bg-yellow-100', borderColor: 'border-yellow-200' }, + processing: { label: 'In Bearbeitung', color: 'text-purple-700', bgColor: 'bg-purple-100', borderColor: 'border-purple-200' }, + completed: { label: 'Abgeschlossen', color: 'text-green-700', bgColor: 'bg-green-100', borderColor: 'border-green-200' }, + rejected: { label: 'Abgelehnt', color: 'text-red-700', bgColor: 'bg-red-100', borderColor: 'border-red-200' }, + cancelled: { label: 'Storniert', color: 'text-gray-700', bgColor: 'bg-gray-100', borderColor: 'border-gray-200' }, +} + +// ============================================================================= +// MAIN INTERFACES +// ============================================================================= + +export interface DSRRequester { + name: string + email: string + phone?: string + address?: string + customerId?: string +} + +export interface DSRIdentityVerification { + verified: boolean + method?: IdentityVerificationMethod + verifiedAt?: string + verifiedBy?: string + notes?: string + documentRef?: string +} + +export interface DSRAssignment { + assignedTo: string | null + assignedAt?: string + assignedBy?: string +} + +export interface DSRDeadline { + originalDeadline: string + currentDeadline: string + extended: boolean + extensionReason?: string + extensionApprovedBy?: string + extensionApprovedAt?: string +} + +export interface DSRRequest { + id: string + referenceNumber: string + type: DSRType + status: DSRStatus + priority: DSRPriority + requester: DSRRequester + source: DSRSource + sourceDetails?: string + requestText?: string + receivedAt: string + deadline: DSRDeadline + completedAt?: string + identityVerification: DSRIdentityVerification + assignment: DSRAssignment + notes?: string + internalNotes?: string + erasureChecklist?: DSRErasureChecklist + dataExport?: DSRDataExport + rectificationDetails?: DSRRectificationDetails + objectionDetails?: DSRObjectionDetails + createdAt: string + createdBy: string + updatedAt: string + updatedBy?: string + tenantId: string +} + +// ============================================================================= +// TYPE-SPECIFIC INTERFACES +// ============================================================================= + +export interface DSRErasureChecklistItem { + id: string + article: string + label: string + description: string + checked: boolean + applies: boolean + notes?: string +} + +export interface DSRErasureChecklist { + items: DSRErasureChecklistItem[] + canProceedWithErasure: boolean + reviewedBy?: string + reviewedAt?: string +} + +export const ERASURE_EXCEPTIONS: Omit[] = [ + { id: 'art17_3_a', article: '17(3)(a)', label: 'Meinungs- und Informationsfreiheit', description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information' }, + { id: 'art17_3_b', article: '17(3)(b)', label: 'Rechtliche Verpflichtung', description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)' }, + { id: 'art17_3_c', article: '17(3)(c)', label: 'Oeffentliches Interesse', description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit' }, + { id: 'art17_3_d', article: '17(3)(d)', label: 'Archivzwecke', description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik' }, + { id: 'art17_3_e', article: '17(3)(e)', label: 'Rechtsansprueche', description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen' }, +] + +export interface DSRDataExport { + format: 'json' | 'csv' | 'xml' | 'pdf' + generatedAt?: string + generatedBy?: string + fileUrl?: string + fileName?: string + fileSize?: number + includesThirdPartyData: boolean + anonymizedFields?: string[] + transferMethod?: 'download' | 'email' | 'third_party' + transferRecipient?: string +} + +export interface DSRRectificationDetails { + fieldsToCorrect: { + field: string + currentValue: string + requestedValue: string + corrected: boolean + correctedAt?: string + correctedBy?: string + }[] +} + +export interface DSRObjectionDetails { + processingPurpose: string + legalBasis: string + objectionGrounds: string + decision: 'accepted' | 'rejected' | 'pending' + decisionReason?: string + decisionBy?: string + decisionAt?: string +} diff --git a/admin-compliance/lib/sdk/dsr/types.ts b/admin-compliance/lib/sdk/dsr/types.ts index 71feee3..eb4d433 100644 --- a/admin-compliance/lib/sdk/dsr/types.ts +++ b/admin-compliance/lib/sdk/dsr/types.ts @@ -1,581 +1,60 @@ /** - * DSR (Data Subject Request) Types + * DSR (Data Subject Request) Types — barrel re-export * - * TypeScript definitions for GDPR Art. 15-21 Data Subject Requests - * Based on the Go Consent Service backend API structure + * Split into: + * - types-core.ts (enums, constants, metadata, main interfaces) + * - types-api.ts (API types, communication, audit, templates, helpers) */ -// ============================================================================= -// ENUMS & CONSTANTS -// ============================================================================= - -export type DSRType = - | 'access' // Art. 15 - Auskunftsrecht - | 'rectification' // Art. 16 - Berichtigungsrecht - | 'erasure' // Art. 17 - Loeschungsrecht - | 'restriction' // Art. 18 - Einschraenkungsrecht - | 'portability' // Art. 20 - Datenuebertragbarkeit - | 'objection' // Art. 21 - Widerspruchsrecht - -export type DSRStatus = - | 'intake' // Eingang - Anfrage dokumentiert - | 'identity_verification' // Identitaetspruefung - | 'processing' // In Bearbeitung - | 'completed' // Abgeschlossen - | 'rejected' // Abgelehnt - | 'cancelled' // Storniert - -export type DSRPriority = 'low' | 'normal' | 'high' | 'critical' - -export type DSRSource = - | 'web_form' // Kontaktformular/Portal - | 'email' // E-Mail - | 'letter' // Brief - | 'phone' // Telefon - | 'in_person' // Persoenlich - | 'other' // Sonstiges - -export type IdentityVerificationMethod = - | 'id_document' // Ausweiskopie - | 'email' // E-Mail-Bestaetigung - | 'phone' // Telefonische Bestaetigung - | 'postal' // Postalische Bestaetigung - | 'existing_account' // Bestehendes Kundenkonto - | 'other' // Sonstiges - -export type CommunicationType = - | 'incoming' // Eingehend (vom Betroffenen) - | 'outgoing' // Ausgehend (an Betroffenen) - | 'internal' // Intern (Notizen) - -export type CommunicationChannel = - | 'email' - | 'letter' - | 'phone' - | 'portal' - | 'internal_note' - -// ============================================================================= -// DSR TYPE METADATA -// ============================================================================= - -export interface DSRTypeInfo { - type: DSRType - article: string - label: string - labelShort: string - description: string - defaultDeadlineDays: number - maxExtensionMonths: number - color: string - bgColor: string - processDocument?: string // Reference to process document -} - -export const DSR_TYPE_INFO: Record = { - access: { - type: 'access', - article: 'Art. 15', - label: 'Auskunftsrecht', - labelShort: 'Auskunft', - description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten', - defaultDeadlineDays: 30, - maxExtensionMonths: 2, - color: 'text-blue-700', - bgColor: 'bg-blue-100', - processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf' - }, - rectification: { - type: 'rectification', - article: 'Art. 16', - label: 'Berichtigungsrecht', - labelShort: 'Berichtigung', - description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten', - defaultDeadlineDays: 14, - maxExtensionMonths: 2, - color: 'text-yellow-700', - bgColor: 'bg-yellow-100', - processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf' - }, - erasure: { - type: 'erasure', - article: 'Art. 17', - label: 'Loeschungsrecht', - labelShort: 'Loeschung', - description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")', - defaultDeadlineDays: 14, - maxExtensionMonths: 2, - color: 'text-red-700', - bgColor: 'bg-red-100', - processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf' - }, - restriction: { - type: 'restriction', - article: 'Art. 18', - label: 'Einschraenkungsrecht', - labelShort: 'Einschraenkung', - description: 'Recht auf Einschraenkung der Verarbeitung', - defaultDeadlineDays: 14, - maxExtensionMonths: 2, - color: 'text-orange-700', - bgColor: 'bg-orange-100', - processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf' - }, - portability: { - type: 'portability', - article: 'Art. 20', - label: 'Datenuebertragbarkeit', - labelShort: 'Uebertragung', - description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format', - defaultDeadlineDays: 30, - maxExtensionMonths: 2, - color: 'text-purple-700', - bgColor: 'bg-purple-100', - processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf' - }, - objection: { - type: 'objection', - article: 'Art. 21', - label: 'Widerspruchsrecht', - labelShort: 'Widerspruch', - description: 'Recht auf Widerspruch gegen die Verarbeitung', - defaultDeadlineDays: 30, - maxExtensionMonths: 0, // No extension allowed for objections - color: 'text-gray-700', - bgColor: 'bg-gray-100' - } -} - -export const DSR_STATUS_INFO: Record = { - intake: { - label: 'Eingang', - color: 'text-blue-700', - bgColor: 'bg-blue-100', - borderColor: 'border-blue-200' - }, - identity_verification: { - label: 'ID-Pruefung', - color: 'text-yellow-700', - bgColor: 'bg-yellow-100', - borderColor: 'border-yellow-200' - }, - processing: { - label: 'In Bearbeitung', - color: 'text-purple-700', - bgColor: 'bg-purple-100', - borderColor: 'border-purple-200' - }, - completed: { - label: 'Abgeschlossen', - color: 'text-green-700', - bgColor: 'bg-green-100', - borderColor: 'border-green-200' - }, - rejected: { - label: 'Abgelehnt', - color: 'text-red-700', - bgColor: 'bg-red-100', - borderColor: 'border-red-200' - }, - cancelled: { - label: 'Storniert', - color: 'text-gray-700', - bgColor: 'bg-gray-100', - borderColor: 'border-gray-200' - } -} - -// ============================================================================= -// MAIN INTERFACES -// ============================================================================= - -export interface DSRRequester { - name: string - email: string - phone?: string - address?: string - customerId?: string // If existing customer -} - -export interface DSRIdentityVerification { - verified: boolean - method?: IdentityVerificationMethod - verifiedAt?: string - verifiedBy?: string - notes?: string - documentRef?: string // Reference to uploaded ID document -} - -export interface DSRAssignment { - assignedTo: string | null - assignedAt?: string - assignedBy?: string -} - -export interface DSRDeadline { - originalDeadline: string - currentDeadline: string - extended: boolean - extensionReason?: string - extensionApprovedBy?: string - extensionApprovedAt?: string -} - -export interface DSRRequest { - id: string - referenceNumber: string // e.g., "DSR-2025-000042" - type: DSRType - status: DSRStatus - priority: DSRPriority - - // Requester info - requester: DSRRequester - - // Request details - source: DSRSource - sourceDetails?: string // e.g., "Kontaktformular auf website.de" - requestText?: string // Original request text - - // Dates - receivedAt: string - deadline: DSRDeadline - completedAt?: string - - // Verification - identityVerification: DSRIdentityVerification - - // Assignment - assignment: DSRAssignment - - // Processing - notes?: string - internalNotes?: string - - // Type-specific data - erasureChecklist?: DSRErasureChecklist // For Art. 17 - dataExport?: DSRDataExport // For Art. 15, 20 - rectificationDetails?: DSRRectificationDetails // For Art. 16 - objectionDetails?: DSRObjectionDetails // For Art. 21 - - // Audit - createdAt: string - createdBy: string - updatedAt: string - updatedBy?: string - - // Metadata - tenantId: string -} - -// ============================================================================= -// TYPE-SPECIFIC INTERFACES -// ============================================================================= - -// Art. 17(3) Erasure Exceptions Checklist -export interface DSRErasureChecklistItem { - id: string - article: string // e.g., "17(3)(a)" - label: string - description: string - checked: boolean - applies: boolean - notes?: string -} - -export interface DSRErasureChecklist { - items: DSRErasureChecklistItem[] - canProceedWithErasure: boolean - reviewedBy?: string - reviewedAt?: string -} - -export const ERASURE_EXCEPTIONS: Omit[] = [ - { - id: 'art17_3_a', - article: '17(3)(a)', - label: 'Meinungs- und Informationsfreiheit', - description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information' - }, - { - id: 'art17_3_b', - article: '17(3)(b)', - label: 'Rechtliche Verpflichtung', - description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)' - }, - { - id: 'art17_3_c', - article: '17(3)(c)', - label: 'Oeffentliches Interesse', - description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit' - }, - { - id: 'art17_3_d', - article: '17(3)(d)', - label: 'Archivzwecke', - description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik' - }, - { - id: 'art17_3_e', - article: '17(3)(e)', - label: 'Rechtsansprueche', - description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen' - } -] - -// Data Export for Art. 15, 20 -export interface DSRDataExport { - format: 'json' | 'csv' | 'xml' | 'pdf' - generatedAt?: string - generatedBy?: string - fileUrl?: string - fileName?: string - fileSize?: number - includesThirdPartyData: boolean - anonymizedFields?: string[] - transferMethod?: 'download' | 'email' | 'third_party' // For Art. 20 transfer - transferRecipient?: string // For Art. 20 transfer to another controller -} - -// Rectification Details for Art. 16 -export interface DSRRectificationDetails { - fieldsToCorrect: { - field: string - currentValue: string - requestedValue: string - corrected: boolean - correctedAt?: string - correctedBy?: string - }[] -} - -// Objection Details for Art. 21 -export interface DSRObjectionDetails { - processingPurpose: string - legalBasis: string - objectionGrounds: string - decision: 'accepted' | 'rejected' | 'pending' - decisionReason?: string - decisionBy?: string - decisionAt?: string -} - -// ============================================================================= -// COMMUNICATION -// ============================================================================= - -export interface DSRCommunication { - id: string - dsrId: string - type: CommunicationType - channel: CommunicationChannel - subject?: string - content: string - templateUsed?: string // Reference to email template - attachments?: { - name: string - url: string - size: number - type: string - }[] - sentAt?: string - sentBy?: string - receivedAt?: string - createdAt: string - createdBy: string -} - -// ============================================================================= -// AUDIT LOG -// ============================================================================= - -export interface DSRAuditEntry { - id: string - dsrId: string - action: string // e.g., "status_changed", "identity_verified", "assigned" - previousValue?: string - newValue?: string - performedBy: string - performedAt: string - notes?: string -} - -// ============================================================================= -// EMAIL TEMPLATES -// ============================================================================= - -export interface DSREmailTemplate { - id: string - name: string - subject: string - body: string - type: DSRType | 'general' - stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion' - language: 'de' | 'en' - variables: string[] // e.g., ["requesterName", "referenceNumber", "deadline"] -} - -export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [ - { - id: 'intake_confirmation', - name: 'Eingangsbestaetigung', - subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}', - body: `Sehr geehrte(r) {{requesterName}}, - -wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}. - -Referenznummer: {{referenceNumber}} -Art der Anfrage: {{requestType}} - -Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten. - -Mit freundlichen Gruessen -{{senderName}} -Datenschutzbeauftragter`, - type: 'general', - stage: 'intake', - language: 'de', - variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName'] - }, - { - id: 'identity_request', - name: 'Identitaetsanfrage', - subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}', - body: `Sehr geehrte(r) {{requesterName}}, - -um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet. - -Bitte senden Sie uns eines der folgenden Dokumente: -- Kopie Ihres Personalausweises (Vorder- und Rueckseite) -- Kopie Ihres Reisepasses - -Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht. - -Mit freundlichen Gruessen -{{senderName}} -Datenschutzbeauftragter`, - type: 'general', - stage: 'identity_request', - language: 'de', - variables: ['requesterName', 'referenceNumber', 'senderName'] - } -] - -// ============================================================================= -// API TYPES -// ============================================================================= - -export interface DSRFilters { - status?: DSRStatus | DSRStatus[] - type?: DSRType | DSRType[] - priority?: DSRPriority - assignedTo?: string - overdue?: boolean - search?: string - dateFrom?: string - dateTo?: string -} - -export interface DSRListResponse { - requests: DSRRequest[] - total: number - page: number - pageSize: number -} - -export interface DSRCreateRequest { - type: DSRType - requester: DSRRequester - source: DSRSource - sourceDetails?: string - requestText?: string - priority?: DSRPriority -} - -export interface DSRUpdateRequest { - status?: DSRStatus - priority?: DSRPriority - notes?: string - internalNotes?: string - assignment?: DSRAssignment -} - -export interface DSRVerifyIdentityRequest { - method: IdentityVerificationMethod - notes?: string - documentRef?: string -} - -export interface DSRCompleteRequest { - completionNotes?: string - dataExport?: DSRDataExport -} - -export interface DSRRejectRequest { - reason: string - legalBasis?: string // e.g., Art. 17(3) exception -} - -export interface DSRExtendDeadlineRequest { - extensionMonths: 1 | 2 - reason: string -} - -export interface DSRSendCommunicationRequest { - type: CommunicationType - channel: CommunicationChannel - subject?: string - content: string - templateId?: string -} - -// ============================================================================= -// STATISTICS -// ============================================================================= - -export interface DSRStatistics { - total: number - byStatus: Record - byType: Record - overdue: number - dueThisWeek: number - averageProcessingDays: number - completedThisMonth: number -} - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -export function getDaysRemaining(deadline: string): number { - const deadlineDate = new Date(deadline) - const now = new Date() - const diff = deadlineDate.getTime() - now.getTime() - return Math.ceil(diff / (1000 * 60 * 60 * 24)) -} - -export function isOverdue(request: DSRRequest): boolean { - if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') { - return false - } - return getDaysRemaining(request.deadline.currentDeadline) < 0 -} - -export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean { - if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') { - return false - } - const daysRemaining = getDaysRemaining(request.deadline.currentDeadline) - return daysRemaining >= 0 && daysRemaining <= thresholdDays -} - -export function generateReferenceNumber(year: number, sequence: number): string { - return `DSR-${year}-${String(sequence).padStart(6, '0')}` -} - -export function getTypeInfo(type: DSRType): DSRTypeInfo { - return DSR_TYPE_INFO[type] -} - -export function getStatusInfo(status: DSRStatus) { - return DSR_STATUS_INFO[status] -} +export type { + DSRType, + DSRStatus, + DSRPriority, + DSRSource, + IdentityVerificationMethod, + CommunicationType, + CommunicationChannel, + DSRTypeInfo, + DSRRequester, + DSRIdentityVerification, + DSRAssignment, + DSRDeadline, + DSRRequest, + DSRErasureChecklistItem, + DSRErasureChecklist, + DSRDataExport, + DSRRectificationDetails, + DSRObjectionDetails, +} from './types-core' + +export { + DSR_TYPE_INFO, + DSR_STATUS_INFO, + ERASURE_EXCEPTIONS, +} from './types-core' + +export type { + DSRCommunication, + DSRAuditEntry, + DSREmailTemplate, + DSRFilters, + DSRListResponse, + DSRCreateRequest, + DSRUpdateRequest, + DSRVerifyIdentityRequest, + DSRCompleteRequest, + DSRRejectRequest, + DSRExtendDeadlineRequest, + DSRSendCommunicationRequest, + DSRStatistics, +} from './types-api' + +export { + DSR_EMAIL_TEMPLATES, + getDaysRemaining, + isOverdue, + isUrgent, + generateReferenceNumber, + getTypeInfo, + getStatusInfo, +} from './types-api' diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts new file mode 100644 index 0000000..3e31731 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-config.ts @@ -0,0 +1,119 @@ +/** + * Cookie Banner — Configuration & Category Generation + * + * Default texts, styling, and category generation from data points. + */ + +import { + DataPoint, + CookieBannerCategory, + CookieBannerConfig, + CookieBannerStyling, + CookieBannerTexts, + CookieInfo, + LocalizedText, + SupportedLanguage, +} from '../types' +import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader' + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function t(text: LocalizedText, language: SupportedLanguage): string { + return text[language] +} + +// ============================================================================= +// DEFAULT CONFIGURATION +// ============================================================================= + +export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = { + title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' }, + description: { + de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.', + en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.', + }, + acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' }, + rejectAll: { de: 'Nur notwendige', en: 'Essential Only' }, + customize: { de: 'Einstellungen', en: 'Customize' }, + save: { de: 'Auswahl speichern', en: 'Save Selection' }, + privacyPolicyLink: { + de: 'Mehr in unserer Datenschutzerklaerung', + en: 'More in our Privacy Policy', + }, +} + +export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = { + position: 'BOTTOM', + theme: 'LIGHT', + primaryColor: '#6366f1', + secondaryColor: '#f1f5f9', + textColor: '#1e293b', + backgroundColor: '#ffffff', + borderRadius: 12, + maxWidth: 480, +} + +// ============================================================================= +// GENERATOR FUNCTIONS +// ============================================================================= + +function getExpiryFromRetention(retention: string): string { + const mapping: Record = { + '24_HOURS': '24 Stunden / 24 hours', + '30_DAYS': '30 Tage / 30 days', + '90_DAYS': '90 Tage / 90 days', + '12_MONTHS': '1 Jahr / 1 year', + '24_MONTHS': '2 Jahre / 2 years', + '36_MONTHS': '3 Jahre / 3 years', + 'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation', + 'UNTIL_PURPOSE_FULFILLED': 'Session', + 'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion', + } + return mapping[retention] || 'Session' +} + +export function generateCookieCategories( + dataPoints: DataPoint[] +): CookieBannerCategory[] { + const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null) + + return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => { + const categoryDataPoints = cookieDataPoints.filter( + (dp) => dp.cookieCategory === defaultCat.id + ) + + const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({ + name: dp.code, + provider: 'First Party', + purpose: dp.purpose, + expiry: getExpiryFromRetention(dp.retentionPeriod), + type: 'FIRST_PARTY', + })) + + return { + ...defaultCat, + dataPointIds: categoryDataPoints.map((dp) => dp.id), + cookies, + } + }).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired) +} + +export function generateCookieBannerConfig( + tenantId: string, + dataPoints: DataPoint[], + customTexts?: Partial, + customStyling?: Partial +): CookieBannerConfig { + const categories = generateCookieCategories(dataPoints) + + return { + id: `cookie-banner-${tenantId}`, + tenantId, + categories, + styling: { ...DEFAULT_COOKIE_BANNER_STYLING, ...customStyling }, + texts: { ...DEFAULT_COOKIE_BANNER_TEXTS, ...customTexts }, + updatedAt: new Date(), + } +} diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts new file mode 100644 index 0000000..6b553d7 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts @@ -0,0 +1,418 @@ +/** + * Cookie Banner — Embed Code Generation (CSS, HTML, JS) + * + * Generates the embeddable cookie banner code from configuration. + */ + +import { + CookieBannerConfig, + CookieBannerStyling, + CookieBannerEmbedCode, + LocalizedText, + SupportedLanguage, +} from '../types' + +// ============================================================================= +// MAIN EXPORT +// ============================================================================= + +export function generateEmbedCode( + config: CookieBannerConfig, + privacyPolicyUrl: string = '/datenschutz' +): CookieBannerEmbedCode { + const css = generateCSS(config.styling) + const html = generateHTML(config, privacyPolicyUrl) + const js = generateJS(config) + + const scriptTag = `` + + return { html, css, js, scriptTag } +} + +// ============================================================================= +// CSS GENERATION +// ============================================================================= + +function generateCSS(styling: CookieBannerStyling): string { + const positionStyles: Record = { + BOTTOM: 'bottom: 0; left: 0; right: 0;', + TOP: 'top: 0; left: 0; right: 0;', + CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);', + } + + const isDark = styling.theme === 'DARK' + const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff' + const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b' + const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' + + return ` +/* Cookie Banner Styles */ +.cookie-banner-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 9998; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.cookie-banner-overlay.active { + opacity: 1; + visibility: visible; +} + +.cookie-banner { + position: fixed; + ${positionStyles[styling.position]} + z-index: 9999; + background: ${bgColor}; + color: ${textColor}; + border-radius: ${styling.borderRadius || 12}px; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1); + padding: 24px; + max-width: ${styling.maxWidth}px; + margin: ${styling.position === 'CENTER' ? '0' : '16px'}; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + transform: translateY(100%); + opacity: 0; + transition: all 0.3s ease; +} + +.cookie-banner.active { + transform: translateY(0); + opacity: 1; +} + +.cookie-banner-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 12px; +} + +.cookie-banner-description { + font-size: 14px; + line-height: 1.5; + margin-bottom: 16px; + opacity: 0.8; +} + +.cookie-banner-buttons { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.cookie-banner-btn { + flex: 1; + min-width: 120px; + padding: 12px 20px; + border-radius: ${(styling.borderRadius || 12) / 2}px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.cookie-banner-btn-primary { + background: ${styling.primaryColor}; + color: white; +} + +.cookie-banner-btn-primary:hover { + filter: brightness(1.1); +} + +.cookie-banner-btn-secondary { + background: ${styling.secondaryColor || borderColor}; + color: ${textColor}; +} + +.cookie-banner-btn-secondary:hover { + filter: brightness(0.95); +} + +.cookie-banner-link { + display: block; + margin-top: 16px; + font-size: 12px; + color: ${styling.primaryColor}; + text-decoration: none; +} + +.cookie-banner-link:hover { + text-decoration: underline; +} + +/* Category Details */ +.cookie-banner-details { + margin-top: 16px; + border-top: 1px solid ${borderColor}; + padding-top: 16px; + display: none; +} + +.cookie-banner-details.active { + display: block; +} + +.cookie-banner-category { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid ${borderColor}; +} + +.cookie-banner-category:last-child { + border-bottom: none; +} + +.cookie-banner-category-info { + flex: 1; +} + +.cookie-banner-category-name { + font-weight: 500; + font-size: 14px; +} + +.cookie-banner-category-desc { + font-size: 12px; + opacity: 0.7; + margin-top: 4px; +} + +.cookie-banner-toggle { + position: relative; + width: 48px; + height: 28px; + background: ${borderColor}; + border-radius: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.cookie-banner-toggle.active { + background: ${styling.primaryColor}; +} + +.cookie-banner-toggle.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.cookie-banner-toggle::after { + content: ''; + position: absolute; + top: 4px; + left: 4px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: all 0.2s ease; +} + +.cookie-banner-toggle.active::after { + left: 24px; +} + +@media (max-width: 640px) { + .cookie-banner { + margin: 0; + border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px; + max-width: 100%; + } + + .cookie-banner-buttons { + flex-direction: column; + } + + .cookie-banner-btn { + width: 100%; + } +} +`.trim() +} + +// ============================================================================= +// HTML GENERATION +// ============================================================================= + +function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string { + const categoriesHTML = config.categories + .map((cat) => { + const isRequired = cat.isRequired + return ` + + ` + }) + .join('') + + return ` + + +`.trim() +} + +// ============================================================================= +// JS GENERATION +// ============================================================================= + +function generateJS(config: CookieBannerConfig): string { + const categoryIds = config.categories.map((c) => c.id) + const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id) + + return ` +(function() { + 'use strict'; + + const COOKIE_NAME = 'cookie_consent'; + const COOKIE_EXPIRY_DAYS = 365; + const CATEGORIES = ${JSON.stringify(categoryIds)}; + const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)}; + + function getConsent() { + const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '=')); + if (!cookie) return null; + try { + return JSON.parse(decodeURIComponent(cookie.split('=')[1])); + } catch { + return null; + } + } + + function saveConsent(consent) { + const date = new Date(); + date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000)); + document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) + + ';expires=' + date.toUTCString() + + ';path=/;SameSite=Lax'; + window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent })); + } + + function hasConsent(category) { + const consent = getConsent(); + if (!consent) return REQUIRED_CATEGORIES.includes(category); + return consent[category] === true; + } + + function initBanner() { + const banner = document.getElementById('cookieBanner'); + const overlay = document.getElementById('cookieBannerOverlay'); + const details = document.getElementById('cookieBannerDetails'); + + if (!banner) return; + + const consent = getConsent(); + if (consent) return; + + setTimeout(() => { + banner.classList.add('active'); + overlay.classList.add('active'); + }, 500); + + document.getElementById('cookieBannerAccept')?.addEventListener('click', () => { + const consent = {}; + CATEGORIES.forEach(cat => consent[cat] = true); + saveConsent(consent); + closeBanner(); + }); + + document.getElementById('cookieBannerReject')?.addEventListener('click', () => { + const consent = {}; + CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat)); + saveConsent(consent); + closeBanner(); + }); + + document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => { + details.classList.toggle('active'); + }); + + document.getElementById('cookieBannerSave')?.addEventListener('click', () => { + const consent = {}; + CATEGORIES.forEach(cat => { + const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]'); + consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat); + }); + saveConsent(consent); + closeBanner(); + }); + + document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => { + if (toggle.dataset.required === 'true') return; + toggle.addEventListener('click', () => { + toggle.classList.toggle('active'); + }); + }); + + overlay?.addEventListener('click', () => { + // Don't close - user must make a choice + }); + } + + function closeBanner() { + const banner = document.getElementById('cookieBanner'); + const overlay = document.getElementById('cookieBannerOverlay'); + banner?.classList.remove('active'); + overlay?.classList.remove('active'); + } + + window.CookieConsent = { + getConsent, + saveConsent, + hasConsent, + show: () => { + document.getElementById('cookieBanner')?.classList.add('active'); + document.getElementById('cookieBannerOverlay')?.classList.add('active'); + }, + hide: closeBanner + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initBanner); + } else { + initBanner(); + } +})(); +`.trim() +} diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts index 95626cf..7fb2058 100644 --- a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts +++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner.ts @@ -1,595 +1,18 @@ /** - * Cookie Banner Generator + * Cookie Banner Generator — barrel re-export * - * Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog. - * Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet. + * Split into: + * - cookie-banner-config.ts (defaults, category generation, config builder) + * - cookie-banner-embed.ts (CSS, HTML, JS embed code generation) */ -import { - DataPoint, - CookieCategory, - CookieBannerCategory, - CookieBannerConfig, - CookieBannerStyling, - CookieBannerTexts, - CookieBannerEmbedCode, - CookieInfo, - LocalizedText, - SupportedLanguage, -} from '../types' -import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader' - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -/** - * Holt den lokalisierten Text - */ -function t(text: LocalizedText, language: SupportedLanguage): string { - return text[language] -} - -// ============================================================================= -// COOKIE BANNER CONFIGURATION -// ============================================================================= - -/** - * Standard Cookie Banner Texte - */ -export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = { - title: { - de: 'Cookie-Einstellungen', - en: 'Cookie Settings', - }, - description: { - de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.', - en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.', - }, - acceptAll: { - de: 'Alle akzeptieren', - en: 'Accept All', - }, - rejectAll: { - de: 'Nur notwendige', - en: 'Essential Only', - }, - customize: { - de: 'Einstellungen', - en: 'Customize', - }, - save: { - de: 'Auswahl speichern', - en: 'Save Selection', - }, - privacyPolicyLink: { - de: 'Mehr in unserer Datenschutzerklaerung', - en: 'More in our Privacy Policy', - }, -} - -/** - * Standard Styling fuer Cookie Banner - */ -export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = { - position: 'BOTTOM', - theme: 'LIGHT', - primaryColor: '#6366f1', // Indigo - secondaryColor: '#f1f5f9', // Slate-100 - textColor: '#1e293b', // Slate-800 - backgroundColor: '#ffffff', - borderRadius: 12, - maxWidth: 480, -} - -// ============================================================================= -// GENERATOR FUNCTIONS -// ============================================================================= - -/** - * Generiert Cookie-Banner Kategorien aus Datenpunkten - */ -export function generateCookieCategories( - dataPoints: DataPoint[] -): CookieBannerCategory[] { - // Filtere nur Datenpunkte mit Cookie-Kategorie - const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null) - - // Erstelle die Kategorien basierend auf den Defaults - return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => { - // Filtere die Datenpunkte fuer diese Kategorie - const categoryDataPoints = cookieDataPoints.filter( - (dp) => dp.cookieCategory === defaultCat.id - ) - - // Erstelle Cookie-Infos aus den Datenpunkten - const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({ - name: dp.code, - provider: 'First Party', - purpose: dp.purpose, - expiry: getExpiryFromRetention(dp.retentionPeriod), - type: 'FIRST_PARTY', - })) - - return { - ...defaultCat, - dataPointIds: categoryDataPoints.map((dp) => dp.id), - cookies, - } - }).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired) -} - -/** - * Konvertiert Retention Period zu Cookie-Expiry String - */ -function getExpiryFromRetention(retention: string): string { - const mapping: Record = { - '24_HOURS': '24 Stunden / 24 hours', - '30_DAYS': '30 Tage / 30 days', - '90_DAYS': '90 Tage / 90 days', - '12_MONTHS': '1 Jahr / 1 year', - '24_MONTHS': '2 Jahre / 2 years', - '36_MONTHS': '3 Jahre / 3 years', - 'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation', - 'UNTIL_PURPOSE_FULFILLED': 'Session', - 'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion', - } - return mapping[retention] || 'Session' -} - -/** - * Generiert die vollstaendige Cookie Banner Konfiguration - */ -export function generateCookieBannerConfig( - tenantId: string, - dataPoints: DataPoint[], - customTexts?: Partial, - customStyling?: Partial -): CookieBannerConfig { - const categories = generateCookieCategories(dataPoints) - - return { - id: `cookie-banner-${tenantId}`, - tenantId, - categories, - styling: { - ...DEFAULT_COOKIE_BANNER_STYLING, - ...customStyling, - }, - texts: { - ...DEFAULT_COOKIE_BANNER_TEXTS, - ...customTexts, - }, - updatedAt: new Date(), - } -} - -// ============================================================================= -// EMBED CODE GENERATION -// ============================================================================= - -/** - * Generiert den Embed-Code fuer den Cookie Banner - */ -export function generateEmbedCode( - config: CookieBannerConfig, - privacyPolicyUrl: string = '/datenschutz' -): CookieBannerEmbedCode { - const css = generateCSS(config.styling) - const html = generateHTML(config, privacyPolicyUrl) - const js = generateJS(config) - - const scriptTag = `` - - return { - html, - css, - js, - scriptTag, - } -} - -/** - * Generiert das CSS fuer den Cookie Banner - */ -function generateCSS(styling: CookieBannerStyling): string { - const positionStyles: Record = { - BOTTOM: 'bottom: 0; left: 0; right: 0;', - TOP: 'top: 0; left: 0; right: 0;', - CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);', - } - - const isDark = styling.theme === 'DARK' - const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff' - const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b' - const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' - - return ` -/* Cookie Banner Styles */ -.cookie-banner-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.4); - z-index: 9998; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; -} - -.cookie-banner-overlay.active { - opacity: 1; - visibility: visible; -} - -.cookie-banner { - position: fixed; - ${positionStyles[styling.position]} - z-index: 9999; - background: ${bgColor}; - color: ${textColor}; - border-radius: ${styling.borderRadius || 12}px; - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1); - padding: 24px; - max-width: ${styling.maxWidth}px; - margin: ${styling.position === 'CENTER' ? '0' : '16px'}; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - transform: translateY(100%); - opacity: 0; - transition: all 0.3s ease; -} - -.cookie-banner.active { - transform: translateY(0); - opacity: 1; -} - -.cookie-banner-title { - font-size: 18px; - font-weight: 600; - margin-bottom: 12px; -} - -.cookie-banner-description { - font-size: 14px; - line-height: 1.5; - margin-bottom: 16px; - opacity: 0.8; -} - -.cookie-banner-buttons { - display: flex; - gap: 12px; - flex-wrap: wrap; -} - -.cookie-banner-btn { - flex: 1; - min-width: 120px; - padding: 12px 20px; - border-radius: ${(styling.borderRadius || 12) / 2}px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - border: none; -} - -.cookie-banner-btn-primary { - background: ${styling.primaryColor}; - color: white; -} - -.cookie-banner-btn-primary:hover { - filter: brightness(1.1); -} - -.cookie-banner-btn-secondary { - background: ${styling.secondaryColor || borderColor}; - color: ${textColor}; -} - -.cookie-banner-btn-secondary:hover { - filter: brightness(0.95); -} - -.cookie-banner-link { - display: block; - margin-top: 16px; - font-size: 12px; - color: ${styling.primaryColor}; - text-decoration: none; -} - -.cookie-banner-link:hover { - text-decoration: underline; -} - -/* Category Details */ -.cookie-banner-details { - margin-top: 16px; - border-top: 1px solid ${borderColor}; - padding-top: 16px; - display: none; -} - -.cookie-banner-details.active { - display: block; -} - -.cookie-banner-category { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 0; - border-bottom: 1px solid ${borderColor}; -} - -.cookie-banner-category:last-child { - border-bottom: none; -} - -.cookie-banner-category-info { - flex: 1; -} - -.cookie-banner-category-name { - font-weight: 500; - font-size: 14px; -} - -.cookie-banner-category-desc { - font-size: 12px; - opacity: 0.7; - margin-top: 4px; -} - -.cookie-banner-toggle { - position: relative; - width: 48px; - height: 28px; - background: ${borderColor}; - border-radius: 14px; - cursor: pointer; - transition: all 0.2s ease; -} - -.cookie-banner-toggle.active { - background: ${styling.primaryColor}; -} - -.cookie-banner-toggle.disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.cookie-banner-toggle::after { - content: ''; - position: absolute; - top: 4px; - left: 4px; - width: 20px; - height: 20px; - background: white; - border-radius: 50%; - transition: all 0.2s ease; -} - -.cookie-banner-toggle.active::after { - left: 24px; -} - -@media (max-width: 640px) { - .cookie-banner { - margin: 0; - border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px; - max-width: 100%; - } - - .cookie-banner-buttons { - flex-direction: column; - } - - .cookie-banner-btn { - width: 100%; - } -} -`.trim() -} - -/** - * Generiert das HTML fuer den Cookie Banner - */ -function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string { - const categoriesHTML = config.categories - .map((cat) => { - const isRequired = cat.isRequired - return ` - - ` - }) - .join('') - - return ` - - -`.trim() -} - -/** - * Generiert das JavaScript fuer den Cookie Banner - */ -function generateJS(config: CookieBannerConfig): string { - const categoryIds = config.categories.map((c) => c.id) - const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id) - - return ` -(function() { - 'use strict'; - - const COOKIE_NAME = 'cookie_consent'; - const COOKIE_EXPIRY_DAYS = 365; - const CATEGORIES = ${JSON.stringify(categoryIds)}; - const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)}; - - // Get consent from cookie - function getConsent() { - const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '=')); - if (!cookie) return null; - try { - return JSON.parse(decodeURIComponent(cookie.split('=')[1])); - } catch { - return null; - } - } - - // Save consent to cookie - function saveConsent(consent) { - const date = new Date(); - date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000)); - document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) + - ';expires=' + date.toUTCString() + - ';path=/;SameSite=Lax'; - - // Dispatch event - window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent })); - } - - // Check if category is consented - function hasConsent(category) { - const consent = getConsent(); - if (!consent) return REQUIRED_CATEGORIES.includes(category); - return consent[category] === true; - } - - // Initialize banner - function initBanner() { - const banner = document.getElementById('cookieBanner'); - const overlay = document.getElementById('cookieBannerOverlay'); - const details = document.getElementById('cookieBannerDetails'); - - if (!banner) return; - - const consent = getConsent(); - if (consent) { - // User has already consented - return; - } - - // Show banner - setTimeout(() => { - banner.classList.add('active'); - overlay.classList.add('active'); - }, 500); - - // Accept all - document.getElementById('cookieBannerAccept')?.addEventListener('click', () => { - const consent = {}; - CATEGORIES.forEach(cat => consent[cat] = true); - saveConsent(consent); - closeBanner(); - }); - - // Reject all (only essential) - document.getElementById('cookieBannerReject')?.addEventListener('click', () => { - const consent = {}; - CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat)); - saveConsent(consent); - closeBanner(); - }); - - // Customize - document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => { - details.classList.toggle('active'); - }); - - // Save selection - document.getElementById('cookieBannerSave')?.addEventListener('click', () => { - const consent = {}; - CATEGORIES.forEach(cat => { - const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]'); - consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat); - }); - saveConsent(consent); - closeBanner(); - }); - - // Toggle handlers - document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => { - if (toggle.dataset.required === 'true') return; - toggle.addEventListener('click', () => { - toggle.classList.toggle('active'); - }); - }); - - // Close on overlay click - overlay?.addEventListener('click', () => { - // Don't close - user must make a choice - }); - } - - function closeBanner() { - const banner = document.getElementById('cookieBanner'); - const overlay = document.getElementById('cookieBannerOverlay'); - banner?.classList.remove('active'); - overlay?.classList.remove('active'); - } - - // Expose API - window.CookieConsent = { - getConsent, - saveConsent, - hasConsent, - show: () => { - document.getElementById('cookieBanner')?.classList.add('active'); - document.getElementById('cookieBannerOverlay')?.classList.add('active'); - }, - hide: closeBanner - }; - - // Initialize on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initBanner); - } else { - initBanner(); - } -})(); -`.trim() -} - -// Note: All exports are defined inline with 'export const' and 'export function' +export { + DEFAULT_COOKIE_BANNER_TEXTS, + DEFAULT_COOKIE_BANNER_STYLING, + generateCookieCategories, + generateCookieBannerConfig, +} from './cookie-banner-config' + +export { + generateEmbedCode, +} from './cookie-banner-embed' diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts new file mode 100644 index 0000000..9c7b1f3 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-renderers.ts @@ -0,0 +1,322 @@ +/** + * Privacy Policy Renderers & Main Generator + * + * Cookies section, changes section, rendering (HTML/Markdown), + * and the main generatePrivacyPolicy entry point. + */ + +import { + DataPoint, + CompanyInfo, + PrivacyPolicySection, + GeneratedPrivacyPolicy, + SupportedLanguage, + ExportFormat, + LocalizedText, +} from '../types' +import { RETENTION_MATRIX } from '../catalog/loader' + +import { + formatDate, + generateControllerSection, + generateDataCollectionSection, + generatePurposesSection, + generateLegalBasisSection, + generateRecipientsSection, + generateRetentionSection, + generateSpecialCategoriesSection, + generateRightsSection, +} from './privacy-policy-sections' + +// ============================================================================= +// HELPER +// ============================================================================= + +function t(text: LocalizedText, language: SupportedLanguage): string { + return text[language] +} + +// ============================================================================= +// SECTION GENERATORS (cookies + changes) +// ============================================================================= + +export function generateCookiesSection( + dataPoints: DataPoint[], + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '8. Cookies und aehnliche Technologien', + en: '8. Cookies and Similar Technologies', + } + + const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null) + + if (cookieDataPoints.length === 0) { + const content: LocalizedText = { + de: 'Wir verwenden auf dieser Website keine Cookies.', + en: 'We do not use cookies on this website.', + } + return { + id: 'cookies', + order: 8, + title, + content, + dataPointIds: [], + isRequired: false, + isGenerated: false, + } + } + + const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL') + const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE') + const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION') + const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA') + + const sections: string[] = [] + + if (essential.length > 0) { + const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') + sections.push( + language === 'de' + ? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}` + : `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}` + ) + } + + if (performance.length > 0) { + const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') + sections.push( + language === 'de' + ? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}` + : `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}` + ) + } + + if (personalization.length > 0) { + const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') + sections.push( + language === 'de' + ? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}` + : `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}` + ) + } + + if (externalMedia.length > 0) { + const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') + sections.push( + language === 'de' + ? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}` + : `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}` + ) + } + + const intro: LocalizedText = { + de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`, + en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`, + } + + const content: LocalizedText = { + de: `${intro.de}\n\n${sections.join('\n\n')}`, + en: `${intro.en}\n\n${sections.join('\n\n')}`, + } + + return { + id: 'cookies', + order: 8, + title, + content, + dataPointIds: cookieDataPoints.map((dp) => dp.id), + isRequired: true, + isGenerated: true, + } +} + +export function generateChangesSection( + version: string, + date: Date, + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '9. Aenderungen dieser Datenschutzerklaerung', + en: '9. Changes to this Privacy Policy', + } + + const formattedDate = formatDate(date, language) + + const content: LocalizedText = { + de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}). + +Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen. + +Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`, + en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}). + +We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services. + +The new privacy policy will then apply for your next visit.`, + } + + return { + id: 'changes', + order: 9, + title, + content, + dataPointIds: [], + isRequired: true, + isGenerated: false, + } +} + +// ============================================================================= +// MAIN GENERATOR FUNCTIONS +// ============================================================================= + +export function generatePrivacyPolicySections( + dataPoints: DataPoint[], + companyInfo: CompanyInfo, + language: SupportedLanguage, + version: string = '1.0.0' +): PrivacyPolicySection[] { + const now = new Date() + + const sections: PrivacyPolicySection[] = [ + generateControllerSection(companyInfo, language), + generateDataCollectionSection(dataPoints, language), + generatePurposesSection(dataPoints, language), + generateLegalBasisSection(dataPoints, language), + generateRecipientsSection(dataPoints, language), + generateRetentionSection(dataPoints, RETENTION_MATRIX, language), + ] + + const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language) + if (specialCategoriesSection) { + sections.push(specialCategoriesSection) + } + + sections.push( + generateRightsSection(language), + generateCookiesSection(dataPoints, language), + generateChangesSection(version, now, language) + ) + + sections.forEach((section, index) => { + section.order = index + 1 + const titleDe = section.title.de + const titleEn = section.title.en + if (titleDe.match(/^\d+[a-z]?\./)) { + section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`) + } + if (titleEn.match(/^\d+[a-z]?\./)) { + section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`) + } + }) + + return sections +} + +export function generatePrivacyPolicy( + tenantId: string, + dataPoints: DataPoint[], + companyInfo: CompanyInfo, + language: SupportedLanguage, + format: ExportFormat = 'HTML' +): GeneratedPrivacyPolicy { + const version = '1.0.0' + const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version) + const content = renderPrivacyPolicy(sections, language, format) + + return { + id: `privacy-policy-${tenantId}-${Date.now()}`, + tenantId, + language, + sections, + companyInfo, + generatedAt: new Date(), + version, + format, + content, + } +} + +// ============================================================================= +// RENDERERS +// ============================================================================= + +function renderPrivacyPolicy( + sections: PrivacyPolicySection[], + language: SupportedLanguage, + format: ExportFormat +): string { + switch (format) { + case 'HTML': + return renderAsHTML(sections, language) + case 'MARKDOWN': + return renderAsMarkdown(sections, language) + default: + return renderAsMarkdown(sections, language) + } +} + +export function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string { + const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy' + + const sectionsHTML = sections + .map((section) => { + const content = t(section.content, language) + .replace(/\n\n/g, '

') + .replace(/\n/g, '
') + .replace(/### (.+)/g, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/- (.+)(?:
|$)/g, '
  • $1
  • ') + + return ` +
    +

    ${t(section.title, language)}

    +

    ${content}

    +
    + ` + }) + .join('\n') + + return ` + + + + + ${title} + + + +

    ${title}

    + ${sectionsHTML} + +` +} + +export function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string { + const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy' + + const sectionsMarkdown = sections + .map((section) => { + return `## ${t(section.title, language)}\n\n${t(section.content, language)}` + }) + .join('\n\n---\n\n') + + return `# ${title}\n\n${sectionsMarkdown}` +} diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts new file mode 100644 index 0000000..689dda2 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts @@ -0,0 +1,559 @@ +/** + * Privacy Policy Section Generators + * + * Generiert die 9 Abschnitte der Datenschutzerklaerung (DSI) + * aus dem Datenpunktkatalog. + */ + +import { + DataPoint, + DataPointCategory, + CompanyInfo, + PrivacyPolicySection, + SupportedLanguage, + LocalizedText, + RetentionMatrixEntry, + LegalBasis, + CATEGORY_METADATA, + LEGAL_BASIS_INFO, + RETENTION_PERIOD_INFO, +} from '../types' + +// ============================================================================= +// KONSTANTEN +// ============================================================================= + +const ALL_CATEGORIES: DataPointCategory[] = [ + 'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT', + 'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION', + 'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA', + 'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA', + 'AI_DATA', 'SECURITY', +] + +const ALL_LEGAL_BASES: LegalBasis[] = [ + 'CONTRACT', 'CONSENT', 'EXPLICIT_CONSENT', 'LEGITIMATE_INTEREST', + 'LEGAL_OBLIGATION', 'VITAL_INTERESTS', 'PUBLIC_INTEREST', +] + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function t(text: LocalizedText, language: SupportedLanguage): string { + return text[language] +} + +function groupByCategory(dataPoints: DataPoint[]): Map { + const grouped = new Map() + for (const dp of dataPoints) { + const existing = grouped.get(dp.category) || [] + grouped.set(dp.category, [...existing, dp]) + } + return grouped +} + +function groupByLegalBasis(dataPoints: DataPoint[]): Map { + const grouped = new Map() + for (const dp of dataPoints) { + const existing = grouped.get(dp.legalBasis) || [] + grouped.set(dp.legalBasis, [...existing, dp]) + } + return grouped +} + +function extractThirdParties(dataPoints: DataPoint[]): string[] { + const thirdParties = new Set() + for (const dp of dataPoints) { + for (const recipient of dp.thirdPartyRecipients) { + thirdParties.add(recipient) + } + } + return Array.from(thirdParties).sort() +} + +export function formatDate(date: Date, language: SupportedLanguage): string { + return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) +} + +// ============================================================================= +// SECTION GENERATORS +// ============================================================================= + +export function generateControllerSection( + companyInfo: CompanyInfo, + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '1. Verantwortlicher', + en: '1. Data Controller', + } + + const dpoSection = companyInfo.dpoName + ? language === 'de' + ? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}` + : `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}` + : '' + + const content: LocalizedText = { + de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist: + +**${companyInfo.name}** +${companyInfo.address} +${companyInfo.postalCode} ${companyInfo.city} +${companyInfo.country} + +E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`, + en: `The controller responsible for data processing on this website is: + +**${companyInfo.name}** +${companyInfo.address} +${companyInfo.postalCode} ${companyInfo.city} +${companyInfo.country} + +Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`, + } + + return { + id: 'controller', + order: 1, + title, + content, + dataPointIds: [], + isRequired: true, + isGenerated: false, + } +} + +export function generateDataCollectionSection( + dataPoints: DataPoint[], + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '2. Erhobene personenbezogene Daten', + en: '2. Personal Data We Collect', + } + + const grouped = groupByCategory(dataPoints) + const sections: string[] = [] + const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA') + + for (const category of ALL_CATEGORIES) { + const categoryData = grouped.get(category) + if (!categoryData || categoryData.length === 0) continue + + const categoryMeta = CATEGORY_METADATA[category] + if (!categoryMeta) continue + + const categoryTitle = t(categoryMeta.name, language) + + let categoryNote = '' + if (category === 'HEALTH_DATA') { + categoryNote = language === 'de' + ? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.` + : `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.` + } else if (category === 'EMPLOYEE_DATA') { + categoryNote = language === 'de' + ? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.` + : `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).` + } else if (category === 'AI_DATA') { + categoryNote = language === 'de' + ? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.` + : `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.` + } + + const dataList = categoryData + .map((dp) => { + const specialTag = dp.isSpecialCategory + ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*') + : '' + return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}` + }) + .join('\n') + + sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`) + } + + const intro: LocalizedText = { + de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:', + en: 'We collect and process the following personal data:', + } + + const specialCategoryNote: LocalizedText = hasSpecialCategoryData + ? { + de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.', + en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.', + } + : { de: '', en: '' } + + const content: LocalizedText = { + de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`, + en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`, + } + + return { + id: 'data-collection', + order: 2, + title, + content, + dataPointIds: dataPoints.map((dp) => dp.id), + isRequired: true, + isGenerated: true, + } +} + +export function generatePurposesSection( + dataPoints: DataPoint[], + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '3. Zwecke der Datenverarbeitung', + en: '3. Purposes of Data Processing', + } + + const purposes = new Map() + for (const dp of dataPoints) { + const purpose = t(dp.purpose, language) + const existing = purposes.get(purpose) || [] + purposes.set(purpose, [...existing, dp]) + } + + const purposeList = Array.from(purposes.entries()) + .map(([purpose, dps]) => { + const dataNames = dps.map((dp) => t(dp.name, language)).join(', ') + return `- **${purpose}**\n Betroffene Daten: ${dataNames}` + }) + .join('\n\n') + + const content: LocalizedText = { + de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`, + en: `We process your personal data for the following purposes:\n\n${purposeList}`, + } + + return { + id: 'purposes', + order: 3, + title, + content, + dataPointIds: dataPoints.map((dp) => dp.id), + isRequired: true, + isGenerated: true, + } +} + +export function generateLegalBasisSection( + dataPoints: DataPoint[], + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '4. Rechtsgrundlagen der Verarbeitung', + en: '4. Legal Basis for Processing', + } + + const grouped = groupByLegalBasis(dataPoints) + const sections: string[] = [] + + for (const basis of ALL_LEGAL_BASES) { + const basisData = grouped.get(basis) + if (!basisData || basisData.length === 0) continue + + const basisInfo = LEGAL_BASIS_INFO[basis] + if (!basisInfo) continue + + const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})` + const basisDesc = t(basisInfo.description, language) + + let additionalWarning = '' + if (basis === 'EXPLICIT_CONSENT') { + additionalWarning = language === 'de' + ? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.` + : `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.` + } + + const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data' + const dataList = basisData + .map((dp) => { + const specialTag = dp.isSpecialCategory + ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*') + : '' + return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}` + }) + .join('\n') + + sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`) + } + + const content: LocalizedText = { + de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`, + en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`, + } + + return { + id: 'legal-basis', + order: 4, + title, + content, + dataPointIds: dataPoints.map((dp) => dp.id), + isRequired: true, + isGenerated: true, + } +} + +export function generateRecipientsSection( + dataPoints: DataPoint[], + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '5. Empfaenger und Datenweitergabe', + en: '5. Recipients and Data Sharing', + } + + const thirdParties = extractThirdParties(dataPoints) + + if (thirdParties.length === 0) { + const content: LocalizedText = { + de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.', + en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.', + } + return { + id: 'recipients', + order: 5, + title, + content, + dataPointIds: [], + isRequired: true, + isGenerated: false, + } + } + + const recipientDetails = new Map() + for (const dp of dataPoints) { + for (const recipient of dp.thirdPartyRecipients) { + const existing = recipientDetails.get(recipient) || [] + recipientDetails.set(recipient, [...existing, dp]) + } + } + + const recipientList = Array.from(recipientDetails.entries()) + .map(([recipient, dps]) => { + const dataNames = dps.map((dp) => t(dp.name, language)).join(', ') + return `- **${recipient}**: ${dataNames}` + }) + .join('\n') + + const content: LocalizedText = { + de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`, + en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`, + } + + return { + id: 'recipients', + order: 5, + title, + content, + dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id), + isRequired: true, + isGenerated: true, + } +} + +export function generateRetentionSection( + dataPoints: DataPoint[], + retentionMatrix: RetentionMatrixEntry[], + language: SupportedLanguage +): PrivacyPolicySection { + const title: LocalizedText = { + de: '6. Speicherdauer', + en: '6. Data Retention', + } + + const grouped = groupByCategory(dataPoints) + const sections: string[] = [] + + for (const entry of retentionMatrix) { + const categoryData = grouped.get(entry.category) + if (!categoryData || categoryData.length === 0) continue + + const categoryName = t(entry.categoryName, language) + const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language) + + const dataRetention = categoryData + .map((dp) => { + const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language) + return `- ${t(dp.name, language)}: ${period}` + }) + .join('\n') + + sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`) + } + + const content: LocalizedText = { + de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`, + en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`, + } + + return { + id: 'retention', + order: 6, + title, + content, + dataPointIds: dataPoints.map((dp) => dp.id), + isRequired: true, + isGenerated: true, + } +} + +export function generateSpecialCategoriesSection( + dataPoints: DataPoint[], + language: SupportedLanguage +): PrivacyPolicySection | null { + const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA') + + if (specialCategoryDataPoints.length === 0) { + return null + } + + const title: LocalizedText = { + de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)', + en: '6a. Special Categories of Personal Data (Art. 9 GDPR)', + } + + const dataList = specialCategoryDataPoints + .map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`) + .join('\n') + + const content: LocalizedText = { + de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen: + +${dataList} + +### Ihre ausdrueckliche Einwilligung + +Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO. + +### Ihre Rechte bei Art. 9 Daten + +- Sie koennen Ihre Einwilligung **jederzeit widerrufen** +- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung +- Bei Widerruf werden Ihre Daten unverzueglich geloescht +- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung** + +### Besondere Schutzmassnahmen + +Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert: +- Ende-zu-Ende-Verschluesselung +- Strenge Zugriffskontrolle (Need-to-Know-Prinzip) +- Audit-Logging aller Zugriffe +- Regelmaessige Datenschutz-Folgenabschaetzungen`, + en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes: + +${dataList} + +### Your Explicit Consent + +Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR. + +### Your Rights Regarding Art. 9 Data + +- You can **withdraw your consent at any time** +- Withdrawal does not affect the lawfulness of previous processing +- Upon withdrawal, your data will be deleted immediately +- You have the right to **access, rectification, and erasure** + +### Special Protection Measures + +For this sensitive data, we have implemented special technical and organizational measures: +- End-to-end encryption +- Strict access control (need-to-know principle) +- Audit logging of all access +- Regular data protection impact assessments`, + } + + return { + id: 'special-categories', + order: 6.5, + title, + content, + dataPointIds: specialCategoryDataPoints.map((dp) => dp.id), + isRequired: false, + isGenerated: true, + } +} + +export function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection { + const title: LocalizedText = { + de: '7. Ihre Rechte als betroffene Person', + en: '7. Your Rights as a Data Subject', + } + + const content: LocalizedText = { + de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten: + +### Auskunftsrecht (Art. 15 DSGVO) +Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen. + +### Recht auf Berichtigung (Art. 16 DSGVO) +Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen. + +### Recht auf Loeschung (Art. 17 DSGVO) +Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen. + +### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO) +Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen. + +### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO) +Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten. + +### Widerspruchsrecht (Art. 21 DSGVO) +Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht. + +### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO) +Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt. + +### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO) +Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren. + +**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`, + en: `You have the following rights regarding your personal data: + +### Right of Access (Art. 15 GDPR) +You have the right to request information about the personal data we process about you. + +### Right to Rectification (Art. 16 GDPR) +You have the right to request the correction of inaccurate data or the completion of incomplete data. + +### Right to Erasure (Art. 17 GDPR) +You have the right to request the deletion of your personal data, unless statutory retention obligations apply. + +### Right to Restriction of Processing (Art. 18 GDPR) +You have the right to request the restriction of processing of your data. + +### Right to Data Portability (Art. 20 GDPR) +You have the right to receive your data in a structured, commonly used, and machine-readable format. + +### Right to Object (Art. 21 GDPR) +You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest. + +### Right to Withdraw Consent (Art. 7(3) GDPR) +You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected. + +### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR) +You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data. + +**To exercise your rights, please contact us using the contact details provided above.**`, + } + + return { + id: 'rights', + order: 7, + title, + content, + dataPointIds: [], + isRequired: true, + isGenerated: false, + } +} diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts index 83fbeb8..0e859ec 100644 --- a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts +++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy.ts @@ -1,954 +1,11 @@ /** - * Privacy Policy Generator + * Privacy Policy Generator — barrel re-export * - * Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog. - * Die DSI wird aus 9 Abschnitten generiert: - * - * 1. Verantwortlicher (companyInfo) - * 2. Erhobene Daten (dataPoints nach Kategorie) - * 3. Verarbeitungszwecke (dataPoints.purpose) - * 4. Rechtsgrundlagen (dataPoints.legalBasis) - * 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients) - * 6. Speicherdauer (retentionMatrix) - * 7. Betroffenenrechte (statischer Text + Links) - * 8. Cookies (cookieCategory-basiert) - * 9. Aenderungen (statischer Text + Versionierung) + * Split into: + * - privacy-policy-sections.ts (section generators 1-7) + * - privacy-policy-renderers.ts (sections 8-9, renderers, main generator) */ -import { - DataPoint, - DataPointCategory, - CompanyInfo, - PrivacyPolicySection, - GeneratedPrivacyPolicy, - SupportedLanguage, - ExportFormat, - LocalizedText, - RetentionMatrixEntry, - LegalBasis, - CATEGORY_METADATA, - LEGAL_BASIS_INFO, - RETENTION_PERIOD_INFO, - ARTICLE_9_WARNING, -} from '../types' -import { RETENTION_MATRIX } from '../catalog/loader' - -// ============================================================================= -// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge -// ============================================================================= - -const ALL_CATEGORIES: DataPointCategory[] = [ - 'MASTER_DATA', // A - 'CONTACT_DATA', // B - 'AUTHENTICATION', // C - 'CONSENT', // D - 'COMMUNICATION', // E - 'PAYMENT', // F - 'USAGE_DATA', // G - 'LOCATION', // H - 'DEVICE_DATA', // I - 'MARKETING', // J - 'ANALYTICS', // K - 'SOCIAL_MEDIA', // L - 'HEALTH_DATA', // M - Art. 9 DSGVO - 'EMPLOYEE_DATA', // N - BDSG § 26 - 'CONTRACT_DATA', // O - 'LOG_DATA', // P - 'AI_DATA', // Q - AI Act - 'SECURITY', // R -] - -// Alle Rechtsgrundlagen in der richtigen Reihenfolge -const ALL_LEGAL_BASES: LegalBasis[] = [ - 'CONTRACT', - 'CONSENT', - 'EXPLICIT_CONSENT', - 'LEGITIMATE_INTEREST', - 'LEGAL_OBLIGATION', - 'VITAL_INTERESTS', - 'PUBLIC_INTEREST', -] - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -/** - * Holt den lokalisierten Text - */ -function t(text: LocalizedText, language: SupportedLanguage): string { - return text[language] -} - -/** - * Gruppiert Datenpunkte nach Kategorie - */ -function groupByCategory(dataPoints: DataPoint[]): Map { - const grouped = new Map() - for (const dp of dataPoints) { - const existing = grouped.get(dp.category) || [] - grouped.set(dp.category, [...existing, dp]) - } - return grouped -} - -/** - * Gruppiert Datenpunkte nach Rechtsgrundlage - */ -function groupByLegalBasis(dataPoints: DataPoint[]): Map { - const grouped = new Map() - for (const dp of dataPoints) { - const existing = grouped.get(dp.legalBasis) || [] - grouped.set(dp.legalBasis, [...existing, dp]) - } - return grouped -} - -/** - * Extrahiert alle einzigartigen Drittanbieter - */ -function extractThirdParties(dataPoints: DataPoint[]): string[] { - const thirdParties = new Set() - for (const dp of dataPoints) { - for (const recipient of dp.thirdPartyRecipients) { - thirdParties.add(recipient) - } - } - return Array.from(thirdParties).sort() -} - -/** - * Formatiert ein Datum fuer die Anzeige - */ -function formatDate(date: Date, language: SupportedLanguage): string { - return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }) -} - -// ============================================================================= -// SECTION GENERATORS -// ============================================================================= - -/** - * Abschnitt 1: Verantwortlicher - */ -function generateControllerSection( - companyInfo: CompanyInfo, - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '1. Verantwortlicher', - en: '1. Data Controller', - } - - const dpoSection = companyInfo.dpoName - ? language === 'de' - ? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}` - : `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}` - : '' - - const content: LocalizedText = { - de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist: - -**${companyInfo.name}** -${companyInfo.address} -${companyInfo.postalCode} ${companyInfo.city} -${companyInfo.country} - -E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`, - en: `The controller responsible for data processing on this website is: - -**${companyInfo.name}** -${companyInfo.address} -${companyInfo.postalCode} ${companyInfo.city} -${companyInfo.country} - -Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`, - } - - return { - id: 'controller', - order: 1, - title, - content, - dataPointIds: [], - isRequired: true, - isGenerated: false, - } -} - -/** - * Abschnitt 2: Erhobene Daten (18 Kategorien) - */ -function generateDataCollectionSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '2. Erhobene personenbezogene Daten', - en: '2. Personal Data We Collect', - } - - const grouped = groupByCategory(dataPoints) - const sections: string[] = [] - - // Prüfe ob Art. 9 Daten enthalten sind - const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA') - - for (const category of ALL_CATEGORIES) { - const categoryData = grouped.get(category) - if (!categoryData || categoryData.length === 0) continue - - const categoryMeta = CATEGORY_METADATA[category] - if (!categoryMeta) continue - - const categoryTitle = t(categoryMeta.name, language) - - // Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten) - let categoryNote = '' - if (category === 'HEALTH_DATA') { - categoryNote = language === 'de' - ? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.` - : `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.` - } else if (category === 'EMPLOYEE_DATA') { - categoryNote = language === 'de' - ? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.` - : `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).` - } else if (category === 'AI_DATA') { - categoryNote = language === 'de' - ? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.` - : `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.` - } - - const dataList = categoryData - .map((dp) => { - const specialTag = dp.isSpecialCategory - ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*') - : '' - return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}` - }) - .join('\n') - - sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`) - } - - const intro: LocalizedText = { - de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:', - en: 'We collect and process the following personal data:', - } - - // Zusätzlicher Hinweis für Art. 9 Daten - const specialCategoryNote: LocalizedText = hasSpecialCategoryData - ? { - de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.', - en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.', - } - : { de: '', en: '' } - - const content: LocalizedText = { - de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`, - en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`, - } - - return { - id: 'data-collection', - order: 2, - title, - content, - dataPointIds: dataPoints.map((dp) => dp.id), - isRequired: true, - isGenerated: true, - } -} - -/** - * Abschnitt 3: Verarbeitungszwecke - */ -function generatePurposesSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '3. Zwecke der Datenverarbeitung', - en: '3. Purposes of Data Processing', - } - - // Gruppiere nach Zweck (unique purposes) - const purposes = new Map() - for (const dp of dataPoints) { - const purpose = t(dp.purpose, language) - const existing = purposes.get(purpose) || [] - purposes.set(purpose, [...existing, dp]) - } - - const purposeList = Array.from(purposes.entries()) - .map(([purpose, dps]) => { - const dataNames = dps.map((dp) => t(dp.name, language)).join(', ') - return `- **${purpose}**\n Betroffene Daten: ${dataNames}` - }) - .join('\n\n') - - const content: LocalizedText = { - de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`, - en: `We process your personal data for the following purposes:\n\n${purposeList}`, - } - - return { - id: 'purposes', - order: 3, - title, - content, - dataPointIds: dataPoints.map((dp) => dp.id), - isRequired: true, - isGenerated: true, - } -} - -/** - * Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen) - */ -function generateLegalBasisSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '4. Rechtsgrundlagen der Verarbeitung', - en: '4. Legal Basis for Processing', - } - - const grouped = groupByLegalBasis(dataPoints) - const sections: string[] = [] - - // Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge - for (const basis of ALL_LEGAL_BASES) { - const basisData = grouped.get(basis) - if (!basisData || basisData.length === 0) continue - - const basisInfo = LEGAL_BASIS_INFO[basis] - if (!basisInfo) continue - - const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})` - const basisDesc = t(basisInfo.description, language) - - // Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen - let additionalWarning = '' - if (basis === 'EXPLICIT_CONSENT') { - additionalWarning = language === 'de' - ? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.` - : `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.` - } - - const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data' - const dataList = basisData - .map((dp) => { - const specialTag = dp.isSpecialCategory - ? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*') - : '' - return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}` - }) - .join('\n') - - sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`) - } - - const content: LocalizedText = { - de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`, - en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`, - } - - return { - id: 'legal-basis', - order: 4, - title, - content, - dataPointIds: dataPoints.map((dp) => dp.id), - isRequired: true, - isGenerated: true, - } -} - -/** - * Abschnitt 5: Empfaenger / Dritte - */ -function generateRecipientsSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '5. Empfaenger und Datenweitergabe', - en: '5. Recipients and Data Sharing', - } - - const thirdParties = extractThirdParties(dataPoints) - - if (thirdParties.length === 0) { - const content: LocalizedText = { - de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.', - en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.', - } - return { - id: 'recipients', - order: 5, - title, - content, - dataPointIds: [], - isRequired: true, - isGenerated: false, - } - } - - // Gruppiere nach Drittanbieter - const recipientDetails = new Map() - for (const dp of dataPoints) { - for (const recipient of dp.thirdPartyRecipients) { - const existing = recipientDetails.get(recipient) || [] - recipientDetails.set(recipient, [...existing, dp]) - } - } - - const recipientList = Array.from(recipientDetails.entries()) - .map(([recipient, dps]) => { - const dataNames = dps.map((dp) => t(dp.name, language)).join(', ') - return `- **${recipient}**: ${dataNames}` - }) - .join('\n') - - const content: LocalizedText = { - de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`, - en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`, - } - - return { - id: 'recipients', - order: 5, - title, - content, - dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id), - isRequired: true, - isGenerated: true, - } -} - -/** - * Abschnitt 6: Speicherdauer - */ -function generateRetentionSection( - dataPoints: DataPoint[], - retentionMatrix: RetentionMatrixEntry[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '6. Speicherdauer', - en: '6. Data Retention', - } - - const grouped = groupByCategory(dataPoints) - const sections: string[] = [] - - for (const entry of retentionMatrix) { - const categoryData = grouped.get(entry.category) - if (!categoryData || categoryData.length === 0) continue - - const categoryName = t(entry.categoryName, language) - const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language) - - const dataRetention = categoryData - .map((dp) => { - const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language) - return `- ${t(dp.name, language)}: ${period}` - }) - .join('\n') - - sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`) - } - - const content: LocalizedText = { - de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`, - en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`, - } - - return { - id: 'retention', - order: 6, - title, - content, - dataPointIds: dataPoints.map((dp) => dp.id), - isRequired: true, - isGenerated: true, - } -} - -/** - * Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO) - * Wird nur generiert, wenn Art. 9 Daten vorhanden sind - */ -function generateSpecialCategoriesSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection | null { - // Filtere Art. 9 Datenpunkte - const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA') - - if (specialCategoryDataPoints.length === 0) { - return null - } - - const title: LocalizedText = { - de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)', - en: '6a. Special Categories of Personal Data (Art. 9 GDPR)', - } - - const dataList = specialCategoryDataPoints - .map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`) - .join('\n') - - const content: LocalizedText = { - de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen: - -${dataList} - -### Ihre ausdrueckliche Einwilligung - -Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO. - -### Ihre Rechte bei Art. 9 Daten - -- Sie koennen Ihre Einwilligung **jederzeit widerrufen** -- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung -- Bei Widerruf werden Ihre Daten unverzueglich geloescht -- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung** - -### Besondere Schutzmassnahmen - -Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert: -- Ende-zu-Ende-Verschluesselung -- Strenge Zugriffskontrolle (Need-to-Know-Prinzip) -- Audit-Logging aller Zugriffe -- Regelmaessige Datenschutz-Folgenabschaetzungen`, - en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes: - -${dataList} - -### Your Explicit Consent - -Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR. - -### Your Rights Regarding Art. 9 Data - -- You can **withdraw your consent at any time** -- Withdrawal does not affect the lawfulness of previous processing -- Upon withdrawal, your data will be deleted immediately -- You have the right to **access, rectification, and erasure** - -### Special Protection Measures - -For this sensitive data, we have implemented special technical and organizational measures: -- End-to-end encryption -- Strict access control (need-to-know principle) -- Audit logging of all access -- Regular data protection impact assessments`, - } - - return { - id: 'special-categories', - order: 6.5, // Zwischen Speicherdauer (6) und Rechte (7) - title, - content, - dataPointIds: specialCategoryDataPoints.map((dp) => dp.id), - isRequired: false, - isGenerated: true, - } -} - -/** - * Abschnitt 7: Betroffenenrechte - */ -function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection { - const title: LocalizedText = { - de: '7. Ihre Rechte als betroffene Person', - en: '7. Your Rights as a Data Subject', - } - - const content: LocalizedText = { - de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten: - -### Auskunftsrecht (Art. 15 DSGVO) -Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen. - -### Recht auf Berichtigung (Art. 16 DSGVO) -Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen. - -### Recht auf Loeschung (Art. 17 DSGVO) -Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen. - -### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO) -Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen. - -### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO) -Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten. - -### Widerspruchsrecht (Art. 21 DSGVO) -Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht. - -### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO) -Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt. - -### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO) -Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren. - -**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`, - en: `You have the following rights regarding your personal data: - -### Right of Access (Art. 15 GDPR) -You have the right to request information about the personal data we process about you. - -### Right to Rectification (Art. 16 GDPR) -You have the right to request the correction of inaccurate data or the completion of incomplete data. - -### Right to Erasure (Art. 17 GDPR) -You have the right to request the deletion of your personal data, unless statutory retention obligations apply. - -### Right to Restriction of Processing (Art. 18 GDPR) -You have the right to request the restriction of processing of your data. - -### Right to Data Portability (Art. 20 GDPR) -You have the right to receive your data in a structured, commonly used, and machine-readable format. - -### Right to Object (Art. 21 GDPR) -You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest. - -### Right to Withdraw Consent (Art. 7(3) GDPR) -You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected. - -### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR) -You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data. - -**To exercise your rights, please contact us using the contact details provided above.**`, - } - - return { - id: 'rights', - order: 7, - title, - content, - dataPointIds: [], - isRequired: true, - isGenerated: false, - } -} - -/** - * Abschnitt 8: Cookies - */ -function generateCookiesSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '8. Cookies und aehnliche Technologien', - en: '8. Cookies and Similar Technologies', - } - - // Filtere Datenpunkte mit Cookie-Kategorie - const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null) - - if (cookieDataPoints.length === 0) { - const content: LocalizedText = { - de: 'Wir verwenden auf dieser Website keine Cookies.', - en: 'We do not use cookies on this website.', - } - return { - id: 'cookies', - order: 8, - title, - content, - dataPointIds: [], - isRequired: false, - isGenerated: false, - } - } - - // Gruppiere nach Cookie-Kategorie - const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL') - const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE') - const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION') - const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA') - - const sections: string[] = [] - - if (essential.length > 0) { - const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') - sections.push( - language === 'de' - ? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}` - : `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}` - ) - } - - if (performance.length > 0) { - const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') - sections.push( - language === 'de' - ? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}` - : `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}` - ) - } - - if (personalization.length > 0) { - const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') - sections.push( - language === 'de' - ? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}` - : `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}` - ) - } - - if (externalMedia.length > 0) { - const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n') - sections.push( - language === 'de' - ? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}` - : `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}` - ) - } - - const intro: LocalizedText = { - de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`, - en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`, - } - - const content: LocalizedText = { - de: `${intro.de}\n\n${sections.join('\n\n')}`, - en: `${intro.en}\n\n${sections.join('\n\n')}`, - } - - return { - id: 'cookies', - order: 8, - title, - content, - dataPointIds: cookieDataPoints.map((dp) => dp.id), - isRequired: true, - isGenerated: true, - } -} - -/** - * Abschnitt 9: Aenderungen - */ -function generateChangesSection( - version: string, - date: Date, - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '9. Aenderungen dieser Datenschutzerklaerung', - en: '9. Changes to this Privacy Policy', - } - - const formattedDate = formatDate(date, language) - - const content: LocalizedText = { - de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}). - -Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen. - -Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`, - en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}). - -We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services. - -The new privacy policy will then apply for your next visit.`, - } - - return { - id: 'changes', - order: 9, - title, - content, - dataPointIds: [], - isRequired: true, - isGenerated: false, - } -} - -// ============================================================================= -// MAIN GENERATOR FUNCTIONS -// ============================================================================= - -/** - * Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9) - */ -export function generatePrivacyPolicySections( - dataPoints: DataPoint[], - companyInfo: CompanyInfo, - language: SupportedLanguage, - version: string = '1.0.0' -): PrivacyPolicySection[] { - const now = new Date() - - const sections: PrivacyPolicySection[] = [ - generateControllerSection(companyInfo, language), - generateDataCollectionSection(dataPoints, language), - generatePurposesSection(dataPoints, language), - generateLegalBasisSection(dataPoints, language), - generateRecipientsSection(dataPoints, language), - generateRetentionSection(dataPoints, RETENTION_MATRIX, language), - ] - - // Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden - const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language) - if (specialCategoriesSection) { - sections.push(specialCategoriesSection) - } - - sections.push( - generateRightsSection(language), - generateCookiesSection(dataPoints, language), - generateChangesSection(version, now, language) - ) - - // Abschnittsnummern neu vergeben - sections.forEach((section, index) => { - section.order = index + 1 - // Titel-Nummer aktualisieren - const titleDe = section.title.de - const titleEn = section.title.en - if (titleDe.match(/^\d+[a-z]?\./)) { - section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`) - } - if (titleEn.match(/^\d+[a-z]?\./)) { - section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`) - } - }) - - return sections -} - -/** - * Generiert die vollstaendige Privacy Policy - */ -export function generatePrivacyPolicy( - tenantId: string, - dataPoints: DataPoint[], - companyInfo: CompanyInfo, - language: SupportedLanguage, - format: ExportFormat = 'HTML' -): GeneratedPrivacyPolicy { - const version = '1.0.0' - const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version) - - // Generiere den Inhalt - const content = renderPrivacyPolicy(sections, language, format) - - return { - id: `privacy-policy-${tenantId}-${Date.now()}`, - tenantId, - language, - sections, - companyInfo, - generatedAt: new Date(), - version, - format, - content, - } -} - -/** - * Rendert die Privacy Policy im gewuenschten Format - */ -function renderPrivacyPolicy( - sections: PrivacyPolicySection[], - language: SupportedLanguage, - format: ExportFormat -): string { - switch (format) { - case 'HTML': - return renderAsHTML(sections, language) - case 'MARKDOWN': - return renderAsMarkdown(sections, language) - default: - return renderAsMarkdown(sections, language) - } -} - -/** - * Rendert als HTML - */ -function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string { - const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy' - - const sectionsHTML = sections - .map((section) => { - const content = t(section.content, language) - .replace(/\n\n/g, '

    ') - .replace(/\n/g, '
    ') - .replace(/### (.+)/g, '

    $1

    ') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/- (.+)(?:
    |$)/g, '
  • $1
  • ') - - return ` -
    -

    ${t(section.title, language)}

    -

    ${content}

    -
    - ` - }) - .join('\n') - - return ` - - - - - ${title} - - - -

    ${title}

    - ${sectionsHTML} - -` -} - -/** - * Rendert als Markdown - */ -function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string { - const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy' - - const sectionsMarkdown = sections - .map((section) => { - return `## ${t(section.title, language)}\n\n${t(section.content, language)}` - }) - .join('\n\n---\n\n') - - return `# ${title}\n\n${sectionsMarkdown}` -} - -// ============================================================================= -// EXPORTS -// ============================================================================= - export { generateControllerSection, generateDataCollectionSection, @@ -958,8 +15,13 @@ export { generateRetentionSection, generateSpecialCategoriesSection, generateRightsSection, +} from './privacy-policy-sections' + +export { generateCookiesSection, generateChangesSection, + generatePrivacyPolicySections, + generatePrivacyPolicy, renderAsHTML, renderAsMarkdown, -} +} from './privacy-policy-renderers' diff --git a/admin-compliance/lib/sdk/tom-generator/gap-analysis.ts b/admin-compliance/lib/sdk/tom-generator/gap-analysis.ts new file mode 100644 index 0000000..c27f6a1 --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/gap-analysis.ts @@ -0,0 +1,171 @@ +/** + * TOM Rules Engine — Gap Analysis & Helper Functions + * + * Singleton instance, convenience functions, and gap analysis logic. + */ + +import { + DerivedTOM, + EvidenceDocument, + GapAnalysisResult, + MissingControl, + PartialControl, + MissingEvidence, + RulesEngineResult, + RulesEngineEvaluationContext, +} from './types' +import { getControlById } from './controls/loader' +import { TOMRulesEngine } from './rules-evaluator' + +// ============================================================================= +// SINGLETON INSTANCE +// ============================================================================= + +let rulesEngineInstance: TOMRulesEngine | null = null + +export function getTOMRulesEngine(): TOMRulesEngine { + if (!rulesEngineInstance) { + rulesEngineInstance = new TOMRulesEngine() + } + return rulesEngineInstance +} + +// ============================================================================= +// GAP ANALYSIS +// ============================================================================= + +export function performGapAnalysis( + derivedTOMs: DerivedTOM[], + documents: EvidenceDocument[] +): GapAnalysisResult { + const missingControls: MissingControl[] = [] + const partialControls: PartialControl[] = [] + const missingEvidence: MissingEvidence[] = [] + const recommendations: string[] = [] + + let totalScore = 0 + let totalWeight = 0 + + const applicableTOMs = derivedTOMs.filter( + (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' + ) + + for (const tom of applicableTOMs) { + const control = getControlById(tom.controlId) + if (!control) continue + + const weight = tom.applicability === 'REQUIRED' ? 3 : 1 + totalWeight += weight + + if (tom.implementationStatus === 'NOT_IMPLEMENTED') { + missingControls.push({ + controlId: tom.controlId, + reason: `${control.name.de} ist nicht implementiert`, + priority: control.priority, + }) + } else if (tom.implementationStatus === 'PARTIAL') { + partialControls.push({ + controlId: tom.controlId, + missingAspects: tom.evidenceGaps, + }) + totalScore += weight * 0.5 + } else { + totalScore += weight + } + + const linkedEvidenceIds = tom.linkedEvidence + const requiredEvidence = control.evidenceRequirements + const providedEvidence = documents.filter((doc) => + linkedEvidenceIds.includes(doc.id) + ) + + if (providedEvidence.length < requiredEvidence.length) { + const missing = requiredEvidence.filter( + (req) => + !providedEvidence.some( + (doc) => + doc.documentType === 'POLICY' || + doc.documentType === 'CERTIFICATE' || + doc.originalName.toLowerCase().includes(req.toLowerCase()) + ) + ) + + if (missing.length > 0) { + missingEvidence.push({ + controlId: tom.controlId, + requiredEvidence: missing, + }) + } + } + } + + const overallScore = + totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0 + + if (missingControls.length > 0) { + const criticalMissing = missingControls.filter((mc) => mc.priority === 'CRITICAL') + if (criticalMissing.length > 0) { + recommendations.push( + `${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.` + ) + } + } + + if (partialControls.length > 0) { + recommendations.push( + `${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollstaendigen Sie die Implementierung.` + ) + } + + if (missingEvidence.length > 0) { + recommendations.push( + `Fuer ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.` + ) + } + + if (overallScore >= 80) { + recommendations.push( + 'Ihr TOM-Compliance-Score ist gut. Fuehren Sie regelmaessige Ueberpruefungen durch.' + ) + } else if (overallScore >= 50) { + recommendations.push( + 'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Luecken.' + ) + } else { + recommendations.push( + 'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Ueberarbeitung der Massnahmen wird empfohlen.' + ) + } + + return { + overallScore, + missingControls, + partialControls, + missingEvidence, + recommendations, + generatedAt: new Date(), + } +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +export function evaluateControlsForContext( + context: RulesEngineEvaluationContext +): RulesEngineResult[] { + return getTOMRulesEngine().evaluateControls(context) +} + +export function deriveTOMsForContext( + context: RulesEngineEvaluationContext +): DerivedTOM[] { + return getTOMRulesEngine().deriveAllTOMs(context) +} + +export function performQuickGapAnalysis( + derivedTOMs: DerivedTOM[], + documents: EvidenceDocument[] +): GapAnalysisResult { + return performGapAnalysis(derivedTOMs, documents) +} diff --git a/admin-compliance/lib/sdk/tom-generator/rules-engine.ts b/admin-compliance/lib/sdk/tom-generator/rules-engine.ts index b1eb99b..05a47ba 100644 --- a/admin-compliance/lib/sdk/tom-generator/rules-engine.ts +++ b/admin-compliance/lib/sdk/tom-generator/rules-engine.ts @@ -1,560 +1,17 @@ -// ============================================================================= -// TOM Rules Engine -// Evaluates control applicability based on company context -// ============================================================================= - -import { - ControlLibraryEntry, - ApplicabilityCondition, - ControlApplicability, - RulesEngineResult, - RulesEngineEvaluationContext, - DerivedTOM, - EvidenceDocument, - GapAnalysisResult, - MissingControl, - PartialControl, - MissingEvidence, - ConditionOperator, -} from './types' -import { getAllControls, getControlById } from './controls/loader' - -// ============================================================================= -// RULES ENGINE CLASS -// ============================================================================= - -export class TOMRulesEngine { - private controls: ControlLibraryEntry[] - - constructor() { - this.controls = getAllControls() - } - - /** - * Evaluate all controls against the current context - */ - evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] { - return this.controls.map((control) => this.evaluateControl(control, context)) - } - - /** - * Evaluate a single control against the context - */ - evaluateControl( - control: ControlLibraryEntry, - context: RulesEngineEvaluationContext - ): RulesEngineResult { - // Sort conditions by priority (highest first) - const sortedConditions = [...control.applicabilityConditions].sort( - (a, b) => b.priority - a.priority - ) - - // Evaluate conditions in priority order - for (const condition of sortedConditions) { - const matches = this.evaluateCondition(condition, context) - if (matches) { - return { - controlId: control.id, - applicability: condition.result, - reason: this.formatConditionReason(condition, context), - matchedCondition: condition, - } - } - } - - // No condition matched, use default applicability - return { - controlId: control.id, - applicability: control.defaultApplicability, - reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfüllt)', - } - } - - /** - * Evaluate a single condition - */ - private evaluateCondition( - condition: ApplicabilityCondition, - context: RulesEngineEvaluationContext - ): boolean { - const value = this.getFieldValue(condition.field, context) - - if (value === undefined || value === null) { - return false - } - - return this.evaluateOperator(condition.operator, value, condition.value) - } - - /** - * Get a nested field value from the context - */ - private getFieldValue( - fieldPath: string, - context: RulesEngineEvaluationContext - ): unknown { - const parts = fieldPath.split('.') - let current: unknown = context - - for (const part of parts) { - if (current === null || current === undefined) { - return undefined - } - if (typeof current === 'object') { - current = (current as Record)[part] - } else { - return undefined - } - } - - return current - } - - /** - * Evaluate an operator with given values - */ - private evaluateOperator( - operator: ConditionOperator, - actualValue: unknown, - expectedValue: unknown - ): boolean { - switch (operator) { - case 'EQUALS': - return actualValue === expectedValue - - case 'NOT_EQUALS': - return actualValue !== expectedValue - - case 'CONTAINS': - if (Array.isArray(actualValue)) { - return actualValue.includes(expectedValue) - } - if (typeof actualValue === 'string' && typeof expectedValue === 'string') { - return actualValue.includes(expectedValue) - } - return false - - case 'GREATER_THAN': - if (typeof actualValue === 'number' && typeof expectedValue === 'number') { - return actualValue > expectedValue - } - return false - - case 'IN': - if (Array.isArray(expectedValue)) { - return expectedValue.includes(actualValue) - } - return false - - default: - return false - } - } - - /** - * Format a human-readable reason for the condition match - */ - private formatConditionReason( - condition: ApplicabilityCondition, - context: RulesEngineEvaluationContext - ): string { - const fieldValue = this.getFieldValue(condition.field, context) - const fieldLabel = this.getFieldLabel(condition.field) - - switch (condition.operator) { - case 'EQUALS': - return `${fieldLabel} ist "${this.formatValue(fieldValue)}"` - - case 'NOT_EQUALS': - return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"` - - case 'CONTAINS': - return `${fieldLabel} enthält "${this.formatValue(condition.value)}"` - - case 'GREATER_THAN': - return `${fieldLabel} ist größer als ${this.formatValue(condition.value)}` - - case 'IN': - return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]` - - default: - return `Bedingung erfüllt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}` - } - } - - /** - * Get a human-readable label for a field path - */ - private getFieldLabel(fieldPath: string): string { - const labels: Record = { - 'companyProfile.role': 'Unternehmensrolle', - 'companyProfile.size': 'Unternehmensgröße', - 'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien', - 'dataProfile.processesMinors': 'Verarbeitung von Minderjährigen-Daten', - 'dataProfile.dataVolume': 'Datenvolumen', - 'dataProfile.thirdCountryTransfers': 'Drittlandübermittlungen', - 'architectureProfile.hostingModel': 'Hosting-Modell', - 'architectureProfile.hostingLocation': 'Hosting-Standort', - 'architectureProfile.multiTenancy': 'Mandantentrennung', - 'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter', - 'architectureProfile.encryptionAtRest': 'Verschlüsselung ruhender Daten', - 'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung', - 'securityProfile.hasSSO': 'Single Sign-On', - 'securityProfile.hasPAM': 'Privileged Access Management', - 'riskProfile.protectionLevel': 'Schutzbedarf', - 'riskProfile.dsfaRequired': 'DSFA erforderlich', - 'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit', - 'riskProfile.ciaAssessment.integrity': 'Integrität', - 'riskProfile.ciaAssessment.availability': 'Verfügbarkeit', - } - - return labels[fieldPath] || fieldPath - } - - /** - * Format a value for display - */ - private formatValue(value: unknown): string { - if (value === true) return 'Ja' - if (value === false) return 'Nein' - if (value === null || value === undefined) return 'nicht gesetzt' - if (Array.isArray(value)) return value.join(', ') - return String(value) - } - - /** - * Derive all TOMs based on the current context - */ - deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { - const results = this.evaluateControls(context) - - return results.map((result) => { - const control = getControlById(result.controlId) - if (!control) { - throw new Error(`Control not found: ${result.controlId}`) - } - - return { - id: `derived-${result.controlId}`, - controlId: result.controlId, - name: control.name.de, - description: control.description.de, - applicability: result.applicability, - applicabilityReason: result.reason, - implementationStatus: 'NOT_IMPLEMENTED', - responsiblePerson: null, - responsibleDepartment: null, - implementationDate: null, - reviewDate: null, - linkedEvidence: [], - evidenceGaps: [...control.evidenceRequirements], - aiGeneratedDescription: null, - aiRecommendations: [], - } - }) - } - - /** - * Get only required and recommended TOMs - */ - getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { - const allTOMs = this.deriveAllTOMs(context) - return allTOMs.filter( - (tom) => - tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' - ) - } - - /** - * Get only required TOMs - */ - getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { - const allTOMs = this.deriveAllTOMs(context) - return allTOMs.filter((tom) => tom.applicability === 'REQUIRED') - } - - /** - * Perform gap analysis on derived TOMs and evidence - */ - performGapAnalysis( - derivedTOMs: DerivedTOM[], - documents: EvidenceDocument[] - ): GapAnalysisResult { - const missingControls: MissingControl[] = [] - const partialControls: PartialControl[] = [] - const missingEvidence: MissingEvidence[] = [] - const recommendations: string[] = [] - - let totalScore = 0 - let totalWeight = 0 - - // Analyze each required/recommended TOM - const applicableTOMs = derivedTOMs.filter( - (tom) => - tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' - ) - - for (const tom of applicableTOMs) { - const control = getControlById(tom.controlId) - if (!control) continue - - const weight = tom.applicability === 'REQUIRED' ? 3 : 1 - totalWeight += weight - - // Check implementation status - if (tom.implementationStatus === 'NOT_IMPLEMENTED') { - missingControls.push({ - controlId: tom.controlId, - reason: `${control.name.de} ist nicht implementiert`, - priority: control.priority, - }) - // Score: 0 for not implemented - } else if (tom.implementationStatus === 'PARTIAL') { - partialControls.push({ - controlId: tom.controlId, - missingAspects: tom.evidenceGaps, - }) - // Score: 50% for partial - totalScore += weight * 0.5 - } else { - // Fully implemented - totalScore += weight - } - - // Check evidence - const linkedEvidenceIds = tom.linkedEvidence - const requiredEvidence = control.evidenceRequirements - const providedEvidence = documents.filter((doc) => - linkedEvidenceIds.includes(doc.id) - ) - - if (providedEvidence.length < requiredEvidence.length) { - const missing = requiredEvidence.filter( - (req) => - !providedEvidence.some( - (doc) => - doc.documentType === 'POLICY' || - doc.documentType === 'CERTIFICATE' || - doc.originalName.toLowerCase().includes(req.toLowerCase()) - ) - ) - - if (missing.length > 0) { - missingEvidence.push({ - controlId: tom.controlId, - requiredEvidence: missing, - }) - } - } - } - - // Calculate overall score as percentage - const overallScore = - totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0 - - // Generate recommendations - if (missingControls.length > 0) { - const criticalMissing = missingControls.filter( - (mc) => mc.priority === 'CRITICAL' - ) - if (criticalMissing.length > 0) { - recommendations.push( - `${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.` - ) - } - } - - if (partialControls.length > 0) { - recommendations.push( - `${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollständigen Sie die Implementierung.` - ) - } - - if (missingEvidence.length > 0) { - recommendations.push( - `Für ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.` - ) - } - - if (overallScore >= 80) { - recommendations.push( - 'Ihr TOM-Compliance-Score ist gut. Führen Sie regelmäßige Überprüfungen durch.' - ) - } else if (overallScore >= 50) { - recommendations.push( - 'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Lücken.' - ) - } else { - recommendations.push( - 'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Überarbeitung der Maßnahmen wird empfohlen.' - ) - } - - return { - overallScore, - missingControls, - partialControls, - missingEvidence, - recommendations, - generatedAt: new Date(), - } - } - - /** - * Get controls by applicability level - */ - getControlsByApplicability( - context: RulesEngineEvaluationContext, - applicability: ControlApplicability - ): ControlLibraryEntry[] { - const results = this.evaluateControls(context) - return results - .filter((r) => r.applicability === applicability) - .map((r) => getControlById(r.controlId)) - .filter((c): c is ControlLibraryEntry => c !== undefined) - } - - /** - * Get summary statistics for the evaluation - */ - getSummaryStatistics(context: RulesEngineEvaluationContext): { - total: number - required: number - recommended: number - optional: number - notApplicable: number - byCategory: Map - } { - const results = this.evaluateControls(context) - - const stats = { - total: results.length, - required: 0, - recommended: 0, - optional: 0, - notApplicable: 0, - byCategory: new Map(), - } - - for (const result of results) { - switch (result.applicability) { - case 'REQUIRED': - stats.required++ - break - case 'RECOMMENDED': - stats.recommended++ - break - case 'OPTIONAL': - stats.optional++ - break - case 'NOT_APPLICABLE': - stats.notApplicable++ - break - } - - // Count by category - const control = getControlById(result.controlId) - if (control) { - const category = control.category - const existing = stats.byCategory.get(category) || { - required: 0, - recommended: 0, - } - - if (result.applicability === 'REQUIRED') { - existing.required++ - } else if (result.applicability === 'RECOMMENDED') { - existing.recommended++ - } - - stats.byCategory.set(category, existing) - } - } - - return stats - } - - /** - * Check if a specific control is applicable - */ - isControlApplicable( - controlId: string, - context: RulesEngineEvaluationContext - ): boolean { - const control = getControlById(controlId) - if (!control) return false - - const result = this.evaluateControl(control, context) - return ( - result.applicability === 'REQUIRED' || - result.applicability === 'RECOMMENDED' - ) - } - - /** - * Get all controls that match a specific tag - */ - getControlsByTagWithApplicability( - tag: string, - context: RulesEngineEvaluationContext - ): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> { - return this.controls - .filter((control) => control.tags.includes(tag)) - .map((control) => ({ - control, - result: this.evaluateControl(control, context), - })) - } - - /** - * Reload controls (useful if the control library is updated) - */ - reloadControls(): void { - this.controls = getAllControls() - } -} - -// ============================================================================= -// SINGLETON INSTANCE -// ============================================================================= - -let rulesEngineInstance: TOMRulesEngine | null = null - -export function getTOMRulesEngine(): TOMRulesEngine { - if (!rulesEngineInstance) { - rulesEngineInstance = new TOMRulesEngine() - } - return rulesEngineInstance -} - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - /** - * Quick evaluation of controls for a context + * TOM Rules Engine — barrel re-export + * + * Split into: + * - rules-evaluator.ts (TOMRulesEngine class with condition evaluation) + * - gap-analysis.ts (gap analysis, singleton, helper functions) */ -export function evaluateControlsForContext( - context: RulesEngineEvaluationContext -): RulesEngineResult[] { - return getTOMRulesEngine().evaluateControls(context) -} -/** - * Quick derivation of TOMs for a context - */ -export function deriveTOMsForContext( - context: RulesEngineEvaluationContext -): DerivedTOM[] { - return getTOMRulesEngine().deriveAllTOMs(context) -} +export { TOMRulesEngine } from './rules-evaluator' -/** - * Quick gap analysis - */ -export function performQuickGapAnalysis( - derivedTOMs: DerivedTOM[], - documents: EvidenceDocument[] -): GapAnalysisResult { - return getTOMRulesEngine().performGapAnalysis(derivedTOMs, documents) -} +export { + getTOMRulesEngine, + performGapAnalysis, + evaluateControlsForContext, + deriveTOMsForContext, + performQuickGapAnalysis, +} from './gap-analysis' diff --git a/admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts b/admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts new file mode 100644 index 0000000..265b0c1 --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/rules-evaluator.ts @@ -0,0 +1,276 @@ +/** + * TOM Rules Engine — Control Evaluation + * + * Evaluates control applicability based on company context. + * Core engine class with condition evaluation and TOM derivation. + */ + +import { + ControlLibraryEntry, + ApplicabilityCondition, + ControlApplicability, + RulesEngineResult, + RulesEngineEvaluationContext, + DerivedTOM, + EvidenceDocument, + GapAnalysisResult, + ConditionOperator, +} from './types' +import { getAllControls, getControlById } from './controls/loader' + +// ============================================================================= +// RULES ENGINE CLASS — Evaluation Methods +// ============================================================================= + +export class TOMRulesEngine { + private controls: ControlLibraryEntry[] + + constructor() { + this.controls = getAllControls() + } + + evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] { + return this.controls.map((control) => this.evaluateControl(control, context)) + } + + evaluateControl( + control: ControlLibraryEntry, + context: RulesEngineEvaluationContext + ): RulesEngineResult { + const sortedConditions = [...control.applicabilityConditions].sort( + (a, b) => b.priority - a.priority + ) + + for (const condition of sortedConditions) { + const matches = this.evaluateCondition(condition, context) + if (matches) { + return { + controlId: control.id, + applicability: condition.result, + reason: this.formatConditionReason(condition, context), + matchedCondition: condition, + } + } + } + + return { + controlId: control.id, + applicability: control.defaultApplicability, + reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfuellt)', + } + } + + private evaluateCondition( + condition: ApplicabilityCondition, + context: RulesEngineEvaluationContext + ): boolean { + const value = this.getFieldValue(condition.field, context) + if (value === undefined || value === null) return false + return this.evaluateOperator(condition.operator, value, condition.value) + } + + private getFieldValue(fieldPath: string, context: RulesEngineEvaluationContext): unknown { + const parts = fieldPath.split('.') + let current: unknown = context + + for (const part of parts) { + if (current === null || current === undefined) return undefined + if (typeof current === 'object') { + current = (current as Record)[part] + } else { + return undefined + } + } + + return current + } + + private evaluateOperator( + operator: ConditionOperator, + actualValue: unknown, + expectedValue: unknown + ): boolean { + switch (operator) { + case 'EQUALS': + return actualValue === expectedValue + case 'NOT_EQUALS': + return actualValue !== expectedValue + case 'CONTAINS': + if (Array.isArray(actualValue)) return actualValue.includes(expectedValue) + if (typeof actualValue === 'string' && typeof expectedValue === 'string') + return actualValue.includes(expectedValue) + return false + case 'GREATER_THAN': + if (typeof actualValue === 'number' && typeof expectedValue === 'number') + return actualValue > expectedValue + return false + case 'IN': + if (Array.isArray(expectedValue)) return expectedValue.includes(actualValue) + return false + default: + return false + } + } + + private formatConditionReason( + condition: ApplicabilityCondition, + context: RulesEngineEvaluationContext + ): string { + const fieldValue = this.getFieldValue(condition.field, context) + const fieldLabel = this.getFieldLabel(condition.field) + + switch (condition.operator) { + case 'EQUALS': + return `${fieldLabel} ist "${this.formatValue(fieldValue)}"` + case 'NOT_EQUALS': + return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"` + case 'CONTAINS': + return `${fieldLabel} enthaelt "${this.formatValue(condition.value)}"` + case 'GREATER_THAN': + return `${fieldLabel} ist groesser als ${this.formatValue(condition.value)}` + case 'IN': + return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]` + default: + return `Bedingung erfuellt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}` + } + } + + private getFieldLabel(fieldPath: string): string { + const labels: Record = { + 'companyProfile.role': 'Unternehmensrolle', + 'companyProfile.size': 'Unternehmensgroesse', + 'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien', + 'dataProfile.processesMinors': 'Verarbeitung von Minderjaehrigen-Daten', + 'dataProfile.dataVolume': 'Datenvolumen', + 'dataProfile.thirdCountryTransfers': 'Drittlanduebermittlungen', + 'architectureProfile.hostingModel': 'Hosting-Modell', + 'architectureProfile.hostingLocation': 'Hosting-Standort', + 'architectureProfile.multiTenancy': 'Mandantentrennung', + 'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter', + 'architectureProfile.encryptionAtRest': 'Verschluesselung ruhender Daten', + 'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung', + 'securityProfile.hasSSO': 'Single Sign-On', + 'securityProfile.hasPAM': 'Privileged Access Management', + 'riskProfile.protectionLevel': 'Schutzbedarf', + 'riskProfile.dsfaRequired': 'DSFA erforderlich', + 'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit', + 'riskProfile.ciaAssessment.integrity': 'Integritaet', + 'riskProfile.ciaAssessment.availability': 'Verfuegbarkeit', + } + return labels[fieldPath] || fieldPath + } + + private formatValue(value: unknown): string { + if (value === true) return 'Ja' + if (value === false) return 'Nein' + if (value === null || value === undefined) return 'nicht gesetzt' + if (Array.isArray(value)) return value.join(', ') + return String(value) + } + + deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { + const results = this.evaluateControls(context) + + return results.map((result) => { + const control = getControlById(result.controlId) + if (!control) throw new Error(`Control not found: ${result.controlId}`) + + return { + id: `derived-${result.controlId}`, + controlId: result.controlId, + name: control.name.de, + description: control.description.de, + applicability: result.applicability, + applicabilityReason: result.reason, + implementationStatus: 'NOT_IMPLEMENTED', + responsiblePerson: null, + responsibleDepartment: null, + implementationDate: null, + reviewDate: null, + linkedEvidence: [], + evidenceGaps: [...control.evidenceRequirements], + aiGeneratedDescription: null, + aiRecommendations: [], + } + }) + } + + getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { + return this.deriveAllTOMs(context).filter( + (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' + ) + } + + getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { + return this.deriveAllTOMs(context).filter((tom) => tom.applicability === 'REQUIRED') + } + + getControlsByApplicability( + context: RulesEngineEvaluationContext, + applicability: ControlApplicability + ): ControlLibraryEntry[] { + return this.evaluateControls(context) + .filter((r) => r.applicability === applicability) + .map((r) => getControlById(r.controlId)) + .filter((c): c is ControlLibraryEntry => c !== undefined) + } + + getSummaryStatistics(context: RulesEngineEvaluationContext): { + total: number; required: number; recommended: number; optional: number; notApplicable: number + byCategory: Map + } { + const results = this.evaluateControls(context) + const stats = { + total: results.length, required: 0, recommended: 0, optional: 0, notApplicable: 0, + byCategory: new Map(), + } + + for (const result of results) { + switch (result.applicability) { + case 'REQUIRED': stats.required++; break + case 'RECOMMENDED': stats.recommended++; break + case 'OPTIONAL': stats.optional++; break + case 'NOT_APPLICABLE': stats.notApplicable++; break + } + + const control = getControlById(result.controlId) + if (control) { + const existing = stats.byCategory.get(control.category) || { required: 0, recommended: 0 } + if (result.applicability === 'REQUIRED') existing.required++ + else if (result.applicability === 'RECOMMENDED') existing.recommended++ + stats.byCategory.set(control.category, existing) + } + } + + return stats + } + + isControlApplicable(controlId: string, context: RulesEngineEvaluationContext): boolean { + const control = getControlById(controlId) + if (!control) return false + const result = this.evaluateControl(control, context) + return result.applicability === 'REQUIRED' || result.applicability === 'RECOMMENDED' + } + + getControlsByTagWithApplicability( + tag: string, + context: RulesEngineEvaluationContext + ): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> { + return this.controls + .filter((control) => control.tags.includes(tag)) + .map((control) => ({ control, result: this.evaluateControl(control, context) })) + } + + performGapAnalysis( + derivedTOMs: DerivedTOM[], + documents: EvidenceDocument[] + ): GapAnalysisResult { + // Delegate to standalone function to keep this class focused on evaluation + const { performGapAnalysis: doGapAnalysis } = require('./gap-analysis') + return doGapAnalysis(derivedTOMs, documents) + } + + reloadControls(): void { + this.controls = getAllControls() + } +} diff --git a/admin-compliance/lib/sdk/vvt-profiling-data.ts b/admin-compliance/lib/sdk/vvt-profiling-data.ts new file mode 100644 index 0000000..fca78a1 --- /dev/null +++ b/admin-compliance/lib/sdk/vvt-profiling-data.ts @@ -0,0 +1,286 @@ +/** + * VVT Profiling — Questions, Steps & Department Data Categories + * + * Static data for the profiling questionnaire (~25 questions in 6 steps). + */ + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface ProfilingQuestion { + id: string + step: number + question: string + type: 'single_choice' | 'multi_choice' | 'number' | 'text' | 'boolean' + options?: { value: string; label: string }[] + helpText?: string + triggersTemplates: string[] +} + +export interface ProfilingStep { + step: number + title: string + description: string +} + +export interface ProfilingAnswers { + [questionId: string]: string | string[] | number | boolean +} + +export interface ProfilingResult { + answers: ProfilingAnswers + generatedActivities: import('./vvt-types').VVTActivity[] + coverageScore: number + art30Abs5Exempt: boolean +} + +export interface DepartmentCategory { + id: string + label: string + info: string + isArt9?: boolean + isTypical?: boolean +} + +export interface DepartmentDataConfig { + label: string + icon: string + categories: DepartmentCategory[] +} + +// ============================================================================= +// STEPS +// ============================================================================= + +export const PROFILING_STEPS: ProfilingStep[] = [ + { step: 1, title: 'Organisation', description: 'Grunddaten zu Ihrem Unternehmen' }, + { step: 2, title: 'Geschaeftsbereiche', description: 'Welche Bereiche sind aktiv?' }, + { step: 3, title: 'Systeme & Tools', description: 'Welche IT-Systeme nutzen Sie?' }, + { step: 4, title: 'Datenkategorien', description: 'Welche besonderen Daten verarbeiten Sie?' }, + { step: 5, title: 'Drittlandtransfers', description: 'Transfers ausserhalb der EU/EWR' }, + { step: 6, title: 'Besondere Verarbeitungen', description: 'KI, Scoring, Ueberwachung' }, +] + +// ============================================================================= +// QUESTIONS +// ============================================================================= + +export const PROFILING_QUESTIONS: ProfilingQuestion[] = [ + // === STEP 1: Organisation === + { + id: 'org_industry', step: 1, + question: 'In welcher Branche ist Ihr Unternehmen taetig?', + type: 'single_choice', + options: [ + { value: 'it_software', label: 'IT & Software' }, + { value: 'healthcare', label: 'Gesundheitswesen' }, + { value: 'education', label: 'Bildung & Erziehung' }, + { value: 'finance', label: 'Finanzdienstleistungen' }, + { value: 'retail', label: 'Handel & E-Commerce' }, + { value: 'manufacturing', label: 'Produktion & Industrie' }, + { value: 'consulting', label: 'Beratung & Dienstleistung' }, + { value: 'public', label: 'Oeffentlicher Sektor' }, + { value: 'other', label: 'Sonstige' }, + ], + triggersTemplates: [], + }, + { id: 'org_employees', step: 1, question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?', type: 'number', helpText: 'Relevant fuer Art. 30 Abs. 5 DSGVO (Ausnahme < 250 Mitarbeiter)', triggersTemplates: [] }, + { + id: 'org_locations', step: 1, + question: 'An wie vielen Standorten ist Ihr Unternehmen taetig?', + type: 'single_choice', + options: [ + { value: '1', label: '1 Standort' }, + { value: '2-5', label: '2-5 Standorte' }, + { value: '6-20', label: '6-20 Standorte' }, + { value: '20+', label: 'Mehr als 20 Standorte' }, + ], + triggersTemplates: [], + }, + { + id: 'org_b2b_b2c', step: 1, + question: 'Welches Geschaeftsmodell betreiben Sie?', + type: 'single_choice', + options: [ + { value: 'b2b', label: 'B2B (Geschaeftskunden)' }, + { value: 'b2c', label: 'B2C (Endkunden)' }, + { value: 'both', label: 'Beides (B2B + B2C)' }, + { value: 'b2g', label: 'B2G (Oeffentlicher Sektor)' }, + ], + triggersTemplates: [], + }, + + // === STEP 2: Geschaeftsbereiche === + { id: 'dept_hr', step: 2, question: 'Haben Sie eine Personalabteilung / HR?', type: 'boolean', triggersTemplates: ['hr-mitarbeiterverwaltung', 'hr-gehaltsabrechnung', 'hr-zeiterfassung'] }, + { id: 'dept_recruiting', step: 2, question: 'Betreiben Sie aktives Recruiting / Bewerbermanagement?', type: 'boolean', triggersTemplates: ['hr-bewerbermanagement'] }, + { id: 'dept_finance', step: 2, question: 'Haben Sie eine Finanz-/Buchhaltungsabteilung?', type: 'boolean', triggersTemplates: ['finance-buchhaltung', 'finance-zahlungsverkehr'] }, + { id: 'dept_sales', step: 2, question: 'Haben Sie einen Vertrieb / Kundenverwaltung?', type: 'boolean', triggersTemplates: ['sales-kundenverwaltung', 'sales-vertriebssteuerung'] }, + { id: 'dept_marketing', step: 2, question: 'Betreiben Sie Marketing-Aktivitaeten?', type: 'boolean', triggersTemplates: ['marketing-social-media'] }, + { id: 'dept_support', step: 2, question: 'Haben Sie einen Kundenservice / Support?', type: 'boolean', triggersTemplates: ['support-ticketsystem'] }, + + // === STEP 3: Systeme & Tools === + { id: 'sys_crm', step: 3, question: 'Nutzen Sie ein CRM-System (z.B. Salesforce, HubSpot, Pipedrive)?', type: 'boolean', triggersTemplates: ['sales-kundenverwaltung'] }, + { id: 'sys_website_analytics', step: 3, question: 'Nutzen Sie Website-Analytics (z.B. Matomo, Google Analytics)?', type: 'boolean', triggersTemplates: ['marketing-website-analytics'] }, + { id: 'sys_newsletter', step: 3, question: 'Versenden Sie Newsletter (z.B. Mailchimp, CleverReach)?', type: 'boolean', triggersTemplates: ['marketing-newsletter'] }, + { id: 'sys_video', step: 3, question: 'Nutzen Sie Videokonferenz-Tools (z.B. Zoom, Teams, Jitsi)?', type: 'boolean', triggersTemplates: ['other-videokonferenz'] }, + { id: 'sys_erp', step: 3, question: 'Nutzen Sie ein ERP-System?', type: 'boolean', helpText: 'z.B. SAP, ERPNext, Microsoft Dynamics', triggersTemplates: ['finance-buchhaltung'] }, + { id: 'sys_visitor', step: 3, question: 'Haben Sie ein Besuchermanagement-System?', type: 'boolean', triggersTemplates: ['other-besuchermanagement'] }, + + // === STEP 4: Datenkategorien === + { id: 'data_health', step: 4, question: 'Verarbeiten Sie Gesundheitsdaten (Art. 9 DSGVO)?', type: 'boolean', helpText: 'z.B. Krankmeldungen, Arbeitsmedizin, Gesundheitsversorgung', triggersTemplates: [] }, + { id: 'data_minors', step: 4, question: 'Verarbeiten Sie Daten von Minderjaehrigen?', type: 'boolean', helpText: 'z.B. Schueler, Kinder unter 16 Jahren', triggersTemplates: [] }, + { id: 'data_biometric', step: 4, question: 'Verarbeiten Sie biometrische Daten zur Identifizierung?', type: 'boolean', helpText: 'z.B. Fingerabdruck, Gesichtserkennung, Stimmerkennung', triggersTemplates: [] }, + { id: 'data_criminal', step: 4, question: 'Verarbeiten Sie Daten ueber strafrechtliche Verurteilungen (Art. 10 DSGVO)?', type: 'boolean', helpText: 'z.B. Fuehrungszeugnisse', triggersTemplates: [] }, + + // === STEP 5: Drittlandtransfers === + { id: 'transfer_cloud_us', step: 5, question: 'Nutzen Sie Cloud-Dienste mit Sitz in den USA?', type: 'boolean', helpText: 'z.B. AWS, Azure, Google Cloud, Microsoft 365', triggersTemplates: [] }, + { id: 'transfer_support_non_eu', step: 5, question: 'Haben Sie Support-Mitarbeiter oder Dienstleister ausserhalb der EU?', type: 'boolean', triggersTemplates: [] }, + { id: 'transfer_subprocessor', step: 5, question: 'Nutzen Sie Auftragsverarbeiter mit Unteraufragnehmern in Drittlaendern?', type: 'boolean', triggersTemplates: [] }, + + // === STEP 6: Besondere Verarbeitungen === + { id: 'special_ai', step: 6, question: 'Setzen Sie KI oder automatisierte Entscheidungsfindung ein?', type: 'boolean', helpText: 'z.B. Chatbots, Scoring, Profiling, automatische Bewertungen', triggersTemplates: [] }, + { id: 'special_video_surveillance', step: 6, question: 'Betreiben Sie Videoueberwachung?', type: 'boolean', triggersTemplates: [] }, + { id: 'special_tracking', step: 6, question: 'Betreiben Sie umfangreiches Nutzer-Tracking oder Profiling?', type: 'boolean', helpText: 'z.B. Verhaltensprofiling, Cross-Device-Tracking', triggersTemplates: [] }, +] + +// ============================================================================= +// DEPARTMENT DATA CATEGORIES +// ============================================================================= + +export const DEPARTMENT_DATA_CATEGORIES: Record = { + dept_hr: { + label: 'Personal (HR)', icon: '\u{1F465}', + categories: [ + { id: 'NAME', label: 'Stammdaten', info: 'Vor-/Nachname, Titel, Geschlecht, Geburtsdatum', isTypical: true }, + { id: 'ADDRESS', label: 'Adressdaten', info: 'Wohn-/Melde-/Lieferadresse, Telefon, E-Mail', isTypical: true }, + { id: 'SOCIAL_SECURITY', label: 'Sozialversicherungsnr.', info: 'SV-Nummer fuer Meldungen an DRV, Krankenkasse', isTypical: true }, + { id: 'TAX_ID', label: 'Steuer-ID', info: 'Steueridentifikationsnummer, Steuerklasse, Freibetraege', isTypical: true }, + { id: 'BANK_ACCOUNT', label: 'Bankverbindung', info: 'IBAN, BIC fuer Gehaltsueberweisungen', isTypical: true }, + { id: 'SALARY_DATA', label: 'Gehaltsdaten', info: 'Bruttogehalt, Zulagen, Praemien, VWL, Abzuege', isTypical: true }, + { id: 'EMPLOYMENT_DATA', label: 'Beschaeftigungsdaten', info: 'Vertrag, Eintrittsdatum, Abteilung, Position, Arbeitszeitmodell', isTypical: true }, + { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Krankheitstage (AU-Bescheinigungen), BEM-Daten, Schwerbehinderung', isArt9: true }, + { id: 'RELIGIOUS_BELIEFS', label: 'Religionszugehoerigkeit', info: 'Konfession fuer Kirchensteuer-Abfuehrung', isArt9: true }, + { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Zertifikate, Weiterbildungen, Schulungsnachweise' }, + { id: 'PHOTO_VIDEO', label: 'Mitarbeiterfotos', info: 'Passbilder fuer Ausweise, Intranet-Profilbilder' }, + ] + }, + dept_recruiting: { + label: 'Recruiting / Bewerbermanagement', icon: '\u{1F4CB}', + categories: [ + { id: 'NAME', label: 'Bewerberstammdaten', info: 'Name, Anschrift, Kontaktdaten der Bewerber', isTypical: true }, + { id: 'APPLICATION_DATA', label: 'Bewerbungsunterlagen', info: 'Lebenslauf, Anschreiben, Zeugnisse, Zertifikate', isTypical: true }, + { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Berufserfahrung, Sprachkenntnisse', isTypical: true }, + { id: 'ASSESSMENT_DATA', label: 'Bewertungsdaten', info: 'Interviewnotizen, Assessment-Ergebnisse, Eignungstests' }, + { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Schwerbehinderung (freiwillige Angabe), Eignungsuntersuchung', isArt9: true }, + { id: 'PHOTO_VIDEO', label: 'Bewerbungsfotos', info: 'Bewerbungsfoto (freiwillig), Video-Interview-Aufnahmen' }, + ] + }, + dept_finance: { + label: 'Finanzen & Buchhaltung', icon: '\u{1F4B0}', + categories: [ + { id: 'NAME', label: 'Kunden-/Lieferantenstammdaten', info: 'Firmenname, Ansprechpartner, Kontaktdaten', isTypical: true }, + { id: 'ADDRESS', label: 'Rechnungsadressen', info: 'Rechnungs-/Lieferadressen, USt-IdNr.', isTypical: true }, + { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC, SEPA-Mandate, Zahlungsbedingungen', isTypical: true }, + { id: 'TAX_ID', label: 'Steuer-IDs', info: 'Steuernummer, USt-IdNr., Steueridentifikationsnr.', isTypical: true }, + { id: 'INVOICE_DATA', label: 'Rechnungsdaten', info: 'Rechnungen, Gutschriften, Mahnungen, Zahlungshistorie', isTypical: true }, + { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertragskonditionen, Laufzeiten, Kuendigungsfristen' }, + ] + }, + dept_sales: { + label: 'Vertrieb & CRM', icon: '\u{1F91D}', + categories: [ + { id: 'NAME', label: 'Kontaktdaten', info: 'Name, E-Mail, Telefon, Position der Ansprechpartner', isTypical: true }, + { id: 'ADDRESS', label: 'Firmenadresse', info: 'Firmenanschrift, Standorte', isTypical: true }, + { id: 'CRM_DATA', label: 'CRM-Daten', info: 'Lead-Status, Opportunities, Sales-Pipeline, Umsatzhistorie', isTypical: true }, + { id: 'COMMUNICATION_DATA', label: 'Kommunikation', info: 'E-Mail-Verlauf, Gespraechsnotizen, Meeting-Protokolle', isTypical: true }, + { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Angebote, Bestellungen, Rahmenvertraege' }, + { id: 'PREFERENCE_DATA', label: 'Praeferenzen', info: 'Produktinteressen, Kaufhistorie, Kundensegmentierung' }, + ] + }, + dept_marketing: { + label: 'Marketing', icon: '\u{1F4E2}', + categories: [ + { id: 'EMAIL', label: 'E-Mail-Adressen', info: 'Newsletter-Abonnenten, Kampagnen-Empfaenger', isTypical: true }, + { id: 'TRACKING_DATA', label: 'Tracking-/Analytics-Daten', info: 'IP-Adressen, Cookies, Seitenaufrufe, Klickpfade', isTypical: true }, + { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Cookie-Consent, Newsletter-Opt-in, Widerrufe', isTypical: true }, + { id: 'SOCIAL_MEDIA_DATA', label: 'Social-Media-Daten', info: 'Follower-Interaktionen, Kommentare, Reichweitendaten' }, + { id: 'PREFERENCE_DATA', label: 'Interessenprofil', info: 'Produktinteressen, Segmentierung, A/B-Test-Zuordnungen' }, + { id: 'PHOTO_VIDEO', label: 'Bild-/Videomaterial', info: 'Kundenfotos (Testimonials), Event-Aufnahmen, UGC' }, + ] + }, + dept_support: { + label: 'Kundenservice / Support', icon: '\u{1F3A7}', + categories: [ + { id: 'NAME', label: 'Kundenstammdaten', info: 'Name, E-Mail, Telefon, Kundennummer', isTypical: true }, + { id: 'TICKET_DATA', label: 'Ticket-/Anfragedaten', info: 'Ticketnummer, Betreff, Beschreibung, Status, Prioritaet', isTypical: true }, + { id: 'COMMUNICATION_DATA', label: 'Kommunikationsverlauf', info: 'E-Mails, Chat-Protokolle, Anrufnotizen', isTypical: true }, + { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Produktversion, Lizenz, SLA-Status', isTypical: true }, + { id: 'TECHNICAL_DATA', label: 'Technische Daten', info: 'Systeminfos, Logdateien, Screenshots bei Fehlermeldungen' }, + ] + }, + dept_it: { + label: 'IT / Administration', icon: '\u{1F4BB}', + categories: [ + { id: 'USER_ACCOUNTS', label: 'Benutzerkonten', info: 'Benutzernamen, Passwort-Hashes, Rollen, Berechtigungen', isTypical: true }, + { id: 'LOG_DATA', label: 'Log-/Protokolldaten', info: 'System-Logs, Zugriffsprotokolle, Fehlerprotokolle, IP-Adressen', isTypical: true }, + { id: 'DEVICE_DATA', label: 'Geraetedaten', info: 'Inventar, Seriennummern, MAC-Adressen, zugewiesene Geraete', isTypical: true }, + { id: 'NETWORK_DATA', label: 'Netzwerkdaten', info: 'IP-Adressen, VPN-Verbindungen, Firewall-Logs', isTypical: true }, + { id: 'EMAIL_DATA', label: 'E-Mail-/Kommunikation', info: 'E-Mail-Konten, Verteiler, Archivierung', isTypical: true }, + { id: 'BACKUP_DATA', label: 'Backup-Daten', info: 'Sicherungskopien mit personenbezogenen Inhalten' }, + { id: 'MONITORING_DATA', label: 'Monitoring-Daten', info: 'Systemueberwachung, Performance-Metriken mit Nutzerbezug' }, + ] + }, + dept_recht: { + label: 'Recht / Compliance', icon: '\u{2696}\u{FE0F}', + categories: [ + { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertraege, NDAs, AVVs, Rahmenvereinbarungen', isTypical: true }, + { id: 'NAME', label: 'Ansprechpartner', info: 'Namen, Kontaktdaten von Vertragspartnern und Anwaelten', isTypical: true }, + { id: 'COMPLIANCE_DATA', label: 'Compliance-Daten', info: 'Datenschutzanfragen, Meldungen, Audit-Ergebnisse', isTypical: true }, + { id: 'INCIDENT_DATA', label: 'Vorfallsdaten', info: 'Datenschutzvorfaelle, Beschwerden, Meldungen an Aufsichtsbehoerden' }, + { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Consent-Nachweise, Widerrufe, Opt-in/Opt-out-Protokolle' }, + { id: 'CRIMINAL_DATA', label: 'Strafrechtliche Daten', info: 'Fuehrungszeugnisse, Compliance-Pruefungen (Art. 10 DSGVO)', isArt9: true }, + ] + }, + dept_produktion: { + label: 'Produktion / Fertigung', icon: '\u{1F3ED}', + categories: [ + { id: 'EMPLOYMENT_DATA', label: 'Schichtplaene', info: 'Schichtzuordnung, Arbeitszeiten, Anwesenheitslisten', isTypical: true }, + { id: 'NAME', label: 'Mitarbeiterstammdaten', info: 'Name, Personalnummer, Qualifikation, Maschinenberechtigungen', isTypical: true }, + { id: 'HEALTH_DATA', label: 'Arbeitsschutzdaten', info: 'Arbeitsmedizinische Vorsorge, Unfallmeldungen, Gefahrstoff-Expositionen', isArt9: true }, + { id: 'ACCESS_DATA', label: 'Zugangsdaten', info: 'Zutrittskontrolle, Badge-Protokolle, Bereichsberechtigungen', isTypical: true }, + { id: 'QUALITY_DATA', label: 'Qualitaetsdaten', info: 'Pruefprotokolle mit Pruefernamen, Fehlerberichte' }, + { id: 'PHOTO_VIDEO', label: 'Bild-/Videodaten', info: 'Kameraueberwachung in Produktionsbereichen' }, + ] + }, + dept_logistik: { + label: 'Logistik / Versand', icon: '\u{1F69A}', + categories: [ + { id: 'NAME', label: 'Empfaengerdaten', info: 'Name, Lieferadresse, Telefon fuer Zustellung', isTypical: true }, + { id: 'ADDRESS', label: 'Versandadressen', info: 'Liefer-/Abholadressen, Paketshop-Zuordnung', isTypical: true }, + { id: 'TRACKING_DATA', label: 'Sendungsverfolgung', info: 'Tracking-Nummern, Zustellstatus, Lieferzeitfenster', isTypical: true }, + { id: 'DRIVER_DATA', label: 'Fahrerdaten', info: 'Fahrerlaubnis, Touren, GPS-Standortdaten', isTypical: true }, + { id: 'CUSTOMS_DATA', label: 'Zolldaten', info: 'Zollerklaerungen, EORI-Nummern bei internationalem Versand' }, + ] + }, + dept_einkauf: { + label: 'Einkauf / Beschaffung', icon: '\u{1F6D2}', + categories: [ + { id: 'NAME', label: 'Lieferantenkontakte', info: 'Ansprechpartner, E-Mail, Telefon der Lieferanten', isTypical: true }, + { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Rahmenvertraege, Bestellungen, Konditionen, Laufzeiten', isTypical: true }, + { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC der Lieferanten fuer Zahlungsabwicklung', isTypical: true }, + { id: 'TAX_ID', label: 'Steuer-IDs', info: 'USt-IdNr., Steuernummer der Lieferanten', isTypical: true }, + { id: 'COMPLIANCE_DATA', label: 'Lieferantenbewertung', info: 'Qualitaetsbewertungen, Audit-Ergebnisse, Zertifizierungen' }, + ] + }, + dept_facility: { + label: 'Facility Management', icon: '\u{1F3E2}', + categories: [ + { id: 'ACCESS_DATA', label: 'Zutrittsdaten', info: 'Schluesselausgaben, Badge-Protokolle, Zutrittslisten', isTypical: true }, + { id: 'NAME', label: 'Dienstleisterkontakte', info: 'Reinigung, Wartung, Sicherheitsdienst — Namen und Kontaktdaten', isTypical: true }, + { id: 'PHOTO_VIDEO', label: 'Videoueberwachung', info: 'Kameraaufnahmen in/an Gebaeuden, Parkplaetzen', isTypical: true }, + { id: 'VISITOR_DATA', label: 'Besucherdaten', info: 'Name, Firma, Besuchsgrund, Ein-/Austrittszeiten', isTypical: true }, + { id: 'HEALTH_DATA', label: 'Gesundheits-/Sicherheitsdaten', info: 'Unfallmeldungen, Evakuierungslisten, Ersthelfer-Register', isArt9: true }, + ] + }, +} diff --git a/admin-compliance/lib/sdk/vvt-profiling-logic.ts b/admin-compliance/lib/sdk/vvt-profiling-logic.ts new file mode 100644 index 0000000..a16618a --- /dev/null +++ b/admin-compliance/lib/sdk/vvt-profiling-logic.ts @@ -0,0 +1,187 @@ +/** + * VVT Profiling — Generator Logic, Enrichment & Helpers + * + * Generates baseline VVT activities from profiling answers. + */ + +import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog' +import { generateVVTId } from './vvt-types' +import type { VVTActivity } from './vvt-types' +import { + PROFILING_QUESTIONS, + type ProfilingAnswers, + type ProfilingResult, +} from './vvt-profiling-data' + +// ============================================================================= +// GENERATOR LOGIC +// ============================================================================= + +export function generateActivities(answers: ProfilingAnswers): ProfilingResult { + const triggeredIds = new Set() + + for (const question of PROFILING_QUESTIONS) { + const answer = answers[question.id] + if (!answer) continue + + if (question.type === 'boolean' && answer === true) { + question.triggersTemplates.forEach(id => triggeredIds.add(id)) + } + } + + // Always add IT baseline templates + triggeredIds.add('it-systemadministration') + triggeredIds.add('it-backup') + triggeredIds.add('it-logging') + triggeredIds.add('it-iam') + + const existingIds: string[] = [] + const activities: VVTActivity[] = [] + + for (const templateId of triggeredIds) { + const template = VVT_BASELINE_CATALOG.find(t => t.templateId === templateId) + if (!template) continue + + const vvtId = generateVVTId(existingIds) + existingIds.push(vvtId) + + const activity = templateToActivity(template, vvtId) + enrichActivityFromAnswers(activity, answers) + activities.push(activity) + } + + // Calculate coverage score + const totalFields = activities.length * 12 + let filledFields = 0 + for (const a of activities) { + if (a.name) filledFields++ + if (a.description) filledFields++ + if (a.purposes.length > 0) filledFields++ + if (a.legalBases.length > 0) filledFields++ + if (a.dataSubjectCategories.length > 0) filledFields++ + if (a.personalDataCategories.length > 0) filledFields++ + if (a.recipientCategories.length > 0) filledFields++ + if (a.retentionPeriod.description) filledFields++ + if (a.tomDescription) filledFields++ + if (a.businessFunction !== 'other') filledFields++ + if (a.structuredToms.accessControl.length > 0) filledFields++ + if (a.responsible || a.owner) filledFields++ + } + + const coverageScore = totalFields > 0 ? Math.round((filledFields / totalFields) * 100) : 0 + + const employeeCount = typeof answers.org_employees === 'number' ? answers.org_employees : 0 + const hasSpecialCategories = answers.data_health === true || answers.data_biometric === true || answers.data_criminal === true + const art30Abs5Exempt = employeeCount < 250 && !hasSpecialCategories + + return { + answers, + generatedActivities: activities, + coverageScore, + art30Abs5Exempt, + } +} + +// ============================================================================= +// ENRICHMENT +// ============================================================================= + +function enrichActivityFromAnswers(activity: VVTActivity, answers: ProfilingAnswers): void { + if (answers.transfer_cloud_us === true) { + activity.thirdCountryTransfers.push({ + country: 'US', + recipient: 'Cloud-Dienstleister (USA)', + transferMechanism: 'SCC_PROCESSOR', + additionalMeasures: ['Verschluesselung at-rest', 'Transfer Impact Assessment'], + }) + } + + if (answers.data_health === true) { + if (!activity.personalDataCategories.includes('HEALTH_DATA')) { + if (activity.businessFunction === 'hr') { + activity.personalDataCategories.push('HEALTH_DATA') + if (!activity.legalBases.some(lb => lb.type.startsWith('ART9_'))) { + activity.legalBases.push({ + type: 'ART9_EMPLOYMENT', + description: 'Arbeitsrechtliche Verarbeitung', + reference: 'Art. 9 Abs. 2 lit. b DSGVO', + }) + } + } + } + } + + if (answers.data_minors === true) { + if (!activity.dataSubjectCategories.includes('MINORS')) { + if (activity.businessFunction === 'support' || activity.businessFunction === 'product_engineering') { + activity.dataSubjectCategories.push('MINORS') + } + } + } + + if (answers.special_ai === true || answers.special_video_surveillance === true || answers.special_tracking === true) { + if (answers.special_ai === true && activity.businessFunction === 'product_engineering') { + activity.dpiaRequired = true + } + } +} + +// ============================================================================= +// HELPERS +// ============================================================================= + +export function getQuestionsForStep(step: number) { + return PROFILING_QUESTIONS.filter(q => q.step === step) +} + +export function getStepProgress(answers: ProfilingAnswers, step: number): number { + const questions = getQuestionsForStep(step) + if (questions.length === 0) return 100 + + const answered = questions.filter(q => { + const a = answers[q.id] + return a !== undefined && a !== null && a !== '' + }).length + + return Math.round((answered / questions.length) * 100) +} + +export function getTotalProgress(answers: ProfilingAnswers): number { + const total = PROFILING_QUESTIONS.length + if (total === 0) return 100 + + const answered = PROFILING_QUESTIONS.filter(q => { + const a = answers[q.id] + return a !== undefined && a !== null && a !== '' + }).length + + return Math.round((answered / total) * 100) +} + +// ============================================================================= +// COMPLIANCE SCOPE INTEGRATION +// ============================================================================= + +export function prefillFromScopeAnswers( + scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[] +): ProfilingAnswers { + const { exportToVVTAnswers } = require('./compliance-scope-profiling') + const exported = exportToVVTAnswers(scopeAnswers) as Record + const prefilled: ProfilingAnswers = {} + + for (const [key, value] of Object.entries(exported)) { + if (value !== undefined && value !== null) { + prefilled[key] = value as string | string[] | number | boolean + } + } + + return prefilled +} + +export const SCOPE_PREFILLED_VVT_QUESTIONS = [ + 'org_industry', 'org_employees', 'org_b2b_b2c', + 'dept_hr', 'dept_finance', 'dept_marketing', + 'data_health', 'data_minors', 'data_biometric', 'data_criminal', + 'special_ai', 'special_video_surveillance', 'special_tracking', + 'transfer_cloud_us', 'transfer_subprocessor', 'transfer_support_non_eu', +] diff --git a/admin-compliance/lib/sdk/vvt-profiling.ts b/admin-compliance/lib/sdk/vvt-profiling.ts index 294e211..1e26bcb 100644 --- a/admin-compliance/lib/sdk/vvt-profiling.ts +++ b/admin-compliance/lib/sdk/vvt-profiling.ts @@ -1,659 +1,31 @@ /** - * VVT Profiling — Generator-Fragebogen + * VVT Profiling — barrel re-export * - * ~25 Fragen in 6 Schritten, die auf Basis der Antworten - * Baseline-Verarbeitungstaetigkeiten generieren. + * Split into: + * - vvt-profiling-data.ts (types, steps, questions, department categories) + * - vvt-profiling-logic.ts (generator, enrichment, helpers, scope integration) */ -import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog' -import { generateVVTId } from './vvt-types' -import type { VVTActivity, BusinessFunction } from './vvt-types' +export type { + ProfilingQuestion, + ProfilingStep, + ProfilingAnswers, + ProfilingResult, + DepartmentCategory, + DepartmentDataConfig, +} from './vvt-profiling-data' -// ============================================================================= -// TYPES -// ============================================================================= +export { + PROFILING_STEPS, + PROFILING_QUESTIONS, + DEPARTMENT_DATA_CATEGORIES, +} from './vvt-profiling-data' -export interface ProfilingQuestion { - id: string - step: number - question: string - type: 'single_choice' | 'multi_choice' | 'number' | 'text' | 'boolean' - options?: { value: string; label: string }[] - helpText?: string - triggersTemplates: string[] // Template-IDs that get activated when answered positively -} - -export interface ProfilingStep { - step: number - title: string - description: string -} - -export interface ProfilingAnswers { - [questionId: string]: string | string[] | number | boolean -} - -export interface ProfilingResult { - answers: ProfilingAnswers - generatedActivities: VVTActivity[] - coverageScore: number - art30Abs5Exempt: boolean -} - -// ============================================================================= -// STEPS -// ============================================================================= - -export const PROFILING_STEPS: ProfilingStep[] = [ - { step: 1, title: 'Organisation', description: 'Grunddaten zu Ihrem Unternehmen' }, - { step: 2, title: 'Geschaeftsbereiche', description: 'Welche Bereiche sind aktiv?' }, - { step: 3, title: 'Systeme & Tools', description: 'Welche IT-Systeme nutzen Sie?' }, - { step: 4, title: 'Datenkategorien', description: 'Welche besonderen Daten verarbeiten Sie?' }, - { step: 5, title: 'Drittlandtransfers', description: 'Transfers ausserhalb der EU/EWR' }, - { step: 6, title: 'Besondere Verarbeitungen', description: 'KI, Scoring, Ueberwachung' }, -] - -// ============================================================================= -// QUESTIONS -// ============================================================================= - -export const PROFILING_QUESTIONS: ProfilingQuestion[] = [ - // === STEP 1: Organisation === - { - id: 'org_industry', - step: 1, - question: 'In welcher Branche ist Ihr Unternehmen taetig?', - type: 'single_choice', - options: [ - { value: 'it_software', label: 'IT & Software' }, - { value: 'healthcare', label: 'Gesundheitswesen' }, - { value: 'education', label: 'Bildung & Erziehung' }, - { value: 'finance', label: 'Finanzdienstleistungen' }, - { value: 'retail', label: 'Handel & E-Commerce' }, - { value: 'manufacturing', label: 'Produktion & Industrie' }, - { value: 'consulting', label: 'Beratung & Dienstleistung' }, - { value: 'public', label: 'Oeffentlicher Sektor' }, - { value: 'other', label: 'Sonstige' }, - ], - triggersTemplates: [], - }, - { - id: 'org_employees', - step: 1, - question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?', - type: 'number', - helpText: 'Relevant fuer Art. 30 Abs. 5 DSGVO (Ausnahme < 250 Mitarbeiter)', - triggersTemplates: [], - }, - { - id: 'org_locations', - step: 1, - question: 'An wie vielen Standorten ist Ihr Unternehmen taetig?', - type: 'single_choice', - options: [ - { value: '1', label: '1 Standort' }, - { value: '2-5', label: '2-5 Standorte' }, - { value: '6-20', label: '6-20 Standorte' }, - { value: '20+', label: 'Mehr als 20 Standorte' }, - ], - triggersTemplates: [], - }, - { - id: 'org_b2b_b2c', - step: 1, - question: 'Welches Geschaeftsmodell betreiben Sie?', - type: 'single_choice', - options: [ - { value: 'b2b', label: 'B2B (Geschaeftskunden)' }, - { value: 'b2c', label: 'B2C (Endkunden)' }, - { value: 'both', label: 'Beides (B2B + B2C)' }, - { value: 'b2g', label: 'B2G (Oeffentlicher Sektor)' }, - ], - triggersTemplates: [], - }, - - // === STEP 2: Geschaeftsbereiche === - { - id: 'dept_hr', - step: 2, - question: 'Haben Sie eine Personalabteilung / HR?', - type: 'boolean', - triggersTemplates: ['hr-mitarbeiterverwaltung', 'hr-gehaltsabrechnung', 'hr-zeiterfassung'], - }, - { - id: 'dept_recruiting', - step: 2, - question: 'Betreiben Sie aktives Recruiting / Bewerbermanagement?', - type: 'boolean', - triggersTemplates: ['hr-bewerbermanagement'], - }, - { - id: 'dept_finance', - step: 2, - question: 'Haben Sie eine Finanz-/Buchhaltungsabteilung?', - type: 'boolean', - triggersTemplates: ['finance-buchhaltung', 'finance-zahlungsverkehr'], - }, - { - id: 'dept_sales', - step: 2, - question: 'Haben Sie einen Vertrieb / Kundenverwaltung?', - type: 'boolean', - triggersTemplates: ['sales-kundenverwaltung', 'sales-vertriebssteuerung'], - }, - { - id: 'dept_marketing', - step: 2, - question: 'Betreiben Sie Marketing-Aktivitaeten?', - type: 'boolean', - triggersTemplates: ['marketing-social-media'], - }, - { - id: 'dept_support', - step: 2, - question: 'Haben Sie einen Kundenservice / Support?', - type: 'boolean', - triggersTemplates: ['support-ticketsystem'], - }, - - // === STEP 3: Systeme & Tools === - { - id: 'sys_crm', - step: 3, - question: 'Nutzen Sie ein CRM-System (z.B. Salesforce, HubSpot, Pipedrive)?', - type: 'boolean', - triggersTemplates: ['sales-kundenverwaltung'], - }, - { - id: 'sys_website_analytics', - step: 3, - question: 'Nutzen Sie Website-Analytics (z.B. Matomo, Google Analytics)?', - type: 'boolean', - triggersTemplates: ['marketing-website-analytics'], - }, - { - id: 'sys_newsletter', - step: 3, - question: 'Versenden Sie Newsletter (z.B. Mailchimp, CleverReach)?', - type: 'boolean', - triggersTemplates: ['marketing-newsletter'], - }, - { - id: 'sys_video', - step: 3, - question: 'Nutzen Sie Videokonferenz-Tools (z.B. Zoom, Teams, Jitsi)?', - type: 'boolean', - triggersTemplates: ['other-videokonferenz'], - }, - { - id: 'sys_erp', - step: 3, - question: 'Nutzen Sie ein ERP-System?', - type: 'boolean', - helpText: 'z.B. SAP, ERPNext, Microsoft Dynamics', - triggersTemplates: ['finance-buchhaltung'], - }, - { - id: 'sys_visitor', - step: 3, - question: 'Haben Sie ein Besuchermanagement-System?', - type: 'boolean', - triggersTemplates: ['other-besuchermanagement'], - }, - - // === STEP 4: Datenkategorien === - { - id: 'data_health', - step: 4, - question: 'Verarbeiten Sie Gesundheitsdaten (Art. 9 DSGVO)?', - type: 'boolean', - helpText: 'z.B. Krankmeldungen, Arbeitsmedizin, Gesundheitsversorgung', - triggersTemplates: [], - }, - { - id: 'data_minors', - step: 4, - question: 'Verarbeiten Sie Daten von Minderjaehrigen?', - type: 'boolean', - helpText: 'z.B. Schueler, Kinder unter 16 Jahren', - triggersTemplates: [], - }, - { - id: 'data_biometric', - step: 4, - question: 'Verarbeiten Sie biometrische Daten zur Identifizierung?', - type: 'boolean', - helpText: 'z.B. Fingerabdruck, Gesichtserkennung, Stimmerkennung', - triggersTemplates: [], - }, - { - id: 'data_criminal', - step: 4, - question: 'Verarbeiten Sie Daten ueber strafrechtliche Verurteilungen (Art. 10 DSGVO)?', - type: 'boolean', - helpText: 'z.B. Fuehrungszeugnisse', - triggersTemplates: [], - }, - - // === STEP 5: Drittlandtransfers === - { - id: 'transfer_cloud_us', - step: 5, - question: 'Nutzen Sie Cloud-Dienste mit Sitz in den USA?', - type: 'boolean', - helpText: 'z.B. AWS, Azure, Google Cloud, Microsoft 365', - triggersTemplates: [], - }, - { - id: 'transfer_support_non_eu', - step: 5, - question: 'Haben Sie Support-Mitarbeiter oder Dienstleister ausserhalb der EU?', - type: 'boolean', - triggersTemplates: [], - }, - { - id: 'transfer_subprocessor', - step: 5, - question: 'Nutzen Sie Auftragsverarbeiter mit Unteraufragnehmern in Drittlaendern?', - type: 'boolean', - triggersTemplates: [], - }, - - // === STEP 6: Besondere Verarbeitungen === - { - id: 'special_ai', - step: 6, - question: 'Setzen Sie KI oder automatisierte Entscheidungsfindung ein?', - type: 'boolean', - helpText: 'z.B. Chatbots, Scoring, Profiling, automatische Bewertungen', - triggersTemplates: [], - }, - { - id: 'special_video_surveillance', - step: 6, - question: 'Betreiben Sie Videoueberwachung?', - type: 'boolean', - triggersTemplates: [], - }, - { - id: 'special_tracking', - step: 6, - question: 'Betreiben Sie umfangreiches Nutzer-Tracking oder Profiling?', - type: 'boolean', - helpText: 'z.B. Verhaltensprofiling, Cross-Device-Tracking', - triggersTemplates: [], - }, -] - -// ============================================================================= -// DEPARTMENT DATA CATEGORIES (Aufklappbare Kacheln Step 2) -// ============================================================================= - -export interface DepartmentCategory { - id: string - label: string - info: string - isArt9?: boolean - isTypical?: boolean -} - -export interface DepartmentDataConfig { - label: string - icon: string - categories: DepartmentCategory[] -} - -export const DEPARTMENT_DATA_CATEGORIES: Record = { - dept_hr: { - label: 'Personal (HR)', - icon: '👥', - categories: [ - { id: 'NAME', label: 'Stammdaten', info: 'Vor-/Nachname, Titel, Geschlecht, Geburtsdatum', isTypical: true }, - { id: 'ADDRESS', label: 'Adressdaten', info: 'Wohn-/Melde-/Lieferadresse, Telefon, E-Mail', isTypical: true }, - { id: 'SOCIAL_SECURITY', label: 'Sozialversicherungsnr.', info: 'SV-Nummer fuer Meldungen an DRV, Krankenkasse', isTypical: true }, - { id: 'TAX_ID', label: 'Steuer-ID', info: 'Steueridentifikationsnummer, Steuerklasse, Freibetraege', isTypical: true }, - { id: 'BANK_ACCOUNT', label: 'Bankverbindung', info: 'IBAN, BIC fuer Gehaltsueberweisungen', isTypical: true }, - { id: 'SALARY_DATA', label: 'Gehaltsdaten', info: 'Bruttogehalt, Zulagen, Praemien, VWL, Abzuege', isTypical: true }, - { id: 'EMPLOYMENT_DATA', label: 'Beschaeftigungsdaten', info: 'Vertrag, Eintrittsdatum, Abteilung, Position, Arbeitszeitmodell', isTypical: true }, - { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Krankheitstage (AU-Bescheinigungen), BEM-Daten, Schwerbehinderung', isArt9: true }, - { id: 'RELIGIOUS_BELIEFS', label: 'Religionszugehoerigkeit', info: 'Konfession fuer Kirchensteuer-Abfuehrung', isArt9: true }, - { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Zertifikate, Weiterbildungen, Schulungsnachweise' }, - { id: 'PHOTO_VIDEO', label: 'Mitarbeiterfotos', info: 'Passbilder fuer Ausweise, Intranet-Profilbilder' }, - ] - }, - dept_recruiting: { - label: 'Recruiting / Bewerbermanagement', - icon: '📋', - categories: [ - { id: 'NAME', label: 'Bewerberstammdaten', info: 'Name, Anschrift, Kontaktdaten der Bewerber', isTypical: true }, - { id: 'APPLICATION_DATA', label: 'Bewerbungsunterlagen', info: 'Lebenslauf, Anschreiben, Zeugnisse, Zertifikate', isTypical: true }, - { id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Berufserfahrung, Sprachkenntnisse', isTypical: true }, - { id: 'ASSESSMENT_DATA', label: 'Bewertungsdaten', info: 'Interviewnotizen, Assessment-Ergebnisse, Eignungstests' }, - { id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Schwerbehinderung (freiwillige Angabe), Eignungsuntersuchung', isArt9: true }, - { id: 'PHOTO_VIDEO', label: 'Bewerbungsfotos', info: 'Bewerbungsfoto (freiwillig), Video-Interview-Aufnahmen' }, - ] - }, - dept_finance: { - label: 'Finanzen & Buchhaltung', - icon: '💰', - categories: [ - { id: 'NAME', label: 'Kunden-/Lieferantenstammdaten', info: 'Firmenname, Ansprechpartner, Kontaktdaten', isTypical: true }, - { id: 'ADDRESS', label: 'Rechnungsadressen', info: 'Rechnungs-/Lieferadressen, USt-IdNr.', isTypical: true }, - { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC, SEPA-Mandate, Zahlungsbedingungen', isTypical: true }, - { id: 'TAX_ID', label: 'Steuer-IDs', info: 'Steuernummer, USt-IdNr., Steueridentifikationsnr.', isTypical: true }, - { id: 'INVOICE_DATA', label: 'Rechnungsdaten', info: 'Rechnungen, Gutschriften, Mahnungen, Zahlungshistorie', isTypical: true }, - { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertragskonditionen, Laufzeiten, Kuendigungsfristen' }, - ] - }, - dept_sales: { - label: 'Vertrieb & CRM', - icon: '🤝', - categories: [ - { id: 'NAME', label: 'Kontaktdaten', info: 'Name, E-Mail, Telefon, Position der Ansprechpartner', isTypical: true }, - { id: 'ADDRESS', label: 'Firmenadresse', info: 'Firmenanschrift, Standorte', isTypical: true }, - { id: 'CRM_DATA', label: 'CRM-Daten', info: 'Lead-Status, Opportunities, Sales-Pipeline, Umsatzhistorie', isTypical: true }, - { id: 'COMMUNICATION_DATA', label: 'Kommunikation', info: 'E-Mail-Verlauf, Gespraechsnotizen, Meeting-Protokolle', isTypical: true }, - { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Angebote, Bestellungen, Rahmenvertraege' }, - { id: 'PREFERENCE_DATA', label: 'Praeferenzen', info: 'Produktinteressen, Kaufhistorie, Kundensegmentierung' }, - ] - }, - dept_marketing: { - label: 'Marketing', - icon: '📢', - categories: [ - { id: 'EMAIL', label: 'E-Mail-Adressen', info: 'Newsletter-Abonnenten, Kampagnen-Empfaenger', isTypical: true }, - { id: 'TRACKING_DATA', label: 'Tracking-/Analytics-Daten', info: 'IP-Adressen, Cookies, Seitenaufrufe, Klickpfade', isTypical: true }, - { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Cookie-Consent, Newsletter-Opt-in, Widerrufe', isTypical: true }, - { id: 'SOCIAL_MEDIA_DATA', label: 'Social-Media-Daten', info: 'Follower-Interaktionen, Kommentare, Reichweitendaten' }, - { id: 'PREFERENCE_DATA', label: 'Interessenprofil', info: 'Produktinteressen, Segmentierung, A/B-Test-Zuordnungen' }, - { id: 'PHOTO_VIDEO', label: 'Bild-/Videomaterial', info: 'Kundenfotos (Testimonials), Event-Aufnahmen, UGC' }, - ] - }, - dept_support: { - label: 'Kundenservice / Support', - icon: '🎧', - categories: [ - { id: 'NAME', label: 'Kundenstammdaten', info: 'Name, E-Mail, Telefon, Kundennummer', isTypical: true }, - { id: 'TICKET_DATA', label: 'Ticket-/Anfragedaten', info: 'Ticketnummer, Betreff, Beschreibung, Status, Prioritaet', isTypical: true }, - { id: 'COMMUNICATION_DATA', label: 'Kommunikationsverlauf', info: 'E-Mails, Chat-Protokolle, Anrufnotizen', isTypical: true }, - { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Produktversion, Lizenz, SLA-Status', isTypical: true }, - { id: 'TECHNICAL_DATA', label: 'Technische Daten', info: 'Systeminfos, Logdateien, Screenshots bei Fehlermeldungen' }, - ] - }, - dept_it: { - label: 'IT / Administration', - icon: '💻', - categories: [ - { id: 'USER_ACCOUNTS', label: 'Benutzerkonten', info: 'Benutzernamen, Passwort-Hashes, Rollen, Berechtigungen', isTypical: true }, - { id: 'LOG_DATA', label: 'Log-/Protokolldaten', info: 'System-Logs, Zugriffsprotokolle, Fehlerprotokolle, IP-Adressen', isTypical: true }, - { id: 'DEVICE_DATA', label: 'Geraetedaten', info: 'Inventar, Seriennummern, MAC-Adressen, zugewiesene Geraete', isTypical: true }, - { id: 'NETWORK_DATA', label: 'Netzwerkdaten', info: 'IP-Adressen, VPN-Verbindungen, Firewall-Logs', isTypical: true }, - { id: 'EMAIL_DATA', label: 'E-Mail-/Kommunikation', info: 'E-Mail-Konten, Verteiler, Archivierung', isTypical: true }, - { id: 'BACKUP_DATA', label: 'Backup-Daten', info: 'Sicherungskopien mit personenbezogenen Inhalten' }, - { id: 'MONITORING_DATA', label: 'Monitoring-Daten', info: 'Systemueberwachung, Performance-Metriken mit Nutzerbezug' }, - ] - }, - dept_recht: { - label: 'Recht / Compliance', - icon: '⚖️', - categories: [ - { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertraege, NDAs, AVVs, Rahmenvereinbarungen', isTypical: true }, - { id: 'NAME', label: 'Ansprechpartner', info: 'Namen, Kontaktdaten von Vertragspartnern und Anwaelten', isTypical: true }, - { id: 'COMPLIANCE_DATA', label: 'Compliance-Daten', info: 'Datenschutzanfragen, Meldungen, Audit-Ergebnisse', isTypical: true }, - { id: 'INCIDENT_DATA', label: 'Vorfallsdaten', info: 'Datenschutzvorfaelle, Beschwerden, Meldungen an Aufsichtsbehoerden' }, - { id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Consent-Nachweise, Widerrufe, Opt-in/Opt-out-Protokolle' }, - { id: 'CRIMINAL_DATA', label: 'Strafrechtliche Daten', info: 'Fuehrungszeugnisse, Compliance-Pruefungen (Art. 10 DSGVO)', isArt9: true }, - ] - }, - dept_produktion: { - label: 'Produktion / Fertigung', - icon: '🏭', - categories: [ - { id: 'EMPLOYMENT_DATA', label: 'Schichtplaene', info: 'Schichtzuordnung, Arbeitszeiten, Anwesenheitslisten', isTypical: true }, - { id: 'NAME', label: 'Mitarbeiterstammdaten', info: 'Name, Personalnummer, Qualifikation, Maschinenberechtigungen', isTypical: true }, - { id: 'HEALTH_DATA', label: 'Arbeitsschutzdaten', info: 'Arbeitsmedizinische Vorsorge, Unfallmeldungen, Gefahrstoff-Expositionen', isArt9: true }, - { id: 'ACCESS_DATA', label: 'Zugangsdaten', info: 'Zutrittskontrolle, Badge-Protokolle, Bereichsberechtigungen', isTypical: true }, - { id: 'QUALITY_DATA', label: 'Qualitaetsdaten', info: 'Pruefprotokolle mit Pruefernamen, Fehlerberichte' }, - { id: 'PHOTO_VIDEO', label: 'Bild-/Videodaten', info: 'Kameraueberwachung in Produktionsbereichen' }, - ] - }, - dept_logistik: { - label: 'Logistik / Versand', - icon: '🚚', - categories: [ - { id: 'NAME', label: 'Empfaengerdaten', info: 'Name, Lieferadresse, Telefon fuer Zustellung', isTypical: true }, - { id: 'ADDRESS', label: 'Versandadressen', info: 'Liefer-/Abholadressen, Paketshop-Zuordnung', isTypical: true }, - { id: 'TRACKING_DATA', label: 'Sendungsverfolgung', info: 'Tracking-Nummern, Zustellstatus, Lieferzeitfenster', isTypical: true }, - { id: 'DRIVER_DATA', label: 'Fahrerdaten', info: 'Fahrerlaubnis, Touren, GPS-Standortdaten', isTypical: true }, - { id: 'CUSTOMS_DATA', label: 'Zolldaten', info: 'Zollerklaerungen, EORI-Nummern bei internationalem Versand' }, - ] - }, - dept_einkauf: { - label: 'Einkauf / Beschaffung', - icon: '🛒', - categories: [ - { id: 'NAME', label: 'Lieferantenkontakte', info: 'Ansprechpartner, E-Mail, Telefon der Lieferanten', isTypical: true }, - { id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Rahmenvertraege, Bestellungen, Konditionen, Laufzeiten', isTypical: true }, - { id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC der Lieferanten fuer Zahlungsabwicklung', isTypical: true }, - { id: 'TAX_ID', label: 'Steuer-IDs', info: 'USt-IdNr., Steuernummer der Lieferanten', isTypical: true }, - { id: 'COMPLIANCE_DATA', label: 'Lieferantenbewertung', info: 'Qualitaetsbewertungen, Audit-Ergebnisse, Zertifizierungen' }, - ] - }, - dept_facility: { - label: 'Facility Management', - icon: '🏢', - categories: [ - { id: 'ACCESS_DATA', label: 'Zutrittsdaten', info: 'Schluesselausgaben, Badge-Protokolle, Zutrittslisten', isTypical: true }, - { id: 'NAME', label: 'Dienstleisterkontakte', info: 'Reinigung, Wartung, Sicherheitsdienst — Namen und Kontaktdaten', isTypical: true }, - { id: 'PHOTO_VIDEO', label: 'Videoueberwachung', info: 'Kameraaufnahmen in/an Gebaeuden, Parkplaetzen', isTypical: true }, - { id: 'VISITOR_DATA', label: 'Besucherdaten', info: 'Name, Firma, Besuchsgrund, Ein-/Austrittszeiten', isTypical: true }, - { id: 'HEALTH_DATA', label: 'Gesundheits-/Sicherheitsdaten', info: 'Unfallmeldungen, Evakuierungslisten, Ersthelfer-Register', isArt9: true }, - ] - }, -} - -// ============================================================================= -// GENERATOR LOGIC -// ============================================================================= - -export function generateActivities(answers: ProfilingAnswers): ProfilingResult { - // Collect all triggered template IDs - const triggeredIds = new Set() - - for (const question of PROFILING_QUESTIONS) { - const answer = answers[question.id] - if (!answer) continue - - // Boolean questions: if true, trigger templates - if (question.type === 'boolean' && answer === true) { - question.triggersTemplates.forEach(id => triggeredIds.add(id)) - } - } - - // Always add IT baseline templates (every company needs these) - triggeredIds.add('it-systemadministration') - triggeredIds.add('it-backup') - triggeredIds.add('it-logging') - triggeredIds.add('it-iam') - - // Generate activities from triggered templates - const existingIds: string[] = [] - const activities: VVTActivity[] = [] - - for (const templateId of triggeredIds) { - const template = VVT_BASELINE_CATALOG.find(t => t.templateId === templateId) - if (!template) continue - - const vvtId = generateVVTId(existingIds) - existingIds.push(vvtId) - - const activity = templateToActivity(template, vvtId) - - // Enrich with profiling answers - enrichActivityFromAnswers(activity, answers) - - activities.push(activity) - } - - // Calculate coverage score - const totalFields = activities.length * 12 // 12 key fields per activity - let filledFields = 0 - for (const a of activities) { - if (a.name) filledFields++ - if (a.description) filledFields++ - if (a.purposes.length > 0) filledFields++ - if (a.legalBases.length > 0) filledFields++ - if (a.dataSubjectCategories.length > 0) filledFields++ - if (a.personalDataCategories.length > 0) filledFields++ - if (a.recipientCategories.length > 0) filledFields++ - if (a.retentionPeriod.description) filledFields++ - if (a.tomDescription) filledFields++ - if (a.businessFunction !== 'other') filledFields++ - if (a.structuredToms.accessControl.length > 0) filledFields++ - if (a.responsible || a.owner) filledFields++ - } - - const coverageScore = totalFields > 0 ? Math.round((filledFields / totalFields) * 100) : 0 - - // Art. 30 Abs. 5 check - const employeeCount = typeof answers.org_employees === 'number' ? answers.org_employees : 0 - const hasSpecialCategories = answers.data_health === true || answers.data_biometric === true || answers.data_criminal === true - const art30Abs5Exempt = employeeCount < 250 && !hasSpecialCategories - - return { - answers, - generatedActivities: activities, - coverageScore, - art30Abs5Exempt, - } -} - -// ============================================================================= -// ENRICHMENT -// ============================================================================= - -function enrichActivityFromAnswers(activity: VVTActivity, answers: ProfilingAnswers): void { - // Add third-country transfers if US cloud is used - if (answers.transfer_cloud_us === true) { - activity.thirdCountryTransfers.push({ - country: 'US', - recipient: 'Cloud-Dienstleister (USA)', - transferMechanism: 'SCC_PROCESSOR', - additionalMeasures: ['Verschluesselung at-rest', 'Transfer Impact Assessment'], - }) - } - - // Add special data categories if applicable - if (answers.data_health === true) { - if (!activity.personalDataCategories.includes('HEALTH_DATA')) { - // Only add to HR activities - if (activity.businessFunction === 'hr') { - activity.personalDataCategories.push('HEALTH_DATA') - // Ensure Art. 9 legal basis - if (!activity.legalBases.some(lb => lb.type.startsWith('ART9_'))) { - activity.legalBases.push({ - type: 'ART9_EMPLOYMENT', - description: 'Arbeitsrechtliche Verarbeitung', - reference: 'Art. 9 Abs. 2 lit. b DSGVO', - }) - } - } - } - } - - if (answers.data_minors === true) { - if (!activity.dataSubjectCategories.includes('MINORS')) { - // Add to relevant activities (education, app users) - if (activity.businessFunction === 'support' || activity.businessFunction === 'product_engineering') { - activity.dataSubjectCategories.push('MINORS') - } - } - } - - // Set DPIA required for special processing - if (answers.special_ai === true || answers.special_video_surveillance === true || answers.special_tracking === true) { - if (answers.special_ai === true && activity.businessFunction === 'product_engineering') { - activity.dpiaRequired = true - } - } -} - -// ============================================================================= -// HELPERS -// ============================================================================= - -export function getQuestionsForStep(step: number): ProfilingQuestion[] { - return PROFILING_QUESTIONS.filter(q => q.step === step) -} - -export function getStepProgress(answers: ProfilingAnswers, step: number): number { - const questions = getQuestionsForStep(step) - if (questions.length === 0) return 100 - - const answered = questions.filter(q => { - const a = answers[q.id] - return a !== undefined && a !== null && a !== '' - }).length - - return Math.round((answered / questions.length) * 100) -} - -export function getTotalProgress(answers: ProfilingAnswers): number { - const total = PROFILING_QUESTIONS.length - if (total === 0) return 100 - - const answered = PROFILING_QUESTIONS.filter(q => { - const a = answers[q.id] - return a !== undefined && a !== null && a !== '' - }).length - - return Math.round((answered / total) * 100) -} - -// ============================================================================= -// COMPLIANCE SCOPE INTEGRATION -// ============================================================================= - -/** - * Prefill VVT profiling answers from Compliance Scope Engine answers. - * The Scope Engine acts as the "Single Source of Truth" for organizational questions. - * Redundant questions are auto-filled with a "prefilled" marker. - */ -export function prefillFromScopeAnswers( - scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[] -): ProfilingAnswers { - const { exportToVVTAnswers } = require('./compliance-scope-profiling') - const exported = exportToVVTAnswers(scopeAnswers) as Record - const prefilled: ProfilingAnswers = {} - - for (const [key, value] of Object.entries(exported)) { - if (value !== undefined && value !== null) { - prefilled[key] = value as string | string[] | number | boolean - } - } - - return prefilled -} - -/** - * Get the list of VVT question IDs that are prefilled from Scope answers. - * These questions should show "Aus Scope-Analyse uebernommen" hint. - */ -export const SCOPE_PREFILLED_VVT_QUESTIONS = [ - 'org_industry', - 'org_employees', - 'org_b2b_b2c', - 'dept_hr', - 'dept_finance', - 'dept_marketing', - 'data_health', - 'data_minors', - 'data_biometric', - 'data_criminal', - 'special_ai', - 'special_video_surveillance', - 'special_tracking', - 'transfer_cloud_us', - 'transfer_subprocessor', - 'transfer_support_non_eu', -] +export { + generateActivities, + getQuestionsForStep, + getStepProgress, + getTotalProgress, + prefillFromScopeAnswers, + SCOPE_PREFILLED_VVT_QUESTIONS, +} from './vvt-profiling-logic' diff --git a/admin-compliance/lib/sdk/whistleblower/api-mock-data.ts b/admin-compliance/lib/sdk/whistleblower/api-mock-data.ts new file mode 100644 index 0000000..d2cfccb --- /dev/null +++ b/admin-compliance/lib/sdk/whistleblower/api-mock-data.ts @@ -0,0 +1,187 @@ +/** + * Whistleblower API — Mock Data & SDK Proxy + * + * Fallback mock data for development and SDK proxy function + */ + +import { + WhistleblowerReport, + WhistleblowerStatistics, + ReportCategory, + ReportStatus, + generateAccessKey, +} from './types' +import { + fetchReports, + fetchWhistleblowerStatistics, +} from './api-operations' + +// ============================================================================= +// SDK PROXY FUNCTION +// ============================================================================= + +export async function fetchSDKWhistleblowerList(): Promise<{ + reports: WhistleblowerReport[] + statistics: WhistleblowerStatistics +}> { + try { + const [reportsResponse, statsResponse] = await Promise.all([ + fetchReports(), + fetchWhistleblowerStatistics() + ]) + return { + reports: reportsResponse.reports, + statistics: statsResponse + } + } catch (error) { + console.error('Failed to load Whistleblower data from API, using mock data:', error) + const reports = createMockReports() + const statistics = createMockStatistics() + return { reports, statistics } + } +} + +// ============================================================================= +// MOCK DATA +// ============================================================================= + +export function createMockReports(): WhistleblowerReport[] { + const now = new Date() + + function calcDeadlines(receivedAt: Date): { ack: string; fb: string } { + const ack = new Date(receivedAt) + ack.setDate(ack.getDate() + 7) + const fb = new Date(receivedAt) + fb.setMonth(fb.getMonth() + 3) + return { ack: ack.toISOString(), fb: fb.toISOString() } + } + + const received1 = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000) + const deadlines1 = calcDeadlines(received1) + const received2 = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000) + const deadlines2 = calcDeadlines(received2) + const received3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000) + const deadlines3 = calcDeadlines(received3) + const received4 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000) + const deadlines4 = calcDeadlines(received4) + + return [ + { + id: 'wb-001', referenceNumber: 'WB-2026-000001', accessKey: generateAccessKey(), + category: 'corruption', status: 'new', priority: 'high', + title: 'Unregelmaessigkeiten bei Auftragsvergabe', + description: 'Bei der Vergabe des IT-Rahmenvertrags im November wurden offenbar Angebote eines bestimmten Anbieters bevorzugt. Der zustaendige Abteilungsleiter hat private Verbindungen zum Geschaeftsfuehrer des Anbieters.', + isAnonymous: true, receivedAt: received1.toISOString(), + deadlineAcknowledgment: deadlines1.ack, deadlineFeedback: deadlines1.fb, + measures: [], messages: [], attachments: [], + auditTrail: [{ id: 'audit-001', action: 'report_created', description: 'Meldung ueber Online-Meldeformular eingegangen', performedBy: 'system', performedAt: received1.toISOString() }] + }, + { + id: 'wb-002', referenceNumber: 'WB-2026-000002', accessKey: generateAccessKey(), + category: 'data_protection', status: 'under_review', priority: 'normal', + title: 'Unerlaubte Weitergabe von Kundendaten', + description: 'Ein Mitarbeiter der Vertriebsabteilung gibt regelmaessig Kundenlisten an externe Dienstleister weiter, ohne dass eine Auftragsverarbeitungsvereinbarung vorliegt.', + isAnonymous: false, reporterName: 'Maria Schmidt', reporterEmail: 'maria.schmidt@example.de', + assignedTo: 'DSB Mueller', receivedAt: received2.toISOString(), + acknowledgedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(), + deadlineAcknowledgment: deadlines2.ack, deadlineFeedback: deadlines2.fb, + measures: [], + messages: [ + { id: 'msg-001', reportId: 'wb-002', senderRole: 'ombudsperson', message: 'Vielen Dank fuer Ihre Meldung. Koennen Sie uns mitteilen, welche Dienstleister konkret betroffen sind?', createdAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), isRead: true }, + { id: 'msg-002', reportId: 'wb-002', senderRole: 'reporter', message: 'Es handelt sich um die Firma DataServ GmbH und MarketPro AG. Die Listen werden per unverschluesselter E-Mail versendet.', createdAt: new Date(received2.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), isRead: true }, + ], + attachments: [{ id: 'att-001', fileName: 'email_screenshot_vertrieb.png', fileSize: 245000, mimeType: 'image/png', uploadedAt: received2.toISOString(), uploadedBy: 'reporter' }], + auditTrail: [ + { id: 'audit-002', action: 'report_created', description: 'Meldung per E-Mail eingegangen', performedBy: 'system', performedAt: received2.toISOString() }, + { id: 'audit-003', action: 'acknowledged', description: 'Eingangsbestaetigung an Hinweisgeber versendet', performedBy: 'DSB Mueller', performedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'audit-004', action: 'status_changed', description: 'Status geaendert: Bestaetigt -> In Pruefung', performedBy: 'DSB Mueller', performedAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() }, + ] + }, + { + id: 'wb-003', referenceNumber: 'WB-2026-000003', accessKey: generateAccessKey(), + category: 'product_safety', status: 'investigation', priority: 'critical', + title: 'Fehlende Sicherheitspruefungen bei Produktfreigabe', + description: 'In der Fertigung werden seit Wochen Produkte ohne die vorgeschriebenen Sicherheitspruefungen freigegeben. Pruefprotokolle werden nachtraeglich erstellt, ohne dass tatsaechliche Pruefungen stattfinden.', + isAnonymous: true, assignedTo: 'Qualitaetsbeauftragter Weber', + receivedAt: received3.toISOString(), + acknowledgedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(), + deadlineAcknowledgment: deadlines3.ack, deadlineFeedback: deadlines3.fb, + measures: [ + { id: 'msr-001', reportId: 'wb-003', title: 'Sofortiger Produktionsstopp fuer betroffene Charge', description: 'Produktion der betroffenen Produktlinie stoppen bis Pruefverfahren sichergestellt ist', status: 'completed', responsible: 'Fertigungsleitung', dueDate: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(received3.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'msr-002', reportId: 'wb-003', title: 'Externe Pruefung der Pruefprotokolle', description: 'Unabhaengige Pruefstelle mit der Revision aller Pruefprotokolle der letzten 6 Monate beauftragen', status: 'in_progress', responsible: 'Qualitaetsmanagement', dueDate: new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString() }, + ], + messages: [], + attachments: [{ id: 'att-002', fileName: 'pruefprotokoll_vergleich.pdf', fileSize: 890000, mimeType: 'application/pdf', uploadedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), uploadedBy: 'ombudsperson' }], + auditTrail: [ + { id: 'audit-005', action: 'report_created', description: 'Meldung ueber Online-Meldeformular eingegangen', performedBy: 'system', performedAt: received3.toISOString() }, + { id: 'audit-006', action: 'acknowledged', description: 'Eingangsbestaetigung versendet', performedBy: 'Qualitaetsbeauftragter Weber', performedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'audit-007', action: 'investigation_started', description: 'Formelle Untersuchung eingeleitet', performedBy: 'Qualitaetsbeauftragter Weber', performedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() }, + ] + }, + { + id: 'wb-004', referenceNumber: 'WB-2026-000004', accessKey: generateAccessKey(), + category: 'fraud', status: 'closed', priority: 'high', + title: 'Gefaelschte Reisekostenabrechnungen', + description: 'Ein leitender Mitarbeiter reicht seit ueber einem Jahr gefaelschte Reisekostenabrechnungen ein. Hotelrechnungen werden manipuliert, Taxiquittungen erfunden.', + isAnonymous: false, reporterName: 'Thomas Klein', reporterEmail: 'thomas.klein@example.de', reporterPhone: '+49 170 9876543', + assignedTo: 'Compliance-Abteilung', receivedAt: received4.toISOString(), + acknowledgedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(), + deadlineAcknowledgment: deadlines4.ack, deadlineFeedback: deadlines4.fb, + closedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), + measures: [ + { id: 'msr-003', reportId: 'wb-004', title: 'Interne Revision der Reisekosten', description: 'Pruefung aller Reisekostenabrechnungen des betroffenen Mitarbeiters der letzten 24 Monate', status: 'completed', responsible: 'Interne Revision', dueDate: new Date(received4.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(received4.getTime() + 25 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'msr-004', reportId: 'wb-004', title: 'Arbeitsrechtliche Konsequenzen', description: 'Einleitung arbeitsrechtlicher Schritte nach Bestaetigung des Betrugs', status: 'completed', responsible: 'Personalabteilung', dueDate: new Date(received4.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(received4.getTime() + 55 * 24 * 60 * 60 * 1000).toISOString() }, + ], + messages: [], + attachments: [{ id: 'att-003', fileName: 'vergleich_originalrechnung_einreichung.pdf', fileSize: 567000, mimeType: 'application/pdf', uploadedAt: received4.toISOString(), uploadedBy: 'reporter' }], + auditTrail: [ + { id: 'audit-008', action: 'report_created', description: 'Meldung per Brief eingegangen', performedBy: 'system', performedAt: received4.toISOString() }, + { id: 'audit-009', action: 'acknowledged', description: 'Eingangsbestaetigung versendet', performedBy: 'Compliance-Abteilung', performedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() }, + { id: 'audit-010', action: 'closed', description: 'Fall abgeschlossen - Betrug bestaetigt, arbeitsrechtliche Massnahmen eingeleitet', performedBy: 'Compliance-Abteilung', performedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString() }, + ] + } + ] +} + +export function createMockStatistics(): WhistleblowerStatistics { + const reports = createMockReports() + const now = new Date() + + const byStatus: Record = { + new: 0, acknowledged: 0, under_review: 0, investigation: 0, + measures_taken: 0, closed: 0, rejected: 0 + } + + const byCategory: Record = { + corruption: 0, fraud: 0, data_protection: 0, discrimination: 0, + environment: 0, competition: 0, product_safety: 0, tax_evasion: 0, other: 0 + } + + reports.forEach(r => { + byStatus[r.status]++ + byCategory[r.category]++ + }) + + const closedStatuses: ReportStatus[] = ['closed', 'rejected'] + + const overdueAcknowledgment = reports.filter(r => { + if (r.status !== 'new') return false + return now > new Date(r.deadlineAcknowledgment) + }).length + + const overdueFeedback = reports.filter(r => { + if (closedStatuses.includes(r.status)) return false + return now > new Date(r.deadlineFeedback) + }).length + + return { + totalReports: reports.length, + newReports: byStatus.new, + underReview: byStatus.under_review + byStatus.investigation, + closed: byStatus.closed + byStatus.rejected, + overdueAcknowledgment, + overdueFeedback, + byCategory, + byStatus + } +} diff --git a/admin-compliance/lib/sdk/whistleblower/api-operations.ts b/admin-compliance/lib/sdk/whistleblower/api-operations.ts new file mode 100644 index 0000000..f5363a3 --- /dev/null +++ b/admin-compliance/lib/sdk/whistleblower/api-operations.ts @@ -0,0 +1,306 @@ +/** + * Whistleblower API Client — CRUD, Workflow, Messaging, Attachments, Statistics + * + * API client for Hinweisgeberschutzgesetz (HinSchG) compliant + * Whistleblower/Hinweisgebersystem management + */ + +import { + WhistleblowerReport, + WhistleblowerStatistics, + ReportListResponse, + ReportFilters, + PublicReportSubmission, + ReportUpdateRequest, + AnonymousMessage, + WhistleblowerMeasure, + FileAttachment, +} from './types' + +// ============================================================================= +// CONFIGURATION +// ============================================================================= + +const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093' +const API_TIMEOUT = 30000 + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function getTenantId(): string { + if (typeof window !== 'undefined') { + return localStorage.getItem('bp_tenant_id') || 'default-tenant' + } + return 'default-tenant' +} + +function getAuthHeaders(): HeadersInit { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + 'X-Tenant-ID': getTenantId() + } + + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken') + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + const userId = localStorage.getItem('bp_user_id') + if (userId) { + headers['X-User-ID'] = userId + } + } + + return headers +} + +async function fetchWithTimeout( + url: string, + options: RequestInit = {}, + timeout: number = API_TIMEOUT +): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + ...getAuthHeaders(), + ...options.headers + } + }) + + if (!response.ok) { + const errorBody = await response.text() + let errorMessage = `HTTP ${response.status}: ${response.statusText}` + try { + const errorJson = JSON.parse(errorBody) + errorMessage = errorJson.error || errorJson.message || errorMessage + } catch { + // Keep the HTTP status message + } + throw new Error(errorMessage) + } + + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + return response.json() + } + + return {} as T + } finally { + clearTimeout(timeoutId) + } +} + +// ============================================================================= +// ADMIN CRUD - Reports +// ============================================================================= + +export async function fetchReports(filters?: ReportFilters): Promise { + const params = new URLSearchParams() + + if (filters) { + if (filters.status) { + const statuses = Array.isArray(filters.status) ? filters.status : [filters.status] + statuses.forEach(s => params.append('status', s)) + } + if (filters.category) { + const categories = Array.isArray(filters.category) ? filters.category : [filters.category] + categories.forEach(c => params.append('category', c)) + } + if (filters.priority) params.set('priority', filters.priority) + if (filters.assignedTo) params.set('assignedTo', filters.assignedTo) + if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous)) + if (filters.search) params.set('search', filters.search) + if (filters.dateFrom) params.set('dateFrom', filters.dateFrom) + if (filters.dateTo) params.set('dateTo', filters.dateTo) + } + + const queryString = params.toString() + const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}` + + return fetchWithTimeout(url) +} + +export async function fetchReport(id: string): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}` + ) +} + +export async function updateReport(id: string, update: ReportUpdateRequest): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`, + { method: 'PUT', body: JSON.stringify(update) } + ) +} + +export async function deleteReport(id: string): Promise { + await fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`, + { method: 'DELETE' } + ) +} + +// ============================================================================= +// PUBLIC ENDPOINTS +// ============================================================================= + +export async function submitPublicReport( + data: PublicReportSubmission +): Promise<{ report: WhistleblowerReport; accessKey: string }> { + const response = await fetch( + `${WB_API_BASE}/api/v1/public/whistleblower/submit`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + } + ) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + return response.json() +} + +export async function fetchReportByAccessKey( + accessKey: string +): Promise { + const response = await fetch( + `${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`, + { method: 'GET', headers: { 'Content-Type': 'application/json' } } + ) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + return response.json() +} + +// ============================================================================= +// WORKFLOW ACTIONS +// ============================================================================= + +export async function acknowledgeReport(id: string): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`, + { method: 'POST' } + ) +} + +export async function startInvestigation(id: string): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`, + { method: 'POST' } + ) +} + +export async function addMeasure( + id: string, + measure: Omit +): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`, + { method: 'POST', body: JSON.stringify(measure) } + ) +} + +export async function closeReport( + id: string, + resolution: { reason: string; notes: string } +): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`, + { method: 'POST', body: JSON.stringify(resolution) } + ) +} + +// ============================================================================= +// ANONYMOUS MESSAGING +// ============================================================================= + +export async function sendMessage( + reportId: string, + message: string, + role: 'reporter' | 'ombudsperson' +): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`, + { method: 'POST', body: JSON.stringify({ senderRole: role, message }) } + ) +} + +export async function fetchMessages(reportId: string): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages` + ) +} + +// ============================================================================= +// ATTACHMENTS +// ============================================================================= + +export async function uploadAttachment( + reportId: string, + file: File +): Promise { + const formData = new FormData() + formData.append('file', file) + + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 60000) + + try { + const headers: HeadersInit = { + 'X-Tenant-ID': getTenantId() + } + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken') + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + } + + const response = await fetch( + `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`, + { + method: 'POST', + headers, + body: formData, + signal: controller.signal + } + ) + + if (!response.ok) { + throw new Error(`Upload fehlgeschlagen: ${response.statusText}`) + } + + return response.json() + } finally { + clearTimeout(timeoutId) + } +} + +export async function deleteAttachment(id: string): Promise { + await fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`, + { method: 'DELETE' } + ) +} + +// ============================================================================= +// STATISTICS +// ============================================================================= + +export async function fetchWhistleblowerStatistics(): Promise { + return fetchWithTimeout( + `${WB_API_BASE}/api/v1/admin/whistleblower/statistics` + ) +} diff --git a/admin-compliance/lib/sdk/whistleblower/api.ts b/admin-compliance/lib/sdk/whistleblower/api.ts index 0e07909..09a545a 100644 --- a/admin-compliance/lib/sdk/whistleblower/api.ts +++ b/admin-compliance/lib/sdk/whistleblower/api.ts @@ -1,755 +1,31 @@ /** - * Whistleblower System API Client + * Whistleblower System API Client — barrel re-export * - * API client for Hinweisgeberschutzgesetz (HinSchG) compliant - * Whistleblower/Hinweisgebersystem management - * Connects to the ai-compliance-sdk backend + * Split into: + * - api-operations.ts (CRUD, workflow, messaging, attachments, statistics) + * - api-mock-data.ts (mock data + SDK proxy) */ -import { - WhistleblowerReport, - WhistleblowerStatistics, - ReportListResponse, - ReportFilters, - PublicReportSubmission, - ReportUpdateRequest, - MessageSendRequest, - AnonymousMessage, - WhistleblowerMeasure, - FileAttachment, - ReportCategory, - ReportStatus, - ReportPriority, - generateAccessKey -} from './types' - -// ============================================================================= -// CONFIGURATION -// ============================================================================= - -const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093' -const API_TIMEOUT = 30000 // 30 seconds - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -function getTenantId(): string { - if (typeof window !== 'undefined') { - return localStorage.getItem('bp_tenant_id') || 'default-tenant' - } - return 'default-tenant' -} - -function getAuthHeaders(): HeadersInit { - const headers: HeadersInit = { - 'Content-Type': 'application/json', - 'X-Tenant-ID': getTenantId() - } - - if (typeof window !== 'undefined') { - const token = localStorage.getItem('authToken') - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - const userId = localStorage.getItem('bp_user_id') - if (userId) { - headers['X-User-ID'] = userId - } - } - - return headers -} - -async function fetchWithTimeout( - url: string, - options: RequestInit = {}, - timeout: number = API_TIMEOUT -): Promise { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) - - try { - const response = await fetch(url, { - ...options, - signal: controller.signal, - headers: { - ...getAuthHeaders(), - ...options.headers - } - }) - - if (!response.ok) { - const errorBody = await response.text() - let errorMessage = `HTTP ${response.status}: ${response.statusText}` - try { - const errorJson = JSON.parse(errorBody) - errorMessage = errorJson.error || errorJson.message || errorMessage - } catch { - // Keep the HTTP status message - } - throw new Error(errorMessage) - } - - // Handle empty responses - const contentType = response.headers.get('content-type') - if (contentType && contentType.includes('application/json')) { - return response.json() - } - - return {} as T - } finally { - clearTimeout(timeoutId) - } -} - -// ============================================================================= -// ADMIN CRUD - Reports -// ============================================================================= - -/** - * Alle Meldungen abrufen (Admin) - */ -export async function fetchReports(filters?: ReportFilters): Promise { - const params = new URLSearchParams() - - if (filters) { - if (filters.status) { - const statuses = Array.isArray(filters.status) ? filters.status : [filters.status] - statuses.forEach(s => params.append('status', s)) - } - if (filters.category) { - const categories = Array.isArray(filters.category) ? filters.category : [filters.category] - categories.forEach(c => params.append('category', c)) - } - if (filters.priority) params.set('priority', filters.priority) - if (filters.assignedTo) params.set('assignedTo', filters.assignedTo) - if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous)) - if (filters.search) params.set('search', filters.search) - if (filters.dateFrom) params.set('dateFrom', filters.dateFrom) - if (filters.dateTo) params.set('dateTo', filters.dateTo) - } - - const queryString = params.toString() - const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}` - - return fetchWithTimeout(url) -} - -/** - * Einzelne Meldung abrufen (Admin) - */ -export async function fetchReport(id: string): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}` - ) -} - -/** - * Meldung aktualisieren (Status, Prioritaet, Kategorie, Zuweisung) - */ -export async function updateReport(id: string, update: ReportUpdateRequest): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`, - { - method: 'PUT', - body: JSON.stringify(update) - } - ) -} - -/** - * Meldung loeschen (soft delete) - */ -export async function deleteReport(id: string): Promise { - await fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`, - { - method: 'DELETE' - } - ) -} - -// ============================================================================= -// PUBLIC ENDPOINTS - Kein Auth erforderlich -// ============================================================================= - -/** - * Neue Meldung einreichen (oeffentlich, keine Auth) - */ -export async function submitPublicReport( - data: PublicReportSubmission -): Promise<{ report: WhistleblowerReport; accessKey: string }> { - const response = await fetch( - `${WB_API_BASE}/api/v1/public/whistleblower/submit`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - } - ) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - return response.json() -} - -/** - * Meldung ueber Zugangscode abrufen (oeffentlich, keine Auth) - */ -export async function fetchReportByAccessKey( - accessKey: string -): Promise { - const response = await fetch( - `${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`, - { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - } - ) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - return response.json() -} - -// ============================================================================= -// WORKFLOW ACTIONS -// ============================================================================= - -/** - * Eingangsbestaetigung versenden (HinSchG ss 17 Abs. 1) - */ -export async function acknowledgeReport(id: string): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`, - { - method: 'POST' - } - ) -} - -/** - * Untersuchung starten - */ -export async function startInvestigation(id: string): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`, - { - method: 'POST' - } - ) -} - -/** - * Massnahme zu einer Meldung hinzufuegen - */ -export async function addMeasure( - id: string, - measure: Omit -): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`, - { - method: 'POST', - body: JSON.stringify(measure) - } - ) -} - -/** - * Meldung abschliessen mit Begruendung - */ -export async function closeReport( - id: string, - resolution: { reason: string; notes: string } -): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`, - { - method: 'POST', - body: JSON.stringify(resolution) - } - ) -} - -// ============================================================================= -// ANONYMOUS MESSAGING -// ============================================================================= - -/** - * Nachricht im anonymen Kanal senden - */ -export async function sendMessage( - reportId: string, - message: string, - role: 'reporter' | 'ombudsperson' -): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`, - { - method: 'POST', - body: JSON.stringify({ senderRole: role, message }) - } - ) -} - -/** - * Nachrichten fuer eine Meldung abrufen - */ -export async function fetchMessages(reportId: string): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages` - ) -} - -// ============================================================================= -// ATTACHMENTS -// ============================================================================= - -/** - * Anhang zu einer Meldung hochladen - */ -export async function uploadAttachment( - reportId: string, - file: File -): Promise { - const formData = new FormData() - formData.append('file', file) - - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 60000) // 60s for uploads - - try { - const headers: HeadersInit = { - 'X-Tenant-ID': getTenantId() - } - if (typeof window !== 'undefined') { - const token = localStorage.getItem('authToken') - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - } - - const response = await fetch( - `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`, - { - method: 'POST', - headers, - body: formData, - signal: controller.signal - } - ) - - if (!response.ok) { - throw new Error(`Upload fehlgeschlagen: ${response.statusText}`) - } - - return response.json() - } finally { - clearTimeout(timeoutId) - } -} - -/** - * Anhang loeschen - */ -export async function deleteAttachment(id: string): Promise { - await fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`, - { - method: 'DELETE' - } - ) -} - -// ============================================================================= -// STATISTICS -// ============================================================================= - -/** - * Statistiken fuer das Whistleblower-Dashboard abrufen - */ -export async function fetchWhistleblowerStatistics(): Promise { - return fetchWithTimeout( - `${WB_API_BASE}/api/v1/admin/whistleblower/statistics` - ) -} - -// ============================================================================= -// SDK PROXY FUNCTION (via Next.js proxy) -// ============================================================================= - -/** - * Fetch Whistleblower-Daten via SDK Proxy mit Fallback auf Mock-Daten - */ -export async function fetchSDKWhistleblowerList(): Promise<{ - reports: WhistleblowerReport[] - statistics: WhistleblowerStatistics -}> { - try { - const [reportsResponse, statsResponse] = await Promise.all([ - fetchReports(), - fetchWhistleblowerStatistics() - ]) - return { - reports: reportsResponse.reports, - statistics: statsResponse - } - } catch (error) { - console.error('Failed to load Whistleblower data from API, using mock data:', error) - // Fallback to mock data - const reports = createMockReports() - const statistics = createMockStatistics() - return { reports, statistics } - } -} - -// ============================================================================= -// MOCK DATA (Demo/Entwicklung) -// ============================================================================= - -/** - * Erstellt Demo-Meldungen fuer Entwicklung und Praesentationen - */ -export function createMockReports(): WhistleblowerReport[] { - const now = new Date() - - // Helper: Berechne Fristen - function calcDeadlines(receivedAt: Date): { ack: string; fb: string } { - const ack = new Date(receivedAt) - ack.setDate(ack.getDate() + 7) - const fb = new Date(receivedAt) - fb.setMonth(fb.getMonth() + 3) - return { ack: ack.toISOString(), fb: fb.toISOString() } - } - - const received1 = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000) - const deadlines1 = calcDeadlines(received1) - - const received2 = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000) - const deadlines2 = calcDeadlines(received2) - - const received3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000) - const deadlines3 = calcDeadlines(received3) - - const received4 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000) - const deadlines4 = calcDeadlines(received4) - - return [ - // Report 1: Neu - { - id: 'wb-001', - referenceNumber: 'WB-2026-000001', - accessKey: generateAccessKey(), - category: 'corruption', - status: 'new', - priority: 'high', - title: 'Unregelmaessigkeiten bei Auftragsvergabe', - description: 'Bei der Vergabe des IT-Rahmenvertrags im November wurden offenbar Angebote eines bestimmten Anbieters bevorzugt. Der zustaendige Abteilungsleiter hat private Verbindungen zum Geschaeftsfuehrer des Anbieters.', - isAnonymous: true, - receivedAt: received1.toISOString(), - deadlineAcknowledgment: deadlines1.ack, - deadlineFeedback: deadlines1.fb, - measures: [], - messages: [], - attachments: [], - auditTrail: [ - { - id: 'audit-001', - action: 'report_created', - description: 'Meldung ueber Online-Meldeformular eingegangen', - performedBy: 'system', - performedAt: received1.toISOString() - } - ] - }, - - // Report 2: In Pruefung (under_review) - { - id: 'wb-002', - referenceNumber: 'WB-2026-000002', - accessKey: generateAccessKey(), - category: 'data_protection', - status: 'under_review', - priority: 'normal', - title: 'Unerlaubte Weitergabe von Kundendaten', - description: 'Ein Mitarbeiter der Vertriebsabteilung gibt regelmaessig Kundenlisten an externe Dienstleister weiter, ohne dass eine Auftragsverarbeitungsvereinbarung vorliegt.', - isAnonymous: false, - reporterName: 'Maria Schmidt', - reporterEmail: 'maria.schmidt@example.de', - assignedTo: 'DSB Mueller', - receivedAt: received2.toISOString(), - acknowledgedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(), - deadlineAcknowledgment: deadlines2.ack, - deadlineFeedback: deadlines2.fb, - measures: [], - messages: [ - { - id: 'msg-001', - reportId: 'wb-002', - senderRole: 'ombudsperson', - message: 'Vielen Dank fuer Ihre Meldung. Koennen Sie uns mitteilen, welche Dienstleister konkret betroffen sind?', - createdAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), - isRead: true - }, - { - id: 'msg-002', - reportId: 'wb-002', - senderRole: 'reporter', - message: 'Es handelt sich um die Firma DataServ GmbH und MarketPro AG. Die Listen werden per unverschluesselter E-Mail versendet.', - createdAt: new Date(received2.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), - isRead: true - } - ], - attachments: [ - { - id: 'att-001', - fileName: 'email_screenshot_vertrieb.png', - fileSize: 245000, - mimeType: 'image/png', - uploadedAt: received2.toISOString(), - uploadedBy: 'reporter' - } - ], - auditTrail: [ - { - id: 'audit-002', - action: 'report_created', - description: 'Meldung per E-Mail eingegangen', - performedBy: 'system', - performedAt: received2.toISOString() - }, - { - id: 'audit-003', - action: 'acknowledged', - description: 'Eingangsbestaetigung an Hinweisgeber versendet', - performedBy: 'DSB Mueller', - performedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'audit-004', - action: 'status_changed', - description: 'Status geaendert: Bestaetigt -> In Pruefung', - performedBy: 'DSB Mueller', - performedAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() - } - ] - }, - - // Report 3: Untersuchung (investigation) - { - id: 'wb-003', - referenceNumber: 'WB-2026-000003', - accessKey: generateAccessKey(), - category: 'product_safety', - status: 'investigation', - priority: 'critical', - title: 'Fehlende Sicherheitspruefungen bei Produktfreigabe', - description: 'In der Fertigung werden seit Wochen Produkte ohne die vorgeschriebenen Sicherheitspruefungen freigegeben. Pruefprotokolle werden nachtraeglich erstellt, ohne dass tatsaechliche Pruefungen stattfinden.', - isAnonymous: true, - assignedTo: 'Qualitaetsbeauftragter Weber', - receivedAt: received3.toISOString(), - acknowledgedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(), - deadlineAcknowledgment: deadlines3.ack, - deadlineFeedback: deadlines3.fb, - measures: [ - { - id: 'msr-001', - reportId: 'wb-003', - title: 'Sofortiger Produktionsstopp fuer betroffene Charge', - description: 'Produktion der betroffenen Produktlinie stoppen bis Pruefverfahren sichergestellt ist', - status: 'completed', - responsible: 'Fertigungsleitung', - dueDate: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(received3.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'msr-002', - reportId: 'wb-003', - title: 'Externe Pruefung der Pruefprotokolle', - description: 'Unabhaengige Pruefstelle mit der Revision aller Pruefprotokolle der letzten 6 Monate beauftragen', - status: 'in_progress', - responsible: 'Qualitaetsmanagement', - dueDate: new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString() - } - ], - messages: [], - attachments: [ - { - id: 'att-002', - fileName: 'pruefprotokoll_vergleich.pdf', - fileSize: 890000, - mimeType: 'application/pdf', - uploadedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(), - uploadedBy: 'ombudsperson' - } - ], - auditTrail: [ - { - id: 'audit-005', - action: 'report_created', - description: 'Meldung ueber Online-Meldeformular eingegangen', - performedBy: 'system', - performedAt: received3.toISOString() - }, - { - id: 'audit-006', - action: 'acknowledged', - description: 'Eingangsbestaetigung versendet', - performedBy: 'Qualitaetsbeauftragter Weber', - performedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'audit-007', - action: 'investigation_started', - description: 'Formelle Untersuchung eingeleitet', - performedBy: 'Qualitaetsbeauftragter Weber', - performedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() - } - ] - }, - - // Report 4: Abgeschlossen (closed) - { - id: 'wb-004', - referenceNumber: 'WB-2026-000004', - accessKey: generateAccessKey(), - category: 'fraud', - status: 'closed', - priority: 'high', - title: 'Gefaelschte Reisekostenabrechnungen', - description: 'Ein leitender Mitarbeiter reicht seit ueber einem Jahr gefaelschte Reisekostenabrechnungen ein. Hotelrechnungen werden manipuliert, Taxiquittungen erfunden.', - isAnonymous: false, - reporterName: 'Thomas Klein', - reporterEmail: 'thomas.klein@example.de', - reporterPhone: '+49 170 9876543', - assignedTo: 'Compliance-Abteilung', - receivedAt: received4.toISOString(), - acknowledgedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(), - deadlineAcknowledgment: deadlines4.ack, - deadlineFeedback: deadlines4.fb, - closedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), - measures: [ - { - id: 'msr-003', - reportId: 'wb-004', - title: 'Interne Revision der Reisekosten', - description: 'Pruefung aller Reisekostenabrechnungen des betroffenen Mitarbeiters der letzten 24 Monate', - status: 'completed', - responsible: 'Interne Revision', - dueDate: new Date(received4.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(received4.getTime() + 25 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'msr-004', - reportId: 'wb-004', - title: 'Arbeitsrechtliche Konsequenzen', - description: 'Einleitung arbeitsrechtlicher Schritte nach Bestaetigung des Betrugs', - status: 'completed', - responsible: 'Personalabteilung', - dueDate: new Date(received4.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString(), - completedAt: new Date(received4.getTime() + 55 * 24 * 60 * 60 * 1000).toISOString() - } - ], - messages: [], - attachments: [ - { - id: 'att-003', - fileName: 'vergleich_originalrechnung_einreichung.pdf', - fileSize: 567000, - mimeType: 'application/pdf', - uploadedAt: received4.toISOString(), - uploadedBy: 'reporter' - } - ], - auditTrail: [ - { - id: 'audit-008', - action: 'report_created', - description: 'Meldung per Brief eingegangen', - performedBy: 'system', - performedAt: received4.toISOString() - }, - { - id: 'audit-009', - action: 'acknowledged', - description: 'Eingangsbestaetigung versendet', - performedBy: 'Compliance-Abteilung', - performedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString() - }, - { - id: 'audit-010', - action: 'closed', - description: 'Fall abgeschlossen - Betrug bestaetigt, arbeitsrechtliche Massnahmen eingeleitet', - performedBy: 'Compliance-Abteilung', - performedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString() - } - ] - } - ] -} - -/** - * Berechnet Statistiken aus den Mock-Daten - */ -export function createMockStatistics(): WhistleblowerStatistics { - const reports = createMockReports() - const now = new Date() - - const byStatus: Record = { - new: 0, - acknowledged: 0, - under_review: 0, - investigation: 0, - measures_taken: 0, - closed: 0, - rejected: 0 - } - - const byCategory: Record = { - corruption: 0, - fraud: 0, - data_protection: 0, - discrimination: 0, - environment: 0, - competition: 0, - product_safety: 0, - tax_evasion: 0, - other: 0 - } - - reports.forEach(r => { - byStatus[r.status]++ - byCategory[r.category]++ - }) - - const closedStatuses: ReportStatus[] = ['closed', 'rejected'] - - // Pruefe ueberfaellige Eingangsbestaetigungen - const overdueAcknowledgment = reports.filter(r => { - if (r.status !== 'new') return false - return now > new Date(r.deadlineAcknowledgment) - }).length - - // Pruefe ueberfaellige Rueckmeldungen - const overdueFeedback = reports.filter(r => { - if (closedStatuses.includes(r.status)) return false - return now > new Date(r.deadlineFeedback) - }).length - - return { - totalReports: reports.length, - newReports: byStatus.new, - underReview: byStatus.under_review + byStatus.investigation, - closed: byStatus.closed + byStatus.rejected, - overdueAcknowledgment, - overdueFeedback, - byCategory, - byStatus - } -} +export { + fetchReports, + fetchReport, + updateReport, + deleteReport, + submitPublicReport, + fetchReportByAccessKey, + acknowledgeReport, + startInvestigation, + addMeasure, + closeReport, + sendMessage, + fetchMessages, + uploadAttachment, + deleteAttachment, + fetchWhistleblowerStatistics, +} from './api-operations' + +export { + fetchSDKWhistleblowerList, + createMockReports, + createMockStatistics, +} from './api-mock-data'