2026-06-01 20:49:07 +02:00
import React , { useState , useEffect } from 'react' ;
2026-06-11 12:02:39 +02:00
import { useNavigate } from 'react-router-dom' ;
2026-06-01 20:49:07 +02:00
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 < p style = { { color : 'var(--text-muted)' , fontSize : '0.85rem' } } > Keine Prozesse konfiguriert . Bitte unter ⚙ ️ Prozesse verwalten . < / p > ;
}
// 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 (
< div >
{ /* Progress bar */ }
< div style = { { marginBottom : 14 , padding : '10px 14px' , background : 'var(--bg-secondary)' , borderRadius : 8 , border : '1px solid var(--border-color)' } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 5 } } >
< span style = { { fontSize : '11px' , fontWeight : 700 , color : 'var(--text-secondary)' , textTransform : 'uppercase' , letterSpacing : '0.05em' } } > Gesamtfortschritt < / span >
< span style = { { fontSize : '12px' , fontFamily : 'monospace' , color : totalPct === 100 ? '#10b981' : 'var(--text-muted)' } } >
{ totalChecked } / { totalItems } · { totalPct } %
< / span >
< / div >
< div style = { { height : 4 , background : 'var(--border-color)' , borderRadius : 2 , overflow : 'hidden' } } >
< div style = { { height : '100%' , width : ` ${ totalPct } % ` , background : totalPct === 100 ? '#10b981' : 'var(--cereda-primary)' , borderRadius : 2 , transition : 'width 0.3s' } } / >
< / div >
< / div >
{ /* 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 (
< div key = { team } style = { { marginBottom : 10 , border : ` 1px solid var(--border-color) ` , borderLeft : ` 3px solid ${ tc } ` , borderRadius : 8 , overflow : 'hidden' , opacity : isMyTeam ? 1 : 0.5 } } >
{ /* Team header */ }
< div style = { { padding : '8px 14px' , background : 'var(--bg-tertiary)' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , alignItems : 'center' , gap : 10 } } >
< span > { TEAM _ICONS [ team ] } < / span >
< span style = { { fontWeight : 700 , fontSize : '0.85rem' , flex : 1 , color : tc } } > { TEAM _LABELS [ team ] } < / span >
< span style = { { fontSize : '11px' , fontFamily : 'monospace' , color : grpPct === 100 ? '#10b981' : 'var(--text-muted)' } } >
{ grpChecked } / { items . length }
< / span >
< div style = { { width : 50 , height : 3 , background : 'var(--border-color)' , borderRadius : 2 , overflow : 'hidden' } } >
< div style = { { height : '100%' , width : ` ${ grpPct } % ` , background : grpPct === 100 ? '#10b981' : tc , transition : 'width 0.3s' } } / >
< / div >
< / div >
{ /* Processes */ }
< div style = { { padding : '8px 10px' , display : 'flex' , flexDirection : 'column' , gap : 5 } } >
{ items . map ( proc => {
const key = ` proc_ ${ proc . id } ` ;
const checked = ! ! checkedItems [ key ] ;
const tagColor = TAG _COLORS [ proc . tag ] || '#64748b' ;
const editable = ! disabled && isMyTeam ;
return (
< div
key = { key }
onClick = { ( ) => 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' ,
} }
>
< div style = { { display : 'flex' , alignItems : 'flex-start' , gap : 8 } } >
< div style = { {
width : 15 , height : 15 , flexShrink : 0 , marginTop : 1 ,
border : ` 1.5px solid ${ checked ? tagColor : 'var(--border-color)' } ` ,
borderRadius : 3 , background : checked ? tagColor : 'transparent' ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
fontSize : 9 , color : '#fff' ,
} } >
{ checked ? '✓' : '' }
< / div >
< div style = { { flex : 1 } } >
< div style = { {
fontSize : '12px' , fontWeight : 600 , lineHeight : 1.4 ,
color : checked ? 'var(--text-muted)' : 'var(--text-primary)' ,
textDecoration : checked ? 'line-through' : 'none' ,
} } >
{ proc . title }
< / div >
{ proc . description && (
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' , marginTop : 2 , lineHeight : 1.35 } } >
{ proc . description }
< / div >
) }
{ proc . tag !== 'normal' && (
< span style = { { display : 'inline-block' , marginTop : 3 , fontSize : '10px' , fontFamily : 'monospace' , padding : '1px 5px' , borderRadius : 3 , background : ` ${ tagColor } 20 ` , color : tagColor } } >
{ proc . tag === 'critical' ? '🔴 ' : '⚡ ' } { TAG _LABELS [ proc . tag ] }
< / span >
) }
< / div >
< / div >
< / div >
) ;
} ) }
< / div >
< / div >
) ;
} ) }
< / div >
) ;
} ;
/* 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 ───────────────────────────────────────────────────────
2026-06-11 12:02:39 +02:00
const ProtocolTable = ( { protocols , dateKey , dateLabel , onEdit , onOpen , onAction , actionLabel , actionClass , onDelete , onDownload , canDelete = true } ) => (
2026-06-01 20:49:07 +02:00
< div className = "card" style = { { overflowX : 'auto' } } >
< table className = "table" >
< thead >
< tr >
< th > Mitarbeiter < / th >
< th > { dateLabel } < / th >
< th > Status < / th >
< th > PDF < / th >
< th > Erstellt von < / th >
< th > Aktionen < / th >
< / tr >
< / thead >
< tbody >
{ protocols . length === 0 ? (
< tr > < td colSpan = "6" style = { { textAlign : 'center' , color : 'var(--text-muted)' , padding : '2rem' } } > Keine Protokolle vorhanden < / td > < / tr >
) : protocols . map ( p => (
< tr key = { p . id } >
< td >
< div style = { { fontWeight : 600 } } >
{ ( p . emp _first _name || p . employee _sys _first _name || '' ) } { ( p . emp _last _name || p . employee _sys _last _name || '' ) }
< / div >
< div style = { { fontSize : '0.75rem' , color : 'var(--text-muted)' } } >
{ p . emp _private _email || p . employee _email || p . department || '' }
< / div >
< / td >
< td > { formatDate ( p [ dateKey ] ) } < / td >
< td >
< span className = { ` status-badge ${ STATUS _CSS [ p . status ] || '' } ` } > { STATUS _LABEL [ p . status ] || p . status } < / span >
{ p . employee _confirmed _at && (
< div style = { { marginTop : 4 } } >
< span style = { { fontSize : '0.7rem' , background : '#dcfce7' , color : '#16a34a' , borderRadius : 4 , padding : '1px 6px' , fontWeight : 600 } } >
✅ Bestätigt { formatDate ( p . employee _confirmed _at ) }
< / span >
< / div >
) }
{ ! p . employee _confirmed _at && p . confirm _token && (
< div style = { { marginTop : 4 } } >
< span style = { { fontSize : '0.7rem' , background : '#fef9c3' , color : '#ca8a04' , borderRadius : 4 , padding : '1px 6px' , fontWeight : 600 } } >
⏳ Ausstehend
< / span >
< / div >
) }
< / td >
< td >
{ p . pdf _file _path
? < button onClick = { ( ) => onDownload ( p ) } className = "btn btn-success btn-small" > 📄 Download < / button >
: < span style = { { color : 'var(--text-muted)' , fontSize : '0.8rem' } } > — < / span > }
< / td >
< td style = { { fontSize : '0.85rem' } } > { p . created _by _username } < / td >
< td >
< div className = "table-actions" >
2026-06-11 12:02:39 +02:00
{ onOpen
? < button onClick = { ( ) => onOpen ( p ) } className = "btn btn-primary btn-small" > Öffnen → < / button >
: < >
< button onClick = { ( ) => onEdit ( p ) } className = "btn btn-primary btn-small" > Bearbeiten < / button >
{ onAction && < button onClick = { ( ) => onAction ( p ) } className = { ` btn ${ actionClass } btn-small ` } > { actionLabel } < / button > }
{ canDelete && < button onClick = { ( ) => onDelete ( p . id ) } className = "btn btn-danger btn-small" > Löschen < / button > }
< / >
}
2026-06-01 20:49:07 +02:00
< / div >
< / td >
< / tr >
) ) }
< / tbody >
< / table >
< / div >
) ;
// ── Hauptkomponente ───────────────────────────────────────────────────────────
const OnOffboardingPage = ( ) => {
2026-06-11 12:02:39 +02:00
const navigate = useNavigate ( ) ;
2026-06-01 20:49:07 +02:00
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 ( ) => {
2026-06-11 11:35:09 +02:00
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 ;
}
2026-06-01 20:49:07 +02:00
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 ( ) => {
2026-06-11 11:35:09 +02:00
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 ;
}
2026-06-01 20:49:07 +02:00
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 < div className = "main-content" > < LoadingSpinner / > < / div > ;
// 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 (
< div className = "main-content" >
< div className = "container" >
{ /* Header */ }
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-end' , marginBottom : 24 , flexWrap : 'wrap' , gap : 12 } } >
< div >
< h1 style = { { margin : 0 , fontSize : '1.5rem' , fontWeight : 700 } } > 👥 Mitarbeiter - Lifecycle < / h1 >
< p style = { { margin : '4px 0 0' , color : 'var(--text-muted)' , fontSize : '0.875rem' } } >
On - und Offboarding - Protokolle verwalten
< / p >
< / div >
{ canCreate && tab !== 'prozesse' && (
< button
className = "btn btn-primary"
onClick = { tab === 'onboarding' ? openOnCreate : openOffCreate }
>
+ { tab === 'onboarding' ? 'Neues Onboarding' : 'Neues Offboarding' }
< / button >
) }
< / div >
{ /* Tabs */ }
< div style = { { display : 'flex' , gap : 4 , marginBottom : 20 , borderBottom : '1px solid var(--border-color)' , paddingBottom : 0 } } >
{ [
{ key : 'onboarding' , label : '🟢 Onboarding' , count : onPending } ,
{ key : 'offboarding' , label : '🔴 Offboarding' , count : offPending } ,
{ key : 'prozesse' , label : '⚙️ Prozesse' , count : 0 } ,
] . map ( t => (
< button
key = { t . key }
onClick = { ( ) => setTab ( t . key ) }
style = { {
padding : '10px 20px' , border : 'none' , background : 'none' , cursor : 'pointer' ,
fontWeight : 600 , fontSize : '0.9rem' ,
color : tab === t . key ? 'var(--primary)' : 'var(--text-muted)' ,
borderBottom : tab === t . key ? '2px solid var(--primary)' : '2px solid transparent' ,
marginBottom : - 1 , transition : 'all 0.15s' ,
} }
>
{ t . label }
{ t . count > 0 && (
< span style = { { marginLeft : 6 , background : tab === t . key ? 'var(--primary)' : 'var(--text-muted)' , color : '#fff' , borderRadius : 10 , padding : '1px 7px' , fontSize : '0.72rem' } } >
{ t . count }
< / span >
) }
< / button >
) ) }
< / div >
{ /* Content */ }
{ tab === 'onboarding' && (
< ProtocolTable
protocols = { visibleOnProtocols }
dateKey = "start_date" dateLabel = "Startdatum"
onEdit = { openOnEdit }
onDelete = { deleteOn }
canDelete = { canManage }
onDownload = { p => p . pdf _file _path && window . open ( onboardingService . downloadPdf ( p . pdf _file _path ) , '_blank' ) }
/ >
) }
{ tab === 'offboarding' && (
< ProtocolTable
protocols = { offProtocols }
dateKey = "exit_date" dateLabel = "Austrittsdatum"
2026-06-11 12:02:39 +02:00
onOpen = { p => navigate ( ` /lifecycle/offboarding/ ${ p . id } ` ) }
2026-06-01 20:49:07 +02:00
onDownload = { p => p . pdf _file _path && window . open ( offboardingService . downloadPdf ( p . pdf _file _path ) , '_blank' ) }
/ >
) }
{ tab === 'prozesse' && < ProcessManagementPage embedded / > }
< / div >
{ /* ── Onboarding: Wizard Modal ──────────────────────────────────────── */ }
{ showOnModal && (
< div className = "modal-overlay" onClick = { ( ) => setShowOnModal ( false ) } >
< div className = "modal-content modal-large" onClick = { e => e . stopPropagation ( ) } style = { { maxWidth : 680 } } >
{ /* Wizard header */ }
< div className = "modal-header" >
< div >
< h2 className = "modal-title" style = { { marginBottom : 2 } } > Neues Onboarding < / h2 >
< div style = { { fontSize : '12px' , color : 'var(--text-muted)' } } >
Schritt { wizardStep } von 2 — { wizardStep === 1 ? 'Mitarbeiterdaten' : 'Abteilungen & Verantwortliche' }
< / div >
< / div >
< button className = "modal-close" onClick = { ( ) => setShowOnModal ( false ) } > × < / button >
< / div >
{ /* Step indicator */ }
< div style = { { padding : '12px 24px 0' , display : 'flex' , gap : 16 , alignItems : 'center' } } >
{ [ { n : 1 , label : 'Persönliche Daten' } , { n : 2 , label : 'Abteilungen' } ] . map ( ( s , i ) => (
< React.Fragment key = { s . n } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 6 } } >
< div style = { {
width : 26 , height : 26 , borderRadius : '50%' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
fontSize : '11px' , fontWeight : 700 ,
background : wizardStep >= 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 } < / div >
< span style = { { fontSize : '12px' , fontWeight : 600 , color : wizardStep >= s . n ? 'var(--text-primary)' : 'var(--text-muted)' } } > { s . label } < / span >
< / div >
{ i < 1 && < div style = { { flex : 1 , height : 2 , background : wizardStep > s . n ? 'var(--cereda-primary)' : 'var(--border-color)' , borderRadius : 2 } } / > }
< / React.Fragment >
) ) }
< / div >
{ /* Step 1: Personal data */ }
{ wizardStep === 1 && (
< form onSubmit = { submitWizardStep1 } >
< div style = { { padding : '20px 24px' } } >
{ /* Name row */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 16 } } >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Vorname * < / label >
< input className = "form-input" type = "text" placeholder = "Max" required
value = { wizardForm . emp _first _name }
onChange = { e => setWizardForm ( { ... wizardForm , emp _first _name : e . target . value } ) } / >
< / div >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Nachname * < / label >
< input className = "form-input" type = "text" placeholder = "Mustermann" required
value = { wizardForm . emp _last _name }
onChange = { e => setWizardForm ( { ... wizardForm , emp _last _name : e . target . value } ) } / >
< / div >
< / div >
{ /* Contact row */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 16 } } >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Private E - Mail * < / label >
< input className = "form-input" type = "email" placeholder = "max@privat.de" required
value = { wizardForm . emp _private _email }
onChange = { e => setWizardForm ( { ... wizardForm , emp _private _email : e . target . value } ) } / >
< / div >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Telefonnummer < / label >
< input className = "form-input" type = "tel" placeholder = "+49 151 1234567"
value = { wizardForm . emp _phone }
onChange = { e => setWizardForm ( { ... wizardForm , emp _phone : e . target . value } ) } / >
< / div >
< / div >
{ /* Address */ }
< div className = "form-group" style = { { marginBottom : 16 } } >
< label className = "form-label" > Adresse < / label >
< input className = "form-input" type = "text" placeholder = "Musterstraße 1, 12345 Musterstadt"
value = { wizardForm . emp _address }
onChange = { e => setWizardForm ( { ... wizardForm , emp _address : e . target . value } ) } / >
< / div >
{ /* Job info row */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 16 } } >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Abteilung * < / label >
< select className = "form-select" required
value = { wizardForm . department }
onChange = { e => setWizardForm ( { ... wizardForm , department : e . target . value } ) } >
< option value = "" > — Bitte wählen — < / option >
{ departments . map ( d => < option key = { d . id } value = { d . name } > { d . icon } { d . name } < / option > ) }
< / select >
< / div >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Position * < / label >
< input className = "form-input" type = "text" placeholder = "z.B. Softwareentwickler" required
value = { wizardForm . position }
onChange = { e => setWizardForm ( { ... wizardForm , position : e . target . value } ) } / >
< / div >
< / div >
{ /* Start date + work location */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 16 } } >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Startdatum * < / label >
< input className = "form-input" type = "date" required
value = { wizardForm . start _date }
onChange = { e => setWizardForm ( { ... wizardForm , start _date : e . target . value } ) } / >
< / div >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Arbeitsort < / label >
< select className = "form-select" value = { wizardForm . work _location } onChange = { e => setWizardForm ( { ... wizardForm , work _location : e . target . value } ) } >
< option > Büro < / option >
< option > Homeoffice < / option >
< option > Hybrid < / option >
< / select >
< / div >
< / div >
{ /* Hours + vacation model */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 16 } } >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Stundenmodell < / label >
< select className = "form-select" value = { wizardForm . hours _model } onChange = { e => setWizardForm ( { ... wizardForm , hours _model : e . target . value } ) } >
< option > Vollzeit ( 40 h ) < / option >
< option > Vollzeit ( 38 h ) < / option >
< option > Teilzeit ( 30 h ) < / option >
< option > Teilzeit ( 20 h ) < / option >
< option > Minijob < / option >
< / select >
< / div >
< div className = "form-group" style = { { margin : 0 } } >
< label className = "form-label" > Urlaubsmodell < / label >
< select className = "form-select" value = { wizardForm . vacation _model } onChange = { e => setWizardForm ( { ... wizardForm , vacation _model : e . target . value } ) } >
< option > 25 Tage < / option >
< option > 27 Tage < / option >
< option > 28 Tage < / option >
< option > 30 Tage < / option >
< option > Gesetzlich ( 20 Tage ) < / option >
< / select >
< / div >
< / div >
{ /* Notes */ }
< div className = "form-group" style = { { marginBottom : 0 } } >
< label className = "form-label" > Interne Notizen < / label >
< textarea className = "form-textarea" rows = "2" placeholder = "Besonderheiten, Hinweise für IT / HR…"
value = { wizardForm . notes }
onChange = { e => setWizardForm ( { ... wizardForm , notes : e . target . value } ) } / >
< / div >
< / div >
< div className = "card-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowOnModal ( false ) } > Abbrechen < / button >
< button type = "submit" className = "btn btn-primary" > Weiter → < / button >
< / div >
< / form >
) }
{ /* Step 2: Rollen-Info + Vorgesetzter via Entra */ }
{ wizardStep === 2 && (
< form onSubmit = { submitWizard } >
< div style = { { padding : '20px 24px' } } >
{ /* Role-based info */ }
< div style = { { marginBottom : 20 , padding : '14px 16px' , background : 'var(--bg-secondary)' , borderRadius : 8 , border : '1px solid var(--border-color)' } } >
< div style = { { fontSize : '12px' , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.06em' , marginBottom : 10 } } >
Rollenbasierte Sichtbarkeit
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 8 } } >
{ [
{ color : '#3b82f6' , label : '🧑💼 HR / Personal' , role : 'hr_personal' , desc : 'Personalakte, Formulare, Empfang' } ,
{ color : '#10b981' , label : '💻 IT' , role : 'support' , desc : 'Accounts, Hardware, Software' } ,
{ color : '#a78bfa' , label : '💶 Buchhaltung' , role : 'buchhaltung' , desc : 'DATEV, Lohnkonto, SV-Anmeldung' } ,
] . map ( d => (
< div key = { d . role } style = { { display : 'flex' , alignItems : 'flex-start' , gap : 8 , padding : '8px 10px' , borderRadius : 6 , borderLeft : ` 3px solid ${ d . color } ` , background : d . color + '0a' } } >
< div >
< div style = { { fontSize : '12px' , fontWeight : 700 , color : d . color } } > { d . label } < / div >
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' } } >
Rolle : < code style = { { background : 'var(--bg-tertiary)' , padding : '1px 5px' , borderRadius : 3 } } > { d . role } < / code >
< / div >
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' , marginTop : 2 } } > { d . desc } < / div >
< / div >
< / div >
) ) }
< / div >
< / div >
{ /* VG via Entra Namenssuche */ }
< div style = { { padding : '14px 16px' , border : ` 1px solid ${ wizardForm . dept _contacts . VG ? '#f59e0b60' : 'var(--border-color)' } ` , borderLeft : '4px solid #f59e0b' , borderRadius : 8 , background : wizardForm . dept _contacts . VG ? '#f59e0b0a' : 'var(--bg-secondary)' } } >
< div style = { { marginBottom : 10 } } >
< div style = { { fontSize : '13px' , fontWeight : 700 , color : '#f59e0b' , marginBottom : 2 } } > 👔 Vorgesetzter < / div >
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' } } > Einarbeitungsplan , Buddy , Team - Integration < / div >
< / div >
{ /* Entra name search */ }
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' , marginBottom : 6 } } > In Entra nach Name suchen : < / div >
< div style = { { display : 'flex' , gap : 8 , marginBottom : 10 } } >
< input className = "form-input" style = { { fontSize : '13px' , flex : 1 } }
type = "text" placeholder = "Name eingeben (mind. 2 Zeichen)…"
value = { entraSearchQuery }
onChange = { e => setEntraSearchQuery ( e . target . value ) }
onKeyDown = { e => e . key === 'Enter' && ( e . preventDefault ( ) , searchEntraByName ( ) ) } / >
< button type = "button" className = "btn btn-secondary" style = { { fontSize : '12px' , padding : '6px 12px' , whiteSpace : 'nowrap' } }
onClick = { searchEntraByName }
disabled = { entraLoading || entraSearchQuery . trim ( ) . length < 2 } >
{ entraLoading ? '⏳' : '🔍 Suchen' }
< / button >
< / div >
{ /* Search results */ }
{ entraSearchResults !== null && entraSearchResults . length === 0 && (
< div style = { { fontSize : '12px' , color : 'var(--text-muted)' , fontStyle : 'italic' , marginBottom : 10 } } >
Keine Ergebnisse für „ { entraSearchQuery } " .
< / div >
) }
{ entraSearchResults && entraSearchResults . length > 0 && (
< div style = { { marginBottom : 10 , borderRadius : 6 , border : '1px solid var(--border-color)' , overflow : 'hidden' } } >
{ entraSearchResults . map ( u => (
< div key = { u . entraId } style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , padding : '8px 12px' , borderBottom : '1px solid var(--border-color)' , background : 'var(--bg-primary)' , gap : 8 } } >
< div >
< div style = { { fontSize : '13px' , fontWeight : 600 } } > { u . displayName } < / div >
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' } } >
{ [ u . jobTitle , u . department , u . mail ] . filter ( Boolean ) . join ( ' · ' ) }
< / div >
< / div >
< button type = "button" className = "btn btn-primary" style = { { fontSize : '11px' , padding : '4px 10px' , whiteSpace : 'nowrap' } }
onClick = { ( ) => {
const match = users . find ( x => x . email ? . toLowerCase ( ) === u . mail ? . toLowerCase ( ) ) ;
setWizardForm ( { ... wizardForm , dept _contacts : { ... wizardForm . dept _contacts , VG : match ? String ( match . id ) : u . mail } } ) ;
setEntraSearchResults ( null ) ;
setEntraSearchQuery ( '' ) ;
} } >
✓ Wählen
< / button >
< / div >
) ) }
< / div >
) }
{ /* Manual fallback */ }
< div style = { { fontSize : '11px' , color : 'var(--text-muted)' , marginBottom : 6 } } > Oder direkt aus Systembenutzer wählen : < / div >
< select className = "form-select" style = { { fontSize : '13px' } }
value = { wizardForm . dept _contacts . VG }
onChange = { e => setWizardForm ( { ... wizardForm , dept _contacts : { ... wizardForm . dept _contacts , VG : e . target . value } } ) } >
< option value = "" > — Kein Vorgesetzter zugewiesen — < / option >
{ users . map ( u => < option key = { u . id } value = { u . id } > { u . first _name } { u . last _name } < / option > ) }
< / select >
< / div >
< / div >
< div className = "card-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setWizardStep ( 1 ) } > ← Zurück < / button >
< button type = "submit" className = "btn btn-primary" > Onboarding starten ✓ < / button >
< / div >
< / form >
) }
< / div >
< / div >
) }
{ /* ── Onboarding: Edit/Detail Modal ──────────────────────────────────── */ }
{ showOnDetail && editingOn && (
< div className = "modal-overlay" onClick = { ( ) => setShowOnDetail ( false ) } >
< div className = "modal-content modal-large" onClick = { e => e . stopPropagation ( ) } >
< div className = "modal-header" >
< h2 className = "modal-title" > Onboarding : { getOnName ( editingOn ) } < / h2 >
< button className = "modal-close" onClick = { ( ) => setShowOnDetail ( false ) } > × < / button >
< / div >
< form onSubmit = { submitOn } >
{ /* Employee info summary */ }
{ editingOn && ( editingOn . emp _first _name || editingOn . department ) && ( ( ) => {
const dc = editingOn . dept _contacts ? JSON . parse ( editingOn . dept _contacts ) : { } ;
const deptLabels = { HR : '🧑💼 HR' , IT : '💻 IT' , VG : '👔 VG' , BK : '💶 BK' } ;
const deptColors = { HR : '#3b82f6' , IT : '#10b981' , VG : '#f59e0b' , BK : '#a78bfa' } ;
const getUserName = ( uid ) => {
const u = users . find ( x => String ( x . id ) === String ( uid ) ) ;
return u ? ` ${ u . first _name } ${ u . last _name } ` : null ;
} ;
return (
< div style = { { margin : '0 0 0' , background : 'var(--bg-secondary)' , borderBottom : '1px solid var(--border-color)' } } >
{ /* Personal info row */ }
< div style = { { padding : '10px 24px' , display : 'flex' , flexWrap : 'wrap' , gap : '16px' , borderBottom : '1px solid var(--border-color)' } } >
{ editingOn . emp _private _email && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > E - Mail : < / span > < span style = { { fontWeight : 600 } } > { editingOn . emp _private _email } < / span > < / div > }
{ editingOn . department && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > Abteilung : < / span > < span style = { { fontWeight : 600 } } > { editingOn . department } < / span > < / div > }
{ editingOn . position && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > Position : < / span > < span style = { { fontWeight : 600 } } > { editingOn . position } < / span > < / div > }
{ editingOn . start _date && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > Start : < / span > < span style = { { fontWeight : 600 } } > { formatDate ( editingOn . start _date ) } < / span > < / div > }
{ editingOn . hours _model && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > Stunden : < / span > < span style = { { fontWeight : 600 } } > { editingOn . hours _model } < / span > < / div > }
{ editingOn . vacation _model && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > Urlaub : < / span > < span style = { { fontWeight : 600 } } > { editingOn . vacation _model } < / span > < / div > }
{ editingOn . work _location && < div style = { { fontSize : '12px' } } > < span style = { { color : 'var(--text-muted)' } } > Ort : < / span > < span style = { { fontWeight : 600 } } > { editingOn . work _location } < / span > < / div > }
{ editingOn . employee _confirmed _at
? < div style = { { fontSize : '12px' } } > < span style = { { background : '#dcfce7' , color : '#16a34a' , borderRadius : 4 , padding : '2px 8px' , fontWeight : 600 } } > ✅ Bestätigt am { formatDate ( editingOn . employee _confirmed _at ) } < / span > < / div >
: editingOn . confirm _token
? < div style = { { fontSize : '12px' } } > < span style = { { background : '#fef9c3' , color : '#ca8a04' , borderRadius : 4 , padding : '2px 8px' , fontWeight : 600 } } > ⏳ Bestätigung ausstehend < / span > < / div >
: null
}
< / div >
{ /* Dept contacts row */ }
{ Object . keys ( dc ) . some ( k => dc [ k ] ) && (
< div style = { { padding : '8px 24px' , display : 'flex' , flexWrap : 'wrap' , gap : '12px' } } >
{ Object . entries ( dc ) . filter ( ( [ , v ] ) => v ) . map ( ( [ k , v ] ) => {
const name = getUserName ( v ) ;
return name ? (
< div key = { k } style = { { fontSize : '11px' , display : 'flex' , alignItems : 'center' , gap : 4 } } >
< span style = { { color : deptColors [ k ] , fontWeight : 700 } } > { deptLabels [ k ] } : < / span >
< span style = { { color : 'var(--text-primary)' } } > { name } < / span >
< / div >
) : null ;
} ) }
< / div >
) }
< / div >
) ;
} ) ( ) }
{ canManage && (
< div className = "form-group" >
< label className = "form-label" > Status * < / label >
< select className = "form-select" value = { onForm . status } onChange = { e => setOnForm ( { ... onForm , status : e . target . value } ) } required >
< option value = "pending" > Ausstehend < / option >
< option value = "in_progress" > In Bearbeitung < / option >
< option value = "completed" > Abgeschlossen < / option >
< / select >
< / div >
) }
< div className = "form-group" >
< label className = "form-label" >
Checkliste
{ editingOn && ! canManage && ( ( ) => {
const dc = editingOn . dept _contacts ? JSON . parse ( editingOn . dept _contacts ) : { } ;
const myDept = Object . entries ( dc ) . find ( ( [ , uid ] ) => String ( uid ) === String ( user ? . id ) ) ;
return myDept ? (
< span style = { { marginLeft : 8 , fontSize : '11px' , padding : '2px 7px' , borderRadius : 4 , background : 'rgba(16,185,129,0.12)' , color : '#10b981' , fontWeight : 600 } } >
Deine Aufgaben ( { myDept [ 0 ] } )
< / span >
) : null ;
} ) ( ) }
< / label >
< ProcessChecklist
processes = { onProcesses . filter ( p => ! editingOn ? . department || p . department _name === editingOn . department ) }
checkedItems = { onForm . checklist _data }
onChange = { ci => setOnForm ( { ... onForm , checklist _data : ci } ) }
disabled = { editingOn ? ! canEditChecklist ( editingOn ) : false }
userTeam = { ( ! canManage && ! isSuperAdmin ( ) ) ? getUserTeam ( ) : null }
/ >
< / div >
< div className = "form-group" >
< label className = "form-label" > Notizen < / label >
< textarea className = "form-textarea" rows = "4" value = { onForm . notes } onChange = { e => setOnForm ( { ... onForm , notes : e . target . value } ) }
disabled = { editingOn ? ! canEditChecklist ( editingOn ) : false } / >
< / div >
< div className = "card-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowOnDetail ( false ) } > Schließen < / button >
{ canEditChecklist ( editingOn ) && < button type = "submit" className = "btn btn-primary" > Speichern < / button > }
{ canManage && < button type = "button" className = "btn btn-secondary" onClick = { regenerateOnPdf } > 📄 PDF neu erstellen < / button > }
{ editingOn ? . emp _private _email && < button type = "button" className = "btn btn-secondary" onClick = { sendOnConfirmationEmail } title = { ` Bestätigungsmail an ${ editingOn . emp _private _email } senden ` } > 📧 Protokoll zusenden < / button > }
{ canManage && onForm . status !== 'completed' && < button type = "button" className = "btn btn-success" onClick = { completeOn } > ✓ Abschließen < / button > }
< / div >
< / form >
< / div >
< / div >
) }
{ /* ── Offboarding: Create Modal ─────────────────────────────────────── */ }
{ showOffModal && (
< div className = "modal-overlay" onClick = { ( ) => setShowOffModal ( false ) } >
< div className = "modal-content" onClick = { e => e . stopPropagation ( ) } >
< div className = "modal-header" >
< h2 className = "modal-title" > Neues Offboarding < / h2 >
< button className = "modal-close" onClick = { ( ) => setShowOffModal ( false ) } > × < / button >
< / div >
< form onSubmit = { submitOff } >
< div className = "form-group" >
< label className = "form-label" > Mitarbeiter * < / label >
< select className = "form-select" value = { offForm . employee _user _id } onChange = { e => setOffForm ( { ... offForm , employee _user _id : e . target . value } ) } required >
< option value = "" > -- Bitte wählen -- < / option >
{ users . map ( u => < option key = { u . id } value = { u . id } > { u . first _name } { u . last _name } ( { u . username } ) < / option > ) }
< / select >
< / div >
< div className = "form-group" >
< label className = "form-label" > Austrittsdatum * < / label >
< input type = "date" className = "form-input" value = { offForm . exit _date } onChange = { e => setOffForm ( { ... offForm , exit _date : e . target . value } ) } required / >
< / div >
< div className = "form-group" >
< label className = "form-label" > Notizen < / label >
< textarea className = "form-textarea" rows = "3" value = { offForm . notes } onChange = { e => setOffForm ( { ... offForm , notes : e . target . value } ) } / >
< / div >
< div className = "card-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowOffModal ( false ) } > Abbrechen < / button >
< button type = "submit" className = "btn btn-primary" > Erstellen < / button >
< / div >
< / form >
< / div >
< / div >
) }
{ /* ── Offboarding: Edit/Detail Modal ─────────────────────────────────── */ }
{ showOffDetail && editingOff && (
< div className = "modal-overlay" onClick = { ( ) => setShowOffDetail ( false ) } >
< div className = "modal-content modal-large" onClick = { e => e . stopPropagation ( ) } >
< div className = "modal-header" >
< h2 className = "modal-title" > Offboarding : { editingOff . employee _sys _first _name || editingOff . employee _first _name } { editingOff . employee _sys _last _name || editingOff . employee _last _name } < / h2 >
< button className = "modal-close" onClick = { ( ) => setShowOffDetail ( false ) } > × < / button >
< / div >
< form onSubmit = { submitOff } >
< div className = "form-group" >
< label className = "form-label" > Status * < / label >
< select className = "form-select" value = { offForm . status } onChange = { e => setOffForm ( { ... offForm , status : e . target . value } ) } required >
< option value = "pending" > Ausstehend < / option >
< option value = "in_progress" > In Bearbeitung < / option >
< option value = "completed" > Abgeschlossen < / option >
< / select >
< / div >
< div className = "form-group" >
< label className = "form-label" > Checkliste < / label >
< ProcessChecklist
processes = { offProcesses . filter ( ( p , i , arr ) =>
arr . findIndex ( x => x . responsible _team === p . responsible _team && x . title === p . title ) === i
) }
checkedItems = { offForm . checklist _data }
onChange = { ci => setOffForm ( { ... offForm , checklist _data : ci } ) }
userTeam = { ( ! canManage && ! isSuperAdmin ( ) ) ? getUserTeam ( ) : null }
/ >
< / div >
< div className = "form-group" >
< label className = "form-label" > Notizen < / label >
< textarea className = "form-textarea" rows = "4" value = { offForm . notes } onChange = { e => setOffForm ( { ... offForm , notes : e . target . value } ) } / >
< / div >
< div className = "card-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowOffDetail ( false ) } > Abbrechen < / button >
< button type = "submit" className = "btn btn-primary" > Speichern < / button >
{ canManage && < button type = "button" className = "btn btn-secondary" onClick = { regenerateOffPdf } > 📄 PDF neu erstellen < / button > }
{ offForm . status !== 'completed' && < button type = "button" className = "btn btn-success" onClick = { completeOff } > ✓ Abschließen < / button > }
< / div >
< / form >
< / div >
< / div >
) }
{ /* ── Offboarding: Asset-Rückgabe Modal ─────────────────────────────── */ }
{ showReturnModal && (
< div className = "modal-overlay" onClick = { ( ) => setShowReturnModal ( false ) } >
< div className = "modal-content modal-large" onClick = { e => e . stopPropagation ( ) } >
< div className = "modal-header" >
< h2 className = "modal-title" > Assets zurückgeben < / h2 >
< button className = "modal-close" onClick = { ( ) => setShowReturnModal ( false ) } > × < / button >
< / div >
< form onSubmit = { submitReturn } >
< div className = "form-group" >
< label className = "form-label" > Zugewiesene Assets < / label >
{ assignedAssets . length === 0 ? (
< p style = { { color : 'var(--text-muted)' } } > Keine Assets zugewiesen < / p >
) : (
< table className = "table" >
< thead > < tr > < th > Typ < / th > < th > Name < / th > < th > Seriennummer < / th > < th > Zustand < / th > < / tr > < / thead >
< tbody >
{ assignedAssets . map ( a => (
< tr key = { a . id } >
< td > { a . type } < / td >
< td > { a . name } < / td >
< td style = { { fontFamily : 'monospace' , fontSize : '0.8rem' } } > { a . serial _number } < / td >
< td >
< select className = "form-select" value = { assetReturns . find ( r => r . asset _id === a . id ) ? . condition || 'gut' } onChange = { e => setAssetReturns ( assetReturns . map ( r => r . asset _id === a . id ? { ... r , condition : e . target . value } : r ) ) } >
< option value = "gut" > Gut < / option >
< option value = "beschaedigt" > Beschädigt < / option >
< / select >
< / td >
< / tr >
) ) }
< / tbody >
< / table >
) }
< / div >
< div className = "card-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowReturnModal ( false ) } > Abbrechen < / button >
< button type = "submit" className = "btn btn-primary" > Assets zurückgeben & PDF erstellen < / button >
< / div >
< / form >
< / div >
< / div >
) }
< / div >
) ;
} ;
export default OnOffboardingPage ;