feat: Add Academy, Whistleblower, Incidents SDK modules, pitch-deck, blog and CI/CD config
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled

- Academy, Whistleblower, Incidents frontend pages with API proxies and types
- Vendor compliance API proxy route
- Go backend handlers and models for all new SDK modules
- Investor pitch-deck app with interactive slides
- Blog section with DSGVO, AI Act, NIS2, glossary articles
- MkDocs documentation site
- CI/CD pipelines (Woodpecker, GitHub Actions), security scanning config
- Planning and implementation documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-13 21:12:16 +01:00
parent d7ba705562
commit 557305db5d
208 changed files with 141969 additions and 5680 deletions

View File

@@ -0,0 +1,576 @@
/**
* Academy API Client
*
* API client for the Compliance E-Learning Academy module
* Connects to the ai-compliance-sdk backend via Next.js proxy
*/
import {
Course,
CourseCategory,
CourseCreateRequest,
CourseUpdateRequest,
Enrollment,
EnrollmentStatus,
EnrollmentListResponse,
EnrollUserRequest,
UpdateProgressRequest,
Certificate,
AcademyStatistics,
SubmitQuizRequest,
SubmitQuizResponse,
isEnrollmentOverdue
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const ACADEMY_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
const API_TIMEOUT = 30000 // 30 seconds
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function getTenantId(): string {
if (typeof window !== 'undefined') {
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
}
return 'default-tenant'
}
function getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId()
}
if (typeof window !== 'undefined') {
const token = localStorage.getItem('authToken')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const userId = localStorage.getItem('bp_user_id')
if (userId) {
headers['X-User-ID'] = userId
}
}
return headers
}
async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...getAuthHeaders(),
...options.headers
}
})
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep the HTTP status message
}
throw new Error(errorMessage)
}
// Handle empty responses
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
}
return {} as T
} finally {
clearTimeout(timeoutId)
}
}
// =============================================================================
// COURSE CRUD
// =============================================================================
/**
* Alle Kurse abrufen
*/
export async function fetchCourses(): Promise<Course[]> {
return fetchWithTimeout<Course[]>(
`${ACADEMY_API_BASE}/api/v1/academy/courses`
)
}
/**
* Einzelnen Kurs abrufen
*/
export async function fetchCourse(id: string): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`
)
}
/**
* Neuen Kurs erstellen
*/
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/api/v1/academy/courses`,
{
method: 'POST',
body: JSON.stringify(request)
}
)
}
/**
* Kurs aktualisieren
*/
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Kurs loeschen
*/
export async function deleteCourse(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`,
{
method: 'DELETE'
}
)
}
// =============================================================================
// ENROLLMENTS
// =============================================================================
/**
* Einschreibungen abrufen (optional gefiltert nach Kurs-ID)
*/
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
const params = new URLSearchParams()
if (courseId) {
params.set('courseId', courseId)
}
const queryString = params.toString()
const url = `${ACADEMY_API_BASE}/api/v1/academy/enrollments${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<Enrollment[]>(url)
}
/**
* Benutzer in einen Kurs einschreiben
*/
export async function enrollUser(request: EnrollUserRequest): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/api/v1/academy/enrollments`,
{
method: 'POST',
body: JSON.stringify(request)
}
)
}
/**
* Fortschritt einer Einschreibung aktualisieren
*/
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/progress`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Einschreibung als abgeschlossen markieren
*/
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/complete`,
{
method: 'POST'
}
)
}
// =============================================================================
// CERTIFICATES
// =============================================================================
/**
* Zertifikat abrufen
*/
export async function fetchCertificate(id: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/api/v1/academy/certificates/${id}`
)
}
/**
* Zertifikat generieren nach erfolgreichem Kursabschluss
*/
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/certificate`,
{
method: 'POST'
}
)
}
// =============================================================================
// QUIZ
// =============================================================================
/**
* Quiz-Antworten einreichen und auswerten
*/
export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise<SubmitQuizResponse> {
return fetchWithTimeout<SubmitQuizResponse>(
`${ACADEMY_API_BASE}/api/v1/academy/lessons/${lessonId}/quiz`,
{
method: 'POST',
body: JSON.stringify(answers)
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Academy-Statistiken abrufen
*/
export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
return fetchWithTimeout<AcademyStatistics>(
`${ACADEMY_API_BASE}/api/v1/academy/statistics`
)
}
// =============================================================================
// 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,
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,
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,
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,
}
}
}

View File

@@ -0,0 +1,6 @@
/**
* Academy Module Exports
*/
export * from './types'
export * from './api'

View File

@@ -0,0 +1,285 @@
/**
* Academy (E-Learning / Compliance Academy) Types
*
* TypeScript definitions for the E-Learning Academy module
* Provides course management, enrollment tracking, and certificate generation
* for DSGVO, IT-Security, AI Literacy, and Whistleblower compliance training
*/
// =============================================================================
// ENUMS & CONSTANTS
// =============================================================================
export type CourseCategory =
| 'dsgvo_basics' // DSGVO-Grundlagen
| 'it_security' // IT-Sicherheit
| 'ai_literacy' // AI Literacy
| 'whistleblower_protection' // Hinweisgeberschutz
| 'custom' // Benutzerdefiniert
export type EnrollmentStatus =
| 'not_started' // Nicht gestartet
| 'in_progress' // In Bearbeitung
| 'completed' // Abgeschlossen
| 'expired' // Abgelaufen
export type LessonType = 'video' | 'text' | 'quiz'
// =============================================================================
// COURSE CATEGORY METADATA
// =============================================================================
export interface CourseCategoryInfo {
label: string
description: string
icon: string
color: string
bgColor: string
}
export const COURSE_CATEGORY_INFO: Record<CourseCategory, CourseCategoryInfo> = {
dsgvo_basics: {
label: 'DSGVO-Grundlagen',
description: 'Grundlagenwissen zur Datenschutz-Grundverordnung fuer alle Mitarbeiter',
icon: 'Shield',
color: 'text-blue-700',
bgColor: 'bg-blue-100'
},
it_security: {
label: 'IT-Sicherheit',
description: 'Cybersecurity Awareness und sichere IT-Nutzung im Arbeitsalltag',
icon: 'Lock',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
ai_literacy: {
label: 'AI Literacy',
description: 'Sicherer und verantwortungsvoller Umgang mit kuenstlicher Intelligenz',
icon: 'Brain',
color: 'text-purple-700',
bgColor: 'bg-purple-100'
},
whistleblower_protection: {
label: 'Hinweisgeberschutz',
description: 'Hinweisgeberschutzgesetz (HinSchG) und interne Meldestellen',
icon: 'Megaphone',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
custom: {
label: 'Benutzerdefiniert',
description: 'Individuell erstellte Schulungsinhalte und unternehmensspezifische Kurse',
icon: 'Pencil',
color: 'text-gray-700',
bgColor: 'bg-gray-100'
}
}
// =============================================================================
// ENROLLMENT STATUS METADATA
// =============================================================================
export const ENROLLMENT_STATUS_INFO: Record<EnrollmentStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
not_started: {
label: 'Nicht gestartet',
color: 'text-gray-700',
bgColor: 'bg-gray-100',
borderColor: 'border-gray-200'
},
in_progress: {
label: 'In Bearbeitung',
color: 'text-yellow-700',
bgColor: 'bg-yellow-100',
borderColor: 'border-yellow-200'
},
completed: {
label: 'Abgeschlossen',
color: 'text-green-700',
bgColor: 'bg-green-100',
borderColor: 'border-green-200'
},
expired: {
label: 'Abgelaufen',
color: 'text-red-700',
bgColor: 'bg-red-100',
borderColor: 'border-red-200'
}
}
// =============================================================================
// MAIN INTERFACES
// =============================================================================
export interface Course {
id: string
title: string
description: string
category: CourseCategory
lessons: Lesson[]
durationMinutes: number
requiredForRoles: string[]
createdAt: string
updatedAt: string
}
export interface Lesson {
id: string
courseId: string
title: string
type: LessonType
contentMarkdown: string
videoUrl?: string
order: number
durationMinutes: number
}
export interface QuizQuestion {
id: string
lessonId: string
question: string
options: string[]
correctOptionIndex: number
explanation: string
}
export interface Enrollment {
id: string
courseId: string
userId: string
userName: string
userEmail: string
status: EnrollmentStatus
progress: number // 0-100
startedAt: string
completedAt?: string
certificateId?: string
deadline: string
}
export interface Certificate {
id: string
enrollmentId: string
courseId: string
userId: string
userName: string
courseName: string
issuedAt: string
validUntil: string
pdfUrl: string
}
// =============================================================================
// STATISTICS
// =============================================================================
export interface AcademyStatistics {
totalCourses: number
totalEnrollments: number
completionRate: number // 0-100
overdueCount: number
byCategory: Record<CourseCategory, number>
byStatus: Record<EnrollmentStatus, number>
}
// =============================================================================
// API TYPES (REQUEST / RESPONSE)
// =============================================================================
export interface CourseListResponse {
courses: Course[]
total: number
page: number
pageSize: number
}
export interface EnrollmentListResponse {
enrollments: Enrollment[]
total: number
page: number
pageSize: number
}
export interface CourseCreateRequest {
title: string
description: string
category: CourseCategory
durationMinutes: number
requiredForRoles?: string[]
}
export interface CourseUpdateRequest {
title?: string
description?: string
category?: CourseCategory
durationMinutes?: number
requiredForRoles?: string[]
}
export interface EnrollUserRequest {
courseId: string
userId: string
userName: string
userEmail: string
deadline: string
}
export interface UpdateProgressRequest {
progress: number
lessonId?: string
}
export interface SubmitQuizRequest {
answers: number[] // Index der ausgewaehlten Antwort pro Frage
}
export interface SubmitQuizResponse {
score: number
passed: boolean
correctAnswers: number
totalQuestions: number
results: { questionId: string; correct: boolean; explanation: string }[]
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Berechnet die Abschlussrate fuer eine Liste von Einschreibungen in Prozent (0-100)
*/
export function getCompletionPercentage(enrollments: Enrollment[]): number {
if (enrollments.length === 0) return 0
const completed = enrollments.filter(e => e.status === 'completed').length
return Math.round((completed / enrollments.length) * 100)
}
/**
* Prueft ob eine Einschreibung ueberfaellig ist (Deadline ueberschritten und nicht abgeschlossen)
*/
export function isEnrollmentOverdue(enrollment: Enrollment): boolean {
if (enrollment.status === 'completed' || enrollment.status === 'expired') {
return false
}
const deadlineDate = new Date(enrollment.deadline)
const now = new Date()
return deadlineDate.getTime() < now.getTime()
}
/**
* Berechnet die verbleibenden Tage bis zur Deadline
* Negative Werte bedeuten ueberfaellig
*/
export function getDaysUntilDeadline(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 getCategoryInfo(category: CourseCategory): CourseCategoryInfo {
return COURSE_CATEGORY_INFO[category]
}
export function getStatusInfo(status: EnrollmentStatus) {
return ENROLLMENT_STATUS_INFO[status]
}

View File

@@ -0,0 +1,845 @@
/**
* Incident/Breach Management API Client
*
* API client for DSGVO Art. 33/34 Incident & Data Breach Management
* Connects via Next.js proxy to the ai-compliance-sdk backend
*/
import {
Incident,
IncidentListResponse,
IncidentFilters,
IncidentCreateRequest,
IncidentUpdateRequest,
IncidentStatistics,
IncidentMeasure,
TimelineEntry,
RiskAssessmentRequest,
RiskAssessment,
AuthorityNotification,
DataSubjectNotification,
IncidentSeverity,
IncidentStatus,
IncidentCategory,
calculateRiskLevel,
isNotificationRequired,
get72hDeadline
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const INCIDENTS_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
const API_TIMEOUT = 30000 // 30 seconds
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function getTenantId(): string {
if (typeof window !== 'undefined') {
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
}
return 'default-tenant'
}
function getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId()
}
if (typeof window !== 'undefined') {
const token = localStorage.getItem('authToken')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const userId = localStorage.getItem('bp_user_id')
if (userId) {
headers['X-User-ID'] = userId
}
}
return headers
}
async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...getAuthHeaders(),
...options.headers
}
})
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep the HTTP status message
}
throw new Error(errorMessage)
}
// Handle empty responses
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
}
return {} as T
} finally {
clearTimeout(timeoutId)
}
}
// =============================================================================
// INCIDENT LIST & CRUD
// =============================================================================
/**
* Alle Vorfaelle abrufen mit optionalen Filtern
*/
export async function fetchIncidents(filters?: IncidentFilters): Promise<IncidentListResponse> {
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.severity) {
const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity]
severities.forEach(s => params.append('severity', s))
}
if (filters.category) {
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
categories.forEach(c => params.append('category', c))
}
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
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 = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<IncidentListResponse>(url)
}
/**
* Einzelnen Vorfall per ID abrufen
*/
export async function fetchIncident(id: string): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
}
/**
* Neuen Vorfall erstellen
*/
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
method: 'POST',
body: JSON.stringify(request)
})
}
/**
* Vorfall aktualisieren
*/
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
method: 'PUT',
body: JSON.stringify(update)
})
}
/**
* Vorfall loeschen (Soft Delete)
*/
export async function deleteIncident(id: string): Promise<void> {
await fetchWithTimeout<void>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
method: 'DELETE'
})
}
// =============================================================================
// RISK ASSESSMENT
// =============================================================================
/**
* Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO)
*/
export async function submitRiskAssessment(
incidentId: string,
assessment: RiskAssessmentRequest
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`,
{
method: 'POST',
body: JSON.stringify(assessment)
}
)
}
// =============================================================================
// AUTHORITY NOTIFICATION (Art. 33 DSGVO)
// =============================================================================
/**
* Meldeformular fuer die Aufsichtsbehoerde generieren
*/
export async function generateAuthorityForm(incidentId: string): Promise<Blob> {
const response = await fetch(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`,
{
headers: getAuthHeaders()
}
)
if (!response.ok) {
throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`)
}
return response.blob()
}
/**
* Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO)
*/
export async function submitAuthorityNotification(
incidentId: string,
data: Partial<AuthorityNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`,
{
method: 'POST',
body: JSON.stringify(data)
}
)
}
// =============================================================================
// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO)
// =============================================================================
/**
* Betroffene Personen benachrichtigen (Art. 34 DSGVO)
*/
export async function sendDataSubjectNotification(
incidentId: string,
data: Partial<DataSubjectNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`,
{
method: 'POST',
body: JSON.stringify(data)
}
)
}
// =============================================================================
// MEASURES (Massnahmen)
// =============================================================================
/**
* Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme)
*/
export async function addMeasure(
incidentId: string,
measure: Omit<IncidentMeasure, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
{
method: 'POST',
body: JSON.stringify(measure)
}
)
}
/**
* Massnahme aktualisieren
*/
export async function updateMeasure(
measureId: string,
update: Partial<IncidentMeasure>
): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Massnahme als abgeschlossen markieren
*/
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
{
method: 'POST'
}
)
}
// =============================================================================
// TIMELINE
// =============================================================================
/**
* Zeitleisteneintrag hinzufuegen
*/
export async function addTimelineEntry(
incidentId: string,
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`,
{
method: 'POST',
body: JSON.stringify(entry)
}
)
}
// =============================================================================
// CLOSE INCIDENT
// =============================================================================
/**
* Vorfall abschliessen mit Lessons Learned
*/
export async function closeIncident(
incidentId: string,
lessonsLearned: string
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
{
method: 'POST',
body: JSON.stringify({ lessonsLearned })
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Vorfall-Statistiken abrufen
*/
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
return fetchWithTimeout<IncidentStatistics>(
`${INCIDENTS_API_BASE}/api/v1/incidents/statistics`
)
}
// =============================================================================
// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten)
// =============================================================================
/**
* Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten
*/
export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> {
try {
const res = await fetch('/api/sdk/v1/incidents', {
headers: getAuthHeaders()
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
const incidents: Incident[] = data.incidents || []
// Statistiken lokal berechnen
const statistics = computeStatistics(incidents)
return { incidents, statistics }
} catch (error) {
console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error)
const incidents = createMockIncidents()
const statistics = createMockStatistics()
return { incidents, statistics }
}
}
/**
* Statistiken lokal aus Incident-Liste berechnen
*/
function computeStatistics(incidents: Incident[]): IncidentStatistics {
const countBy = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
const result: Record<string, number> = {}
items.forEach(item => {
const key = String(item[field])
result[key] = (result[key] || 0) + 1
})
return result as Record<K, number>
}
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
const categoryCounts = countBy<IncidentCategory>(incidents as unknown as { [key: string]: unknown }[], 'category')
const openIncidents = incidents.filter(i => i.status !== 'closed').length
const notificationsPending = incidents.filter(i =>
i.authorityNotification !== null &&
i.authorityNotification.status === 'pending' &&
i.status !== 'closed'
).length
// Durchschnittliche Reaktionszeit berechnen
let totalResponseHours = 0
let respondedCount = 0
incidents.forEach(i => {
if (i.riskAssessment && i.riskAssessment.assessedAt) {
const detected = new Date(i.detectedAt).getTime()
const assessed = new Date(i.riskAssessment.assessedAt).getTime()
totalResponseHours += (assessed - detected) / (1000 * 60 * 60)
respondedCount++
}
})
return {
totalIncidents: incidents.length,
openIncidents,
notificationsPending,
averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0,
bySeverity: {
low: severityCounts['low'] || 0,
medium: severityCounts['medium'] || 0,
high: severityCounts['high'] || 0,
critical: severityCounts['critical'] || 0
},
byCategory: {
data_breach: categoryCounts['data_breach'] || 0,
unauthorized_access: categoryCounts['unauthorized_access'] || 0,
data_loss: categoryCounts['data_loss'] || 0,
system_compromise: categoryCounts['system_compromise'] || 0,
phishing: categoryCounts['phishing'] || 0,
ransomware: categoryCounts['ransomware'] || 0,
insider_threat: categoryCounts['insider_threat'] || 0,
physical_breach: categoryCounts['physical_breach'] || 0,
other: categoryCounts['other'] || 0
},
byStatus: {
detected: statusCounts['detected'] || 0,
assessment: statusCounts['assessment'] || 0,
containment: statusCounts['containment'] || 0,
notification_required: statusCounts['notification_required'] || 0,
notification_sent: statusCounts['notification_sent'] || 0,
remediation: statusCounts['remediation'] || 0,
closed: statusCounts['closed'] || 0
}
}
}
// =============================================================================
// MOCK DATA (Demo-Daten fuer Entwicklung und Tests)
// =============================================================================
/**
* Erstellt Demo-Vorfaelle fuer die Entwicklung
*/
export function createMockIncidents(): Incident[] {
const now = new Date()
return [
// 1. Gerade erkannt - noch nicht bewertet (detected/new)
{
id: 'inc-001',
referenceNumber: 'INC-2026-000001',
title: 'Unbefugter Zugriff auf Schuelerdatenbank',
description: 'Ein ehemaliger Mitarbeiter hat sich mit noch aktiven Zugangsdaten in die Schuelerdatenbank eingeloggt. Der Zugriff wurde durch die Log-Analyse entdeckt.',
category: 'unauthorized_access',
severity: 'high',
status: 'detected',
detectedAt: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), // 3 Stunden her
detectedBy: 'Log-Analyse (automatisiert)',
affectedSystems: ['Schuelerdatenbank', 'Schulverwaltungssystem'],
affectedDataCategories: ['Personenbezogene Daten', 'Daten von Kindern', 'Gesundheitsdaten'],
estimatedAffectedPersons: 800,
riskAssessment: null,
authorityNotification: null,
dataSubjectNotification: null,
measures: [],
timeline: [
{
id: 'tl-001',
incidentId: 'inc-001',
timestamp: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(),
action: 'Vorfall erkannt',
description: 'Automatische Log-Analyse meldet verdaechtigen Login eines deaktivierten Kontos',
performedBy: 'SIEM-System'
}
],
assignedTo: undefined
},
// 2. In Bewertung (assessment) - Risikobewertung laeuft
{
id: 'inc-002',
referenceNumber: 'INC-2026-000002',
title: 'E-Mail mit Kundendaten an falschen Empfaenger',
description: 'Ein Mitarbeiter hat eine Excel-Datei mit Kundendaten (Name, Adresse, Vertragsnummer) an einen falschen E-Mail-Empfaenger gesendet. Der Empfaenger wurde kontaktiert und hat die Loeschung bestaetigt.',
category: 'data_breach',
severity: 'medium',
status: 'assessment',
detectedAt: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(), // 18 Stunden her
detectedBy: 'Vertriebsabteilung',
affectedSystems: ['E-Mail-System (Exchange)'],
affectedDataCategories: ['Personenbezogene Daten', 'Kundendaten'],
estimatedAffectedPersons: 150,
riskAssessment: {
id: 'ra-002',
assessedBy: 'DSB Mueller',
assessedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(),
likelihoodScore: 3,
impactScore: 2,
overallRisk: 'medium',
notificationRequired: false,
reasoning: 'Empfaenger hat Loeschung bestaetigt. Datenkategorie: allgemeine Kontaktdaten und Vertragsnummern. Geringes Risiko fuer betroffene Personen.'
},
authorityNotification: {
id: 'an-002',
authority: 'LfD Niedersachsen',
deadline72h: new Date(new Date(now.getTime() - 18 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
status: 'pending',
formData: {}
},
dataSubjectNotification: null,
measures: [
{
id: 'meas-001',
incidentId: 'inc-002',
title: 'Empfaenger kontaktiert',
description: 'Falscher Empfaenger kontaktiert mit Bitte um Loeschung',
type: 'immediate',
status: 'completed',
responsible: 'Vertriebsleitung',
dueDate: new Date(now.getTime() - 16 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString()
}
],
timeline: [
{
id: 'tl-002',
incidentId: 'inc-002',
timestamp: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(),
action: 'Vorfall gemeldet',
description: 'Mitarbeiter meldet versehentlichen E-Mail-Versand',
performedBy: 'M. Schmidt (Vertrieb)'
},
{
id: 'tl-003',
incidentId: 'inc-002',
timestamp: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString(),
action: 'Sofortmassnahme',
description: 'Empfaenger kontaktiert und Loeschung bestaetigt',
performedBy: 'Vertriebsleitung'
},
{
id: 'tl-004',
incidentId: 'inc-002',
timestamp: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(),
action: 'Risikobewertung',
description: 'Bewertung durchgefuehrt - mittleres Risiko, keine Meldepflicht',
performedBy: 'DSB Mueller'
}
],
assignedTo: 'DSB Mueller'
},
// 3. Gemeldet (notification_sent) - Ransomware-Angriff
{
id: 'inc-003',
referenceNumber: 'INC-2026-000003',
title: 'Ransomware-Angriff auf Dateiserver',
description: 'Am Montagmorgen wurde ein Ransomware-Angriff auf den zentralen Dateiserver erkannt. Mehrere verschluesselte Dateien wurden identifiziert. Der Angriffsvektor war eine Phishing-E-Mail an einen Mitarbeiter.',
category: 'ransomware',
severity: 'critical',
status: 'notification_sent',
detectedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
detectedBy: 'IT-Sicherheitsteam',
affectedSystems: ['Dateiserver (FS-01)', 'E-Mail-System', 'Backup-Server'],
affectedDataCategories: ['Personenbezogene Daten', 'Beschaeftigtendaten', 'Kundendaten', 'Finanzdaten'],
estimatedAffectedPersons: 2500,
riskAssessment: {
id: 'ra-003',
assessedBy: 'DSB Mueller',
assessedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
likelihoodScore: 5,
impactScore: 5,
overallRisk: 'critical',
notificationRequired: true,
reasoning: 'Hohes Risiko fuer Rechte und Freiheiten der betroffenen Personen durch potentiellen Zugriff auf personenbezogene Daten und Finanzdaten. Verschluesselung betrifft Verfuegbarkeit, Exfiltration nicht auszuschliessen.'
},
authorityNotification: {
id: 'an-003',
authority: 'LfD Niedersachsen',
deadline72h: new Date(new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
submittedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
status: 'submitted',
formData: {
referenceNumber: 'LfD-NI-2026-04821',
incidentType: 'Ransomware',
affectedPersons: 2500
},
pdfUrl: '/api/sdk/v1/incidents/inc-003/authority-form.pdf'
},
dataSubjectNotification: {
id: 'dsn-003',
notificationRequired: true,
templateText: 'Sehr geehrte Damen und Herren, wir informieren Sie ueber einen Sicherheitsvorfall, bei dem moeglicherweise Ihre personenbezogenen Daten betroffen sind...',
sentAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
recipientCount: 2500,
method: 'email'
},
measures: [
{
id: 'meas-002',
incidentId: 'inc-003',
title: 'Netzwerksegmentierung',
description: 'Betroffene Systeme vom Netzwerk isoliert',
type: 'immediate',
status: 'completed',
responsible: 'IT-Sicherheitsteam',
dueDate: new Date(now.getTime() - 4.8 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'meas-003',
incidentId: 'inc-003',
title: 'Passwoerter zuruecksetzen',
description: 'Alle Benutzerpasswoerter zurueckgesetzt',
type: 'immediate',
status: 'completed',
responsible: 'IT-Administration',
dueDate: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'meas-004',
incidentId: 'inc-003',
title: 'E-Mail-Security Gateway implementieren',
description: 'Implementierung eines fortgeschrittenen E-Mail-Sicherheitsgateways mit Sandboxing',
type: 'preventive',
status: 'in_progress',
responsible: 'IT-Sicherheitsteam',
dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'meas-005',
incidentId: 'inc-003',
title: 'Mitarbeiterschulung Phishing',
description: 'Verpflichtende Schulung fuer alle Mitarbeiter zum Thema Phishing-Erkennung',
type: 'preventive',
status: 'planned',
responsible: 'Personalwesen',
dueDate: new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString()
}
],
timeline: [
{
id: 'tl-005',
incidentId: 'inc-003',
timestamp: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Vorfall erkannt',
description: 'IT-Sicherheitsteam erkennt ungewoehnliche Verschluesselungsaktivitaet',
performedBy: 'IT-Sicherheitsteam'
},
{
id: 'tl-006',
incidentId: 'inc-003',
timestamp: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Eindaemmung gestartet',
description: 'Netzwerksegmentierung und Isolation betroffener Systeme',
performedBy: 'IT-Sicherheitsteam'
},
{
id: 'tl-007',
incidentId: 'inc-003',
timestamp: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Risikobewertung abgeschlossen',
description: 'Kritisches Risiko festgestellt - Meldepflicht ausgeloest',
performedBy: 'DSB Mueller'
},
{
id: 'tl-008',
incidentId: 'inc-003',
timestamp: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Behoerdenbenachrichtigung',
description: 'Meldung an LfD Niedersachsen eingereicht',
performedBy: 'DSB Mueller'
},
{
id: 'tl-009',
incidentId: 'inc-003',
timestamp: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Betroffene benachrichtigt',
description: '2.500 betroffene Personen per E-Mail informiert',
performedBy: 'Kommunikationsabteilung'
}
],
assignedTo: 'DSB Mueller'
},
// 4. Abgeschlossener Vorfall (closed) - Phishing
{
id: 'inc-004',
referenceNumber: 'INC-2026-000004',
title: 'Phishing-Angriff auf Personalabteilung',
description: 'Gezielter Phishing-Angriff auf die Personalabteilung. Ein Mitarbeiter hat Zugangsdaten auf einer gefaelschten Login-Seite eingegeben. Das Konto wurde sofort gesperrt. Keine Datenexfiltration festgestellt.',
category: 'phishing',
severity: 'high',
status: 'closed',
detectedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
detectedBy: 'IT-Sicherheitsteam (SIEM-Alert)',
affectedSystems: ['Active Directory', 'HR-Portal'],
affectedDataCategories: ['Beschaeftigtendaten', 'Personenbezogene Daten'],
estimatedAffectedPersons: 0,
riskAssessment: {
id: 'ra-004',
assessedBy: 'DSB Mueller',
assessedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
likelihoodScore: 4,
impactScore: 3,
overallRisk: 'high',
notificationRequired: true,
reasoning: 'Zugangsdaten kompromittiert, potentieller Zugriff auf Personaldaten. Keine Exfiltration festgestellt, dennoch Meldung wegen Kompromittierung der Zugangsdaten.'
},
authorityNotification: {
id: 'an-004',
authority: 'LfD Niedersachsen',
deadline72h: new Date(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
submittedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
status: 'acknowledged',
formData: {
referenceNumber: 'LfD-NI-2026-03912',
incidentType: 'Phishing',
affectedPersons: 0
}
},
dataSubjectNotification: {
id: 'dsn-004',
notificationRequired: false,
templateText: '',
recipientCount: 0,
method: 'email'
},
measures: [
{
id: 'meas-006',
incidentId: 'inc-004',
title: 'Konto gesperrt',
description: 'Kompromittiertes Benutzerkonto sofort gesperrt',
type: 'immediate',
status: 'completed',
responsible: 'IT-Administration',
dueDate: new Date(now.getTime() - 29.8 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(now.getTime() - 29.9 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'meas-007',
incidentId: 'inc-004',
title: 'MFA fuer alle Mitarbeiter',
description: 'Einfuehrung von Multi-Faktor-Authentifizierung fuer alle Konten',
type: 'preventive',
status: 'completed',
responsible: 'IT-Sicherheitsteam',
dueDate: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString()
}
],
timeline: [
{
id: 'tl-010',
incidentId: 'inc-004',
timestamp: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
action: 'SIEM-Alert',
description: 'Verdaechtiger Login-Versuch aus unbekannter Region erkannt',
performedBy: 'IT-Sicherheitsteam'
},
{
id: 'tl-011',
incidentId: 'inc-004',
timestamp: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Behoerdenbenachrichtigung',
description: 'Meldung an LfD Niedersachsen',
performedBy: 'DSB Mueller'
},
{
id: 'tl-012',
incidentId: 'inc-004',
timestamp: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
action: 'Vorfall abgeschlossen',
description: 'Alle Massnahmen umgesetzt, keine Datenexfiltration festgestellt',
performedBy: 'DSB Mueller'
}
],
assignedTo: 'DSB Mueller',
closedAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
lessonsLearned: '1. MFA haette den Zugriff verhindert (jetzt implementiert). 2. E-Mail-Security-Gateway muss verbesserte Phishing-Erkennung erhalten. 3. Regelmaessige Phishing-Simulationen fuer alle Mitarbeiter einfuehren.'
}
]
}
/**
* Erstellt Mock-Statistiken fuer die Entwicklung
*/
export function createMockStatistics(): IncidentStatistics {
return {
totalIncidents: 4,
openIncidents: 3,
notificationsPending: 1,
averageResponseTimeHours: 8.5,
bySeverity: {
low: 0,
medium: 1,
high: 2,
critical: 1
},
byCategory: {
data_breach: 1,
unauthorized_access: 1,
data_loss: 0,
system_compromise: 0,
phishing: 1,
ransomware: 1,
insider_threat: 0,
physical_breach: 0,
other: 0
},
byStatus: {
detected: 1,
assessment: 1,
containment: 0,
notification_required: 0,
notification_sent: 1,
remediation: 0,
closed: 1
}
}
}

View File

@@ -0,0 +1,447 @@
/**
* Incident/Breach Management Types (Datenpannen-Management)
*
* TypeScript definitions for DSGVO Art. 33/34 Incident & Data Breach Management
* 72-Stunden-Meldefrist an die Aufsichtsbehoerde
*/
// =============================================================================
// ENUMS & CONSTANTS
// =============================================================================
export type IncidentSeverity = 'low' | 'medium' | 'high' | 'critical'
export type IncidentStatus =
| 'detected' // Erkannt
| 'assessment' // Bewertung laeuft
| 'containment' // Eindaemmung
| 'notification_required' // Meldepflichtig - Meldung steht aus
| 'notification_sent' // Gemeldet an Aufsichtsbehoerde
| 'remediation' // Behebung laeuft
| 'closed' // Abgeschlossen
export type IncidentCategory =
| 'data_breach' // Datenpanne / Datenschutzverletzung
| 'unauthorized_access' // Unbefugter Zugriff
| 'data_loss' // Datenverlust
| 'system_compromise' // Systemkompromittierung
| 'phishing' // Phishing-Angriff
| 'ransomware' // Ransomware
| 'insider_threat' // Insider-Bedrohung
| 'physical_breach' // Physischer Sicherheitsvorfall
| 'other' // Sonstiges
// =============================================================================
// SEVERITY METADATA
// =============================================================================
export interface IncidentSeverityInfo {
label: string
description: string
color: string
bgColor: string
}
export const INCIDENT_SEVERITY_INFO: Record<IncidentSeverity, IncidentSeverityInfo> = {
low: {
label: 'Niedrig',
description: 'Geringes Risiko fuer betroffene Personen, keine Meldepflicht erwartet',
color: 'text-green-700',
bgColor: 'bg-green-100'
},
medium: {
label: 'Mittel',
description: 'Moderates Risiko, Meldepflicht an Aufsichtsbehoerde moeglich',
color: 'text-yellow-700',
bgColor: 'bg-yellow-100'
},
high: {
label: 'Hoch',
description: 'Hohes Risiko, Meldepflicht an Aufsichtsbehoerde wahrscheinlich',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
critical: {
label: 'Kritisch',
description: 'Sehr hohes Risiko, Meldepflicht an Aufsichtsbehoerde und Betroffene',
color: 'text-red-700',
bgColor: 'bg-red-100'
}
}
// =============================================================================
// STATUS METADATA
// =============================================================================
export interface IncidentStatusInfo {
label: string
description: string
color: string
bgColor: string
}
export const INCIDENT_STATUS_INFO: Record<IncidentStatus, IncidentStatusInfo> = {
detected: {
label: 'Erkannt',
description: 'Vorfall wurde erkannt und dokumentiert',
color: 'text-blue-700',
bgColor: 'bg-blue-100'
},
assessment: {
label: 'Bewertung',
description: 'Risikobewertung und Einschaetzung der Meldepflicht',
color: 'text-yellow-700',
bgColor: 'bg-yellow-100'
},
containment: {
label: 'Eindaemmung',
description: 'Sofortmassnahmen zur Eindaemmung werden durchgefuehrt',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
notification_required: {
label: 'Meldepflichtig',
description: 'Meldung an Aufsichtsbehoerde erforderlich (Art. 33 DSGVO)',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
notification_sent: {
label: 'Gemeldet',
description: 'Meldung an die Aufsichtsbehoerde wurde eingereicht',
color: 'text-purple-700',
bgColor: 'bg-purple-100'
},
remediation: {
label: 'Behebung',
description: 'Langfristige Behebungs- und Praeventionsmassnahmen',
color: 'text-indigo-700',
bgColor: 'bg-indigo-100'
},
closed: {
label: 'Abgeschlossen',
description: 'Vorfall vollstaendig bearbeitet und dokumentiert',
color: 'text-green-700',
bgColor: 'bg-green-100'
}
}
// =============================================================================
// CATEGORY METADATA
// =============================================================================
export interface IncidentCategoryInfo {
label: string
description: string
icon: string
color: string
bgColor: string
}
export const INCIDENT_CATEGORY_INFO: Record<IncidentCategory, IncidentCategoryInfo> = {
data_breach: {
label: 'Datenpanne',
description: 'Allgemeine Datenschutzverletzung mit Offenlegung personenbezogener Daten',
icon: '\u{1F4C4}',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
unauthorized_access: {
label: 'Unbefugter Zugriff',
description: 'Unberechtigter Zugriff auf Systeme oder Daten',
icon: '\u{1F6AB}',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
data_loss: {
label: 'Datenverlust',
description: 'Verlust von Daten durch technischen Fehler oder Versehen',
icon: '\u{1F4BE}',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
system_compromise: {
label: 'Systemkompromittierung',
description: 'System wurde durch Angreifer kompromittiert',
icon: '\u{1F4BB}',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
phishing: {
label: 'Phishing-Angriff',
description: 'Taeuschendes Abfangen von Zugangsdaten oder Daten',
icon: '\u{1F3A3}',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
ransomware: {
label: 'Ransomware',
description: 'Verschluesselung von Daten durch Schadsoftware',
icon: '\u{1F512}',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
insider_threat: {
label: 'Insider-Bedrohung',
description: 'Vorsaetzlicher oder fahrlaessiger Verstoss durch Mitarbeiter',
icon: '\u{1F464}',
color: 'text-purple-700',
bgColor: 'bg-purple-100'
},
physical_breach: {
label: 'Physischer Sicherheitsvorfall',
description: 'Einbruch, Diebstahl von Geraeten oder physische Zugriffe',
icon: '\u{1F3E2}',
color: 'text-gray-700',
bgColor: 'bg-gray-100'
},
other: {
label: 'Sonstiges',
description: 'Sonstiger Datenschutzvorfall',
icon: '\u{2753}',
color: 'text-gray-700',
bgColor: 'bg-gray-100'
}
}
// =============================================================================
// MAIN INTERFACES
// =============================================================================
export interface RiskAssessment {
id: string
assessedBy: string
assessedAt: string
likelihoodScore: number // 1-5 (1 = sehr unwahrscheinlich, 5 = sehr wahrscheinlich)
impactScore: number // 1-5 (1 = gering, 5 = katastrophal)
overallRisk: IncidentSeverity // Berechnetes Gesamtrisiko
notificationRequired: boolean // Art. 33 Bewertung
reasoning: string // Begruendung der Bewertung
}
export interface AuthorityNotification {
id: string
authority: string // z.B. "LfD Niedersachsen"
deadline72h: string // 72 Stunden nach Erkennung (Art. 33)
submittedAt?: string
status: 'pending' | 'submitted' | 'acknowledged'
formData: Record<string, unknown>
pdfUrl?: string
}
export interface DataSubjectNotification {
id: string
notificationRequired: boolean // Art. 34 Bewertung
templateText: string
sentAt?: string
recipientCount: number
method: 'email' | 'letter' | 'portal' | 'public'
}
export interface IncidentMeasure {
id: string
incidentId: string
title: string
description: string
type: 'immediate' | 'corrective' | 'preventive'
status: 'planned' | 'in_progress' | 'completed'
responsible: string
dueDate: string
completedAt?: string
}
export interface TimelineEntry {
id: string
incidentId: string
timestamp: string
action: string
description: string
performedBy: string
}
export interface Incident {
id: string
referenceNumber: string // z.B. "INC-2025-000001"
title: string
description: string
category: IncidentCategory
severity: IncidentSeverity
status: IncidentStatus
// Erkennung
detectedAt: string
detectedBy: string
// Betroffene Systeme & Daten
affectedSystems: string[]
affectedDataCategories: string[]
estimatedAffectedPersons: number
// Risikobewertung
riskAssessment: RiskAssessment | null
// Meldungen
authorityNotification: AuthorityNotification | null
dataSubjectNotification: DataSubjectNotification | null
// Massnahmen & Verlauf
measures: IncidentMeasure[]
timeline: TimelineEntry[]
// Zuweisung
assignedTo?: string
// Abschluss
closedAt?: string
lessonsLearned?: string
}
// =============================================================================
// STATISTICS
// =============================================================================
export interface IncidentStatistics {
totalIncidents: number
openIncidents: number
notificationsPending: number
averageResponseTimeHours: number
bySeverity: Record<IncidentSeverity, number>
byCategory: Record<IncidentCategory, number>
byStatus: Record<IncidentStatus, number>
}
// =============================================================================
// API TYPES
// =============================================================================
export interface IncidentFilters {
status?: IncidentStatus | IncidentStatus[]
severity?: IncidentSeverity | IncidentSeverity[]
category?: IncidentCategory | IncidentCategory[]
assignedTo?: string
overdue?: boolean
search?: string
dateFrom?: string
dateTo?: string
}
export interface IncidentListResponse {
incidents: Incident[]
total: number
page: number
pageSize: number
}
export interface IncidentCreateRequest {
title: string
description: string
category: IncidentCategory
severity: IncidentSeverity
detectedAt: string
detectedBy: string
affectedSystems: string[]
affectedDataCategories: string[]
estimatedAffectedPersons: number
assignedTo?: string
}
export interface IncidentUpdateRequest {
title?: string
description?: string
category?: IncidentCategory
severity?: IncidentSeverity
status?: IncidentStatus
affectedSystems?: string[]
affectedDataCategories?: string[]
estimatedAffectedPersons?: number
assignedTo?: string
}
export interface RiskAssessmentRequest {
likelihoodScore: number // 1-5
impactScore: number // 1-5
reasoning: string
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Berechnet die verbleibenden Stunden bis zur 72h-Meldefrist (Art. 33 DSGVO)
*/
export function getHoursUntil72hDeadline(detectedAt: string): number {
const detected = new Date(detectedAt)
const deadline = new Date(detected.getTime() + 72 * 60 * 60 * 1000)
const now = new Date()
const diff = deadline.getTime() - now.getTime()
return Math.round(diff / (1000 * 60 * 60) * 10) / 10
}
/**
* Prueft ob die 72-Stunden-Meldefrist abgelaufen ist
*/
export function is72hDeadlineExpired(detectedAt: string): boolean {
const detected = new Date(detectedAt)
const deadline = new Date(detected.getTime() + 72 * 60 * 60 * 1000)
return new Date() > deadline
}
/**
* Berechnet die Risikostufe basierend auf Eintrittswahrscheinlichkeit und Auswirkung
* Risiko-Matrix:
* likelihood x impact >= 20 -> critical
* likelihood x impact >= 12 -> high
* likelihood x impact >= 6 -> medium
* sonst -> low
*/
export function calculateRiskLevel(likelihood: number, impact: number): IncidentSeverity {
const riskScore = likelihood * impact
if (riskScore >= 20) return 'critical'
if (riskScore >= 12) return 'high'
if (riskScore >= 6) return 'medium'
return 'low'
}
/**
* Prueft ob eine Meldung an die Aufsichtsbehoerde erforderlich ist
* Bei hohem oder kritischem Risiko ist eine Meldung gemaess Art. 33 DSGVO erforderlich
*/
export function isNotificationRequired(riskAssessment: RiskAssessment): boolean {
return riskAssessment.overallRisk === 'high' || riskAssessment.overallRisk === 'critical'
}
/**
* Generiert eine Referenznummer fuer einen Vorfall
*/
export function generateIncidentReferenceNumber(year: number, sequence: number): string {
return `INC-${year}-${String(sequence).padStart(6, '0')}`
}
/**
* Gibt die 72h-Deadline als Date zurueck
*/
export function get72hDeadline(detectedAt: string): Date {
const detected = new Date(detectedAt)
return new Date(detected.getTime() + 72 * 60 * 60 * 1000)
}
/**
* Gibt die Severity-Info zurueck
*/
export function getSeverityInfo(severity: IncidentSeverity): IncidentSeverityInfo {
return INCIDENT_SEVERITY_INFO[severity]
}
/**
* Gibt die Status-Info zurueck
*/
export function getStatusInfo(status: IncidentStatus): IncidentStatusInfo {
return INCIDENT_STATUS_INFO[status]
}
/**
* Gibt die Kategorie-Info zurueck
*/
export function getCategoryInfo(category: IncidentCategory): IncidentCategoryInfo {
return INCIDENT_CATEGORY_INFO[category]
}

View File

@@ -693,6 +693,45 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['consent-management'],
isOptional: false,
},
{
id: 'incidents',
phase: 2,
package: 'betrieb',
order: 6,
name: 'Incident Management',
nameShort: 'Incidents',
description: 'Datenpannen erfassen, bewerten und melden (Art. 33/34 DSGVO)',
url: '/sdk/incidents',
checkpointId: 'CP-INC',
prerequisiteSteps: ['notfallplan'],
isOptional: false,
},
{
id: 'whistleblower',
phase: 2,
package: 'betrieb',
order: 7,
name: 'Hinweisgebersystem',
nameShort: 'Whistleblower',
description: 'Anonymes Meldesystem gemaess HinSchG',
url: '/sdk/whistleblower',
checkpointId: 'CP-WB',
prerequisiteSteps: ['incidents'],
isOptional: false,
},
{
id: 'academy',
phase: 2,
package: 'betrieb',
order: 8,
name: 'Compliance Academy',
nameShort: 'Academy',
description: 'Mitarbeiter-Schulungen & Zertifikate',
url: '/sdk/academy',
checkpointId: 'CP-ACAD',
prerequisiteSteps: ['whistleblower'],
isOptional: false,
},
]
// =============================================================================

View File

@@ -0,0 +1,755 @@
/**
* Whistleblower System API Client
*
* API client for Hinweisgeberschutzgesetz (HinSchG) compliant
* Whistleblower/Hinweisgebersystem management
* Connects to the ai-compliance-sdk backend
*/
import {
WhistleblowerReport,
WhistleblowerStatistics,
ReportListResponse,
ReportFilters,
PublicReportSubmission,
ReportUpdateRequest,
MessageSendRequest,
AnonymousMessage,
WhistleblowerMeasure,
FileAttachment,
ReportCategory,
ReportStatus,
ReportPriority,
generateAccessKey
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
const API_TIMEOUT = 30000 // 30 seconds
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function getTenantId(): string {
if (typeof window !== 'undefined') {
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
}
return 'default-tenant'
}
function getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId()
}
if (typeof window !== 'undefined') {
const token = localStorage.getItem('authToken')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const userId = localStorage.getItem('bp_user_id')
if (userId) {
headers['X-User-ID'] = userId
}
}
return headers
}
async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...getAuthHeaders(),
...options.headers
}
})
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep the HTTP status message
}
throw new Error(errorMessage)
}
// Handle empty responses
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
}
return {} as T
} finally {
clearTimeout(timeoutId)
}
}
// =============================================================================
// ADMIN CRUD - Reports
// =============================================================================
/**
* Alle Meldungen abrufen (Admin)
*/
export async function fetchReports(filters?: ReportFilters): Promise<ReportListResponse> {
const params = new URLSearchParams()
if (filters) {
if (filters.status) {
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
statuses.forEach(s => params.append('status', s))
}
if (filters.category) {
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
categories.forEach(c => params.append('category', c))
}
if (filters.priority) params.set('priority', filters.priority)
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous))
if (filters.search) params.set('search', filters.search)
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
if (filters.dateTo) params.set('dateTo', filters.dateTo)
}
const queryString = params.toString()
const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<ReportListResponse>(url)
}
/**
* Einzelne Meldung abrufen (Admin)
*/
export async function fetchReport(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
)
}
/**
* Meldung aktualisieren (Status, Prioritaet, Kategorie, Zuweisung)
*/
export async function updateReport(id: string, update: ReportUpdateRequest): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Meldung loeschen (soft delete)
*/
export async function deleteReport(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
{
method: 'DELETE'
}
)
}
// =============================================================================
// PUBLIC ENDPOINTS - Kein Auth erforderlich
// =============================================================================
/**
* Neue Meldung einreichen (oeffentlich, keine Auth)
*/
export async function submitPublicReport(
data: PublicReportSubmission
): Promise<{ report: WhistleblowerReport; accessKey: string }> {
const response = await fetch(
`${WB_API_BASE}/api/v1/public/whistleblower/submit`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
/**
* Meldung ueber Zugangscode abrufen (oeffentlich, keine Auth)
*/
export async function fetchReportByAccessKey(
accessKey: string
): Promise<WhistleblowerReport> {
const response = await fetch(
`${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
// =============================================================================
// WORKFLOW ACTIONS
// =============================================================================
/**
* Eingangsbestaetigung versenden (HinSchG ss 17 Abs. 1)
*/
export async function acknowledgeReport(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
{
method: 'POST'
}
)
}
/**
* Untersuchung starten
*/
export async function startInvestigation(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`,
{
method: 'POST'
}
)
}
/**
* Massnahme zu einer Meldung hinzufuegen
*/
export async function addMeasure(
id: string,
measure: Omit<WhistleblowerMeasure, 'id' | 'reportId' | 'completedAt'>
): Promise<WhistleblowerMeasure> {
return fetchWithTimeout<WhistleblowerMeasure>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`,
{
method: 'POST',
body: JSON.stringify(measure)
}
)
}
/**
* Meldung abschliessen mit Begruendung
*/
export async function closeReport(
id: string,
resolution: { reason: string; notes: string }
): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`,
{
method: 'POST',
body: JSON.stringify(resolution)
}
)
}
// =============================================================================
// ANONYMOUS MESSAGING
// =============================================================================
/**
* Nachricht im anonymen Kanal senden
*/
export async function sendMessage(
reportId: string,
message: string,
role: 'reporter' | 'ombudsperson'
): Promise<AnonymousMessage> {
return fetchWithTimeout<AnonymousMessage>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`,
{
method: 'POST',
body: JSON.stringify({ senderRole: role, message })
}
)
}
/**
* Nachrichten fuer eine Meldung abrufen
*/
export async function fetchMessages(reportId: string): Promise<AnonymousMessage[]> {
return fetchWithTimeout<AnonymousMessage[]>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`
)
}
// =============================================================================
// ATTACHMENTS
// =============================================================================
/**
* Anhang zu einer Meldung hochladen
*/
export async function uploadAttachment(
reportId: string,
file: File
): Promise<FileAttachment> {
const formData = new FormData()
formData.append('file', file)
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 60000) // 60s for uploads
try {
const headers: HeadersInit = {
'X-Tenant-ID': getTenantId()
}
if (typeof window !== 'undefined') {
const token = localStorage.getItem('authToken')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
const response = await fetch(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`,
{
method: 'POST',
headers,
body: formData,
signal: controller.signal
}
)
if (!response.ok) {
throw new Error(`Upload fehlgeschlagen: ${response.statusText}`)
}
return response.json()
} finally {
clearTimeout(timeoutId)
}
}
/**
* Anhang loeschen
*/
export async function deleteAttachment(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
{
method: 'DELETE'
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Statistiken fuer das Whistleblower-Dashboard abrufen
*/
export async function fetchWhistleblowerStatistics(): Promise<WhistleblowerStatistics> {
return fetchWithTimeout<WhistleblowerStatistics>(
`${WB_API_BASE}/api/v1/admin/whistleblower/statistics`
)
}
// =============================================================================
// SDK PROXY FUNCTION (via Next.js proxy)
// =============================================================================
/**
* Fetch Whistleblower-Daten via SDK Proxy mit Fallback auf Mock-Daten
*/
export async function fetchSDKWhistleblowerList(): Promise<{
reports: WhistleblowerReport[]
statistics: WhistleblowerStatistics
}> {
try {
const [reportsResponse, statsResponse] = await Promise.all([
fetchReports(),
fetchWhistleblowerStatistics()
])
return {
reports: reportsResponse.reports,
statistics: statsResponse
}
} catch (error) {
console.error('Failed to load Whistleblower data from API, using mock data:', error)
// Fallback to mock data
const reports = createMockReports()
const statistics = createMockStatistics()
return { reports, statistics }
}
}
// =============================================================================
// MOCK DATA (Demo/Entwicklung)
// =============================================================================
/**
* Erstellt Demo-Meldungen fuer Entwicklung und Praesentationen
*/
export function createMockReports(): WhistleblowerReport[] {
const now = new Date()
// Helper: Berechne Fristen
function calcDeadlines(receivedAt: Date): { ack: string; fb: string } {
const ack = new Date(receivedAt)
ack.setDate(ack.getDate() + 7)
const fb = new Date(receivedAt)
fb.setMonth(fb.getMonth() + 3)
return { ack: ack.toISOString(), fb: fb.toISOString() }
}
const received1 = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
const deadlines1 = calcDeadlines(received1)
const received2 = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
const deadlines2 = calcDeadlines(received2)
const received3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
const deadlines3 = calcDeadlines(received3)
const received4 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
const deadlines4 = calcDeadlines(received4)
return [
// Report 1: Neu
{
id: 'wb-001',
referenceNumber: 'WB-2026-000001',
accessKey: generateAccessKey(),
category: 'corruption',
status: 'new',
priority: 'high',
title: 'Unregelmaessigkeiten bei Auftragsvergabe',
description: 'Bei der Vergabe des IT-Rahmenvertrags im November wurden offenbar Angebote eines bestimmten Anbieters bevorzugt. Der zustaendige Abteilungsleiter hat private Verbindungen zum Geschaeftsfuehrer des Anbieters.',
isAnonymous: true,
receivedAt: received1.toISOString(),
deadlineAcknowledgment: deadlines1.ack,
deadlineFeedback: deadlines1.fb,
measures: [],
messages: [],
attachments: [],
auditTrail: [
{
id: 'audit-001',
action: 'report_created',
description: 'Meldung ueber Online-Meldeformular eingegangen',
performedBy: 'system',
performedAt: received1.toISOString()
}
]
},
// Report 2: In Pruefung (under_review)
{
id: 'wb-002',
referenceNumber: 'WB-2026-000002',
accessKey: generateAccessKey(),
category: 'data_protection',
status: 'under_review',
priority: 'normal',
title: 'Unerlaubte Weitergabe von Kundendaten',
description: 'Ein Mitarbeiter der Vertriebsabteilung gibt regelmaessig Kundenlisten an externe Dienstleister weiter, ohne dass eine Auftragsverarbeitungsvereinbarung vorliegt.',
isAnonymous: false,
reporterName: 'Maria Schmidt',
reporterEmail: 'maria.schmidt@example.de',
assignedTo: 'DSB Mueller',
receivedAt: received2.toISOString(),
acknowledgedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
deadlineAcknowledgment: deadlines2.ack,
deadlineFeedback: deadlines2.fb,
measures: [],
messages: [
{
id: 'msg-001',
reportId: 'wb-002',
senderRole: 'ombudsperson',
message: 'Vielen Dank fuer Ihre Meldung. Koennen Sie uns mitteilen, welche Dienstleister konkret betroffen sind?',
createdAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
isRead: true
},
{
id: 'msg-002',
reportId: 'wb-002',
senderRole: 'reporter',
message: 'Es handelt sich um die Firma DataServ GmbH und MarketPro AG. Die Listen werden per unverschluesselter E-Mail versendet.',
createdAt: new Date(received2.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
isRead: true
}
],
attachments: [
{
id: 'att-001',
fileName: 'email_screenshot_vertrieb.png',
fileSize: 245000,
mimeType: 'image/png',
uploadedAt: received2.toISOString(),
uploadedBy: 'reporter'
}
],
auditTrail: [
{
id: 'audit-002',
action: 'report_created',
description: 'Meldung per E-Mail eingegangen',
performedBy: 'system',
performedAt: received2.toISOString()
},
{
id: 'audit-003',
action: 'acknowledged',
description: 'Eingangsbestaetigung an Hinweisgeber versendet',
performedBy: 'DSB Mueller',
performedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'audit-004',
action: 'status_changed',
description: 'Status geaendert: Bestaetigt -> In Pruefung',
performedBy: 'DSB Mueller',
performedAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
}
]
},
// Report 3: Untersuchung (investigation)
{
id: 'wb-003',
referenceNumber: 'WB-2026-000003',
accessKey: generateAccessKey(),
category: 'product_safety',
status: 'investigation',
priority: 'critical',
title: 'Fehlende Sicherheitspruefungen bei Produktfreigabe',
description: 'In der Fertigung werden seit Wochen Produkte ohne die vorgeschriebenen Sicherheitspruefungen freigegeben. Pruefprotokolle werden nachtraeglich erstellt, ohne dass tatsaechliche Pruefungen stattfinden.',
isAnonymous: true,
assignedTo: 'Qualitaetsbeauftragter Weber',
receivedAt: received3.toISOString(),
acknowledgedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
deadlineAcknowledgment: deadlines3.ack,
deadlineFeedback: deadlines3.fb,
measures: [
{
id: 'msr-001',
reportId: 'wb-003',
title: 'Sofortiger Produktionsstopp fuer betroffene Charge',
description: 'Produktion der betroffenen Produktlinie stoppen bis Pruefverfahren sichergestellt ist',
status: 'completed',
responsible: 'Fertigungsleitung',
dueDate: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(received3.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'msr-002',
reportId: 'wb-003',
title: 'Externe Pruefung der Pruefprotokolle',
description: 'Unabhaengige Pruefstelle mit der Revision aller Pruefprotokolle der letzten 6 Monate beauftragen',
status: 'in_progress',
responsible: 'Qualitaetsmanagement',
dueDate: new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString()
}
],
messages: [],
attachments: [
{
id: 'att-002',
fileName: 'pruefprotokoll_vergleich.pdf',
fileSize: 890000,
mimeType: 'application/pdf',
uploadedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
uploadedBy: 'ombudsperson'
}
],
auditTrail: [
{
id: 'audit-005',
action: 'report_created',
description: 'Meldung ueber Online-Meldeformular eingegangen',
performedBy: 'system',
performedAt: received3.toISOString()
},
{
id: 'audit-006',
action: 'acknowledged',
description: 'Eingangsbestaetigung versendet',
performedBy: 'Qualitaetsbeauftragter Weber',
performedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'audit-007',
action: 'investigation_started',
description: 'Formelle Untersuchung eingeleitet',
performedBy: 'Qualitaetsbeauftragter Weber',
performedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
}
]
},
// Report 4: Abgeschlossen (closed)
{
id: 'wb-004',
referenceNumber: 'WB-2026-000004',
accessKey: generateAccessKey(),
category: 'fraud',
status: 'closed',
priority: 'high',
title: 'Gefaelschte Reisekostenabrechnungen',
description: 'Ein leitender Mitarbeiter reicht seit ueber einem Jahr gefaelschte Reisekostenabrechnungen ein. Hotelrechnungen werden manipuliert, Taxiquittungen erfunden.',
isAnonymous: false,
reporterName: 'Thomas Klein',
reporterEmail: 'thomas.klein@example.de',
reporterPhone: '+49 170 9876543',
assignedTo: 'Compliance-Abteilung',
receivedAt: received4.toISOString(),
acknowledgedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
deadlineAcknowledgment: deadlines4.ack,
deadlineFeedback: deadlines4.fb,
closedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
measures: [
{
id: 'msr-003',
reportId: 'wb-004',
title: 'Interne Revision der Reisekosten',
description: 'Pruefung aller Reisekostenabrechnungen des betroffenen Mitarbeiters der letzten 24 Monate',
status: 'completed',
responsible: 'Interne Revision',
dueDate: new Date(received4.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(received4.getTime() + 25 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'msr-004',
reportId: 'wb-004',
title: 'Arbeitsrechtliche Konsequenzen',
description: 'Einleitung arbeitsrechtlicher Schritte nach Bestaetigung des Betrugs',
status: 'completed',
responsible: 'Personalabteilung',
dueDate: new Date(received4.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString(),
completedAt: new Date(received4.getTime() + 55 * 24 * 60 * 60 * 1000).toISOString()
}
],
messages: [],
attachments: [
{
id: 'att-003',
fileName: 'vergleich_originalrechnung_einreichung.pdf',
fileSize: 567000,
mimeType: 'application/pdf',
uploadedAt: received4.toISOString(),
uploadedBy: 'reporter'
}
],
auditTrail: [
{
id: 'audit-008',
action: 'report_created',
description: 'Meldung per Brief eingegangen',
performedBy: 'system',
performedAt: received4.toISOString()
},
{
id: 'audit-009',
action: 'acknowledged',
description: 'Eingangsbestaetigung versendet',
performedBy: 'Compliance-Abteilung',
performedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: 'audit-010',
action: 'closed',
description: 'Fall abgeschlossen - Betrug bestaetigt, arbeitsrechtliche Massnahmen eingeleitet',
performedBy: 'Compliance-Abteilung',
performedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString()
}
]
}
]
}
/**
* Berechnet Statistiken aus den Mock-Daten
*/
export function createMockStatistics(): WhistleblowerStatistics {
const reports = createMockReports()
const now = new Date()
const byStatus: Record<ReportStatus, number> = {
new: 0,
acknowledged: 0,
under_review: 0,
investigation: 0,
measures_taken: 0,
closed: 0,
rejected: 0
}
const byCategory: Record<ReportCategory, number> = {
corruption: 0,
fraud: 0,
data_protection: 0,
discrimination: 0,
environment: 0,
competition: 0,
product_safety: 0,
tax_evasion: 0,
other: 0
}
reports.forEach(r => {
byStatus[r.status]++
byCategory[r.category]++
})
const closedStatuses: ReportStatus[] = ['closed', 'rejected']
// Pruefe ueberfaellige Eingangsbestaetigungen
const overdueAcknowledgment = reports.filter(r => {
if (r.status !== 'new') return false
return now > new Date(r.deadlineAcknowledgment)
}).length
// Pruefe ueberfaellige Rueckmeldungen
const overdueFeedback = reports.filter(r => {
if (closedStatuses.includes(r.status)) return false
return now > new Date(r.deadlineFeedback)
}).length
return {
totalReports: reports.length,
newReports: byStatus.new,
underReview: byStatus.under_review + byStatus.investigation,
closed: byStatus.closed + byStatus.rejected,
overdueAcknowledgment,
overdueFeedback,
byCategory,
byStatus
}
}

View File

@@ -0,0 +1,381 @@
/**
* Whistleblower System (Hinweisgebersystem) Types
*
* TypeScript definitions for Hinweisgeberschutzgesetz (HinSchG)
* compliant Whistleblower/Hinweisgebersystem module
*/
// =============================================================================
// ENUMS & CONSTANTS
// =============================================================================
export type ReportCategory =
| 'corruption' // Korruption
| 'fraud' // Betrug
| 'data_protection' // Datenschutz
| 'discrimination' // Diskriminierung
| 'environment' // Umwelt
| 'competition' // Wettbewerb
| 'product_safety' // Produktsicherheit
| 'tax_evasion' // Steuerhinterziehung
| 'other' // Sonstiges
export type ReportStatus =
| 'new' // Neu eingegangen
| 'acknowledged' // Eingangsbestaetigung versendet
| 'under_review' // In Pruefung
| 'investigation' // Untersuchung laeuft
| 'measures_taken' // Massnahmen ergriffen
| 'closed' // Abgeschlossen
| 'rejected' // Abgelehnt
export type ReportPriority = 'low' | 'normal' | 'high' | 'critical'
// =============================================================================
// REPORT CATEGORY METADATA
// =============================================================================
export interface ReportCategoryInfo {
category: ReportCategory
label: string
description: string
icon: string
color: string
bgColor: string
}
export const REPORT_CATEGORY_INFO: Record<ReportCategory, ReportCategoryInfo> = {
corruption: {
category: 'corruption',
label: 'Korruption',
description: 'Bestechung, Bestechlichkeit, Vorteilsnahme oder Vorteilsgewaehrung',
icon: '\u{1F4B0}',
color: 'text-red-700',
bgColor: 'bg-red-100'
},
fraud: {
category: 'fraud',
label: 'Betrug',
description: 'Betrug, Untreue, Urkundenfaelschung oder sonstige Vermoegensstraftaten',
icon: '\u{1F3AD}',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
data_protection: {
category: 'data_protection',
label: 'Datenschutz',
description: 'Verstoesse gegen Datenschutzvorschriften (DSGVO, BDSG)',
icon: '\u{1F512}',
color: 'text-blue-700',
bgColor: 'bg-blue-100'
},
discrimination: {
category: 'discrimination',
label: 'Diskriminierung',
description: 'Diskriminierung, Mobbing, sexuelle Belaestigung oder Benachteiligung',
icon: '\u{26A0}\u{FE0F}',
color: 'text-purple-700',
bgColor: 'bg-purple-100'
},
environment: {
category: 'environment',
label: 'Umwelt',
description: 'Umweltverschmutzung, illegale Entsorgung oder Verstoesse gegen Umweltauflagen',
icon: '\u{1F33F}',
color: 'text-green-700',
bgColor: 'bg-green-100'
},
competition: {
category: 'competition',
label: 'Wettbewerb',
description: 'Kartellrechtsverstoesse, unlauterer Wettbewerb, Marktmanipulation',
icon: '\u{2696}\u{FE0F}',
color: 'text-indigo-700',
bgColor: 'bg-indigo-100'
},
product_safety: {
category: 'product_safety',
label: 'Produktsicherheit',
description: 'Verstoesse gegen Produktsicherheitsvorschriften, mangelhafte Produkte, fehlende Warnhinweise',
icon: '\u{1F6E1}\u{FE0F}',
color: 'text-yellow-700',
bgColor: 'bg-yellow-100'
},
tax_evasion: {
category: 'tax_evasion',
label: 'Steuerhinterziehung',
description: 'Steuerhinterziehung, Steuerumgehung oder sonstige Steuerverstoesse',
icon: '\u{1F4C4}',
color: 'text-teal-700',
bgColor: 'bg-teal-100'
},
other: {
category: 'other',
label: 'Sonstiges',
description: 'Sonstige Verstoesse gegen geltendes Recht oder interne Richtlinien',
icon: '\u{1F4CB}',
color: 'text-gray-700',
bgColor: 'bg-gray-100'
}
}
// =============================================================================
// REPORT STATUS METADATA
// =============================================================================
export const REPORT_STATUS_INFO: Record<ReportStatus, { label: string; description: string; color: string; bgColor: string }> = {
new: {
label: 'Neu',
description: 'Meldung ist eingegangen, Eingangsbestaetigung steht aus',
color: 'text-blue-700',
bgColor: 'bg-blue-100'
},
acknowledged: {
label: 'Bestaetigt',
description: 'Eingangsbestaetigung wurde an den Hinweisgeber versendet',
color: 'text-cyan-700',
bgColor: 'bg-cyan-100'
},
under_review: {
label: 'In Pruefung',
description: 'Meldung wird inhaltlich geprueft und bewertet',
color: 'text-yellow-700',
bgColor: 'bg-yellow-100'
},
investigation: {
label: 'Untersuchung',
description: 'Formelle Untersuchung des gemeldeten Sachverhalts laeuft',
color: 'text-purple-700',
bgColor: 'bg-purple-100'
},
measures_taken: {
label: 'Massnahmen ergriffen',
description: 'Folgemaßnahmen wurden eingeleitet oder abgeschlossen',
color: 'text-orange-700',
bgColor: 'bg-orange-100'
},
closed: {
label: 'Abgeschlossen',
description: 'Fall wurde abgeschlossen und dokumentiert',
color: 'text-green-700',
bgColor: 'bg-green-100'
},
rejected: {
label: 'Abgelehnt',
description: 'Meldung wurde als unbegrundet oder nicht zustaendig abgelehnt',
color: 'text-red-700',
bgColor: 'bg-red-100'
}
}
// =============================================================================
// MAIN INTERFACES
// =============================================================================
export interface FileAttachment {
id: string
fileName: string
fileSize: number
mimeType: string
uploadedAt: string
uploadedBy: string
}
export interface AuditEntry {
id: string
action: string
description: string
performedBy: string
performedAt: string
}
export interface AnonymousMessage {
id: string
reportId: string
senderRole: 'reporter' | 'ombudsperson'
message: string
createdAt: string
isRead: boolean
}
export interface WhistleblowerMeasure {
id: string
reportId: string
title: string
description: string
status: 'planned' | 'in_progress' | 'completed'
responsible: string
dueDate: string
completedAt?: string
}
export interface WhistleblowerReport {
id: string
referenceNumber: string // z.B. "WB-2026-000042"
accessKey: string // Anonymer Zugangscode fuer den Hinweisgeber
category: ReportCategory
status: ReportStatus
priority: ReportPriority
title: string
description: string
// Hinweisgeber-Info (optional bei anonymen Meldungen)
isAnonymous: boolean
reporterName?: string
reporterEmail?: string
reporterPhone?: string
// Zuweisung
assignedTo?: string
// Zeitstempel
receivedAt: string
acknowledgedAt?: string
// Fristen gemaess HinSchG
deadlineAcknowledgment: string // 7 Tage nach Eingang (ss 17 Abs. 1 S. 2)
deadlineFeedback: string // 3 Monate nach Eingang (ss 17 Abs. 2)
closedAt?: string
// Verknuepfte Daten
measures: WhistleblowerMeasure[]
messages: AnonymousMessage[]
attachments: FileAttachment[]
auditTrail: AuditEntry[]
}
// =============================================================================
// STATISTICS
// =============================================================================
export interface WhistleblowerStatistics {
totalReports: number
newReports: number
underReview: number
closed: number
overdueAcknowledgment: number
overdueFeedback: number
byCategory: Record<ReportCategory, number>
byStatus: Record<ReportStatus, number>
}
// =============================================================================
// DEADLINE TRACKING (HinSchG)
// =============================================================================
/**
* Gibt die verbleibenden Tage bis zur Eingangsbestaetigung zurueck (7-Tage-Frist)
* Negative Werte bedeuten ueberfaellig
*/
export function getDaysUntilAcknowledgment(report: WhistleblowerReport): number {
if (report.acknowledgedAt || report.status !== 'new') {
return 0
}
const deadline = new Date(report.deadlineAcknowledgment)
const now = new Date()
const diff = deadline.getTime() - now.getTime()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
/**
* Gibt die verbleibenden Tage bis zur Rueckmeldungsfrist zurueck (3-Monate-Frist)
* Negative Werte bedeuten ueberfaellig
*/
export function getDaysUntilFeedback(report: WhistleblowerReport): number {
if (report.status === 'closed' || report.status === 'rejected') {
return 0
}
const deadline = new Date(report.deadlineFeedback)
const now = new Date()
const diff = deadline.getTime() - now.getTime()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
/**
* Prueft ob die Eingangsbestaetigungsfrist ueberschritten ist (7 Tage, HinSchG ss 17 Abs. 1)
*/
export function isAcknowledgmentOverdue(report: WhistleblowerReport): boolean {
if (report.acknowledgedAt || report.status !== 'new') {
return false
}
return new Date() > new Date(report.deadlineAcknowledgment)
}
/**
* Prueft ob die Rueckmeldungsfrist ueberschritten ist (3 Monate, HinSchG ss 17 Abs. 2)
*/
export function isFeedbackOverdue(report: WhistleblowerReport): boolean {
if (report.status === 'closed' || report.status === 'rejected') {
return false
}
return new Date() > new Date(report.deadlineFeedback)
}
/**
* Generiert einen anonymen Zugangscode im Format XXXX-XXXX-XXXX
*/
export function generateAccessKey(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // Kein I, O, 0, 1 fuer Lesbarkeit
let result = ''
for (let i = 0; i < 12; i++) {
if (i > 0 && i % 4 === 0) result += '-'
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result // Format: XXXX-XXXX-XXXX
}
// =============================================================================
// API TYPES
// =============================================================================
export interface ReportFilters {
status?: ReportStatus | ReportStatus[]
category?: ReportCategory | ReportCategory[]
priority?: ReportPriority
assignedTo?: string
isAnonymous?: boolean
search?: string
dateFrom?: string
dateTo?: string
}
export interface ReportListResponse {
reports: WhistleblowerReport[]
total: number
page: number
pageSize: number
}
export interface PublicReportSubmission {
category: ReportCategory
title: string
description: string
isAnonymous: boolean
reporterName?: string
reporterEmail?: string
reporterPhone?: string
}
export interface ReportUpdateRequest {
status?: ReportStatus
priority?: ReportPriority
category?: ReportCategory
assignedTo?: string
}
export interface MessageSendRequest {
senderRole: 'reporter' | 'ombudsperson'
message: string
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function getCategoryInfo(category: ReportCategory): ReportCategoryInfo {
return REPORT_CATEGORY_INFO[category]
}
export function getStatusInfo(status: ReportStatus) {
return REPORT_STATUS_INFO[status]
}