'use client' import React, { useState, useEffect, useCallback, useRef } from 'react' // ============================================================================= // TYPES // ============================================================================= interface AssignmentOverview { id: string dsb_user_id: string tenant_id: string tenant_name: string tenant_slug: string status: string contract_start: string contract_end: string | null monthly_hours_budget: number notes: string compliance_score: number hours_this_month: number hours_budget: number open_task_count: number urgent_task_count: number next_deadline: string | null created_at: string updated_at: string } interface DSBDashboard { assignments: AssignmentOverview[] total_assignments: number active_assignments: number total_hours_this_month: number open_tasks: number urgent_tasks: number generated_at: string } interface HourEntry { id: string assignment_id: string date: string hours: number category: string description: string billable: boolean created_at: string } interface Task { id: string assignment_id: string title: string description: string category: string priority: string status: string due_date: string | null completed_at: string | null created_at: string updated_at: string } interface Communication { id: string assignment_id: string direction: string channel: string subject: string content: string participants: string created_at: string } interface HoursSummary { total_hours: number billable_hours: number by_category: Record period: string } // ============================================================================= // CONSTANTS // ============================================================================= const DSB_USER_ID = '00000000-0000-0000-0000-000000000001' const TASK_CATEGORIES = [ 'DSFA-Pruefung', 'Betroffenenanfrage', 'Vorfall-Pruefung', 'Audit-Vorbereitung', 'Richtlinien-Pruefung', 'Schulung', 'Beratung', 'Sonstiges', ] const HOUR_CATEGORIES = [ 'DSFA-Pruefung', 'Beratung', 'Audit', 'Schulung', 'Vorfallreaktion', 'Dokumentation', 'Besprechung', 'Sonstiges', ] const COMM_CHANNELS = ['E-Mail', 'Telefon', 'Besprechung', 'Portal', 'Brief'] const PRIORITY_LABELS: Record = { urgent: 'Dringend', high: 'Hoch', medium: 'Mittel', low: 'Niedrig', } const PRIORITY_COLORS: Record = { urgent: 'bg-red-100 text-red-700 border-red-200', high: 'bg-orange-100 text-orange-700 border-orange-200', medium: 'bg-blue-100 text-blue-700 border-blue-200', low: 'bg-gray-100 text-gray-500 border-gray-200', } const TASK_STATUS_LABELS: Record = { open: 'Offen', in_progress: 'In Bearbeitung', waiting: 'Wartend', completed: 'Erledigt', cancelled: 'Abgebrochen', } const TASK_STATUS_COLORS: Record = { open: 'bg-blue-100 text-blue-700', in_progress: 'bg-yellow-100 text-yellow-700', waiting: 'bg-orange-100 text-orange-700', completed: 'bg-green-100 text-green-700', cancelled: 'bg-gray-100 text-gray-500', } const ASSIGNMENT_STATUS_COLORS: Record = { active: 'bg-green-100 text-green-700 border-green-300', paused: 'bg-yellow-100 text-yellow-700 border-yellow-300', terminated: 'bg-red-100 text-red-700 border-red-300', } const ASSIGNMENT_STATUS_LABELS: Record = { active: 'Aktiv', paused: 'Pausiert', terminated: 'Beendet', } // ============================================================================= // API HELPERS // ============================================================================= async function apiFetch(url: string, options?: RequestInit): Promise { const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', 'X-User-ID': DSB_USER_ID, ...(options?.headers || {}), }, }) if (!res.ok) { const text = await res.text().catch(() => '') throw new Error(`API Error ${res.status}: ${text || res.statusText}`) } return res.json() } // ============================================================================= // TOAST COMPONENT // ============================================================================= interface ToastMessage { id: number message: string type: 'success' | 'error' } let toastIdCounter = 0 function useToast() { const [toasts, setToasts] = useState([]) const addToast = useCallback((message: string, type: 'success' | 'error' = 'success') => { const id = ++toastIdCounter setToasts((prev) => [...prev, { id, message, type }]) setTimeout(() => { setToasts((prev) => prev.filter((t) => t.id !== id)) }, 3500) }, []) return { toasts, addToast } } function ToastContainer({ toasts }: { toasts: ToastMessage[] }) { if (toasts.length === 0) return null return (
{toasts.map((t) => (
{t.message}
))}
) } // ============================================================================= // LOADING SKELETON // ============================================================================= function Skeleton({ className = '' }: { className?: string }) { return
} function DashboardSkeleton() { return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
{Array.from({ length: 3 }).map((_, i) => ( ))}
) } // ============================================================================= // MODAL COMPONENT // ============================================================================= function Modal({ open, onClose, title, children, maxWidth = 'max-w-lg', }: { open: boolean onClose: () => void title: string children: React.ReactNode maxWidth?: string }) { const overlayRef = useRef(null) useEffect(() => { if (!open) return const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', handleEsc) return () => document.removeEventListener('keydown', handleEsc) }, [open, onClose]) if (!open) return null return (
{ if (e.target === overlayRef.current) onClose() }} >

{title}

{children}
) } // ============================================================================= // STAT CARD // ============================================================================= function StatCard({ title, value, icon, accent = false, }: { title: string value: string | number icon: React.ReactNode accent?: boolean }) { return (
{icon}

{title}

{value}

) } // ============================================================================= // COMPLIANCE PROGRESS BAR // ============================================================================= function ComplianceBar({ score }: { score: number }) { const color = score < 40 ? 'bg-red-500' : score < 70 ? 'bg-yellow-500' : 'bg-green-500' const textColor = score < 40 ? 'text-red-700' : score < 70 ? 'text-yellow-700' : 'text-green-700' return (
{score}%
) } // ============================================================================= // HOURS PROGRESS BAR // ============================================================================= function HoursBar({ used, budget }: { used: number; budget: number }) { const pct = budget > 0 ? Math.min((used / budget) * 100, 100) : 0 const over = used > budget return (
{used}h / {budget}h
) } // ============================================================================= // BADGE // ============================================================================= function Badge({ label, className = '' }: { label: string; className?: string }) { return ( {label} ) } // ============================================================================= // FORM COMPONENTS // ============================================================================= function FormLabel({ children, htmlFor }: { children: React.ReactNode; htmlFor?: string }) { return ( ) } function FormInput({ id, type = 'text', value, onChange, placeholder, required, min, max, step, }: { id?: string type?: string value: string | number onChange: (val: string) => void placeholder?: string required?: boolean min?: string | number max?: string | number step?: string | number }) { return ( onChange(e.target.value)} placeholder={placeholder} required={required} min={min} max={max} step={step} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" /> ) } function FormTextarea({ id, value, onChange, placeholder, rows = 3, }: { id?: string value: string onChange: (val: string) => void placeholder?: string rows?: number }) { return (