// 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 (
);
}
// βββ 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 }) =>
;
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 ?
:
setAdding(true)} scale={0.99}>
}
>
}
{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 }) =>
;
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 ?
:
setAdding(true)} scale={0.99}>
}
>);
}
// ββ 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}>
);
})}
);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 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}>
go('eq')} scale={0.97}>
{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 βΊ
);
}
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 (
moveSection(id, -1)} scale={0.8} disabled={i === 0}>
moveSection(id, 1)} scale={0.8} disabled={i === secState.order.length - 1}>
{meta.label}
{meta.sub &&
{meta.sub}
}
toggleSection(id)} scale={0.9}>
{hidden ? 'Einblenden' : 'Ausblenden'}
);
})}
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
{/* 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}>
)}
{/* 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)}
β {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 && }
)}
{[['Tagespreis', e.price + ' β¬'], ['EinsΓ€tze', e.uses || 0]].map(([k, v]) =>
)}
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 });