// Desktop Equipment-Analyse — comprehensive analytics modal opened from the // Equipment toolbar. Aggregates rentals + maintenance data into KPIs, rankings, // revenue history, and a full detail table. Read-only. // ── Helpers ───────────────────────────────────────────────────────────── // Date arithmetic const _eaYM = (iso) => iso ? iso.slice(0, 7) : ''; const _eaMonthsBack = (n) => { const today = new Date(); const out = []; for (let i = n - 1; i >= 0; i--) { const d = new Date(today.getFullYear(), today.getMonth() - i, 1); out.push({ key: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`, label: DE_MONTHS_SHORT[d.getMonth()], isCurrent: i === 0, }); } return out; }; // Per-rental: which equipment IDs + qty + dailyRate it has function _eaRentalLines(r) { if (Array.isArray(r.items) && r.items.length) { return r.items.map((it) => ({ equipmentId: it.equipmentId, qty: Math.max(1, Number(it.quantity) || 1), dailyRate: Number(it.dailyRate) || 0, })); } if (r.equipmentId) return [{ equipmentId: r.equipmentId, qty: Math.max(1, Number(r.quantity) || 1), dailyRate: Number(r.dailyRate) || 0 }]; return []; } // Revenue contribution of one equipment item across all (non-cancelled) rentals. function eqRevenue(equipmentId, rentals) { let total = 0; for (const r of rentals || []) { const days = Math.max(1, daysBetween(r.start, r.end)); for (const l of _eaRentalLines(r)) { if (l.equipmentId === equipmentId) total += days * l.qty * l.dailyRate; } } return total; } // Monthly revenue by equipment: { '2026-05': 240, ... } over the past `n` months. function eqMonthlyRevenue(equipmentId, rentals, months) { const map = {}; months.forEach((m) => map[m.key] = 0); for (const r of rentals || []) { const key = _eaYM(r.start); if (!(key in map)) continue; const days = Math.max(1, daysBetween(r.start, r.end)); for (const l of _eaRentalLines(r)) { if (l.equipmentId === equipmentId) map[key] += days * l.qty * l.dailyRate; } } return map; } // ── Small atoms ───────────────────────────────────────────────────────── function EaKpi({ label, value, sub, color, icon }) { const t = useTheme(); return (
{icon &&
}
{label}
{value}
{sub &&
{sub}
}
); } function EaSection({ title, sub, children, action }) { const t = useTheme(); return (
{title}
{sub &&
{sub}
}
{action}
{children}
); } // ── Main view ────────────────────────────────────────────────────────── function EquipmentAnalyseSheet({ open, onClose, equipment, rentals, go }) { const t = useTheme(); const [tab, setTab] = useState('overview'); // overview | revenue | maint | table const [revRange, setRevRange] = useState(12); // months useEffect(() => { if (!open) setTab('overview'); }, [open]); if (!open) return null; // ── Aggregates ──────────────────────────────────────────────────── const totalKinds = equipment.length; const totalUnits = equipment.reduce((s, e) => s + Math.max(1, Number(e.qty) || 1), 0); const stockBy = equipment.map((e) => ({ e, st: eqStock(e, rentals) })); const liveOut = stockBy.reduce((s, x) => s + x.st.vermietet, 0); const inRepair = stockBy.reduce((s, x) => s + x.st.reparatur, 0); const available = stockBy.reduce((s, x) => s + x.st.verfuegbar, 0); const liveUtilPct = totalUnits > 0 ? Math.round(liveOut / totalUnits * 100) : 0; const repairPct = totalUnits > 0 ? Math.round(inRepair / totalUnits * 100) : 0; const enriched = equipment.map((e) => { const revenue = eqRevenue(e.id, rentals); const uses = eqUses(e, rentals); const util = eqUtil(e, rentals, equipment); const st = eqStock(e, rentals); const open = (e.maint || []).filter((m) => !m.done).length; const lastService = [...(e.maint || [])].filter((m) => m.done).sort((a, b) => (b.date || '').localeCompare(a.date || ''))[0]; const nextTuev = (e.maint || []).filter((m) => m.type === 'tuev' && !m.done).sort((a, b) => (a.date || '').localeCompare(b.date || ''))[0]; return { e, revenue, uses, util, st, openMaint: open, lastService, nextTuev }; }); const totalRevenue = enriched.reduce((s, x) => s + x.revenue, 0); const totalUses = enriched.reduce((s, x) => s + x.uses, 0); const avgDailyRate = equipment.length > 0 ? Math.round(equipment.reduce((s, e) => s + (Number(e.price) || 0), 0) / equipment.length) : 0; const openMaintTotal = enriched.reduce((s, x) => s + x.openMaint, 0); // By kind (category group) const byKind = {}; equipment.forEach((e) => { const k = e.kind || 'Sonstige'; if (!byKind[k]) byKind[k] = { kind: k, units: 0, revenue: 0, items: 0 }; byKind[k].units += Math.max(1, Number(e.qty) || 1); byKind[k].items += 1; byKind[k].revenue += eqRevenue(e.id, rentals); }); const kinds = Object.values(byKind).sort((a, b) => b.revenue - a.revenue); // Monthly revenue history (combined + per-equipment) const months = _eaMonthsBack(revRange); const monthlyTotal = months.map((m) => ({ ...m, value: 0 })); const perEqMonthly = equipment.map((e) => ({ e, m: eqMonthlyRevenue(e.id, rentals, months) })); perEqMonthly.forEach(({ m }) => { months.forEach((mm, i) => { monthlyTotal[i].value += m[mm.key] || 0; }); }); const maxMonthly = Math.max(1, ...monthlyTotal.map((m) => m.value)); // All maintenance entries, flattened with equipment ref const allMaint = []; equipment.forEach((e) => (e.maint || []).forEach((m) => allMaint.push({ ...m, e }))); allMaint.sort((a, b) => (a.done === b.done ? (b.date || '').localeCompare(a.date || '') : (a.done ? 1 : -1))); // ── Visual styles ───────────────────────────────────────────────── const card = { background: t.card, borderRadius: 14, padding: 18, border: `0.5px solid ${t.sep}` }; const tabs = [ ['overview', 'Übersicht', icons.bolt], ['revenue', 'Umsatz', icons.euro], ['maint', 'Wartung', icons.bell], ['table', 'Tabelle', icons.doc], ]; return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
{/* Header */}
Equipment-Analyse
{totalKinds} Geräte · {totalUnits} Einheiten · {rentals.length} Mietvorgänge ausgewertet
{/* Tabs */}
{tabs.map(([k, label, ic]) => { const sel = tab === k; return ( setTab(k)} scale={0.97}>
{label}
); })}
{/* Body */}
{tab === 'overview' && ( <> {/* KPI row */}
{/* Auslastungs-Ranking */}
{[...enriched].sort((a, b) => b.uses - a.uses || b.revenue - a.revenue).map(({ e, util, uses, revenue }, i, arr) => { const maxUses = Math.max(1, ...arr.map((x) => x.uses)); const pct = uses / maxUses * 100; return ( { go && go('equipment', { equipmentId: e.id }); onClose(); }} scale={0.998} hoverBg={t.cardAlt}>
{i + 1}
{e.name}
{e.cat} · {e.kind}
{uses}×
Anteil {util}%
{Math.round(revenue).toLocaleString('de-DE')} €
Umsatz
); })}
{/* Sortiments-Mix */}
{kinds.map((k, i) => { const unitPct = totalUnits ? (k.units / totalUnits * 100) : 0; const revPct = totalRevenue ? (k.revenue / totalRevenue * 100) : 0; return (
{k.kind}
{k.items} Geräte · {k.units} Einheiten
{Math.round(k.revenue).toLocaleString('de-DE')} €
Bestand
Umsatzanteil
); })}
)} {tab === 'revenue' && ( <> }>
{monthlyTotal.map((m) => { const h = m.value / maxMonthly * 170; return (
0 ? 1 : 0.3, ...NUMS }}>{m.value > 0 ? Math.round(m.value) + '€' : ''}
); })}
{monthlyTotal.map((m) => (
{m.label}
))}
{[...enriched].sort((a, b) => b.revenue - a.revenue).map(({ e, revenue, uses }, i, arr) => { const pct = totalRevenue > 0 ? revenue / totalRevenue * 100 : 0; return ( { go && go('equipment', { equipmentId: e.id }); onClose(); }} scale={0.998} hoverBg={t.cardAlt}>
{e.name}
{uses} Vermietungen · Ø {uses > 0 ? Math.round(revenue / uses) : 0} € pro Vorgang
{Math.round(revenue).toLocaleString('de-DE')} €
{pct.toFixed(1)}% Anteil
); })}
)} {tab === 'maint' && ( <>
!m.done).length}`} sub="Wartung & Reparatur" icon={icons.bell} color={t.orange} /> m.type === 'tuev' && !m.done).length}`} sub="Anhänger gesamt" icon={icons.cal} color="#AF52DE" /> m.done).length}`} sub="Historie" icon={icons.check} color={t.green} />
{allMaint.length === 0 ? (
Keine Einträge.
) : allMaint.map((m, i) => { const mm = maintMeta(m.type); return ( { go && go('equipment', { equipmentId: m.e.id }); onClose(); }} scale={0.998} hoverBg={t.cardAlt}>
{mm.icon}
{m.title} {mm.label}
{m.e.name}{m.note ? ' · ' + m.note : ''}
{m.date ? fmtDateDE(m.date) : '—'}
{m.done ? 'erledigt' : 'offen'}
); })}
)} {tab === 'table' && (
Gerät
Kategorie
Bestand
Reparatur
€ / Tag
Vermietungen
Umsatz
{[...enriched].sort((a, b) => b.revenue - a.revenue).map(({ e, revenue, uses, st }, i, arr) => ( { go && go('equipment', { equipmentId: e.id }); onClose(); }} scale={0.998} hoverBg={t.cardAlt}>
{e.name}
{e.kind}
{e.cat}
{st.verfuegbar} / {st.total}
0 ? t.orange : t.textTer, ...NUMS }}>{st.reparatur || '—'}
{e.price} €
{uses}
{Math.round(revenue).toLocaleString('de-DE')} €
))}
Summe
{kinds.length} Typ{kinds.length === 1 ? '' : 'en'}
{available} / {totalUnits}
0 ? t.orange : t.textTer, ...NUMS }}>{inRepair || '—'}
Ø {avgDailyRate} €
{totalUses}
{Math.round(totalRevenue).toLocaleString('de-DE')} €
)}
); } Object.assign(window, { EquipmentAnalyseSheet, eqRevenue, eqMonthlyRevenue });