// RentFlow Manager — Interactive prototype (root file) // State lifted to App; screens are split across screen-*.jsx files. const { useState, useRef, useEffect, useMemo } = React; // ───────────────────────────────────────────────────────────── // One-shot "clean slate": wipe demo user-data the first time the app boots // after the user asked for a fresh start. Functional config (company profile, // theme, protocol templates, quick-texts, categories, logistik, inbox style) // is preserved so the app stays fully usable. // ───────────────────────────────────────────────────────────── (function rfCleanSlate() { const FLAG = 'rf-clean-slate-v1'; try { if (localStorage.getItem(FLAG)) return; [ 'rf-equipment-v2', 'rf-rentals-v2', 'rf-events-v2', 'rf-emails-v1', 'rf-gendocs', 'rf-tasks', 'rf-blocked-customers', 'rf-last-export', 'rf-categories', ].forEach(k => localStorage.removeItem(k)); localStorage.setItem(FLAG, '1'); } catch {} })(); // ───────────────────────────────────────────────────────────── // Persisted state helper // ───────────────────────────────────────────────────────────── function useLocal(key, init) { const [v, setV] = useState(() => { // Read from RFDB in-memory cache (populated from IndexedDB before React mounts) if (window.RFDB) { const cached = window.RFDB.get(key); if (cached !== undefined) return cached; } // Fallback: localStorage (also kept in sync by RFDB.set) try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : init; } catch { return init; } }); useEffect(() => { if (window.RFDB) window.RFDB.set(key, v); else { try { localStorage.setItem(key, JSON.stringify(v)); } catch {} } }, [key, v]); return [v, setV]; } // ───────────────────────────────────────────────────────────── // Body scroll lock — the page is the scroll container now, so while a modal // is open we freeze document scroll (ref-counted to handle stacked overlays). // ───────────────────────────────────────────────────────────── let __rfScrollLocks = 0; function useBodyScrollLock(active) { useEffect(() => { if (!active) return; __rfScrollLocks += 1; document.body.style.overflow = 'hidden'; return () => { __rfScrollLocks = Math.max(0, __rfScrollLocks - 1); if (__rfScrollLocks === 0) document.body.style.overflow = ''; }; }, [active]); } // ───────────────────────────────────────────────────────────── // Pressable // ───────────────────────────────────────────────────────────── function Pressable({ children, onClick, style = {}, scale = 0.97, disabled }) { const [pressed, setPressed] = useState(false); return (
!disabled && setPressed(true)} onPointerUp={() => setPressed(false)} onPointerLeave={() => setPressed(false)} onClick={disabled ? undefined : onClick} style={{ cursor: disabled ? 'default' : 'pointer', transform: pressed ? `scale(${scale})` : 'scale(1)', transition: 'transform 0.12s cubic-bezier(0.2,0.7,0.3,1), opacity 0.12s', opacity: pressed ? 0.85 : (disabled ? 0.45 : 1), ...style, }} > {children}
); } // ───────────────────────────────────────────────────────────── // Toast // ───────────────────────────────────────────────────────────── function Toast({ msg, onClose }) { const t = useTheme(); useEffect(() => { if (!msg) return; const tm = setTimeout(onClose, 2400); return () => clearTimeout(tm); }, [msg]); return (
{msg}
); } // ───────────────────────────────────────────────────────────── // LongPressRow — tap = onTap; swipe-left = reveal red "Löschen" action; // long-press = floating Bearbeiten/Löschen menu. // Drop-in replacement for a Pressable list row that needs these actions. // ───────────────────────────────────────────────────────────── function LongPressRow({ children, onTap, onEdit, onDelete, style, bg, longDelay = 480, swipeDelete = true }) { const t = useTheme(); const startX = React.useRef(0); const startY = React.useRef(0); const moved = React.useRef(false); const dir = React.useRef(null); // 'h' | 'v' | null — locked gesture axis const baseOffset = React.useRef(0); // offset at gesture start const longTimer = React.useRef(null); const longFired = React.useRef(false); const rowRef = React.useRef(null); const [menu, setMenu] = React.useState(null); const [offset, setOffset] = React.useState(0); const [dragging, setDragging] = React.useState(false); const hasMenu = !!(onEdit || onDelete); const canSwipe = swipeDelete && !!onDelete; const ACTION_W = 84; const cancelLong = () => { if (longTimer.current) { clearTimeout(longTimer.current); longTimer.current = null; } }; const close = () => { baseOffset.current = 0; setOffset(0); }; const open = () => { baseOffset.current = -ACTION_W; setOffset(-ACTION_W); }; const onPointerDown = (ev) => { if (ev.pointerType === 'mouse' && ev.button !== 0) return; startX.current = ev.clientX; startY.current = ev.clientY; moved.current = false; longFired.current = false; dir.current = null; baseOffset.current = offset; cancelLong(); if (hasMenu) { longTimer.current = setTimeout(() => { if (dir.current === 'h') return; longFired.current = true; const rect = rowRef.current && rowRef.current.getBoundingClientRect(); if (rect) setMenu({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }); }, longDelay); } try { ev.currentTarget.setPointerCapture(ev.pointerId); } catch {} }; const onPointerMove = (ev) => { const dx = ev.clientX - startX.current; const dy = ev.clientY - startY.current; if (dir.current === null) { if (Math.abs(dx) > 6 || Math.abs(dy) > 6) { dir.current = Math.abs(dx) > Math.abs(dy) ? 'h' : 'v'; moved.current = true; cancelLong(); if (dir.current === 'h' && canSwipe) setDragging(true); } } if (dir.current === 'h' && canSwipe) { const next = Math.max(-ACTION_W, Math.min(0, baseOffset.current + dx)); setOffset(next); } }; const settle = () => { cancelLong(); if (dir.current === 'h' && canSwipe) { setDragging(false); if (offset < -ACTION_W / 2) open(); else close(); return; } if (longFired.current || moved.current) return; if (offset !== 0) { close(); return; } // tap on an open row → just close it if (onTap) onTap(); }; const onCancel = () => { cancelLong(); if (dir.current === 'h' && canSwipe) { setDragging(false); if (offset < -ACTION_W / 2) open(); else close(); } }; return (
{canSwipe && (
{ onDelete(); close(); }} scale={0.96} style={{ flex: 1 }}>
Löschen
)}
{children}
{menu && setMenu(null)} onEdit={onEdit ? () => { setMenu(null); onEdit(); } : null} onDelete={onDelete ? () => { setMenu(null); onDelete(); } : null}/>}
); } function RowContextMenu({ pos, onClose, onEdit, onDelete }) { const t = useTheme(); const items = []; if (onEdit) items.push({ id: 'edit', label: 'Bearbeiten', color: t.text }); if (onDelete) items.push({ id: 'del', label: 'Löschen', color: t.red }); const W = 220; const H = items.length * 50 + 4; const vw = typeof window !== 'undefined' ? window.innerWidth : 360; const vh = typeof window !== 'undefined' ? window.innerHeight : 720; const left = Math.max(12, Math.min(vw - W - 12, pos.x - W / 2)); const top = Math.max(12, Math.min(vh - H - 12, pos.y - H / 2)); return (
e.stopPropagation()} style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.25)', backdropFilter: 'blur(2px)', WebkitBackdropFilter: 'blur(2px)' }}/>
{items.map((it, i) => { const handler = it.id === 'edit' ? onEdit : onDelete; return (
{it.label} {it.id === 'edit' ? : }
); })}
); } // ───────────────────────────────────────────────────────────── // Modal sheet // ───────────────────────────────────────────────────────────── function Sheet({ open, onClose, children, maxHeight = '85%' }) { const t = useTheme(); useBodyScrollLock(open); const [dragY, setDragY] = React.useState(0); const [dragging, setDragging] = React.useState(false); const [visible, setVisible] = React.useState(open); const startY = React.useRef(0); React.useEffect(() => { setDragY(0); setDragging(false); }, [open]); // Keep the panel rendered through its close animation, then hard-hide it so a // closed sheet can never "peek" into view during page scroll (iOS fixed-paint quirk). React.useEffect(() => { if (open) { setVisible(true); return; } const id = setTimeout(() => setVisible(false), 360); return () => clearTimeout(id); }, [open]); const onGrabDown = (e) => { startY.current = e.clientY; setDragging(true); try { e.currentTarget.setPointerCapture(e.pointerId); } catch {} }; const onGrabMove = (e) => { if (!dragging) return; setDragY(Math.max(0, e.clientY - startY.current)); }; const onGrabUp = (e) => { setDragging(false); const dy = e.clientY - startY.current; if (dy > 100) { onClose && onClose(); } else setDragY(0); }; return ReactDOM.createPortal( <>
{ setDragging(false); setDragY(0); }} style={{ display: 'flex', justifyContent: 'center', padding: '10px 0 8px', position: 'sticky', top: 0, background: t.sheetBg, zIndex: 2, touchAction: 'none', cursor: 'grab' }}>
{children}
, document.body ); } // ───────────────────────────────────────────────────────────── // Confirm dialog // ───────────────────────────────────────────────────────────── function Confirm({ open, title, message, confirmLabel = 'Löschen', danger = true, onCancel, onConfirm }) { const t = useTheme(); useBodyScrollLock(open); return ReactDOM.createPortal( <>
{title}
{message &&
{message}
}
Abbrechen
{confirmLabel}
, document.body ); } // ───────────────────────────────────────────────────────────── // Inline editable field — tap to edit // ───────────────────────────────────────────────────────────── function Field({ label, value, onChange, placeholder = '—', multiline = false, type = 'text' }) { const t = useTheme(); const [editing, setEditing] = useState(false); const [tmp, setTmp] = useState(value || ''); useEffect(() => { setTmp(value || ''); }, [value]); const save = () => { setEditing(false); if (tmp !== value) onChange(tmp); }; return (
{label}
{editing ? ( multiline ? (