diff --git a/frontend/src/pages/OffboardingDetailPage.jsx b/frontend/src/pages/OffboardingDetailPage.jsx
new file mode 100644
index 0000000..6080a30
--- /dev/null
+++ b/frontend/src/pages/OffboardingDetailPage.jsx
@@ -0,0 +1,558 @@
+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 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 }) {
+ if (!processes?.length) return (
+
Keine Prozesse konfiguriert.
+ );
+
+ const teams = ['it', 'hr', 'buchhaltung'];
+ const teamMap = {};
+ teams.forEach(t => { teamMap[t] = []; });
+ processes.forEach(p => { if (teamMap[p.responsible_team]) teamMap[p.responsible_team].push(p); });
+
+ const total = processes.length;
+ const checked = processes.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(); // hr + support + admin dürfen handeln
+
+ useEffect(() => { load(); }, [id]);
+
+ const load = async () => {
+ try {
+ setLoading(true);
+ const [p, procs] = await Promise.all([
+ offboardingService.getById(id),
+ onboardingProcessService.getChecklistProcesses('offboarding').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
+ {/* ── 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}
+
+ ))}
+
+
+ {/* 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' && (
+
+
+
+
📄 PDF-Dokument
+ {protocol.pdf_path ? (
+
+ ) : (
+
+
📄
+
Noch kein PDF generiert
+ {canAct && (
+
+ )}
+
+ )}
+
+
+ )}
+
+ {/* ── Asset-Rückgabe Modal ── */}
+ {showReturnModal && (
+
+
+
+ 📦 Assets zurückgeben
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/OnOffboardingPage.jsx b/frontend/src/pages/OnOffboardingPage.jsx
index dc292ca..76839fd 100644
--- a/frontend/src/pages/OnOffboardingPage.jsx
+++ b/frontend/src/pages/OnOffboardingPage.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import onboardingService from '../services/onboardingService';
import offboardingService from '../services/offboardingService';
@@ -196,7 +197,7 @@ const STATUS_CSS = { pending: 'status-pending', in_progress: 'status-in_progre
const formatDate = (d) => d ? new Date(d).toLocaleDateString('de-DE') : '—';
// ── Kleine Hilfstabelle ───────────────────────────────────────────────────────
-const ProtocolTable = ({ protocols, dateKey, dateLabel, onEdit, onAction, actionLabel, actionClass, onDelete, onDownload, canDelete = true }) => (
+const ProtocolTable = ({ protocols, dateKey, dateLabel, onEdit, onOpen, onAction, actionLabel, actionClass, onDelete, onDownload, canDelete = true }) => (