// Dashboard + Detail screens const { useState, useEffect } = React; // Sun / moon glyph for the dark-mode toggle const ICON_SUN = 'M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M18.4 5.6L17 7M7 17l-1.4 1.4M12 8a4 4 0 100 8 4 4 0 000-8z'; const ICON_MOON = 'M21 12.8A8.5 8.5 0 1111.2 3 6.6 6.6 0 0021 12.8z'; // Auto-generated tasks from rentals + equipment maintenance 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 <= 3) { 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}` }); } const dEnd = daysBetween(today, r.end) - 1; if (r.status === 'aktiv' && dEnd >= -3 && dEnd <= 1) { out.push({ key: 'check:' + r.id, icon: 'πŸ”', text: 'Equipment nach RΓΌckgabe prΓΌfen', sub: `${r.tenantName} Β· ${eqName}` }); } }); 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; } // Round checkbox function Check({ done, color }) { const t = useTheme(); const c = color || t.accent; return (
{done && }
); } // ─── Unified Notifications section β€” merges tasks + upcoming dates into ONE // homogeneous, editable list (no leading colored bars). Two sub-tabs: // β€’ Zu erledigen β€” actionable tasks (auto + manual) // β€’ Anstehend β€” calendar events from today onwards // Tasks support: add, mark done, remove, set reminder date. // ───────────────────────────────────────────────────────────── function NotificationsSection({ rentals, equipment, events = [], tasks, setTasks, toast, go }) { const t = useTheme(); const [tab, setTab] = useState('todo'); const [range, setRange] = useState('week'); // today | week | month const [adding, setAdding] = useState(false); const [text, setText] = useState(''); const [editingId, setEditingId] = useState(null); 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 dismissAuto = (key) => { setTasks((prev) => ({ ...prev, autoDismissed: { ...(prev.autoDismissed || {}), [key]: true } })); toast && toast('Benachrichtigung gelΓΆscht'); }; 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 updateManual = (id, patch) => setTasks((prev) => ({ ...prev, items: (prev.items || []).map((x) => x.id === id ? { ...x, ...patch } : x) })); const addManual = () => { const v = text.trim(); if (!v) {setAdding(false);return;} setTasks((prev) => ({ ...prev, items: [{ id: 'task-' + Date.now(), text: v, done: false, reminder: '' }, ...(prev.items || [])] })); setText('');setAdding(false); toast && toast('Aufgabe hinzugefΓΌgt'); }; const openCount = auto.filter((a) => !autoDone[a.key]).length + items.filter((i) => !i.done).length; // upcoming events β€” merge manual events + rentals const today = todayISO(); const rentalEventList = (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: r.color || statusMeta(r.status).color })); const upcoming = [...events, ...rentalEventList]. filter((e) => e.end >= today). sort((a, b) => a.start.localeCompare(b.start)); // Apply Heute / Woche / Monat range filter const horizonISO = (() => { const d = fromISO(today); if (range === 'today') return today; if (range === 'week') {d.setDate(d.getDate() + 7);return fmtDateISO(d);} /* month */{d.setMonth(d.getMonth() + 1);return fmtDateISO(d);} })(); const filteredUpcoming = upcoming.filter((e) => e.start <= horizonISO).slice(0, 12); const Bullet = ({ done, onClick }) =>
{done && }
; return ( <>
Benachrichtigungen
Aufgaben & anstehende Termine
{openCount} offen
{[ ['todo', 'Zu erledigen', auto.filter((a) => !autoDone[a.key]).length + items.filter((i) => !i.done).length], ['cal', 'Anstehend', filteredUpcoming.length]]. map(([id, label, count]) => setTab(id)} scale={0.96} style={{ flex: 1 }}>
{label} {count > 0 && {count}}
)}
{tab === 'todo' && <> {auto.length === 0 && items.length === 0 && !adding &&
Alles erledigt βœ“
} {auto.map((a, i) => { const done = !!autoDone[a.key]; return (
toggleAuto(a.key)} /> toggleAuto(a.key)} scale={0.99} style={{ flex: 1, minWidth: 0 }}>
{a.text}
{a.sub}
AUTO dismissAuto(a.key)} scale={0.8}>
); })} {items.map((it, i) => { const isEditing = editingId === it.id; return (
toggleManual(it.id)} />
{isEditing ? <> updateManual(it.id, { text: e.target.value })} onKeyDown={(e) => {if (e.key === 'Enter' || e.key === 'Escape') setEditingId(null);}} placeholder="Titel" style={{ width: '100%', border: 'none', outline: 'none', background: 'transparent', fontSize: 14, fontWeight: 600, color: t.text, fontFamily: 'inherit' }} /> updateManual(it.id, { sub: e.target.value })} onBlur={() => setEditingId(null)} onKeyDown={(e) => {if (e.key === 'Enter' || e.key === 'Escape') setEditingId(null);}} placeholder="Notiz Β· optional" style={{ width: '100%', border: 'none', outline: 'none', background: 'transparent', fontSize: 11, color: t.textSec, fontFamily: 'inherit', marginTop: 1 }} /> : setEditingId(it.id)} scale={0.99}>
{it.text}
{it.sub &&
{it.sub}
}
}
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: 14, fontWeight: 600, color: t.text, fontFamily: 'inherit' }} />
: setAdding(true)} scale={0.99}>
Aufgabe hinzufΓΌgen
} } {tab === 'cal' && <> {/* Heute / Woche / Monat filter */}
{[['today', 'Heute'], ['week', '7 Tage'], ['month', '30 Tage']].map(([id, label]) => setRange(id)} scale={0.95} style={{ flex: 1 }}>
{label}
)}
{filteredUpcoming.length === 0 &&
Keine Termine in diesem Zeitraum.
} {filteredUpcoming.map((ev, i) => { const eq = equipment.find((e) => e.id === ev.equipmentId); const isRental = !!ev.fromRental; const isMultiDay = ev.start !== ev.end; return ( isRental ? go('doc', { rentalId: ev.rentalId, origin: 'cal' }) : go('cal')} scale={0.99}>
{/* Icon β€” different for rentals vs general events */} {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) &&
↑ Abholung {ev.startTime || '–'} Β· ↓ RΓΌckgabe {ev.endTime || '–'}
}
); })} }
); } // ── To-do list (auto + manual, checkable) ────────────────── function TodoSection({ rentals, equipment, tasks, setTasks, toast }) { const t = useTheme(); const [adding, setAdding] = useState(false); const [text, setText] = useState(''); const items = tasks && tasks.items || []; const autoDone = tasks && tasks.autoDone || {}; const auto = buildAutoTasks(rentals, equipment); 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 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; const Row = ({ icon, title, sub, done, onToggle, onDelete, accentColor }) =>
{icon && {icon}}
{title}
{sub &&
{sub}
}
{onDelete &&
}
; return ( <>
Zu erledigen
{openCount} offen
{auto.length === 0 && items.length === 0 && !adding &&
Alles erledigt πŸŽ‰
} {auto.map((a, i) =>
toggleAuto(a.key)} />
)} {items.map((it, i) =>
toggleManual(it.id)} onDelete={() => removeManual(it.id)} />
)} {/* Add manual task */} {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: 14, fontWeight: 600, color: t.text, fontFamily: 'inherit' }} />
: setAdding(true)} scale={0.99}>
Aufgabe hinzufΓΌgen
}
); } // ── Reminder banner (push-style) ──────────────────────────── function ReminderBanner({ rentals, equipment, go }) { const t = useTheme(); const reminders = buildReminders(rentals, equipment); if (reminders.length === 0) return null; const tone = (u) => u === 'over' ? t.red : u === 'today' ? t.orange : u === 'soon' ? t.accent : t.textSec; return (
{reminders.slice(0, 3).map((r) => { const c = tone(r.urgency); return ( go(r.id.startsWith('tuev') || r.id.startsWith('def') ? 'eq' : 'doc')} scale={0.98}>
{r.icon}
{r.title}
{r.sub}
); })}
); } // ───────────────────────────────────────────────────────────── // SCREEN: Dashboard (Home) β€” Kontostand Β· aktive Mieten Β· Notifications // Sections are reorderable + hideable via the "Sortieren" button. // ───────────────────────────────────────────────────────────── const DEFAULT_SECTIONS = ['notifications', 'stats', 'rentals', 'equipment']; function ScreenDashboard({ go, equipment, categories, rentals = [], events = [], tasks, setTasks, transactions = [], installedAt, dark, toggleDark, onSearch, toast }) { const t = useTheme(); const active = rentals.filter((r) => r.status === 'aktiv'); // Money figures β€” read from the same source as Finanzen so they stay in sync. const totalAgg = fin_sumAgg(transactions); const CURRENT_BALANCE = totalAgg.profit; const curMonthKey = fin_monthKeyOf(new Date()); const monthAgg = fin_sumAgg(fin_txInMonth(transactions, curMonthKey)); const monthIncome = monthAgg.income; // Mini-Bars: one per active month const monthBars = fin_monthlyIncomeBars(transactions, installedAt, 12); const sparkMax = Math.max(...monthBars.map(b => b.income)) || 1; // Avg utilization across equipment (replaces the heavy utilization card on Equipment) const avgUtil = equipment.length ? Math.round(equipment.reduce((s, e) => s + (Number(e.util) || 0), 0) / equipment.length) : 0; // ── Section ordering + visibility (persisted) ── const [secState, setSecState] = useState(() => { try { const raw = localStorage.getItem('rf-home-sections'); const s = raw ? JSON.parse(raw) : null; if (s && Array.isArray(s.order)) return { order: s.order, hidden: s.hidden || [] }; } catch {} return { order: DEFAULT_SECTIONS, hidden: [] }; }); useEffect(() => { try {localStorage.setItem('rf-home-sections', JSON.stringify(secState));} catch {} }, [secState]); const [reorderOpen, setReorderOpen] = useState(false); const SECTION_META = { notifications: { label: 'Benachrichtigungen', sub: 'Aufgaben + anstehende Termine' }, stats: { label: 'Kennzahlen', sub: 'Aktive Mieten + Auslastung' }, rentals: { label: 'Aktive Mieten', sub: 'Kompakte Liste' }, equipment: { label: 'Equipment Tipps', sub: 'VerfΓΌgbar / vermietet' } }; const visible = secState.order.filter((id) => !secState.hidden.includes(id)); const moveSection = (id, dir) => { setSecState((prev) => { const order = [...prev.order]; const i = order.indexOf(id); const j = i + dir; if (i < 0 || j < 0 || j >= order.length) return prev; [order[i], order[j]] = [order[j], order[i]]; return { ...prev, order }; }); }; const toggleSection = (id) => { setSecState((prev) => ({ ...prev, hidden: prev.hidden.includes(id) ? prev.hidden.filter((x) => x !== id) : [...prev.hidden, id] })); }; return (
{(() => { const d = new Date(); return `${DE_DAYS[d.getDay()]}, ${d.getDate()}. ${DE_MONTHS[d.getMonth()]}`; })()}
Übersicht
setReorderOpen(true)} scale={0.88}>
{/* Hero β€” Kontostand + Einnahmen (replaces Umsatz hero) */}
go('eur')} scale={0.99}>
Kontostand
{CURRENT_BALANCE.toLocaleString('de-DE')} €
VerfΓΌgbares Guthaben
{monthBars.map((b, i) =>
)}
Einnahmen Β· {fin_fmtMonthLabel(curMonthKey)}
+{monthIncome.toLocaleString('de-DE')} €
{monthAgg.expenses > 0 ? <> βˆ’{monthAgg.expenses.toLocaleString('de-DE')} € Ausgaben : Keine Ausgaben}
{/* Quick action β€” Neuer Mietvorgang */}
go('doc', { newRental: true })} scale={0.98}>
Neuer Mietvorgang
Direkt einen Mieter anlegen
{/* Sections in user-selected order */} {visible.map((id) => { if (id === 'notifications') { return ; } if (id === 'stats') { return (
go('doc')} scale={0.97}>
Aktive Mieten
{active.length}
laufend
go('eq')} scale={0.97}>
Auslastung
{avgUtil}%
Ø {equipment.length} GerÀte
); } if (id === 'rentals') { return (
Aktive Mieten
go('doc')} scale={0.96}>
Alle β€Ί
{active.length === 0 &&
Keine laufenden Mieten.
} {active.map((r) => { const eq = equipment.find((e) => e.id === r.equipmentId); return ( go('doc', { rentalId: r.id })} scale={0.98}>
{r.tenantName}
{eq ? eq.name : r.equipmentName} Β· bis {fmtDateDE(r.end).slice(0, -5)}
{rentalTotal(r)} €
); })}
); } if (id === 'equipment') { const verfuegbar = equipment.reduce((s, e) => s + eqStock(e, rentals).verfuegbar, 0); const vermietet = equipment.reduce((s, e) => s + eqStock(e, rentals).vermietet, 0); return (
Equipment
go('eq')} scale={0.96}>
Alle β€Ί
VerfΓΌgbar
{verfuegbar}
Vermietet
{vermietet}
); } return null; })} {/* Reorder / hide sections sheet */} setReorderOpen(false)}>
Bereiche anpassen
Reihenfolge Γ€ndern oder ausblenden
setReorderOpen(false)}>
Fertig
{secState.order.map((id, i) => { const meta = SECTION_META[id] || { label: id }; const hidden = secState.hidden.includes(id); return ( ); })}
setSecState({ order: DEFAULT_SECTIONS, hidden: [] })} scale={0.97} style={{ marginTop: 18 }}>
Standardreihenfolge wiederherstellen
); } // ───────────────────────────────────────────────────────────── // Legacy ScreenDashboard kept for reference (renamed) // ───────────────────────────────────────────────────────────── function ScreenDashboardLegacy({ go, equipment, categories, rentals = [], events = [], tasks, setTasks, dark, toggleDark, onSearch, toast }) { const t = useTheme(); const totalOf = (r) => rentalTotal(r); const active = rentals.filter((r) => r.status === 'aktiv'); const reserved = rentals.filter((r) => r.status === 'reserviert'); const liveRentals = [...active, ...reserved]; // Revenue (consistent with Finanzen) const revenue = 0,revenuePrev = 0; const revPct = Math.round((revenue - revenuePrev) / revenuePrev * 100); // Mini sparkline β€” last days of the month const spark = [0, 180, 340, 180, 580, 760, 440]; const sparkMax = Math.max(...spark); // Deposits currently held (status 'erhalten') const depositsHeld = rentals.filter((r) => r.depositStatus === 'erhalten').reduce((s, r) => s + (Number(r.deposit) || 0), 0); // Upcoming returns/pickups from events const upcoming = events. filter((e) => e.end >= todayISO()). sort((a, b) => a.start.localeCompare(b.start)). slice(0, 3); return (
Mittwoch, 20. Mai
Übersicht
{/* Reminders */} {/* Revenue hero */}
go('eur')} scale={0.99}>
Umsatz Β· Mai 2026
{revenue.toLocaleString('de-DE')} €
= 0 ? 'M5 15l7-7 7 7' : 'M5 9l7 7 7-7'} size={12} sw={2.8} color={revPct >= 0 ? t.green : t.red} /> = 0 ? t.green : t.red, fontWeight: 600 }}>{Math.abs(revPct)}% vs. Vorjahr
{spark.map((v, i) =>
)}
{/* Stat row */}
{[ { label: 'Aktiv', val: active.length, color: t.green, to: 'doc' }, { label: 'Reserviert', val: reserved.length, color: t.orange, to: 'doc' }]. map((s) => go(s.to)} scale={0.97}>
{s.label}
{s.val}
)}
{/* To-do list */} {/* Active rentals */}
Aktive Mieten
go('doc')} scale={0.96}>
Alle β€Ί
{liveRentals.length === 0 && go('doc')}>
Keine laufenden Mieten.
+ Mieter anlegen
} {liveRentals.map((r) => { const m = statusMeta(r.status); const dep = depositMeta(r.depositStatus); const eq = equipment.find((e) => e.id === r.equipmentId); return ( go('doc', { rentalId: r.id })} scale={0.98}>
{r.tenantName}
{eq ? eq.name : r.equipmentName} Β· {fmtRange(r.start, r.end)}
{dep.label}
● {m.label}
{rentalTotal(r)} €
); })}
{/* Upcoming dates */} {upcoming.length > 0 && <>
Anstehende Termine
go('cal')} scale={0.96}>
Kalender β€Ί
{upcoming.map((ev) => { const eq = equipment.find((e) => e.id === ev.equipmentId); return ( go('cal')} scale={0.98}>
{ev.title}
{eq ? eq.name : 'β€”'} Β· {fmtRange(ev.start, ev.end)}
); })}
}
); } // ───────────────────────────────────────────────────────────── // SCREEN: Detail // ───────────────────────────────────────────────────────────── function ScreenDetail({ id, go, equipment, rentals = [], toast }) { const t = useTheme(); const e = equipment.find((x) => x.id === id) || equipment[0]; if (!e) return
Nicht gefunden.
; const [fav, setFav] = useState(false); const stock = eqStock(e, rentals); const history = rentals. filter((r) => r.equipmentId === e.id). sort((a, b) => b.start.localeCompare(a.start)); return ( <>
{e.photo ? :
{e.emoji ? {e.emoji} : }
}
go('home')} scale={0.88}>
{setFav(!fav);toast(fav ? 'Entfernt' : 'Favorit gespeichert');}} scale={0.88}> go('eq')} scale={0.88}>
{e.cat}
{stock.verfuegbar} / {stock.total} verfΓΌgbar
{e.name}
{e.sub}
{[ ['VerfΓΌgbar', stock.verfuegbar, true], ['Vermietet', stock.vermietet, false], ['In Reparatur', stock.reparatur, false]]. map(([k, v, strong], i) => {i > 0 &&
}
{v}
{k}
)}
{[['Tagespreis', e.price + ' €'], ['EinsΓ€tze', e.uses || 0]].map(([k, v]) =>
{v}
{k}
)}
go('cal')} style={{ flex: 1 }}>
Termin anlegen
go('eq')}>
Bearbeiten
{/* Maintenance summary */} {e.maint && e.maint.length > 0 && <>
Wartung & Reparatur
{e.maint.map((m) => { const mm = maintMeta(m.type); return (
{mm.icon}
{m.title}
{mm.label} Β· {fmtDateDE(m.date)}{m.note ? ' Β· ' + m.note : ''}
{m.done ? 'Erledigt' : 'Offen'}
); })}
}
Verlauf
{history.length === 0 &&
Noch keine Vermietungen.
} {history.map((r) =>
{r.tenantName}
{r.purpose || 'β€”'} Β· {fmtRange(r.start, r.end)}
{rentalTotal(r)} €
)}
); } Object.assign(window, { ScreenDashboard, ScreenDetail });