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'; import userService from '../services/userService'; import assetService from '../services/assetService'; import onboardingProcessService from '../services/onboardingProcessService'; import ProcessManagementPage from './ProcessManagementPage'; import LoadingSpinner from '../components/common/LoadingSpinner'; import { toast } from 'react-toastify'; // ── Inline Prozess-Checkliste ───────────────────────────────────────────────── const TAG_COLORS = { critical: '#ef4444', important: '#f59e0b', normal: '#64748b' }; const TAG_LABELS = { critical: 'Kritisch', important: 'Wichtig', normal: 'Standard' }; 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 ProcessChecklist = ({ processes, checkedItems, onChange, disabled = false, userTeam = null }) => { if (!processes || processes.length === 0) { return

Keine Prozesse konfiguriert. Bitte unter ⚙️ Prozesse verwalten.

; } // Group by responsible_team 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 totalItems = processes.length; const totalChecked = processes.filter(p => !!checkedItems[`proc_${p.id}`]).length; const totalPct = totalItems > 0 ? Math.round((totalChecked / totalItems) * 100) : 0; return (
{/* Progress bar */}
Gesamtfortschritt {totalChecked} / {totalItems} · {totalPct}%
{/* Teams */} {teams.map(team => { const items = teamMap[team]; if (items.length === 0) return null; const tc = TEAM_COLORS[team]; const isMyTeam = !userTeam || userTeam === team; const grpChecked = items.filter(p => !!checkedItems[`proc_${p.id}`]).length; const grpPct = Math.round((grpChecked / items.length) * 100); return (
{/* Team header */}
{TEAM_ICONS[team]} {TEAM_LABELS[team]} {grpChecked}/{items.length}
{/* Processes */}
{items.map(proc => { const key = `proc_${proc.id}`; const checked = !!checkedItems[key]; const tagColor = TAG_COLORS[proc.tag] || '#64748b'; const editable = !disabled && isMyTeam; return (
editable && onChange({ ...checkedItems, [key]: !checked })} style={{ padding: '7px 10px', borderRadius: 6, cursor: editable ? 'pointer' : 'default', background: checked ? `${tagColor}0d` : 'var(--bg-secondary)', border: `1px solid ${checked ? `${tagColor}40` : 'var(--border-color)'}`, transition: 'all 0.15s', }} >
{checked ? '✓' : ''}
{proc.title}
{proc.description && (
{proc.description}
)} {proc.tag !== 'normal' && ( {proc.tag === 'critical' ? '🔴 ' : '⚡ '}{TAG_LABELS[proc.tag]} )}
); })}
); })}
); }; /* eslint-disable no-unused-vars */ // Legacy hardcoded checklist kept for reference – replaced by configurable processes from API const ON_CHECKLIST_LEGACY = [ // ── Phase 1: Sofort nach Vertragsunterzeichnung ──────────────────────── { key: 'p1_hr_personalakte', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'HR', label: 'Personalakte anlegen', detail: 'Digitale Akte · Vertragskopie · Eintrittsdatum', tag: 'critical' }, { key: 'p1_hr_abt_info', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'HR', label: 'Alle Abteilungen informieren', detail: 'IT, Vorgesetzten & Buchhaltung: Name, Position, Startdatum, Abteilung', tag: 'critical' }, { key: 'p1_hr_bg_anmeldung', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'HR', label: 'Berufsgenossenschaft vorbereiten', detail: 'BG-Anmeldung vorbereiten', tag: 'normal' }, { key: 'p1_it_ticket', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'IT', label: 'IT-Onboarding-Ticket anlegen', detail: 'Ticket im Helpdesk erstellen · startet alle IT-Aufgaben', tag: 'critical' }, { key: 'p1_it_hardware_check', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'IT', label: 'Hardware prüfen & ggf. bestellen', detail: 'Verfügbaren Laptop prüfen · ggf. Neubestellung (Lieferzeit beachten!)', tag: 'critical' }, { key: 'p1_vg_buddy', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'VG', label: 'Buddy / Paten benennen & informieren', detail: 'Erfahrenen Kollegen als Begleitung für die ersten Wochen auswählen', tag: 'critical' }, { key: 'p1_vg_einarbeitungsplan',phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung',dept: 'VG', label: '30-Tage-Einarbeitungsplan erstellen', detail: 'Aufgaben, Ziele, erste Projekte, Schulungsbedarf', tag: 'important'}, { key: 'p1_bk_datev', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'BK', label: 'Mitarbeiter in DATEV anlegen', detail: 'Stammdaten · Eintrittsdatum · Kostenstelle · Abteilung', tag: 'critical' }, { key: 'p1_bk_bankdaten', phase: 'Phase 1 — Sofort nach Vertragsunterzeichnung', dept: 'BK', label: 'Bankdaten & Steuerklasse anfordern', detail: 'IBAN-Formular senden · Steuerklasse · SV-Nummer anfordern', tag: 'important'}, // ── Phase 2: 2 Wochen vor Start ──────────────────────────────────────── { key: 'p2_hr_welcome_mail', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'HR', label: 'Willkommens-E-Mail senden', detail: 'Startzeit, Ansprechpartner, Parkplatz, Dresscode, Ausweis mitbringen', tag: 'important'}, { key: 'p2_hr_formulare', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'HR', label: 'Formulare vorbereiten', detail: 'Datenschutz · IT-Nutzungsordnung · Betriebsordnung · Schweigepflicht', tag: 'normal' }, { key: 'p2_hr_ausweis', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'HR', label: 'Mitarbeiterausweis beantragen', detail: 'Foto anfordern oder Fototermin Tag 1 planen', tag: 'normal' }, { key: 'p2_it_ad_account', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'IT', label: 'AD-Account erstellen', detail: 'Benutzername nach Konvention · Temp-Passwort · Gruppe/Abteilung', tag: 'critical' }, { key: 'p2_it_email', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'IT', label: 'E-Mail-Adresse einrichten', detail: 'Postfach anlegen · Signatur vorkonfigurieren · Verteiler aufnehmen', tag: 'critical' }, { key: 'p2_it_hardware_setup', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'IT', label: 'Hardware aufsetzen', detail: 'OS-Image · Windows Updates · Antivirus · Endpoint-Agent · Asset erfassen', tag: 'important'}, { key: 'p2_vg_team_inform', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'VG', label: 'Team informieren', detail: 'Teammitglieder über neuen Kollegen informieren · Buddy briefen', tag: 'important'}, { key: 'p2_vg_arbeitsplatz', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'VG', label: 'Arbeitsplatz vorbereiten', detail: 'Schreibtisch · Stuhl · Namensschild · Willkommensmappe bereitstellen', tag: 'normal' }, { key: 'p2_bk_lohnkonto', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'BK', label: 'Lohnkonto einrichten', detail: 'Gehaltsgruppe · Steuerklasse · Krankenkasse · Urlaubsanspruch', tag: 'important'}, { key: 'p2_bk_sv_anmeldung', phase: 'Phase 2 — 2 Wochen vor Arbeitsbeginn', dept: 'BK', label: 'Sozialversicherung anmelden', detail: 'SV-Anmeldung bei Krankenkasse einreichen', tag: 'normal' }, // ── Phase 3: 1 Woche vor Start ──────────────────────────────────────── { key: 'p3_hr_vollstaendigkeit', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'HR', label: 'Vollständigkeits-Check', detail: 'Alle Dokumente vollständig? IBAN? SV-Nummer? Unterweisungen geplant?', tag: 'critical' }, { key: 'p3_hr_unterweisungen', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'HR', label: 'Pflichtunterweisungen planen', detail: 'Arbeitssicherheit · Brandschutz · Datenschutz in KW 1', tag: 'important'}, { key: 'p3_it_systemtest', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'IT', label: 'Kompletttest aller Systeme', detail: 'Login · E-Mail · VPN · Netzlaufwerke · Teams testen', tag: 'critical' }, { key: 'p3_it_software', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'IT', label: 'Software installieren', detail: 'Office 365 · VPN-Client · Teams · ERP/CRM · abteilungsspezifische Tools', tag: 'critical'}, { key: 'p3_it_telefon', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'IT', label: 'Telefon / Durchwahl einrichten', detail: 'IP-Telefon oder Softphone · Durchwahl zuweisen · Mailbox konfigurieren', tag: 'important'}, { key: 'p3_it_begruessung', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'IT', label: 'IT-Begrüßungsmappe drucken', detail: 'Quick-Start-Guide: Login, Helpdesk, WLAN, Drucker, wichtige URLs', tag: 'normal' }, { key: 'p3_vg_erste_aufgaben', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'VG', label: 'Erste Aufgaben vorbereiten', detail: '2–3 konkrete, überschaubare Aufgaben für Tag 1 und Woche 1 definieren', tag: 'important'}, { key: 'p3_vg_checkins', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'VG', label: 'Check-in-Termine planen', detail: 'Tägliche 15-Min-Calls Woche 1 · Wöchentliche 30-Min-Gespräche Monat 1', tag: 'normal' }, { key: 'p3_bk_gehaltsabrechnung',phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn',dept: 'BK', label: 'Gehaltsabrechnung vorbereiten', detail: 'Erstes Gehalt anteilig berechnen · Auszahlungstermin prüfen', tag: 'important'}, { key: 'p3_bk_reisekosten', phase: 'Phase 3 — 1 Woche vor Arbeitsbeginn', dept: 'BK', label: 'Reisekostenformulare bereitstellen', detail: 'Spesenabrechnungsformulare und -richtlinien übergeben', tag: 'normal' }, // ── Phase 4: Erster Arbeitstag ──────────────────────────────────────── { key: 'p4_hr_empfang', phase: 'Phase 4 — Erster Arbeitstag', dept: 'HR', label: 'Empfang & Begrüßung', detail: 'MA am Empfang abholen · Rundgang · Schlüssel/Badge · Parkausweis', tag: 'critical' }, { key: 'p4_hr_formulare_sign', phase: 'Phase 4 — Erster Arbeitstag', dept: 'HR', label: 'Alle Formulare unterzeichnen', detail: 'Betriebsordnung · Datenschutz · IT-Nutzungsordnung · Schweigepflicht', tag: 'critical' }, { key: 'p4_hr_willkommensmappe',phase: 'Phase 4 — Erster Arbeitstag', dept: 'HR', label: 'Willkommensmappe übergeben', detail: 'Unternehmensinfos · Organigramm · Notfallnummern · Kantineninfos', tag: 'normal' }, { key: 'p4_it_hardware_uebergabe',phase: 'Phase 4 — Erster Arbeitstag',dept: 'IT', label: 'Hardware-Übergabe mit Protokoll', detail: 'Laptop + Zubehör übergeben · Übergabeprotokoll unterzeichnen lassen', tag: 'critical' }, { key: 'p4_it_erster_login', phase: 'Phase 4 — Erster Arbeitstag', dept: 'IT', label: 'Ersten Login begleiten', detail: 'Passwort ändern · MFA einrichten · E-Mail · VPN · Teams testen', tag: 'critical' }, { key: 'p4_it_it_mappe', phase: 'Phase 4 — Erster Arbeitstag', dept: 'IT', label: 'IT-Begrüßungsmappe übergeben', detail: 'Helpdesk-Kontakt · Passwortregeln · wichtige URLs · Verhaltensregeln', tag: 'important'}, { key: 'p4_vg_begruessung', phase: 'Phase 4 — Erster Arbeitstag', dept: 'VG', label: 'Persönliche Begrüßung (nicht delegieren)', detail: 'Vorgesetzter begrüßt persönlich · Teamvorstellung · Buddy vorstellen', tag: 'critical' }, { key: 'p4_vg_einarbeitungsplan',phase: 'Phase 4 — Erster Arbeitstag',dept: 'VG', label: 'Einarbeitungsplan besprechen', detail: 'Ziele Probezeit · erste Aufgaben · Erwartungen · Fragen beantworten', tag: 'important'}, { key: 'p4_vg_mittagessen', phase: 'Phase 4 — Erster Arbeitstag', dept: 'VG', label: 'Gemeinsames Mittagessen mit Team', detail: 'Sozialer Anschluss ist entscheidend!', tag: 'normal' }, { key: 'p4_bk_bankverbindung', phase: 'Phase 4 — Erster Arbeitstag', dept: 'BK', label: 'Bankverbindung bestätigen', detail: 'IBAN im System prüfen · erste Gehaltsabrechnung korrekt hinterlegt?', tag: 'important'}, { key: 'p4_bk_zeiterfassung', phase: 'Phase 4 — Erster Arbeitstag', dept: 'BK', label: 'Zeiterfassung erklären', detail: 'System vorstellen · ersten Einstempeln begleiten · Urlaubsantragsprozess', tag: 'normal' }, ]; /* eslint-enable no-unused-vars */ // Mapping: system role → responsible_team const ROLE_TO_TEAM = { 'hr_personal': 'hr', 'buchhaltung': 'buchhaltung', 'support': 'it', }; const STATUS_LABEL = { pending: 'Ausstehend', in_progress: 'In Bearbeitung', completed: 'Abgeschlossen' }; const STATUS_CSS = { pending: 'status-pending', in_progress: 'status-in_progress', completed: 'status-completed' }; const formatDate = (d) => d ? new Date(d).toLocaleDateString('de-DE') : '—'; // ── Kleine Hilfstabelle ─────────────────────────────────────────────────────── const ProtocolTable = ({ protocols, dateKey, dateLabel, onEdit, onOpen, onAction, actionLabel, actionClass, onDelete, onDownload, canDelete = true }) => (
{protocols.length === 0 ? ( ) : protocols.map(p => ( ))}
Mitarbeiter {dateLabel} Status PDF Erstellt von Aktionen
Keine Protokolle vorhanden
{(p.emp_first_name || p.employee_sys_first_name || '')} {(p.emp_last_name || p.employee_sys_last_name || '')}
{p.emp_private_email || p.employee_email || p.department || ''}
{formatDate(p[dateKey])} {STATUS_LABEL[p.status] || p.status} {p.employee_confirmed_at && (
✅ Bestätigt {formatDate(p.employee_confirmed_at)}
)} {!p.employee_confirmed_at && p.confirm_token && (
⏳ Ausstehend
)}
{p.pdf_file_path ? : } {p.created_by_username}
{onOpen ? : <> {onAction && } {canDelete && } }
); // ── Hauptkomponente ─────────────────────────────────────────────────────────── const OnOffboardingPage = () => { const navigate = useNavigate(); const { user, isAdmin, isSuperAdmin, canViewLifecycle } = useAuth(); const canManage = isAdmin() || isSuperAdmin(); const canCreate = canViewLifecycle(); // hr_personal + buchhaltung dürfen anlegen const [tab, setTab] = useState('onboarding'); const [loading, setLoading] = useState(true); // Shared data const [users, setUsers] = useState([]); const [assets, setAssets] = useState([]); // eslint-disable-line no-unused-vars // Onboarding state const [onProtocols, setOnProtocols] = useState([]); const [showOnModal, setShowOnModal] = useState(false); const [wizardStep, setWizardStep] = useState(1); const [wizardForm, setWizardForm] = useState({ emp_first_name: '', emp_last_name: '', emp_private_email: '', emp_phone: '', emp_address: '', department: '', position: '', work_location: 'Büro', hours_model: 'Vollzeit (40h)', vacation_model: '30 Tage', start_date: '', notes: '', dept_contacts: { VG: '' }, }); const [entraLoading, setEntraLoading] = useState(false); const [entraSearchQuery, setEntraSearchQuery] = useState(''); const [entraSearchResults, setEntraSearchResults] = useState(null); // null=no search yet, []=no results const [showOnDetail, setShowOnDetail] = useState(false); const [editingOn, setEditingOn] = useState(null); const [onForm, setOnForm] = useState({ employee_user_id: '', start_date: today(), status: 'pending', checklist_data: {}, notes: '', asset_ids: [] }); // Offboarding state const [offProtocols, setOffProtocols] = useState([]); const [showOffModal, setShowOffModal] = useState(false); const [showOffDetail, setShowOffDetail] = useState(false); const [showReturnModal, setShowReturnModal] = useState(false); const [editingOff, setEditingOff] = useState(null); const [offForm, setOffForm] = useState({ employee_user_id: '', exit_date: today(), status: 'pending', checklist_data: {}, notes: '' }); const [assignedAssets, setAssignedAssets] = useState([]); const [assetReturns, setAssetReturns] = useState([]); // Configurable departments + processes (loaded from API) const [departments, setDepartments] = useState([]); const [onProcesses, setOnProcesses] = useState([]); const [offProcesses, setOffProcesses] = useState([]); function today() { return new Date().toISOString().split('T')[0]; } useEffect(() => { loadAll(); }, []); const loadAll = async () => { try { const [on, off, u, a, depts, onProcs, offProcs] = await Promise.all([ onboardingService.getAll().catch(() => []), offboardingService.getAll().catch(() => []), userService.getAll().catch(() => []), assetService.getAll().catch(() => []), onboardingProcessService.getDepartments().catch(() => []), onboardingProcessService.getChecklistProcesses('onboarding').catch(() => []), onboardingProcessService.getChecklistProcesses('offboarding').catch(() => []), ]); setOnProtocols(on); setOffProtocols(off); setUsers(u.filter(x => x.is_active)); setAssets(a.filter(x => x.status === 'verfuegbar')); setDepartments(depts); setOnProcesses(onProcs); setOffProcesses(offProcs); } catch { toast.error('Fehler beim Laden'); } finally { setLoading(false); } }; // ── Onboarding handlers ─────────────────────────────────────────────────── const openOnCreate = () => { setEditingOn(null); setWizardStep(1); setWizardForm({ emp_first_name: '', emp_last_name: '', emp_private_email: '', emp_phone: '', emp_address: '', department: '', position: '', work_location: 'Büro', hours_model: 'Vollzeit (40h)', vacation_model: '30 Tage', start_date: today(), notes: '', dept_contacts: { VG: '' }, }); setEntraSearchQuery(''); setEntraSearchResults(null); setShowOnModal(true); }; const openOnEdit = (p) => { setEditingOn(p); setOnForm({ employee_user_id: p.employee_user_id, start_date: p.start_date, status: p.status, checklist_data: p.checklist_data ? JSON.parse(p.checklist_data) : {}, notes: p.notes || '', asset_ids: [] }); setShowOnDetail(true); }; // Helper: get display name for a protocol const getOnName = (p) => { const first = p.emp_first_name || p.employee_sys_first_name || ''; const last = p.emp_last_name || p.employee_sys_last_name || ''; return `${first} ${last}`.trim() || p.employee_username || '—'; }; // Returns the user's responsible_team (for ProcessChecklist visual highlight + edit control) const getUserTeam = () => ROLE_TO_TEAM[user?.role_name] || null; // Whether current user may edit the checklist const canEditChecklist = (protocol) => { if (isAdmin() || isSuperAdmin()) return true; if (ROLE_TO_TEAM[user?.role_name]) return true; // role-based team user try { const dc = protocol?.dept_contacts ? JSON.parse(protocol.dept_contacts) : {}; return String(dc.VG) === String(user?.id); // VG from Entra lookup } catch { return false; } }; const submitWizardStep1 = (e) => { e.preventDefault(); setWizardStep(2); }; const searchEntraByName = async () => { if (!entraSearchQuery || entraSearchQuery.trim().length < 2) return; setEntraLoading(true); setEntraSearchResults(null); try { const res = await import('../services/api').then(m => m.default.get(`/onboarding/entra-search?name=${encodeURIComponent(entraSearchQuery.trim())}`)); setEntraSearchResults(res.data.data || []); } catch { setEntraSearchResults([]); } finally { setEntraLoading(false); } }; const submitWizard = async (e) => { e.preventDefault(); try { const payload = { ...wizardForm, status: 'pending', checklist_data: {}, }; const np = await onboardingService.create(payload); toast.success('Onboarding erstellt'); setShowOnModal(false); await loadAll(); const fresh = await onboardingService.getById(np.id); openOnEdit(fresh); } catch (err) { toast.error(err.message || 'Fehler'); } }; const submitOn = async (e) => { e.preventDefault(); try { await onboardingService.update(editingOn.id, { status: onForm.status, checklist_data: onForm.checklist_data, notes: onForm.notes }); toast.success('Gespeichert'); setShowOnDetail(false); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const completeOn = async () => { const relevantProcesses = onProcesses.filter(p => !editingOn?.department || p.department_name === editingOn.department); const unchecked = relevantProcesses.filter(p => !onForm.checklist_data[`proc_${p.id}`]); if (unchecked.length > 0) { toast.error(`Noch ${unchecked.length} Aufgabe(n) offen — bitte zuerst alle Punkte abhaken.`); return; } try { await onboardingService.update(editingOn.id, { status: 'completed', checklist_data: onForm.checklist_data, notes: onForm.notes, completion_date: today() }); toast.success('Onboarding abgeschlossen!'); setShowOnDetail(false); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const regenerateOnPdf = async () => { try { await onboardingService.regeneratePdf(editingOn.id); toast.success('PDF wurde neu erstellt'); await loadAll(); const fresh = await onboardingService.getById(editingOn.id); setEditingOn(fresh); } catch (err) { toast.error(err.message || 'Fehler'); } }; const sendOnConfirmationEmail = async () => { if (!editingOn.emp_private_email) { toast.error('Keine private E-Mail-Adresse beim Mitarbeiter hinterlegt'); return; } try { await onboardingService.sendConfirmationEmail(editingOn.id); toast.success(`📧 Bestätigungsmail an ${editingOn.emp_private_email} gesendet`); } catch (err) { toast.error(err.message || 'Fehler beim Senden'); } }; const regenerateOffPdf = async () => { try { await offboardingService.regeneratePdf(editingOff.id); toast.success('PDF wurde neu erstellt'); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const deleteOn = async (id) => { if (!window.confirm('Protokoll wirklich löschen?')) return; try { await onboardingService.delete(id); toast.success('Gelöscht'); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; // ── Offboarding handlers ────────────────────────────────────────────────── const openOffCreate = () => { setEditingOff(null); setOffForm({ employee_user_id: '', exit_date: today(), status: 'pending', checklist_data: {}, notes: '' }); setShowOffModal(true); }; const openOffEdit = (p) => { setEditingOff(p); setOffForm({ employee_user_id: p.employee_user_id, exit_date: p.exit_date, status: p.status, checklist_data: p.checklist_data ? JSON.parse(p.checklist_data) : {}, notes: p.notes || '' }); setShowOffDetail(true); }; const openReturnAssets = async (p) => { setEditingOff(p); try { const all = await assetService.getAll(); const ua = all.filter(a => a.assigned_to_user_id === p.employee_user_id && a.status === 'zugewiesen'); setAssignedAssets(ua); setAssetReturns(ua.map(a => ({ asset_id: a.id, condition: 'gut' }))); setShowReturnModal(true); } catch { toast.error('Fehler beim Laden der Assets'); } }; const submitOff = async (e) => { e.preventDefault(); try { if (editingOff) { await offboardingService.update(editingOff.id, { status: offForm.status, checklist_data: offForm.checklist_data, notes: offForm.notes }); toast.success('Gespeichert'); setShowOffDetail(false); } else { const np = await offboardingService.create(offForm); toast.success('Offboarding erstellt'); setShowOffModal(false); await loadAll(); const fresh = await offboardingService.getById(np.id); openReturnAssets(fresh); } loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const submitReturn = async (e) => { e.preventDefault(); try { await offboardingService.returnAssets(editingOff.id, assetReturns); toast.success('Assets zurückgegeben & PDF erstellt'); setShowReturnModal(false); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const completeOff = async () => { const relevantProcesses = offProcesses.filter((p, i, arr) => arr.findIndex(x => x.responsible_team === p.responsible_team && x.title === p.title) === i ); const unchecked = relevantProcesses.filter(p => !offForm.checklist_data[`proc_${p.id}`]); if (unchecked.length > 0) { toast.error(`Noch ${unchecked.length} Aufgabe(n) offen — bitte zuerst alle Punkte abhaken.`); return; } try { await offboardingService.update(editingOff.id, { status: 'completed', checklist_data: offForm.checklist_data, notes: offForm.notes, completion_date: today() }); toast.success('Offboarding abgeschlossen!'); setShowOffDetail(false); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; const deleteOff = async (id) => { if (!window.confirm('Protokoll wirklich löschen?')) return; try { await offboardingService.delete(id); toast.success('Gelöscht'); loadAll(); } catch (err) { toast.error(err.message || 'Fehler'); } }; if (loading) return
; // Dept-contacts only see onboardings where they are assigned; hr_personal sees all const visibleOnProtocols = canCreate ? onProtocols : onProtocols.filter(p => { try { const dc = p.dept_contacts ? JSON.parse(p.dept_contacts) : {}; return Object.values(dc).some(uid => String(uid) === String(user?.id)); } catch { return false; } }); const onPending = visibleOnProtocols.filter(p => p.status !== 'completed').length; const offPending = offProtocols.filter(p => p.status !== 'completed').length; return (
{/* Header */}

👥 Mitarbeiter-Lifecycle

On- und Offboarding-Protokolle verwalten

{canCreate && tab !== 'prozesse' && ( )}
{/* Tabs */}
{[ { key: 'onboarding', label: '🟢 Onboarding', count: onPending }, { key: 'offboarding', label: '🔴 Offboarding', count: offPending }, { key: 'prozesse', label: '⚙️ Prozesse', count: 0 }, ].map(t => ( ))}
{/* Content */} {tab === 'onboarding' && ( p.pdf_file_path && window.open(onboardingService.downloadPdf(p.pdf_file_path), '_blank')} /> )} {tab === 'offboarding' && ( navigate(`/lifecycle/offboarding/${p.id}`)} onDownload={p => p.pdf_file_path && window.open(offboardingService.downloadPdf(p.pdf_file_path), '_blank')} /> )} {tab === 'prozesse' && }
{/* ── Onboarding: Wizard Modal ──────────────────────────────────────── */} {showOnModal && (
setShowOnModal(false)}>
e.stopPropagation()} style={{ maxWidth: 680 }}> {/* Wizard header */}

Neues Onboarding

Schritt {wizardStep} von 2 — {wizardStep === 1 ? 'Mitarbeiterdaten' : 'Abteilungen & Verantwortliche'}
{/* Step indicator */}
{[{ n: 1, label: 'Persönliche Daten' }, { n: 2, label: 'Abteilungen' }].map((s, i) => (
= s.n ? 'var(--cereda-primary)' : 'var(--bg-tertiary)', color: wizardStep >= s.n ? '#fff' : 'var(--text-muted)', border: `2px solid ${wizardStep >= s.n ? 'var(--cereda-primary)' : 'var(--border-color)'}`, }}>{wizardStep > s.n ? '✓' : s.n}
= s.n ? 'var(--text-primary)' : 'var(--text-muted)' }}>{s.label}
{i < 1 &&
s.n ? 'var(--cereda-primary)' : 'var(--border-color)', borderRadius: 2 }} />} ))}
{/* Step 1: Personal data */} {wizardStep === 1 && (
{/* Name row */}
setWizardForm({ ...wizardForm, emp_first_name: e.target.value })} />
setWizardForm({ ...wizardForm, emp_last_name: e.target.value })} />
{/* Contact row */}
setWizardForm({ ...wizardForm, emp_private_email: e.target.value })} />
setWizardForm({ ...wizardForm, emp_phone: e.target.value })} />
{/* Address */}
setWizardForm({ ...wizardForm, emp_address: e.target.value })} />
{/* Job info row */}
setWizardForm({ ...wizardForm, position: e.target.value })} />
{/* Start date + work location */}
setWizardForm({ ...wizardForm, start_date: e.target.value })} />
{/* Hours + vacation model */}
{/* Notes */}