Files
IT-Nexus/frontend/src/pages/OffboardingDetailPage.jsx
2026-06-11 12:36:12 +02:00

612 lines
40 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}