612 lines
40 KiB
JavaScript
612 lines
40 KiB
JavaScript
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 (
|
||
<span style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||
fontSize: 12, fontWeight: 600, padding: '3px 10px', borderRadius: 20,
|
||
color, background: `${color}18`, border: `1px solid ${color}40`,
|
||
}}>
|
||
<span style={{ width: 7, height: 7, borderRadius: '50%', background: color, display: 'inline-block' }} />
|
||
{STATUS_LABEL[status] || status}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function ProcessChecklist({ processes, checkedItems, onChange, disabled, userTeam = null }) {
|
||
if (!processes?.length) return (
|
||
<p style={{ color: 'var(--text-muted)', fontSize: 13 }}>Keine Prozesse konfiguriert.</p>
|
||
);
|
||
|
||
// 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 (
|
||
<div>
|
||
<div style={{ marginBottom: 16, padding: '10px 14px', background: 'var(--bg-secondary)', borderRadius: 8, border: '1px solid var(--border-color)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||
<span style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.05em' }}>Gesamtfortschritt</span>
|
||
<span style={{ fontSize: 12, fontFamily: 'monospace', color: pct === 100 ? '#10b981' : 'var(--text-muted)' }}>{checked} / {total} · {pct}%</span>
|
||
</div>
|
||
<div style={{ height: 6, background: 'var(--border-color)', borderRadius: 3, overflow: 'hidden' }}>
|
||
<div style={{ height: '100%', width: `${pct}%`, background: pct === 100 ? '#10b981' : 'var(--cereda-primary)', borderRadius: 3, transition: 'width .3s' }} />
|
||
</div>
|
||
</div>
|
||
|
||
{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 (
|
||
<div key={team} style={{ marginBottom: 10, border: '1px solid var(--border-color)', borderLeft: `3px solid ${tc}`, borderRadius: 8, overflow: 'hidden' }}>
|
||
<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: 13, flex: 1, color: tc }}>{TEAM_LABELS[team]}</span>
|
||
<span style={{ fontSize: 11, fontFamily: 'monospace', color: grpChecked === items.length ? '#10b981' : 'var(--text-muted)' }}>
|
||
{grpChecked}/{items.length}
|
||
</span>
|
||
</div>
|
||
<div style={{ padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||
{items.map(proc => {
|
||
const key = `proc_${proc.id}`;
|
||
const isChecked = !!checkedItems[key];
|
||
const tagColor = TAG_COLORS[proc.tag] || '#64748b';
|
||
return (
|
||
<div key={key}
|
||
onClick={() => !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',
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
|
||
<div style={{
|
||
width: 15, height: 15, flexShrink: 0, marginTop: 2,
|
||
border: `1.5px solid ${isChecked ? tagColor : 'var(--border-color)'}`,
|
||
borderRadius: 3, background: isChecked ? tagColor : 'transparent',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 9, color: '#fff',
|
||
}}>{isChecked ? '✓' : ''}</div>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 500, color: isChecked ? 'var(--text-muted)' : 'var(--text-primary)', textDecoration: isChecked ? 'line-through' : 'none' }}>
|
||
{proc.title}
|
||
</div>
|
||
{proc.description && (
|
||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{proc.description}</div>
|
||
)}
|
||
</div>
|
||
<span style={{ fontSize: 10, fontWeight: 600, padding: '2px 7px', borderRadius: 4, background: `${tc}18`, color: tc, flexShrink: 0 }}>
|
||
{TEAM_LABELS[team]}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 <div className="main-content"><LoadingSpinner /></div>;
|
||
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 (
|
||
<div className="main-content" style={{ paddingBottom: 40 }}>
|
||
{/* ── Breadcrumb ── */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 20, fontSize: 13, color: 'var(--text-muted)' }}>
|
||
<button onClick={() => navigate('/lifecycle')}
|
||
style={{ background: 'none', border: 'none', color: 'var(--cereda-primary)', cursor: 'pointer', padding: 0, fontSize: 13 }}>
|
||
← Lifecycle
|
||
</button>
|
||
<span>/</span>
|
||
<span>Offboarding</span>
|
||
<span>/</span>
|
||
<span style={{ color: 'var(--text-primary)' }}>{protocol.employee_name || protocol.employee_email}</span>
|
||
</div>
|
||
|
||
{/* ── Header Card ── */}
|
||
<div style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 16, marginBottom: 20, overflow: 'hidden' }}>
|
||
{/* farbige Top-Linie */}
|
||
<div style={{ height: 4, background: statusColor }} />
|
||
<div style={{ padding: '20px 24px', display: 'flex', alignItems: 'center', gap: 18, flexWrap: 'wrap' }}>
|
||
{/* Avatar */}
|
||
<div style={{
|
||
width: 56, height: 56, borderRadius: 14, flexShrink: 0,
|
||
background: `linear-gradient(135deg, ${statusColor}cc, ${statusColor}88)`,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 20, fontWeight: 700, color: '#fff',
|
||
}}>{initials}</div>
|
||
|
||
{/* Name + Meta */}
|
||
<div style={{ flex: 1, minWidth: 200 }}>
|
||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 5 }}>
|
||
{protocol.employee_name || protocol.employee_email}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 13, color: 'var(--text-muted)', alignItems: 'center' }}>
|
||
{protocol.employee_email && <span>✉️ {protocol.employee_email}</span>}
|
||
<span>📅 Austritt: {protocol.exit_date ? new Date(protocol.exit_date).toLocaleDateString('de-DE') : '—'}</span>
|
||
{daysLeft !== null && (
|
||
<span style={{ color: daysLeft < 7 ? '#ef4444' : daysLeft < 14 ? '#f59e0b' : 'var(--text-muted)' }}>
|
||
{daysLeft > 0 ? `⏳ noch ${daysLeft} Tage` : daysLeft === 0 ? '⚠️ heute' : `✅ vor ${Math.abs(daysLeft)} Tagen`}
|
||
</span>
|
||
)}
|
||
<StatusBadge status={protocol.status} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Aktionen */}
|
||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||
{canAct && protocol.status !== 'completed' && (
|
||
<button onClick={openReturnAssets}
|
||
style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: 8, color: 'var(--text-primary)', padding: '7px 14px', fontSize: 13, cursor: 'pointer', fontWeight: 500 }}>
|
||
📦 Assets zurück
|
||
</button>
|
||
)}
|
||
{canAct && (
|
||
<button onClick={regeneratePdf}
|
||
style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: 8, color: 'var(--text-primary)', padding: '7px 14px', fontSize: 13, cursor: 'pointer' }}>
|
||
📄 PDF
|
||
</button>
|
||
)}
|
||
{canAct && protocol.status !== 'completed' && (
|
||
<button onClick={complete}
|
||
style={{ background: '#238636', border: 'none', borderRadius: 8, color: '#fff', padding: '7px 16px', fontSize: 13, cursor: 'pointer', fontWeight: 600 }}>
|
||
✓ Abschließen
|
||
</button>
|
||
)}
|
||
{canManage && (
|
||
<button onClick={deleteProtocol}
|
||
style={{ background: 'transparent', border: '1px solid #da363340', borderRadius: 8, color: '#ef4444', padding: '7px 14px', fontSize: 13, cursor: 'pointer' }}>
|
||
🗑️
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Fortschrittsbalken */}
|
||
{total > 0 && (
|
||
<div style={{ padding: '10px 24px 14px', display: 'flex', alignItems: 'center', gap: 14, borderTop: '1px solid var(--border-color)' }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>Checkliste</span>
|
||
<div style={{ flex: 1, height: 6, background: 'var(--border-color)', borderRadius: 3, overflow: 'hidden' }}>
|
||
<div style={{ height: '100%', width: `${pct}%`, background: pct === 100 ? '#10b981' : 'var(--cereda-primary)', borderRadius: 3, transition: 'width .4s' }} />
|
||
</div>
|
||
<span style={{ fontSize: 12, fontWeight: 600, color: pct === 100 ? '#10b981' : 'var(--cereda-primary)', whiteSpace: 'nowrap' }}>
|
||
{pct}% · {checked}/{total}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Tabs ── */}
|
||
<div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border-color)', marginBottom: 20 }}>
|
||
{TABS.map(t => (
|
||
<button key={t.key} onClick={() => setActiveTab(t.key)}
|
||
style={{
|
||
background: 'none', border: 'none', padding: '9px 16px', fontSize: 13,
|
||
cursor: 'pointer', fontWeight: 500,
|
||
color: activeTab === t.key ? 'var(--cereda-primary)' : 'var(--text-muted)',
|
||
borderBottom: `2px solid ${activeTab === t.key ? 'var(--cereda-primary)' : 'transparent'}`,
|
||
marginBottom: -1, transition: 'all .15s',
|
||
}}>
|
||
{t.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ── Tab: Übersicht ── */}
|
||
{activeTab === 'overview' && (
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 16 }}>
|
||
{/* Stat-Kacheln */}
|
||
<div style={{ gridColumn: '1 / -1', display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
|
||
{[
|
||
{ 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) => (
|
||
<div key={i} style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderLeft: `3px solid ${s.color}`, borderRadius: 10, padding: '14px 16px' }}>
|
||
<div style={{ fontSize: 26, fontWeight: 700, color: '#fff' }}>{s.value}</div>
|
||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{s.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Team-Fortschritt */}
|
||
<div style={{ gridColumn: '1 / -1', background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 12, padding: 18 }}>
|
||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.5px', marginBottom: 14 }}>👥 Checklisten-Fortschritt nach Team</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
|
||
{['it', 'hr', 'buchhaltung'].map(team => {
|
||
const teamProcs = processes.filter(p => p.responsible_team === team);
|
||
if (!teamProcs.length) return null;
|
||
const done = teamProcs.filter(p => !!checklist[`proc_${p.id}`]).length;
|
||
const pctT = Math.round(done / teamProcs.length * 100);
|
||
const tc = TEAM_COLORS[team];
|
||
const isMyTeam = userTeam === team;
|
||
return (
|
||
<div key={team} style={{ border: `1px solid ${isMyTeam ? tc : 'var(--border-color)'}`, borderLeft: `3px solid ${tc}`, borderRadius: 8, padding: '12px 14px', background: isMyTeam ? `${tc}0a` : 'transparent' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||
<span style={{ fontSize: 16 }}>{TEAM_ICONS[team]}</span>
|
||
<span style={{ fontWeight: 600, fontSize: 13, color: tc }}>{TEAM_LABELS[team]}</span>
|
||
{isMyTeam && <span style={{ fontSize: 10, background: `${tc}20`, color: tc, borderRadius: 4, padding: '1px 6px', fontWeight: 700, marginLeft: 'auto' }}>Dein Team</span>}
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>{done} / {teamProcs.length} erledigt</span>
|
||
<span style={{ fontSize: 12, fontWeight: 700, color: pctT === 100 ? '#10b981' : 'var(--text-primary)' }}>{pctT}%</span>
|
||
</div>
|
||
<div style={{ height: 6, background: 'var(--border-color)', borderRadius: 3, overflow: 'hidden' }}>
|
||
<div style={{ height: '100%', width: `${pctT}%`, background: pctT === 100 ? '#10b981' : tc, borderRadius: 3, transition: 'width .3s' }} />
|
||
</div>
|
||
{pctT < 100 && (
|
||
<div style={{ marginTop: 8, fontSize: 11, color: '#f59e0b' }}>
|
||
⚠ {teamProcs.length - done} Aufgabe{teamProcs.length - done !== 1 ? 'n' : ''} offen
|
||
</div>
|
||
)}
|
||
{pctT === 100 && (
|
||
<div style={{ marginTop: 8, fontSize: 11, color: '#10b981' }}>✓ Vollständig abgehakt</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Allgemeine Infos */}
|
||
<div style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 12, padding: 18 }}>
|
||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.5px', marginBottom: 12 }}>📋 Allgemeine Infos</div>
|
||
{[
|
||
['Mitarbeiter', protocol.employee_name || '—'],
|
||
['E-Mail', protocol.employee_email || '—'],
|
||
['Austrittsdatum', protocol.exit_date ? new Date(protocol.exit_date).toLocaleDateString('de-DE') : '—'],
|
||
['Status', <StatusBadge status={protocol.status} />],
|
||
['Erstellt von', protocol.created_by_username || '—'],
|
||
['Erstellt am', protocol.created_at ? new Date(protocol.created_at).toLocaleDateString('de-DE') : '—'],
|
||
].map(([label, value]) => (
|
||
<div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '7px 0', borderBottom: '1px solid rgba(255,255,255,.04)', fontSize: 13, gap: 12 }}>
|
||
<span style={{ color: 'var(--text-muted)', flexShrink: 0 }}>{label}</span>
|
||
<span style={{ color: 'var(--text-primary)', fontWeight: 500, textAlign: 'right' }}>{value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Assets Vorschau */}
|
||
<div style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 12, padding: 18 }}>
|
||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.5px', marginBottom: 12 }}>📦 Zugewiesene Assets</div>
|
||
{assets.length === 0 ? (
|
||
<p style={{ fontSize: 13, color: 'var(--text-muted)', textAlign: 'center', padding: '20px 0' }}>Keine Assets zugewiesen</p>
|
||
) : assets.slice(0, 5).map(a => (
|
||
<div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,.04)', fontSize: 13 }}>
|
||
<span style={{ fontSize: 18 }}>{a.type === 'notebook' ? '💻' : a.type === 'smartphone' ? '📱' : '📦'}</span>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 500 }}>{a.name || a.model}</div>
|
||
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{a.serial_number}</div>
|
||
</div>
|
||
<span style={{ fontSize: 11, color: '#f59e0b', background: 'rgba(245,158,11,.1)', padding: '2px 8px', borderRadius: 4 }}>ausstehend</span>
|
||
</div>
|
||
))}
|
||
{assets.length > 5 && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 8 }}>+{assets.length - 5} weitere</div>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Tab: Checkliste ── */}
|
||
{activeTab === 'checklist' && (
|
||
<div>
|
||
<ProcessChecklist
|
||
processes={processes}
|
||
checkedItems={checklist}
|
||
onChange={setChecklist}
|
||
disabled={protocol.status === 'completed'}
|
||
userTeam={userTeam}
|
||
/>
|
||
{protocol.status !== 'completed' && canAct && (
|
||
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||
<button onClick={save} disabled={saving}
|
||
style={{ background: 'var(--cereda-primary)', border: 'none', borderRadius: 8, color: '#fff', padding: '8px 20px', fontSize: 13, fontWeight: 600, cursor: 'pointer', opacity: saving ? .7 : 1 }}>
|
||
{saving ? 'Speichert…' : '💾 Fortschritt speichern'}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Tab: Assets ── */}
|
||
{activeTab === 'assets' && (
|
||
<div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}>
|
||
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>{assets.length} Asset(s) zugewiesen</span>
|
||
{canAct && protocol.status !== 'completed' && (
|
||
<button onClick={openReturnAssets}
|
||
style={{ background: '#238636', border: 'none', borderRadius: 8, color: '#fff', padding: '7px 16px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
|
||
📦 Assets zurückgeben
|
||
</button>
|
||
)}
|
||
</div>
|
||
{assets.length === 0 ? (
|
||
<div style={{ textAlign: 'center', padding: '48px 0', color: 'var(--text-muted)' }}>
|
||
<div style={{ fontSize: 40, marginBottom: 12 }}>📦</div>
|
||
<div>Keine Assets zugewiesen</div>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{assets.map(a => (
|
||
<div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 10 }}>
|
||
<div style={{ width: 38, height: 38, borderRadius: 8, background: 'rgba(63,163,163,.1)', border: '1px solid rgba(63,163,163,.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18 }}>
|
||
{a.type === 'notebook' ? '💻' : a.type === 'smartphone' ? '📱' : '📦'}
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 600, fontSize: 14 }}>{a.name || a.model}</div>
|
||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>SN: {a.serial_number} · {a.type}</div>
|
||
</div>
|
||
<span style={{ fontSize: 11, fontWeight: 600, color: '#f59e0b', background: 'rgba(245,158,11,.12)', border: '1px solid rgba(245,158,11,.3)', padding: '3px 10px', borderRadius: 20 }}>
|
||
Ausstehend
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Tab: Notizen & PDF ── */}
|
||
{activeTab === 'notes' && (
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||
<div>
|
||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 8, textTransform: 'uppercase', letterSpacing: '.5px' }}>Interne Notizen</div>
|
||
<textarea
|
||
value={notes}
|
||
onChange={e => setNotes(e.target.value)}
|
||
disabled={protocol.status === 'completed' || !canAct}
|
||
placeholder="Notizen zum Offboarding..."
|
||
style={{ width: '100%', minHeight: 160, background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 8, color: 'var(--text-primary)', fontSize: 13, padding: '10px 12px', fontFamily: 'inherit', resize: 'vertical' }}
|
||
/>
|
||
{canAct && protocol.status !== 'completed' && (
|
||
<button onClick={save} disabled={saving}
|
||
style={{ marginTop: 10, background: 'var(--cereda-primary)', border: 'none', borderRadius: 8, color: '#fff', padding: '7px 16px', fontSize: 13, fontWeight: 600, cursor: 'pointer', opacity: saving ? .7 : 1 }}>
|
||
{saving ? 'Speichert…' : '💾 Speichern'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 12, padding: 20 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.5px' }}>📄 PDF-Dokument</div>
|
||
{protocol.pdf_file_path && (
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<a href={offboardingService.downloadPdf(protocol.pdf_file_path)} target="_blank" rel="noreferrer"
|
||
style={{ background: 'var(--cereda-primary)', border: 'none', borderRadius: 6, color: '#fff', padding: '5px 12px', fontSize: 12, textDecoration: 'none', fontWeight: 600 }}>
|
||
📥 Download
|
||
</a>
|
||
{canAct && (
|
||
<button onClick={regeneratePdf}
|
||
style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: 6, color: 'var(--text-primary)', padding: '5px 12px', fontSize: 12, cursor: 'pointer' }}>
|
||
🔄 Neu
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{protocol.pdf_file_path ? (
|
||
<iframe
|
||
src={offboardingService.downloadPdf(protocol.pdf_file_path)}
|
||
title="Offboarding PDF"
|
||
style={{ width: '100%', height: 600, border: 'none', borderRadius: 8, background: '#fff' }}
|
||
/>
|
||
) : (
|
||
<div style={{ textAlign: 'center', padding: '32px 0', color: 'var(--text-muted)' }}>
|
||
<div style={{ fontSize: 36, marginBottom: 10 }}>📄</div>
|
||
<div style={{ fontSize: 13, marginBottom: 14 }}>Noch kein PDF generiert</div>
|
||
{canAct && (
|
||
<button onClick={regeneratePdf}
|
||
style={{ background: 'transparent', border: '1px solid var(--cereda-primary)', borderRadius: 8, color: 'var(--cereda-primary)', padding: '7px 16px', fontSize: 13, cursor: 'pointer', fontWeight: 600 }}>
|
||
📄 PDF erstellen
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Asset-Rückgabe Modal ── */}
|
||
{showReturnModal && (
|
||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', backdropFilter: 'blur(4px)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
|
||
<div style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-color)', borderRadius: 16, width: '100%', maxWidth: 540, overflow: 'hidden' }}>
|
||
<div style={{ padding: '18px 24px', borderBottom: '1px solid var(--border-color)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||
<span style={{ fontWeight: 700, fontSize: 15 }}>📦 Assets zurückgeben</span>
|
||
<button onClick={() => setShowReturnModal(false)} style={{ background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', fontSize: 18 }}>✕</button>
|
||
</div>
|
||
<form onSubmit={submitReturn} style={{ padding: 24 }}>
|
||
{assets.length === 0 ? (
|
||
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '16px 0' }}>Keine Assets zum Zurückgeben</p>
|
||
) : assets.map((a, i) => (
|
||
<div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderBottom: '1px solid var(--border-color)' }}>
|
||
<span style={{ fontSize: 18 }}>{a.type === 'notebook' ? '💻' : a.type === 'smartphone' ? '📱' : '📦'}</span>
|
||
<span style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>{a.name || a.model}</span>
|
||
<select
|
||
value={assetReturns[i]?.condition || 'gut'}
|
||
onChange={e => setAssetReturns(prev => prev.map((r, j) => j === i ? { ...r, condition: e.target.value } : r))}
|
||
style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 6, color: 'var(--text-primary)', padding: '4px 8px', fontSize: 12 }}>
|
||
<option value="gut">Gut</option>
|
||
<option value="beschaedigt">Beschädigt</option>
|
||
<option value="verloren">Verloren</option>
|
||
</select>
|
||
</div>
|
||
))}
|
||
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||
<button type="button" onClick={() => setShowReturnModal(false)}
|
||
style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border-color)', borderRadius: 8, color: 'var(--text-primary)', padding: '8px 16px', fontSize: 13, cursor: 'pointer' }}>
|
||
Abbrechen
|
||
</button>
|
||
<button type="submit"
|
||
style={{ background: '#238636', border: 'none', borderRadius: 8, color: '#fff', padding: '8px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
|
||
✓ Bestätigen & PDF erstellen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|