feat: add frontend pages, API routes and libs for all SDK modules
Add Next.js pages for Academy, Whistleblower, Incidents, Document Crawler, DSB Portal, Industry Templates, Multi-Tenant and SSO. Add API proxy routes and TypeScript SDK client libraries. Add server binary to .gitignore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
755
admin-compliance/lib/sdk/whistleblower/api.ts
Normal file
755
admin-compliance/lib/sdk/whistleblower/api.ts
Normal 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
|
||||
}
|
||||
}
|
||||
381
admin-compliance/lib/sdk/whistleblower/types.ts
Normal file
381
admin-compliance/lib/sdk/whistleblower/types.ts
Normal 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]
|
||||
}
|
||||
Reference in New Issue
Block a user