// 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 (
);
}
function EaSection({ title, sub, children, action }) {
const t = useTheme();
return (
);
}
// ── 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}
{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')} €
);
})}
>
)}
{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.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 });