'use client' import React, { useState, useEffect, useMemo } from 'react' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import { WhistleblowerReport, WhistleblowerStatistics, ReportCategory, ReportStatus, ReportPriority, REPORT_CATEGORY_INFO, REPORT_STATUS_INFO, isAcknowledgmentOverdue, isFeedbackOverdue, getDaysUntilAcknowledgment, getDaysUntilFeedback } from '@/lib/sdk/whistleblower/types' import { fetchSDKWhistleblowerList } from '@/lib/sdk/whistleblower/api' // ============================================================================= // TYPES // ============================================================================= type TabId = 'overview' | 'new_reports' | 'investigation' | 'closed' | 'settings' interface Tab { id: TabId label: string count?: number countColor?: string } // ============================================================================= // COMPONENTS // ============================================================================= function TabNavigation({ tabs, activeTab, onTabChange }: { tabs: Tab[] activeTab: TabId onTabChange: (tab: TabId) => void }) { return (
) } function StatCard({ label, value, color = 'gray', icon, trend }: { label: string value: number | string color?: 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'purple' icon?: React.ReactNode trend?: { value: number; label: string } }) { const colorClasses = { gray: 'border-gray-200 text-gray-900', blue: 'border-blue-200 text-blue-600', yellow: 'border-yellow-200 text-yellow-600', red: 'border-red-200 text-red-600', green: 'border-green-200 text-green-600', purple: 'border-purple-200 text-purple-600' } return (
{label}
{value}
{trend && (
= 0 ? 'text-green-600' : 'text-red-600'}`}> {trend.value >= 0 ? '+' : ''}{trend.value} {trend.label}
)}
{icon && (
{icon}
)}
) } function FilterBar({ selectedCategory, selectedStatus, selectedPriority, onCategoryChange, onStatusChange, onPriorityChange, onClear }: { selectedCategory: ReportCategory | 'all' selectedStatus: ReportStatus | 'all' selectedPriority: ReportPriority | 'all' onCategoryChange: (category: ReportCategory | 'all') => void onStatusChange: (status: ReportStatus | 'all') => void onPriorityChange: (priority: ReportPriority | 'all') => void onClear: () => void }) { const hasFilters = selectedCategory !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all' return (
Filter: {/* Category Filter */} {/* Status Filter */} {/* Priority Filter */} {/* Clear Filters */} {hasFilters && ( )}
) } function ReportCard({ report }: { report: WhistleblowerReport }) { const categoryInfo = REPORT_CATEGORY_INFO[report.category] const statusInfo = REPORT_STATUS_INFO[report.status] const isClosed = report.status === 'closed' || report.status === 'rejected' const ackOverdue = isAcknowledgmentOverdue(report) const fbOverdue = isFeedbackOverdue(report) const daysAck = getDaysUntilAcknowledgment(report) const daysFb = getDaysUntilFeedback(report) const completedMeasures = report.measures.filter(m => m.status === 'completed').length const totalMeasures = report.measures.length const priorityLabels: Record = { low: 'Niedrig', normal: 'Normal', high: 'Hoch', critical: 'Kritisch' } return (
{/* Header Badges */}
{report.referenceNumber} {categoryInfo.label} {statusInfo.label} {report.isAnonymous && ( Anonym )} {report.priority === 'critical' && ( Kritisch )} {report.priority === 'high' && ( Hoch )}
{/* Title */}

{report.title}

{/* Description Preview */} {report.description && (

{report.description}

)} {/* Deadline Info */} {!isClosed && (
{report.status === 'new' && ( {ackOverdue ? `Bestaetigung ${Math.abs(daysAck)} Tage ueberfaellig` : `Bestaetigung in ${daysAck} Tagen` } )} {fbOverdue ? `Rueckmeldung ${Math.abs(daysFb)} Tage ueberfaellig` : `Rueckmeldung in ${daysFb} Tagen` }
)}
{/* Right Side - Date & Priority */}
{isClosed ? statusInfo.label : ackOverdue ? 'Ueberfaellig' : priorityLabels[report.priority] }
{new Date(report.receivedAt).toLocaleDateString('de-DE')}
{/* Footer */}
{report.assignedTo ? `Zugewiesen: ${report.assignedTo}` : 'Nicht zugewiesen' }
{report.attachments.length > 0 && ( {report.attachments.length} Anhang{report.attachments.length !== 1 ? 'e' : ''} )} {totalMeasures > 0 && ( {completedMeasures}/{totalMeasures} Massnahmen )} {report.messages.length > 0 && ( {report.messages.length} Nachricht{report.messages.length !== 1 ? 'en' : ''} )}
{!isClosed && ( Bearbeiten )} {isClosed && ( Details )}
) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function WhistleblowerPage() { const { state } = useSDK() const [activeTab, setActiveTab] = useState('overview') const [reports, setReports] = useState([]) const [statistics, setStatistics] = useState(null) const [isLoading, setIsLoading] = useState(true) // Filters const [selectedCategory, setSelectedCategory] = useState('all') const [selectedStatus, setSelectedStatus] = useState('all') const [selectedPriority, setSelectedPriority] = useState('all') // Load data from SDK backend useEffect(() => { const loadData = async () => { setIsLoading(true) try { const { reports: wbReports, statistics: wbStats } = await fetchSDKWhistleblowerList() setReports(wbReports) setStatistics(wbStats) } catch (error) { console.error('Failed to load Whistleblower data:', error) } finally { setIsLoading(false) } } loadData() }, []) // Locally computed overdue counts (always fresh) const overdueCounts = useMemo(() => { const overdueAck = reports.filter(r => isAcknowledgmentOverdue(r)).length const overdueFb = reports.filter(r => isFeedbackOverdue(r)).length return { overdueAck, overdueFb } }, [reports]) // Calculate tab counts const tabCounts = useMemo(() => { const investigationStatuses: ReportStatus[] = ['acknowledged', 'under_review', 'investigation', 'measures_taken'] const closedStatuses: ReportStatus[] = ['closed', 'rejected'] return { new_reports: reports.filter(r => r.status === 'new').length, investigation: reports.filter(r => investigationStatuses.includes(r.status)).length, closed: reports.filter(r => closedStatuses.includes(r.status)).length } }, [reports]) // Filter reports based on active tab and filters const filteredReports = useMemo(() => { let filtered = [...reports] // Tab-based filtering const investigationStatuses: ReportStatus[] = ['acknowledged', 'under_review', 'investigation', 'measures_taken'] const closedStatuses: ReportStatus[] = ['closed', 'rejected'] if (activeTab === 'new_reports') { filtered = filtered.filter(r => r.status === 'new') } else if (activeTab === 'investigation') { filtered = filtered.filter(r => investigationStatuses.includes(r.status)) } else if (activeTab === 'closed') { filtered = filtered.filter(r => closedStatuses.includes(r.status)) } // Category filter if (selectedCategory !== 'all') { filtered = filtered.filter(r => r.category === selectedCategory) } // Status filter if (selectedStatus !== 'all') { filtered = filtered.filter(r => r.status === selectedStatus) } // Priority filter if (selectedPriority !== 'all') { filtered = filtered.filter(r => r.priority === selectedPriority) } // Sort: overdue first, then by priority, then by date return filtered.sort((a, b) => { const closedStatuses: ReportStatus[] = ['closed', 'rejected'] const getUrgency = (r: WhistleblowerReport) => { if (closedStatuses.includes(r.status)) return 1000 const ackOd = isAcknowledgmentOverdue(r) const fbOd = isFeedbackOverdue(r) if (ackOd || fbOd) return -100 const priorityScore = { critical: 0, high: 1, normal: 2, low: 3 } return priorityScore[r.priority] ?? 2 } const urgencyDiff = getUrgency(a) - getUrgency(b) if (urgencyDiff !== 0) return urgencyDiff return new Date(b.receivedAt).getTime() - new Date(a.receivedAt).getTime() }) }, [reports, activeTab, selectedCategory, selectedStatus, selectedPriority]) const tabs: Tab[] = [ { id: 'overview', label: 'Uebersicht' }, { id: 'new_reports', label: 'Neue Meldungen', count: tabCounts.new_reports, countColor: 'bg-blue-100 text-blue-600' }, { id: 'investigation', label: 'In Untersuchung', count: tabCounts.investigation, countColor: 'bg-yellow-100 text-yellow-600' }, { id: 'closed', label: 'Abgeschlossen', count: tabCounts.closed, countColor: 'bg-green-100 text-green-600' }, { id: 'settings', label: 'Einstellungen' } ] const stepInfo = STEP_EXPLANATIONS['whistleblower'] const clearFilters = () => { setSelectedCategory('all') setSelectedStatus('all') setSelectedPriority('all') } return (
{/* Step Header - NO "create report" button (reports come from the public form) */} {/* Tab Navigation */} {/* Loading State */} {isLoading ? (
) : activeTab === 'settings' ? ( /* Settings Tab */

Einstellungen

Hinweisgebersystem-Einstellungen, Meldekanal-Konfiguration, Ombudsperson-Verwaltung und E-Mail-Vorlagen werden in einer spaeteren Version verfuegbar sein.

) : ( <> {/* Statistics (Overview Tab) */} {activeTab === 'overview' && statistics && (
0 ? 'red' : 'green'} />
)} {/* Overdue Alert for Acknowledgment Deadline (7 days HinSchG) */} {(overdueCounts.overdueAck > 0 || overdueCounts.overdueFb > 0) && (activeTab === 'overview' || activeTab === 'new_reports' || activeTab === 'investigation') && (

Achtung: Gesetzliche Fristen ueberschritten

{overdueCounts.overdueAck > 0 && ( {overdueCounts.overdueAck} Meldung(en) ohne Eingangsbestaetigung (mehr als 7 Tage, HinSchG ss 17 Abs. 1). )} {overdueCounts.overdueFb > 0 && ( {overdueCounts.overdueFb} Meldung(en) ohne Rueckmeldung (mehr als 3 Monate, HinSchG ss 17 Abs. 2). )} Handeln Sie umgehend, um Bussgelder und Haftungsrisiken zu vermeiden.

)} {/* Info Box about HinSchG Deadlines (Overview Tab) */} {activeTab === 'overview' && (

HinSchG-Fristen

Nach dem Hinweisgeberschutzgesetz (HinSchG) gelten folgende Fristen: Die Eingangsbestaetigung muss innerhalb von 7 Tagen an den Hinweisgeber versendet werden (ss 17 Abs. 1 S. 2). Eine Rueckmeldung ueber ergriffene Massnahmen muss innerhalb von 3 Monaten nach Eingangsbestaetigung erfolgen (ss 17 Abs. 2). Der Schutz des Hinweisgebers vor Repressalien ist zwingend sicherzustellen (ss 36).

)} {/* Filters */} {/* Report List */}
{filteredReports.map(report => ( ))}
{/* Empty State */} {filteredReports.length === 0 && (

Keine Meldungen gefunden

{selectedCategory !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all' ? 'Passen Sie die Filter an oder setzen Sie sie zurueck.' : 'Es sind noch keine Meldungen im Hinweisgebersystem vorhanden. Meldungen werden ueber das oeffentliche Meldeformular eingereicht.' }

{(selectedCategory !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all') && ( )}
)} )}
) }