/** * Whistleblower API Client — CRUD, Workflow, Messaging, Attachments, Statistics * * API client for Hinweisgeberschutzgesetz (HinSchG) compliant * Whistleblower/Hinweisgebersystem management */ import { WhistleblowerReport, WhistleblowerStatistics, ReportListResponse, ReportFilters, PublicReportSubmission, ReportUpdateRequest, AnonymousMessage, WhistleblowerMeasure, FileAttachment, } from './types' // ============================================================================= // CONFIGURATION // ============================================================================= const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093' const API_TIMEOUT = 30000 // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function getTenantId(): string { if (typeof window !== 'undefined') { return localStorage.getItem('bp_tenant_id') || 'default-tenant' } return 'default-tenant' } function getAuthHeaders(): HeadersInit { const headers: HeadersInit = { 'Content-Type': 'application/json', 'X-Tenant-ID': getTenantId() } if (typeof window !== 'undefined') { const token = localStorage.getItem('authToken') if (token) { headers['Authorization'] = `Bearer ${token}` } const userId = localStorage.getItem('bp_user_id') if (userId) { headers['X-User-ID'] = userId } } return headers } async function fetchWithTimeout( url: string, options: RequestInit = {}, timeout: number = API_TIMEOUT ): Promise { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) try { const response = await fetch(url, { ...options, signal: controller.signal, headers: { ...getAuthHeaders(), ...options.headers } }) if (!response.ok) { const errorBody = await response.text() let errorMessage = `HTTP ${response.status}: ${response.statusText}` try { const errorJson = JSON.parse(errorBody) errorMessage = errorJson.error || errorJson.message || errorMessage } catch { // Keep the HTTP status message } throw new Error(errorMessage) } const contentType = response.headers.get('content-type') if (contentType && contentType.includes('application/json')) { return response.json() } return {} as T } finally { clearTimeout(timeoutId) } } // ============================================================================= // ADMIN CRUD - Reports // ============================================================================= export async function fetchReports(filters?: ReportFilters): Promise { const params = new URLSearchParams() if (filters) { if (filters.status) { const statuses = Array.isArray(filters.status) ? filters.status : [filters.status] statuses.forEach(s => params.append('status', s)) } if (filters.category) { const categories = Array.isArray(filters.category) ? filters.category : [filters.category] categories.forEach(c => params.append('category', c)) } if (filters.priority) params.set('priority', filters.priority) if (filters.assignedTo) params.set('assignedTo', filters.assignedTo) if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous)) if (filters.search) params.set('search', filters.search) if (filters.dateFrom) params.set('dateFrom', filters.dateFrom) if (filters.dateTo) params.set('dateTo', filters.dateTo) } const queryString = params.toString() const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}` return fetchWithTimeout(url) } export async function fetchReport(id: string): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}` ) } export async function updateReport(id: string, update: ReportUpdateRequest): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`, { method: 'PUT', body: JSON.stringify(update) } ) } export async function deleteReport(id: string): Promise { await fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`, { method: 'DELETE' } ) } // ============================================================================= // PUBLIC ENDPOINTS // ============================================================================= export async function submitPublicReport( data: PublicReportSubmission ): Promise<{ report: WhistleblowerReport; accessKey: string }> { const response = await fetch( `${WB_API_BASE}/api/v1/public/whistleblower/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) } ) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } return response.json() } export async function fetchReportByAccessKey( accessKey: string ): Promise { const response = await fetch( `${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } return response.json() } // ============================================================================= // WORKFLOW ACTIONS // ============================================================================= export async function acknowledgeReport(id: string): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`, { method: 'POST' } ) } export async function startInvestigation(id: string): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`, { method: 'POST' } ) } export async function addMeasure( id: string, measure: Omit ): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`, { method: 'POST', body: JSON.stringify(measure) } ) } export async function closeReport( id: string, resolution: { reason: string; notes: string } ): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`, { method: 'POST', body: JSON.stringify(resolution) } ) } // ============================================================================= // ANONYMOUS MESSAGING // ============================================================================= export async function sendMessage( reportId: string, message: string, role: 'reporter' | 'ombudsperson' ): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`, { method: 'POST', body: JSON.stringify({ senderRole: role, message }) } ) } export async function fetchMessages(reportId: string): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages` ) } // ============================================================================= // ATTACHMENTS // ============================================================================= export async function uploadAttachment( reportId: string, file: File ): Promise { const formData = new FormData() formData.append('file', file) const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 60000) try { const headers: HeadersInit = { 'X-Tenant-ID': getTenantId() } if (typeof window !== 'undefined') { const token = localStorage.getItem('authToken') if (token) { headers['Authorization'] = `Bearer ${token}` } } const response = await fetch( `${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`, { method: 'POST', headers, body: formData, signal: controller.signal } ) if (!response.ok) { throw new Error(`Upload fehlgeschlagen: ${response.statusText}`) } return response.json() } finally { clearTimeout(timeoutId) } } export async function deleteAttachment(id: string): Promise { await fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`, { method: 'DELETE' } ) } // ============================================================================= // STATISTICS // ============================================================================= export async function fetchWhistleblowerStatistics(): Promise { return fetchWithTimeout( `${WB_API_BASE}/api/v1/admin/whistleblower/statistics` ) }