import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import offboardingService from '../services/offboardingService'; import assetService from '../services/assetService'; import onboardingProcessService from '../services/onboardingProcessService'; import LoadingSpinner from '../components/common/LoadingSpinner'; import { toast } from 'react-toastify'; const TEAM_COLORS = { it: '#3b82f6', hr: '#10b981', buchhaltung: '#8b5cf6' }; const TEAM_ICONS = { it: '💻', hr: '🧑‍💼', buchhaltung: '💶' }; const TEAM_LABELS = { it: 'IT', hr: 'HR / Personal', buchhaltung: 'Buchhaltung' }; const TAG_COLORS = { critical: '#ef4444', important: '#f59e0b', normal: '#64748b' }; const ROLE_TO_TEAM = { hr_personal: 'hr', buchhaltung: 'buchhaltung', produktion: 'it' }; const STATUS_LABEL = { pending: 'Ausstehend', in_progress: 'In Bearbeitung', completed: 'Abgeschlossen' }; const STATUS_COLOR = { pending: '#f59e0b', in_progress: '#3b82f6', completed: '#34d399' }; function StatusBadge({ status }) { const color = STATUS_COLOR[status] || '#6b7280'; return ( {STATUS_LABEL[status] || status} ); } function ProcessChecklist({ processes, checkedItems, onChange, disabled, userTeam = null }) { if (!processes?.length) return (

Keine Prozesse konfiguriert.

); // Wenn userTeam gesetzt: nur dieses Team anzeigen const visibleTeams = userTeam ? [userTeam] : ['it', 'hr', 'buchhaltung']; const visibleProcesses = userTeam ? processes.filter(p => p.responsible_team === userTeam) : processes; const teams = visibleTeams; const teamMap = {}; teams.forEach(t => { teamMap[t] = []; }); visibleProcesses.forEach(p => { if (teamMap[p.responsible_team]) teamMap[p.responsible_team].push(p); }); const total = visibleProcesses.length; const checked = visibleProcesses.filter(p => !!checkedItems[`proc_${p.id}`]).length; const pct = total > 0 ? Math.round(checked / total * 100) : 0; return (
Gesamtfortschritt {checked} / {total} · {pct}%
{teams.map(team => { const items = teamMap[team]; if (!items.length) return null; const tc = TEAM_COLORS[team]; const grpChecked = items.filter(p => !!checkedItems[`proc_${p.id}`]).length; return (
{TEAM_ICONS[team]} {TEAM_LABELS[team]} {grpChecked}/{items.length}
{items.map(proc => { const key = `proc_${proc.id}`; const isChecked = !!checkedItems[key]; const tagColor = TAG_COLORS[proc.tag] || '#64748b'; return (
!disabled && onChange({ ...checkedItems, [key]: !isChecked })} style={{ padding: '8px 10px', borderRadius: 6, cursor: disabled ? 'default' : 'pointer', background: isChecked ? `${tagColor}0d` : 'var(--bg-secondary)', border: `1px solid ${isChecked ? `${tagColor}40` : 'var(--border-color)'}`, transition: 'all .15s', }}>
{isChecked ? '✓' : ''}
{proc.title}
{proc.description && (
{proc.description}
)}
{TEAM_LABELS[team]}
); })}
); })}
); } export default function OffboardingDetailPage() { const { id } = useParams(); const navigate = useNavigate(); const { user, isAdmin, isSuperAdmin, canViewLifecycle } = useAuth(); const [protocol, setProtocol] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState('overview'); const [processes, setProcesses] = useState([]); const [checklist, setChecklist] = useState({}); const [notes, setNotes] = useState(''); const [assets, setAssets] = useState([]); // Asset-Rückgabe Modal const [showReturnModal, setShowReturnModal] = useState(false); const [assetReturns, setAssetReturns] = useState([]); const canManage = isAdmin() || isSuperAdmin(); const canAct = canViewLifecycle(); const userTeam = (canManage) ? null : (ROLE_TO_TEAM[user?.role_name] || null); useEffect(() => { load(); }, [id]); const load = async () => { try { setLoading(true); const p = await offboardingService.getById(id); const deptId = p.employee_department_id || undefined; const procs = await onboardingProcessService.getChecklistProcesses('offboarding', deptId).catch(() => []); setProtocol(p); setChecklist(p.checklist_data ? JSON.parse(p.checklist_data) : {}); setNotes(p.notes || ''); setProcesses(procs || []); // Assets laden try { const all = await assetService.getAll(); setAssets(all.filter(a => a.assigned_to_user_id === p.employee_user_id)); } catch { setAssets([]); } } catch (err) { toast.error('Protokoll nicht gefunden'); navigate('/lifecycle'); } finally { setLoading(false); } }; const save = async () => { setSaving(true); try { await offboardingService.update(id, { checklist_data: checklist, notes, status: protocol.status }); toast.success('Gespeichert'); load(); } catch (err) { toast.error(err.message || 'Fehler'); } finally { setSaving(false); } }; const complete = async () => { const relevantProcesses = processes.filter((p, i, arr) => arr.findIndex(x => x.responsible_team === p.responsible_team && x.title === p.title) === i ); const unchecked = relevantProcesses.filter(p => !checklist[`proc_${p.id}`]); if (unchecked.length > 0) { toast.error(`Noch ${unchecked.length} Aufgabe(n) offen — erst alle Punkte abhaken.`); return; } if (!window.confirm('Offboarding wirklich abschließen?')) return; try { await offboardingService.update(id, { status: 'completed', checklist_data: checklist, notes, completion_date: new Date().toISOString().slice(0, 10) }); toast.success('Offboarding abgeschlossen!'); load(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const deleteProtocol = async () => { if (!window.confirm('Protokoll wirklich löschen?')) return; try { await offboardingService.delete(id); toast.success('Gelöscht'); navigate('/lifecycle'); } catch (err) { toast.error(err.message || 'Fehler'); } }; const regeneratePdf = async () => { try { await offboardingService.regeneratePdf(id); toast.success('PDF neu erstellt'); load(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const openReturnAssets = async () => { try { const all = await assetService.getAll(); const ua = all.filter(a => a.assigned_to_user_id === protocol.employee_user_id && a.status === 'zugewiesen'); setAssets(ua); setAssetReturns(ua.map(a => ({ asset_id: a.id, condition: 'gut' }))); setShowReturnModal(true); } catch { toast.error('Fehler beim Laden der Assets'); } }; const submitReturn = async (e) => { e.preventDefault(); try { await offboardingService.returnAssets(id, assetReturns); toast.success('Assets zurückgegeben & PDF erstellt'); setShowReturnModal(false); load(); } catch (err) { toast.error(err.message || 'Fehler'); } }; if (loading) return
; if (!protocol) return null; const visibleProcs = userTeam ? processes.filter(p => p.responsible_team === userTeam) : processes; const total = visibleProcs.length; const checked = visibleProcs.filter(p => !!checklist[`proc_${p.id}`]).length; const pct = total > 0 ? Math.round(checked / total * 100) : 0; const statusColor = STATUS_COLOR[protocol.status] || '#6b7280'; const initials = (protocol.employee_name || protocol.employee_email || '??').slice(0, 2).toUpperCase(); const daysLeft = protocol.exit_date ? Math.ceil((new Date(protocol.exit_date) - new Date()) / 86400000) : null; const TABS = [ { key: 'overview', label: 'Übersicht' }, { key: 'checklist', label: `Checkliste${total ? ` (${checked}/${total})` : ''}` }, { key: 'assets', label: `Assets${assets.length ? ` (${assets.length})` : ''}` }, { key: 'notes', label: 'Notizen & PDF' }, ]; return (
{/* ── Breadcrumb ── */}
/ Offboarding / {protocol.employee_name || protocol.employee_email}
{/* ── Header Card ── */}
{/* farbige Top-Linie */}
{/* Avatar */}
{initials}
{/* Name + Meta */}
{protocol.employee_name || protocol.employee_email}
{protocol.employee_email && ✉️ {protocol.employee_email}} 📅 Austritt: {protocol.exit_date ? new Date(protocol.exit_date).toLocaleDateString('de-DE') : '—'} {daysLeft !== null && ( {daysLeft > 0 ? `⏳ noch ${daysLeft} Tage` : daysLeft === 0 ? '⚠️ heute' : `✅ vor ${Math.abs(daysLeft)} Tagen`} )}
{/* Aktionen */}
{canAct && protocol.status !== 'completed' && ( )} {canAct && ( )} {canAct && protocol.status !== 'completed' && ( )} {canManage && ( )}
{/* Fortschrittsbalken */} {total > 0 && (
Checkliste
{pct}% · {checked}/{total}
)}
{/* ── Tabs ── */}
{TABS.map(t => ( ))}
{/* ── Tab: Übersicht ── */} {activeTab === 'overview' && (
{/* Stat-Kacheln */}
{[ { label: 'Offene Aufgaben', value: total - checked, color: total - checked > 0 ? '#f59e0b' : '#34d399' }, { label: 'Erledigt', value: checked, color: '#34d399' }, { label: 'Assets', value: assets.length, color: '#3b82f6' }, { label: 'Tage bis Austritt', value: daysLeft !== null ? Math.max(0, daysLeft) : '—', color: daysLeft < 7 ? '#ef4444' : '#6b7280' }, ].map((s, i) => (
{s.value}
{s.label}
))}
{/* Team-Fortschritt */}
👥 Checklisten-Fortschritt nach Team
{['it', 'hr', 'buchhaltung'].map(team => { const teamProcs = processes.filter(p => p.responsible_team === team); if (!teamProcs.length) return null; const done = teamProcs.filter(p => !!checklist[`proc_${p.id}`]).length; const pctT = Math.round(done / teamProcs.length * 100); const tc = TEAM_COLORS[team]; const isMyTeam = userTeam === team; return (
{TEAM_ICONS[team]} {TEAM_LABELS[team]} {isMyTeam && Dein Team}
{done} / {teamProcs.length} erledigt {pctT}%
{pctT < 100 && (
⚠ {teamProcs.length - done} Aufgabe{teamProcs.length - done !== 1 ? 'n' : ''} offen
)} {pctT === 100 && (
✓ Vollständig abgehakt
)}
); })}
{/* Allgemeine Infos */}
📋 Allgemeine Infos
{[ ['Mitarbeiter', protocol.employee_name || '—'], ['E-Mail', protocol.employee_email || '—'], ['Austrittsdatum', protocol.exit_date ? new Date(protocol.exit_date).toLocaleDateString('de-DE') : '—'], ['Status', ], ['Erstellt von', protocol.created_by_username || '—'], ['Erstellt am', protocol.created_at ? new Date(protocol.created_at).toLocaleDateString('de-DE') : '—'], ].map(([label, value]) => (
{label} {value}
))}
{/* Assets Vorschau */}
📦 Zugewiesene Assets
{assets.length === 0 ? (

Keine Assets zugewiesen

) : assets.slice(0, 5).map(a => (
{a.type === 'notebook' ? '💻' : a.type === 'smartphone' ? '📱' : '📦'}
{a.name || a.model}
{a.serial_number}
ausstehend
))} {assets.length > 5 &&
+{assets.length - 5} weitere
}
)} {/* ── Tab: Checkliste ── */} {activeTab === 'checklist' && (
{protocol.status !== 'completed' && canAct && (
)}
)} {/* ── Tab: Assets ── */} {activeTab === 'assets' && (
{assets.length} Asset(s) zugewiesen {canAct && protocol.status !== 'completed' && ( )}
{assets.length === 0 ? (
📦
Keine Assets zugewiesen
) : (
{assets.map(a => (
{a.type === 'notebook' ? '💻' : a.type === 'smartphone' ? '📱' : '📦'}
{a.name || a.model}
SN: {a.serial_number} · {a.type}
Ausstehend
))}
)}
)} {/* ── Tab: Notizen & PDF ── */} {activeTab === 'notes' && (
Interne Notizen