// Desktop Dashboard — Übersicht. Rich multi-column overview. function buildAutoTasks(rentals = [], equipment = []) { const today = todayISO(); const out = []; rentals.forEach((r) => { const eq = equipment.find((e) => e.id === r.equipmentId); const eqName = eq ? eq.name : r.equipmentName; const dStart = daysBetween(today, r.start) - 1; if (r.status !== 'abgeschlossen' && dStart >= 0 && dStart <= 4) { const when = dStart === 0 ? 'heute' : dStart === 1 ? 'morgen' : `in ${dStart} Tagen`; out.push({ key: 'prep:' + r.id, icon: '📦', text: `Equipment für ${r.tenantName} vorbereiten`, sub: `${eqName} · Abholung ${when}`, rentalId: r.id }); } const dEnd = daysBetween(today, r.end) - 1; if (r.status === 'aktiv' && dEnd >= -2 && dEnd <= 1) { out.push({ key: 'check:' + r.id, icon: '🔍', text: 'Equipment nach Rückgabe prüfen', sub: `${r.tenantName} · ${eqName}`, rentalId: r.id }); } }); equipment.forEach((e) => (e.maint || []).forEach((m) => { if (!m.done && (m.type === 'defect' || m.type === 'repair')) out.push({ key: 'fix:' + m.id, icon: '🔧', text: m.title, sub: e.name }); })); return out; } function Bullet({ done, onClick, color }) { const t = useTheme(); return (
{done && }
); } function ScreenDashboard({ go, equipment, rentals = [], events = [], tasks, setTasks, dark, toggleDark }) { const t = useTheme(); const toast = useToast(); const active = rentals.filter((r) => r.status === 'aktiv'); const reserved = rentals.filter((r) => r.status === 'reserviert'); const fin = buildFinanceSummary(FIN_BARS); const CURRENT_BALANCE = 4820; const topShare = equipment.reduce((m, e) => Math.max(m, eqUtil(e, rentals, equipment)), 0); const verfuegbar = equipment.reduce((s, e) => s + eqStock(e, rentals).verfuegbar, 0); const vermietet = equipment.reduce((s, e) => s + eqStock(e, rentals).vermietet, 0); const totalStock = equipment.reduce((s, e) => s + Math.max(1, Number(e.qty) || 1), 0); const liveUtilPct = totalStock > 0 ? Math.round(vermietet / totalStock * 100) : 0; const depositsHeld = rentals.filter((r) => r.depositStatus === 'erhalten').reduce((s, r) => s + (Number(r.deposit) || 0), 0); const openDeposits = rentals.filter((r) => r.depositStatus === 'offen').length; // tasks const items = (tasks && tasks.items) || []; const autoDone = (tasks && tasks.autoDone) || {}; const autoDismissed = (tasks && tasks.autoDismissed) || {}; const auto = buildAutoTasks(rentals, equipment).filter((a) => !autoDismissed[a.key]); const toggleAuto = (key) => setTasks((prev) => ({ ...prev, autoDone: { ...(prev.autoDone || {}), [key]: !(prev.autoDone || {})[key] } })); const toggleManual = (id) => setTasks((prev) => ({ ...prev, items: (prev.items || []).map((x) => x.id === id ? { ...x, done: !x.done } : x) })); const removeManual = (id) => setTasks((prev) => ({ ...prev, items: (prev.items || []).filter((x) => x.id !== id) })); const dismissAuto = (key) => { setTasks((prev) => ({ ...prev, autoDismissed: { ...(prev.autoDismissed || {}), [key]: true } })); toast('Erledigung entfernt'); }; const [adding, setAdding] = useState(false); const [text, setText] = useState(''); const [showAllEq, setShowAllEq] = useState(false); const addManual = () => { const v = text.trim(); if (!v) { setAdding(false); return; } setTasks((prev) => ({ ...prev, items: [{ id: 'task-' + Date.now(), text: v, done: false }, ...(prev.items || [])] })); setText(''); setAdding(false); toast('Aufgabe hinzugefügt'); }; const openCount = auto.filter((a) => !autoDone[a.key]).length + items.filter((i) => !i.done).length; // tab state for the Benachrichtigungen card const [notifTab, setNotifTab] = useState('todo'); const [upcRange, setUpcRange] = useState('week'); // upcoming const today = todayISO(); const rentalEvents = rentals.filter((r) => r.status !== 'abgeschlossen').map((r) => ({ id: 'r:' + r.id, rentalId: r.id, fromRental: true, title: r.tenantName, equipmentId: r.equipmentId, start: r.start, end: r.end, startTime: r.startTime, endTime: r.endTime, color: statusMeta(r.status).color })); const allUpcoming = [...events, ...rentalEvents].filter((e) => e.end >= today).sort((a, b) => a.start.localeCompare(b.start)); const rangeDays = upcRange === 'today' ? 0 : upcRange === 'week' ? 7 : 30; const upcoming = allUpcoming.filter((e) => { const startsIn = Math.max(0, Math.round((fromISO(e.start) - fromISO(today)) / 86400000)); return startsIn <= rangeDays || e.start <= today; // include running events }).slice(0, 8); const upcomingCount = allUpcoming.length; const card = { background: t.card, borderRadius: 16, boxShadow: t.shadowCard }; return ( <>
{/* LEFT column */}
{/* Balance hero */}
Kontostand
{CURRENT_BALANCE.toLocaleString('de-DE')}
Verfügbares Guthaben · {DEFAULT_COMPANY.name}
{[0, 180, 340, 180, 580, 760, 440].map((v, i, a) => (
))}
{[ { l: 'Einnahmen · Mai', v: `+${fin.income.toLocaleString('de-DE')} €`, c: t.green }, { l: 'Ausgaben · Mai', v: `−${fin.expenses.toLocaleString('de-DE')} €`, c: t.red }, { l: 'Netto · Mai', v: `${fin.net >= 0 ? '+' : ''}${fin.net.toLocaleString('de-DE')} €`, c: t.text }, ].map((x) => (
{x.l}
{x.v}
))}
{/* KPI tiles */}
{[ { label: 'Aktive Mieten', value: active.length, sub: 'laufend', color: t.green, icon: icons.doc, to: 'rentals' }, { label: 'Reserviert', value: reserved.length, sub: 'geplant', color: t.orange, icon: icons.clock, to: 'rentals' }, { label: 'Unterwegs', value: liveUtilPct + '%', sub: `${vermietet} von ${totalStock} Geräten`, color: t.accent, icon: icons.truck, to: 'equipment' }, ].map((s) => go(s.to)}/>)}
{/* Equipment status */}
go('equipment')}>Equipment-Status
{[['Verfügbar', verfuegbar, t.green], ['Vermietet', vermietet, t.accent], ['In Reparatur', equipment.reduce((s, e) => s + eqStock(e, rentals).reparatur, 0), t.orange]].map(([l, v, c]) => (
{l}
{v}
))}
{[...equipment].map((e) => ({ e, u: eqUses(e, rentals), util: eqUtil(e, rentals, equipment) })).sort((a, b) => b.u - a.u || b.util - a.util).slice(0, 4).map(({ e, util }) => { const st = eqStock(e, rentals); return ( go('equipment', { equipmentId: e.id })} scale={0.99} hoverBg={t.cardAlt} style={{ borderRadius: 10 }}>
{e.name}
{e.cat} · {e.price} €/Tag
{util}%
); })}
{equipment.length > 4 && ( setShowAllEq(true)} scale={0.99} hoverBg={t.cardAlt} style={{ borderRadius: 10, marginTop: 6 }}>
Alle {equipment.length} Geräte anzeigen
)}
{/* Active rentals list */}
go('rentals')}>Aktive & reservierte Mieten
{[...active, ...reserved].map((r) => { const eq = equipment.find((e) => e.id === r.equipmentId); const dep = depositMeta(r.depositStatus); return ( go('rentals', { rentalId: r.id })} scale={0.99} hoverBg={t.cardAlt} style={{ borderRadius: 10 }}>
{r.tenantName}
{eq ? eq.name : r.equipmentName} · {fmtRange(r.start, r.end)}
{dep.label}
{rentalTotal(r)} €
); })}
{/* RIGHT column */}
{/* Benachrichtigungen — Aufgaben & anstehende Termine */}
Benachrichtigungen
Aufgaben & anstehende Termine
{openCount > 0 && (
{openCount} offen
)}
{/* Tab switcher */}
{[['todo', 'Zu erledigen', openCount], ['upcoming', 'Anstehend', upcomingCount]].map(([k, l, n]) => { const sel = notifTab === k; return ( setNotifTab(k)} scale={0.97} style={{ flex: 1 }}>
{l} {n}
); })}
{/* TAB: Zu erledigen */} {notifTab === 'todo' && (
{auto.length === 0 && items.length === 0 && !adding &&
Alles erledigt ✓
} {auto.map((a) => { const done = !!autoDone[a.key]; return (
toggleAuto(a.key)}/> a.rentalId ? go('rentals', { rentalId: a.rentalId }) : null} scale={0.99} style={{ flex: 1, minWidth: 0 }}>
{a.icon} {a.text}
{a.sub}
AUTO dismissAuto(a.key)} scale={0.8}>
); })} {items.map((it, i) => (
toggleManual(it.id)}/>
{it.text}
removeManual(it.id)} scale={0.8}>
))} {adding ? (
setText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') addManual(); if (e.key === 'Escape') { setText(''); setAdding(false); } }} onBlur={addManual} placeholder="Neue Aufgabe…" style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13.5, fontWeight: 600, color: t.text, fontFamily: 'inherit' }}/>
) : ( setAdding(true)} scale={0.99}>
Aufgabe hinzufügen
)}
)} {/* TAB: Anstehend */} {notifTab === 'upcoming' && (
{[['today', 'Heute'], ['week', '7 Tage'], ['month', '30 Tage']].map(([k, l]) => { const sel = upcRange === k; return ( setUpcRange(k)} scale={0.96} style={{ flex: 1 }}>
{l}
); })}
{upcoming.length === 0 &&
Keine Termine im Zeitraum.
} {upcoming.map((ev, i) => { const eq = equipment.find((e) => e.id === ev.equipmentId); const isRental = !!ev.fromRental; return ( isRental ? go('rentals', { rentalId: ev.rentalId }) : go('calendar')} scale={0.99} hoverBg={t.cardAlt} style={{ borderRadius: 10 }}>
{isRental ? : <>
{DE_MONTHS_SHORT[fromISO(ev.start).getMonth()]}
{fromISO(ev.start).getDate()}
}
{ev.title}{isRental && MIETE}
{eq ? eq.name + ' · ' : ''}{fmtRange(ev.start, ev.end)}
{isRental && (ev.startTime || ev.endTime) && (
{ev.startTime && ↑ Abholung {ev.startTime}} {ev.endTime && ↓ Rückgabe {ev.endTime}}
)}
); })}
)}
{/* Mini revenue chart */}
go('finance')}>Umsatzverlauf
setShowAllEq(false)} equipment={equipment} rentals={rentals} go={go} /> ); } function AllEquipmentSheet({ open, onClose, equipment, rentals, go }) { const t = useTheme(); const [query, setQuery] = useState(''); const [sortBy, setSortBy] = useState('util'); // util | name | stock useEffect(() => { if (!open) setQuery(''); }, [open]); const enriched = equipment.map((e) => ({ e, util: eqUtil(e, rentals, equipment), uses: eqUses(e, rentals), stock: eqStock(e, rentals) })); const filtered = enriched.filter(({ e }) => { if (!query.trim()) return true; const q = query.toLowerCase(); return (e.name || '').toLowerCase().includes(q) || (e.cat || '').toLowerCase().includes(q) || (e.sub || '').toLowerCase().includes(q); }); const sorted = [...filtered].sort((a, b) => { if (sortBy === 'name') return (a.e.name || '').localeCompare(b.e.name || ''); if (sortBy === 'stock') return b.stock.verfuegbar - a.stock.verfuegbar; return b.uses - a.uses || b.util - a.util; }); return (
Alle Geräte
{equipment.length} Geräte im Bestand · {filtered.length} angezeigt
Schließen
setQuery(e.target.value)} placeholder="Suchen…" style={{ width: '100%', padding: '9px 12px 9px 32px', borderRadius: 10, border: `1px solid ${t.inputBorder}`, background: t.inputBg, fontSize: 13.5, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' }} />
{sorted.length === 0 ? (
Keine Treffer.
) : sorted.map(({ e, util, stock }, i) => { const pillColor = stock.verfuegbar === 0 ? (t.red || '#FF3B30') : stock.verfuegbar < Math.max(1, Number(e.qty) || 1) ? (t.orange || '#FF9500') : (t.green || '#34C759'); return ( { go('equipment', { equipmentId: e.id }); onClose(); }} scale={0.998} hoverBg={t.cardAlt}>
{e.name}
{e.cat}{e.sub ? ' · ' + e.sub : ''} · {e.price} €/Tag
{stock.verfuegbar}/{stock.total} frei
{util}%
); })}
); } function Legend({ color, label }) { const t = useTheme(); return
{label}
; } Object.assign(window, { ScreenDashboard, buildAutoTasks, Bullet, Legend });