import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth } from '../context/AuthContext';
import { toast } from 'react-toastify';
/* ββ helpers ββ */
const authFetch = (url, opts = {}) => {
const token = localStorage.getItem('token');
return fetch(url, {
...opts,
headers: {
...(opts.headers || {}),
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
};
const API = process.env.REACT_APP_API_URL || '/api';
const TYPE_ICONS = {
notebook: 'π»',
server: 'π§',
monitor: 'π₯οΈ',
headset: 'π§',
drucker: 'π¨οΈ',
sonstiges: 'π¦',
};
const getTypeIcon = (type) => {
if (!type) return 'π¦';
const t = type.toLowerCase();
return TYPE_ICONS[t] || 'π¦';
};
const STATUS_LABELS = {
verfuegbar: 'VerfΓΌgbar',
zugewiesen: 'Zugewiesen',
inaktiv: 'Inaktiv',
beschaedigt: 'BeschΓ€digt',
};
const STATUS_CSS = {
verfuegbar: 'ap-badge-available',
zugewiesen: 'ap-badge-assigned',
inaktiv: 'ap-badge-inactive',
beschaedigt: 'ap-badge-damaged',
};
const fmtDate = (d) => {
if (!d) return 'β';
try { return new Date(d).toLocaleDateString('de-DE'); } catch { return d; }
};
const fmtCurrency = (n) => {
if (n === null || n === undefined || n === '') return 'β';
return parseFloat(n).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' β¬';
};
const calcBookValue = (asset) => {
if (!asset.purchase_price || !asset.purchase_date || !asset.useful_life_years) return null;
const price = parseFloat(asset.purchase_price);
const residual = parseFloat(asset.residual_value || 0);
const lifeMonths = parseFloat(asset.useful_life_years) * 12;
const depPerMonth = (price - residual) / lifeMonths;
const months = Math.max(0, (Date.now() - new Date(asset.purchase_date).getTime()) / (1000 * 60 * 60 * 24 * 30.44));
const depreciated = Math.min(price - residual, depPerMonth * months);
return Math.max(residual, price - depreciated);
};
const calcDepPercent = (asset) => {
if (!asset.purchase_price || !asset.purchase_date || !asset.useful_life_years) return 0;
const price = parseFloat(asset.purchase_price);
const residual = parseFloat(asset.residual_value || 0);
const lifeMonths = parseFloat(asset.useful_life_years) * 12;
const depPerMonth = (price - residual) / lifeMonths;
const months = Math.max(0, (Date.now() - new Date(asset.purchase_date).getTime()) / (1000 * 60 * 60 * 24 * 30.44));
const depreciated = Math.min(price - residual, depPerMonth * months);
return Math.round((depreciated / (price - residual || 1)) * 100);
};
const calcMonthlyDep = (asset) => {
if (!asset.purchase_price || !asset.useful_life_years) return null;
const price = parseFloat(asset.purchase_price);
const residual = parseFloat(asset.residual_value || 0);
const lifeMonths = parseFloat(asset.useful_life_years) * 12;
return (price - residual) / lifeMonths;
};
const relativeTime = (d) => {
if (!d) return 'β';
const diff = Date.now() - new Date(d).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 2) return 'gerade eben';
if (mins < 60) return `vor ${mins} Min`;
const h = Math.floor(mins / 60);
if (h < 24) return `vor ${h} Std`;
return `vor ${Math.floor(h / 24)} Tag(en)`;
};
const isOnline = (agent) => {
if (!agent || !agent.last_checkin) return false;
const diff = Date.now() - new Date(agent.last_checkin).getTime();
return diff < 5 * 60 * 1000;
};
const initials = (name) => {
if (!name) return '??';
const parts = name.trim().split(' ');
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
return name.slice(0, 2).toUpperCase();
};
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
EDIT MODAL
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
const EditModal = ({ asset, onClose, onSaved }) => {
const [form, setForm] = useState({
name: asset.name || '',
type: asset.type || '',
model: asset.model || '',
serial_number: asset.serial_number || '',
manufacturer: asset.manufacturer || '',
os: asset.os || '',
ip_address: asset.ip_address || '',
inventory_number: asset.inventory_number || '',
department: asset.department || '',
purchase_date: asset.purchase_date ? asset.purchase_date.slice(0, 10) : '',
purchase_price: asset.purchase_price || '',
useful_life_years: asset.useful_life_years || '',
residual_value: asset.residual_value || '',
next_maintenance_date: asset.next_maintenance_date ? asset.next_maintenance_date.slice(0, 10) : '',
maintenance_interval_months: asset.maintenance_interval_months || '',
maintenance_notes: asset.maintenance_notes || '',
teamviewer_id: asset.teamviewer_id || '',
description: asset.description || '',
});
const [saving, setSaving] = useState(false);
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
const save = async () => {
setSaving(true);
try {
const res = await authFetch(`${API}/assets/${asset.id}`, {
method: 'PUT',
body: JSON.stringify(form),
});
if (!res.ok) throw new Error(await res.text());
toast.success('Asset gespeichert');
onSaved();
} catch (e) {
toast.error('Fehler beim Speichern: ' + e.message);
} finally {
setSaving(false);
}
};
const Field = ({ label, k, type = 'text', placeholder }) => (
set(k, e.target.value)}
/>
);
return (
e.stopPropagation()}>
βοΈ Asset bearbeiten β {asset.name}
GerΓ€tedaten
Finanzen & Wartung
);
};
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ASSIGN MODAL
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
const AssignModal = ({ asset, onClose, onAssigned }) => {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState('');
const [notes, setNotes] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
authFetch(`${API}/users`)
.then(r => r.json())
.then(data => setUsers(data.data || data.users || (Array.isArray(data) ? data : [])))
.catch(() => {});
}, []);
const assign = async () => {
if (!selectedUser) { toast.error('Bitte Benutzer wΓ€hlen'); return; }
setSaving(true);
try {
const res = await authFetch(`${API}/assets/${asset.id}/assign`, {
method: 'POST',
body: JSON.stringify({ user_id: selectedUser, notes }),
});
if (!res.ok) throw new Error(await res.text());
toast.success('Asset zugewiesen');
onAssigned();
} catch (e) {
toast.error('Fehler: ' + e.message);
} finally {
setSaving(false);
}
};
return (
e.stopPropagation()}>
π€ Asset zuweisen β {asset.name}
);
};
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MAIN PAGE
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
const AssetsPage = () => {
const { user, isAdmin, isSuperAdmin, isSupport } = useAuth();
/* ββ State: Liste ββ */
const [assets, setAssets] = useState([]);
const [agents, setAgents] = useState([]);
const [stats, setStats] = useState({ total: 0, assigned: 0, available: 0, maintenance: 0 });
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [searchMode, setSearchMode] = useState('asset'); // 'asset' | 'user'
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
/* ββ State: Auswahl ββ */
const [selectedId, setSelectedId] = useState(null);
const [detail, setDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [history, setHistory] = useState([]);
const [inspections, setInspections] = useState([]);
const [activeTab, setActiveTab] = useState('uebersicht');
/* ββ State: Modals ββ */
const [showEdit, setShowEdit] = useState(false);
const [showAssign, setShowAssign] = useState(false);
const [showMoreMenu, setShowMoreMenu] = useState(false);
const moreMenuRef = useRef(null);
/* ββ State: Wartungsformular ββ */
const [maintForm, setMaintForm] = useState({ type: 'JΓ€hrliche Inspektion', date: '', notes: '', result: '' });
const [maintSaving, setMaintSaving] = useState(false);
/* ββ Permissions ββ */
const canWrite = () => isAdmin() || isSuperAdmin() || isSupport();
/* ββ Load ββ */
const loadAssets = useCallback(async () => {
setLoading(true);
try {
const [aRes, agRes, stRes] = await Promise.all([
authFetch(`${API}/assets`),
authFetch(`${API}/monitoring`),
authFetch(`${API}/assets/stats`),
]);
const aData = aRes.ok ? await aRes.json() : {};
const agData = agRes.ok ? await agRes.json() : {};
const stData = stRes.ok ? await stRes.json() : {};
const aList = aData.data ?? aData.assets ?? (Array.isArray(aData) ? aData : []);
const agList = agData.data ?? agData.agents ?? (Array.isArray(agData) ? agData : []);
const st = stData.data ?? stData;
setAssets(aList);
setAgents(agList);
setStats({
total: st.total ?? aList.length,
assigned: st.assigned ?? aList.filter(a => a.status === 'zugewiesen').length,
available: st.available ?? aList.filter(a => a.status === 'verfuegbar').length,
maintenance: st.maintenance ?? aList.filter(a => a.status === 'inaktiv').length,
});
} catch {
toast.error('Fehler beim Laden der Assets');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadAssets(); }, [loadAssets]);
/* ββ Load Detail ββ */
useEffect(() => {
if (!selectedId) { setDetail(null); return; }
setDetailLoading(true);
setHistory([]);
setInspections([]);
setActiveTab('uebersicht');
Promise.all([
authFetch(`${API}/assets/${selectedId}`).then(r => r.ok ? r.json() : null),
authFetch(`${API}/assets/${selectedId}/history`).then(r => r.ok ? r.json() : []),
authFetch(`${API}/assets/${selectedId}/inspections`).then(r => r.ok ? r.json() : []),
]).then(([d, h, ins]) => {
setDetail(d?.data ?? d);
setHistory(Array.isArray(h) ? h : (h?.data || h?.history || []));
setInspections(Array.isArray(ins) ? ins : (ins?.data || ins?.inspections || []));
}).catch(() => toast.error('Fehler beim Laden der Details'))
.finally(() => setDetailLoading(false));
}, [selectedId]);
/* ββ Close more-menu on outside click ββ */
useEffect(() => {
if (!showMoreMenu) return;
const handler = (e) => {
if (moreMenuRef.current && !moreMenuRef.current.contains(e.target)) {
setShowMoreMenu(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [showMoreMenu]);
/* ββ Filtered list ββ */
const filteredAssets = assets.filter(a => {
if (typeFilter !== 'all' && a.type?.toLowerCase() !== typeFilter) return false;
if (statusFilter !== 'all' && a.status !== statusFilter) return false;
if (!searchTerm) return true;
const q = searchTerm.toLowerCase();
return (
a.name?.toLowerCase().includes(q) ||
a.serial_number?.toLowerCase().includes(q) ||
a.inventory_number?.toLowerCase().includes(q) ||
a.model?.toLowerCase().includes(q) ||
a.assigned_to_username?.toLowerCase().includes(q) ||
a.assigned_to_name?.toLowerCase().includes(q) ||
a.manufacturer?.toLowerCase().includes(q)
);
});
/* ββ Agent lookup: by serial, IP or hostname ββ */
const getAgent = (asset) => {
if (!asset || !agents.length) return null;
return agents.find(ag =>
(asset.serial_number && ag.hardware_serial && ag.hardware_serial.toLowerCase() === asset.serial_number.toLowerCase()) ||
(asset.ip_address && ag.ip_address && ag.ip_address === asset.ip_address) ||
(asset.name && ag.hostname && ag.hostname.toLowerCase() === asset.name.toLowerCase())
) || null;
};
/* ββ PDF ΓΆffnen mit Auth-Token ββ */
const openPdf = async (url) => {
try {
const res = await authFetch(url);
if (!res.ok) throw new Error('Fehler beim Laden');
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, '_blank');
} catch (e) {
toast.error('PDF konnte nicht geΓΆffnet werden: ' + e.message);
}
};
/* ββ Actions ββ */
const handleUnassign = async () => {
if (!detail) return;
if (!window.confirm('Zuweisung wirklich aufheben?')) return;
try {
const res = await authFetch(`${API}/assets/${detail.id}/unassign`, { method: 'POST', body: JSON.stringify({ new_status: 'verfuegbar' }) });
if (!res.ok) throw new Error(await res.text());
toast.success('Zuweisung aufgehoben');
loadAssets();
setSelectedId(null);
} catch (e) {
toast.error('Fehler: ' + e.message);
}
};
const handleDelete = async () => {
if (!detail) return;
if (!window.confirm(`Asset "${detail.name}" wirklich lΓΆschen?`)) return;
setShowMoreMenu(false);
try {
const res = await authFetch(`${API}/assets/${detail.id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(await res.text());
toast.success('Asset gelΓΆscht');
setSelectedId(null);
loadAssets();
} catch (e) {
toast.error('Fehler: ' + e.message);
}
};
const handleMaintSubmit = async () => {
if (!maintForm.date) { toast.error('Bitte Datum angeben'); return; }
setMaintSaving(true);
try {
const res = await authFetch(`${API}/assets/${detail.id}/inspections`, {
method: 'POST',
body: JSON.stringify(maintForm),
});
if (!res.ok) throw new Error(await res.text());
toast.success('Wartung eingetragen');
const ins = await authFetch(`${API}/assets/${detail.id}/inspections`).then(r => r.json());
setInspections(Array.isArray(ins) ? ins : (ins?.inspections || []));
setMaintForm({ type: 'JΓ€hrliche Inspektion', date: '', notes: '', result: '' });
} catch (e) {
toast.error('Fehler: ' + e.message);
} finally {
setMaintSaving(false);
}
};
const copyToClipboard = (text, btn) => {
navigator.clipboard.writeText(text || '').then(() => {
if (btn) { const o = btn.textContent; btn.textContent = 'β'; setTimeout(() => btn.textContent = o, 1500); }
});
};
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RENDER DETAIL
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
const renderDetail = () => {
if (!selectedId) {
return (
π¦
Asset auswΓ€hlen
WΓ€hle ein Asset aus der Liste
);
}
if (detailLoading) {
return ;
}
if (!detail) return null;
const agent = getAgent(detail);
const online = isOnline(agent);
const bookValue = calcBookValue(detail);
const depPercent = calcDepPercent(detail);
const monthlyDep = calcMonthlyDep(detail);
const nextMaint = detail.next_maintenance_date;
const maintSoon = nextMaint && (new Date(nextMaint) - Date.now()) < 30 * 24 * 60 * 60 * 1000;
const maintOverdue = nextMaint && new Date(nextMaint) < new Date();
return (
<>
{/* Header */}
{getTypeIcon(detail.type)}
{detail.name}
{detail.inventory_number && <>{detail.inventory_number} Β· >}
{detail.serial_number && <>SN: {detail.serial_number} Β· >}
{detail.manufacturer || 'β'}
{detail.type && (
{detail.type}
)}
{STATUS_LABELS[detail.status] || detail.status}
{agent && (
{online ? 'Online' : 'Offline'}
)}
{canWrite() && (
)}
{canWrite() && detail.status === 'verfuegbar' && (
)}
{canWrite() && detail.status === 'zugewiesen' && (
)}
{showMoreMenu && (
{detail.status === 'zugewiesen' && (
)}
{detail.status === 'zugewiesen' && canWrite() && (
)}
{canWrite() && (
)}
)}
{/* KPI-Karten */}
π° Buchwert
{bookValue !== null ? fmtCurrency(bookValue) : 'β'}
{detail.purchase_price ? `Kauf: ${fmtCurrency(detail.purchase_price)}` : 'Kein Kaufpreis'}
{detail.purchase_date ? ` (${fmtDate(detail.purchase_date)})` : ''}
π§ NΓ€chste Wartung
{nextMaint ? fmtDate(nextMaint) : 'β'}
{maintOverdue ? 'β οΈ ΓberfΓ€llig' : maintSoon ? 'β οΈ In < 30 Tagen' : nextMaint ? 'Geplant' : 'Kein Termin'}
π€ Zugewiesen an
{detail.assigned_to_name || detail.assigned_to_username || 'β'}
{detail.assigned_since ? `seit ${fmtDate(detail.assigned_since)}` : detail.status === 'zugewiesen' ? 'Zugewiesen' : 'Nicht zugewiesen'}
π€ Agent-Sync
{agent ? relativeTime(agent.last_checkin) : 'β'}
{agent ? `v${agent.agent_version || '?'} Β· ${online ? 'aktiv' : 'offline'}` : 'Kein Agent'}
{/* Tab-Bar */}
{['uebersicht', 'wartung', 'verlauf', 'agent', 'dokumente'].map(tab => (
setActiveTab(tab)}
>
{{ uebersicht: 'Γbersicht', wartung: 'Wartung', verlauf: 'Verlauf', agent: 'Agent-Daten', dokumente: 'Dokumente' }[tab]}
))}
{/* ββ TAB: ΓBERSICHT ββ */}
{activeTab === 'uebersicht' && (
<>
{/* GerΓ€teinformationen */}
π₯οΈ GerΓ€teinformationen
{[
['Typ', detail.type],
['Hersteller', detail.manufacturer],
['Modell', detail.model],
['Seriennummer', detail.serial_number],
['Betriebssystem', detail.os],
['Inventarnummer', detail.inventory_number],
['Abteilung', detail.department],
].map(([label, val]) => (
{label}
{val || 'β'}
))}
IP-Adresse
{detail.ip_address || 'β'}
{detail.ip_address && (
)}
{/* Finanzen & AfA */}
π Finanzdaten & AfA
{[
['Kaufpreis', fmtCurrency(detail.purchase_price)],
['Kaufdatum', fmtDate(detail.purchase_date)],
['Nutzungsdauer', detail.useful_life_years ? `${detail.useful_life_years} Jahre` : 'β'],
['Buchwert aktuell', bookValue !== null ? fmtCurrency(bookValue) : 'β', bookValue !== null ? '#34d399' : null],
['Restwert', fmtCurrency(detail.residual_value)],
['Abschreibung/Monat', monthlyDep !== null ? fmtCurrency(monthlyDep) : 'β'],
].map(([label, val, color]) => (
{label}
{val}
))}
{detail.purchase_price && detail.useful_life_years && (
Abgeschrieben
{depPercent} %
80 ? 'ap-fill-red' : depPercent > 50 ? 'ap-fill-amber' : 'ap-fill-teal'}`}
style={{ width: `${Math.min(100, depPercent)}%` }}
/>
Kauf: {fmtDate(detail.purchase_date)}
{detail.purchase_date && detail.useful_life_years && (
Ende: {fmtDate(new Date(new Date(detail.purchase_date).setFullYear(new Date(detail.purchase_date).getFullYear() + parseInt(detail.useful_life_years))))}
)}
)}
{/* Zuweisung */}
π€ Zuweisung
{(detail.assigned_to_name || detail.assigned_to_username) ? (
<>
{initials(detail.assigned_to_name || detail.assigned_to_username)}
{detail.assigned_to_name || detail.assigned_to_username}
{detail.assigned_to_email && (
{detail.assigned_to_email}
)}
{detail.assigned_since && (
Zugewiesen seit
{fmtDate(detail.assigned_since)}
)}
{canWrite() && (
)}
>
) : (
Kein Benutzer zugewiesen
)}
{/* Agent-Status */}
π€ Agent-Status
{agent ? (
<>
{agent.cpu_usage !== undefined && agent.cpu_usage !== null && (
)}
{agent.ram_used !== undefined && agent.ram_total !== undefined && (
RAM
{(agent.ram_used / 1024).toFixed(1)} / {(agent.ram_total / 1024).toFixed(0)} GB
)}
{agent.disk_used !== undefined && agent.disk_total !== undefined && (
Disk
{Math.round(agent.disk_used)} / {Math.round(agent.disk_total)} GB
)}
{[
['Letzter Check-in', relativeTime(agent.last_checkin), online ? '#34d399' : null],
['Agent-Version', agent.agent_version ? `v${agent.agent_version}` : 'β'],
['Hostname', agent.hostname || agent.computer_name || 'β'],
].map(([label, val, color]) => (
{label}
{val}
))}
>
) : (
Kein Agent-Datensatz gefunden
)}
>
)}
{/* ββ TAB: WARTUNG ββ */}
{activeTab === 'wartung' && (
π
Wartungsplan
{inspections.length === 0 ? (
Noch keine Wartungen erfasst
) : (
{inspections.map((ins, i) => (
{fmtDate(ins.date || ins.inspection_date)} Β· {ins.result || 'Abgeschlossen'}
{ins.type || ins.inspection_type || 'Wartung'}
{ins.notes &&
{ins.notes}
}
))}
)}
{canWrite() && (
π§ Wartung eintragen
Wartungstyp
Ergebnis
)}
)}
{/* ββ TAB: VERLAUF ββ */}
{activeTab === 'verlauf' && (
π Zuweisungshistorie
{history.length === 0 ? (
Keine Historiendaten vorhanden
) : (
| Benutzer |
Von |
Bis |
Notizen |
{history.map((h, i) => (
| {h.username || h.user_name || h.assigned_to_username || 'β'} |
{fmtDate(h.assigned_at || h.from_date)} |
{h.unassigned_at || h.to_date ? fmtDate(h.unassigned_at || h.to_date) : Aktiv} |
{h.notes || 'β'} |
))}
)}
)}
{/* ββ TAB: AGENT-DATEN ββ */}
{activeTab === 'agent' && (
{/* System */}
π» System
{agent ? (
[
['Hostname', agent.hostname || agent.computer_name],
['OS', agent.os_version || agent.os],
['Build', agent.os_build],
['Architektur', agent.architecture],
['Prozessor', agent.cpu_model],
['Kerne', agent.cpu_cores ? `${agent.cpu_cores} Kerne` : undefined],
['RAM gesamt', agent.ram_total ? `${(agent.ram_total / 1024).toFixed(0)} GB` : undefined],
].map(([label, val]) => val ? (
{label}
{val}
) : null)
) :
Kein Agent verbunden
}
{/* Netzwerk */}
π Netzwerk
{agent ? (
[
['IPv4', agent.ip_address],
['IPv6', agent.ipv6_address],
['MAC', agent.mac_address],
['Interface', agent.network_interface],
['Gateway', agent.gateway],
['DNS', agent.dns_server],
].map(([label, val]) => val ? (
{label}
{val}
) : null)
) :
Kein Agent verbunden
}
{/* Sicherheit */}
π‘οΈ Sicherheit
{agent ? (
<>
Windows Defender
{agent.defender_enabled ? 'β Aktiv' : 'β Inaktiv'}
{agent.defender_signatures && (
Signaturen
{agent.defender_signatures}
)}
BitLocker
{agent.bitlocker_enabled ? 'β Aktiv' : 'β Nicht aktiv'}
Firewall
{agent.firewall_enabled ? 'β Aktiv' : 'β Inaktiv'}
{agent.pending_reboot !== undefined && (
Pending Reboot
{agent.pending_reboot ? 'β οΈ Ja' : 'Keine'}
)}
>
) :
Kein Agent verbunden
}
{/* Updates */}
π¦ Updates
{agent ? (
[
['Ausstehend', agent.pending_updates !== undefined ? `${agent.pending_updates} Updates` : undefined, agent.pending_updates > 0 ? '#f59e0b' : '#34d399'],
['Letztes Update', agent.last_update_date ? fmtDate(agent.last_update_date) : undefined],
['Agent-Version', agent.agent_version ? `v${agent.agent_version}` : undefined],
['Letzter Check-in', relativeTime(agent.last_checkin), online ? '#34d399' : null],
].map(([label, val, color]) => val !== undefined ? (
{label}
{val}
) : null)
) :
Kein Agent verbunden
}
{/* Auslastung */}
π Auslastung
{agent ? (() => {
const cpuPct = agent.cpu_usage_percent ?? null;
const ramUsed = agent.ram_used_gb ?? null;
const ramTotal = agent.ram_total_gb ?? null;
const ramPct = ramTotal > 0 ? Math.round((ramUsed / ramTotal) * 100) : null;
const diskTotal = agent.disk_total_gb ?? null;
const diskFree = agent.disk_free_gb ?? null;
const diskUsed = diskTotal != null && diskFree != null ? diskTotal - diskFree : null;
const diskPct = diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100) : null;
const uptimeH = agent.uptime_hours ?? null;
const fmtUptime = (h) => {
if (h == null) return 'β';
if (h < 24) return `${Math.floor(h)} Std.`;
return `${Math.floor(h / 24)} Tag(e)`;
};
const barColor = (p) => p > 80 ? '#ef4444' : p > 60 ? '#f59e0b' : '#34d399';
const Bar = ({ pct: p }) => (
);
return (
<>
{cpuPct != null && (
CPU-Auslastung
{cpuPct.toFixed(1)} %
)}
{ramUsed != null && ramTotal != null && (
RAM
{ramUsed.toFixed(1)} / {ramTotal.toFixed(0)} GB
)}
{diskUsed != null && diskTotal != null && (
Disk
{diskUsed.toFixed(0)} / {diskTotal.toFixed(0)} GB
)}
{uptimeH != null && (
Laufzeit
{fmtUptime(uptimeH)}
)}
{cpuPct == null && ramUsed == null && diskUsed == null && uptimeH == null && (
Keine Auslastungsdaten
)}
>
);
})() :
Kein Agent verbunden
}
)}
{/* ββ TAB: DOKUMENTE ββ */}
{activeTab === 'dokumente' && (
π Dokumente & Aktionen
π·οΈ
Asset-Label drucken
Format wΓ€hlen und als PDF ΓΆffnen
{[
{ key: 'small', label: 'Klein (50Γ25mm)' },
{ key: 'medium', label: 'Mittel (80Γ40mm)' },
{ key: 'large', label: 'GroΓ (A5)' },
{ key: 'ptouch12', label: 'P-Touch 700 (12mm)' },
].map(f => (
))}
{detail.status === 'zugewiesen' && (
openPdf(`${API}/assets/${detail.id}/handover-protocol`)}
>
π
Γbergabeprotokoll
PDF mit GerΓ€tedaten und Unterschriftenfeld
)}
)}
{/* Modals */}
{showEdit && (
setShowEdit(false)}
onSaved={() => {
setShowEdit(false);
loadAssets();
// Reload detail
authFetch(`${API}/assets/${detail.id}`).then(r => r.json()).then(setDetail).catch(() => {});
}}
/>
)}
{showAssign && (
setShowAssign(false)}
onAssigned={() => {
setShowAssign(false);
loadAssets();
authFetch(`${API}/assets/${detail.id}`).then(r => r.json()).then(setDetail).catch(() => {});
}}
/>
)}
>
);
};
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MAIN RENDER
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
if (loading) {
return (
);
}
const userFilterActive = searchMode === 'user' && searchTerm;
const userFilterMatches = userFilterActive
? [...new Set(filteredAssets.map(a => a.assigned_to_name || a.assigned_to_username).filter(Boolean))]
: [];
return (
{/* ββ LEFT PANEL ββ */}
Assets
{assets.length}
{/* Suchfeld */}
π
setSearchTerm(e.target.value)}
/>
{/* Typ-Pills */}
{[
['all', 'Alle'],
['notebook', 'Notebook'],
['server', 'Server'],
['monitor', 'Monitor'],
['headset', 'Headset'],
['drucker', 'Drucker'],
['sonstiges', 'Sonstiges'],
].map(([val, label]) => (
setTypeFilter(val)}
>{label}
))}
{/* Status-Pills */}
{[
['all', 'Alle'],
['verfuegbar', 'VerfΓΌgbar'],
['zugewiesen', 'Zugewiesen'],
['inaktiv', 'Inaktiv'],
['beschaedigt', 'BeschΓ€digt'],
].map(([val, label]) => (
setStatusFilter(val)}
>{label}
))}
{/* User-Filter-Banner */}
{userFilterActive && (
π€
{userFilterMatches.length > 0
? <>Assets von: {userFilterMatches.join(', ')} ({filteredAssets.length})>
: <>Kein Treffer fΓΌr "{searchTerm}">}
)}
{/* Stat-Grid */}
{stats.total || assets.length}
Gesamt
{stats.assigned}
Zugewiesen
{stats.available}
VerfΓΌgbar
{stats.maintenance}
In Wartung
{/* Asset-Liste */}
{filteredAssets.length === 0 && (
Keine Assets gefunden
)}
{filteredAssets.map(asset => {
const agent = getAgent(asset);
const online = isOnline(agent);
return (
setSelectedId(asset.id)}
>
{getTypeIcon(asset.type)}
{asset.name}
{asset.inventory_number && `${asset.inventory_number} Β· `}
{asset.serial_number ? `SN: ${asset.serial_number}` : asset.model || ''}
{STATUS_LABELS[asset.status] || asset.status}
);
})}
{/* ββ RIGHT PANEL ββ */}
{renderDetail()}
);
};
export default AssetsPage;