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) => (
))}
{/* 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
)}
);
}