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>
756 lines
23 KiB
TypeScript
756 lines
23 KiB
TypeScript
/**
|
|
* 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
|
|
}
|
|
}
|