// RentFlow Manager — Interactive prototype (root file)
// State lifted to App; screens are split across screen-*.jsx files.

const { useState, useRef, useEffect, useMemo } = React;

// ─────────────────────────────────────────────────────────────
// Server-gestützter State-Helper (Name "useLocal" aus Kompatibilität mit allen
// Aufrufstellen beibehalten — speichert aber NICHTS mehr lokal).
//
// Der Anfangswert kommt ausschließlich aus window.RFDB.get(key), das von
// db.js VOR dem Mounten der App einmalig komplett vom Server geladen wurde
// (siehe index.html — die App wird gar nicht erst gemountet, wenn dieser
// Abruf fehlschlägt). Jede Änderung wird sofort zum Server geschrieben;
// schlägt das fehl, meldet RFDB.set() das über ein 'rfdb-error'-Event
// (App zeigt dazu ein dauerhaftes Fehler-Banner, siehe unten) — der Wert
// wird dabei NIE lokal zwischengespeichert oder stillschweigend verworfen.
// ─────────────────────────────────────────────────────────────
function useLocal(key, init) {
  const [v, setV] = useState(() => {
    const server = window.RFDB ? window.RFDB.get(key) : undefined;
    return server !== undefined ? server : init;
  });
  const first = useRef(true);
  useEffect(() => {
    // Den allerersten Render (= der gerade vom Server geladene Wert) nicht
    // gleich wieder zurückschreiben — nur echte Änderungen senden.
    if (first.current) { first.current = false; return; }
    if (window.RFDB) window.RFDB.set(key, v).catch(() => {}); // Fehler-Handling via 'rfdb-error'-Event
  }, [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 (
    <div
      onPointerDown={() => !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}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Toast
// ─────────────────────────────────────────────────────────────
function Toast({ msg, onClose }) {
  const t = useTheme();
  useEffect(() => {
    if (!msg) return;
    const tm = setTimeout(onClose, 2400);
    return () => clearTimeout(tm);
  }, [msg]);
  return (
    <div style={{
      position: 'fixed', bottom: 'calc(env(safe-area-inset-bottom, 0px) + 96px)', left: '50%',
      transform: `translateX(-50%) translateY(${msg ? '0' : '20px'})`,
      opacity: msg ? 1 : 0, transition: 'all 0.25s cubic-bezier(0.2,0.7,0.3,1)',
      pointerEvents: 'none', zIndex: 90,
    }}>
      <Glass dark={t.dark} radius={100} padding="10px 18px">
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <Icon d={icons.check} color={t.green} size={16} sw={2.4}/>
          <span style={{ fontSize: 13, fontWeight: 600, color: t.text }}>{msg}</span>
        </div>
      </Glass>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// ErrorBanner — dauerhafter Hinweis, wenn ein Schreibvorgang zum Server
// fehlgeschlagen ist (siehe db.js: window.RFDB.set/remove/importAll/resetAll
// lösen dann ein 'rfdb-error'-Event aus). Verschwindet NICHT von selbst nach
// ein paar Sekunden wie der normale Toast — bewusst, damit eine nicht
// gespeicherte Änderung nicht unbemerkt bleibt. Schließt sich nur, wenn der
// Nutzer es aktiv wegtippt.
// ─────────────────────────────────────────────────────────────
function useRfdbErrorBanner() {
  const [err, setErr] = useState(null);
  useEffect(() => {
    const onErr = (e) => setErr(e.detail);
    window.addEventListener('rfdb-error', onErr);
    return () => window.removeEventListener('rfdb-error', onErr);
  }, []);
  return [err, () => setErr(null)];
}

function ErrorBanner({ err, onClose }) {
  const t = useTheme();
  if (!err) return null;
  const label = {
    save: 'Änderung konnte nicht gespeichert werden.',
    delete: 'Löschen ist fehlgeschlagen.',
    import: 'Sicherung konnte nicht wiederhergestellt werden.',
    reset: 'Zurücksetzen ist fehlgeschlagen.',
  }[err.kind] || 'Verbindung zum Server fehlgeschlagen.';
  return (
    <div style={{
      position: 'fixed', top: 'env(safe-area-inset-top, 0px)', left: 0, right: 0, zIndex: 200,
      background: '#FF3B30', color: '#fff',
      padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10,
      boxShadow: '0 2px 12px rgba(0,0,0,0.2)',
    }}>
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
        <path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
      </svg>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 13, fontWeight: 700 }}>{label}</div>
        <div style={{ fontSize: 11.5, opacity: 0.9, marginTop: 1 }}>Bitte Internetverbindung prüfen und die Änderung erneut vornehmen.</div>
      </div>
      <Pressable onClick={onClose} scale={0.9}>
        <div style={{ width: 26, height: 26, borderRadius: 13, background: 'rgba(255,255,255,0.25)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.6" strokeLinecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
        </div>
      </Pressable>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// 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 (
    <div ref={rowRef} style={{ position: 'relative', borderRadius: 16, overflow: 'hidden', ...style }}>
      {canSwipe && offset < 0 && (
        <div style={{
          position: 'absolute', top: 0, bottom: 0, right: 0, width: ACTION_W + 16,
          background: t.red, display: 'flex', alignItems: 'stretch',
        }}>
          <Pressable onClick={() => { onDelete(); close(); }} scale={0.96} style={{ flex: 1 }}>
            <div style={{
              height: '100%', display: 'flex', flexDirection: 'column',
              alignItems: 'center', justifyContent: 'center', gap: 4,
              paddingLeft: 16, color: '#fff',
            }}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>
              <span style={{ fontSize: 12, fontWeight: 600 }}>Löschen</span>
            </div>
          </Pressable>
        </div>
      )}
      <div
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={settle}
        onPointerCancel={onCancel}
        style={{
          position: 'relative', zIndex: 1, background: bg,
          touchAction: 'pan-y', cursor: onTap ? 'pointer' : 'default', userSelect: 'none',
          transform: `translateX(${offset}px)`,
          transition: dragging ? 'none' : 'transform 0.28s cubic-bezier(0.2,0.7,0.3,1)',
        }}>
        {children}
      </div>
      {menu && <RowContextMenu pos={menu}
        onClose={() => setMenu(null)}
        onEdit={onEdit ? () => { setMenu(null); onEdit(); } : null}
        onDelete={onDelete ? () => { setMenu(null); onDelete(); } : null}/>}
    </div>
  );
}

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 (
    <React.Fragment>
      <div onClick={onClose} onPointerDown={(e) => e.stopPropagation()}
        style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.25)', backdropFilter: 'blur(2px)', WebkitBackdropFilter: 'blur(2px)' }}/>
      <div style={{
        position: 'fixed', left, top, width: W, zIndex: 201,
        background: t.card, borderRadius: 14,
        boxShadow: '0 16px 40px rgba(0,0,0,0.35)',
        overflow: 'hidden',
        animation: 'rfctxpop 0.16s cubic-bezier(.2,.8,.2,1.05)',
      }}>
        {items.map((it, i) => {
          const handler = it.id === 'edit' ? onEdit : onDelete;
          return (
            <Pressable key={it.id} onClick={handler} scale={1}>
              <div style={{
                padding: '14px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                borderBottom: i < items.length - 1 ? `0.5px solid ${t.sep}` : 'none',
              }}>
                <span style={{ fontSize: 15, color: it.color, fontWeight: 500 }}>{it.label}</span>
                {it.id === 'edit'
                  ? <Icon d={icons.edit} color={it.color} size={16} sw={1.8}/>
                  : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={it.color} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>}
              </div>
            </Pressable>
          );
        })}
      </div>
      <style>{`@keyframes rfctxpop { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } }`}</style>
    </React.Fragment>
  );
}

// ─────────────────────────────────────────────────────────────
// 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(
    <>
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, zIndex: 80,
        background: t.scrim,
        opacity: open ? Math.max(0, 1 - dragY / 420) : 0, pointerEvents: open ? 'auto' : 'none',
        visibility: (open || visible) ? 'visible' : 'hidden',
        transition: dragging ? 'none' : 'opacity 0.25s',
      }}/>
      <div style={{
        position: 'fixed', left: 0, right: 0, bottom: 0, zIndex: 81,
        background: t.sheetBg,
        borderTopLeftRadius: 20, borderTopRightRadius: 20,
        transform: open ? `translateY(${dragY}px)` : 'translateY(100%)',
        transition: dragging ? 'none' : 'transform 0.32s cubic-bezier(0.2,0.7,0.3,1)',
        boxShadow: open ? '0 -10px 40px rgba(0,0,0,0.15)' : 'none',
        visibility: (open || visible) ? 'visible' : 'hidden',
        paddingBottom: 36, maxHeight, overflow: 'auto',
      }}>
        <div
          onPointerDown={onGrabDown}
          onPointerMove={onGrabMove}
          onPointerUp={onGrabUp}
          onPointerCancel={() => { 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' }}>
          <div style={{ width: 36, height: 5, borderRadius: 3, background: t.grabber }}/>
        </div>
        {children}
      </div>
    </>,
    document.body
  );
}

// ─────────────────────────────────────────────────────────────
// Confirm dialog
// ─────────────────────────────────────────────────────────────
function Confirm({ open, title, message, confirmLabel = 'Löschen', danger = true, onCancel, onConfirm }) {
  const t = useTheme();
  useBodyScrollLock(open);
  return ReactDOM.createPortal(
    <>
      <div onClick={onCancel} style={{
        position: 'fixed', inset: 0, zIndex: 90,
        background: t.scrim,
        opacity: open ? 1 : 0, pointerEvents: open ? 'auto' : 'none',
        transition: 'opacity 0.2s',
      }}/>
      <div style={{
        position: 'fixed', left: '50%', top: '50%',
        transform: `translate(-50%, -50%) scale(${open ? 1 : 0.92})`,
        opacity: open ? 1 : 0, pointerEvents: open ? 'auto' : 'none',
        transition: 'all 0.2s', zIndex: 91,
        width: 280, background: t.dark ? 'rgba(44,44,46,0.98)' : 'rgba(248,248,250,0.96)',
        backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
        borderRadius: 16, overflow: 'hidden',
        boxShadow: '0 20px 60px rgba(0,0,0,0.25)',
      }}>
        <div style={{ padding: '18px 18px 14px', textAlign: 'center' }}>
          <div style={{ fontSize: 16, fontWeight: 700, color: t.text, letterSpacing: -0.2 }}>{title}</div>
          {message && <div style={{ fontSize: 12, color: t.textSec, marginTop: 4, lineHeight: 1.4 }}>{message}</div>}
        </div>
        <div style={{ display: 'flex', borderTop: `0.5px solid ${t.sep}` }}>
          <Pressable onClick={onCancel} style={{ flex: 1 }} scale={0.98}>
            <div style={{ padding: '12px 0', textAlign: 'center', fontSize: 15, color: t.accent, fontWeight: 500 }}>Abbrechen</div>
          </Pressable>
          <div style={{ width: 0.5, background: t.sep }}/>
          <Pressable onClick={onConfirm} style={{ flex: 1 }} scale={0.98}>
            <div style={{ padding: '12px 0', textAlign: 'center', fontSize: 15, color: danger ? t.red : t.accent, fontWeight: 600 }}>{confirmLabel}</div>
          </Pressable>
        </div>
      </div>
    </>,
    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 (
    <div style={{
      padding: '11px 14px', display: 'flex', justifyContent: 'space-between',
      alignItems: 'flex-start', gap: 12, position: 'relative',
    }}>
      <div style={{ fontSize: 12, color: t.textSec, fontWeight: 500, minWidth: 64, flexShrink: 0, paddingTop: editing ? 8 : 0 }}>{label}</div>
      {editing ? (
        multiline ? (
          <textarea
            autoFocus value={tmp} onChange={e => setTmp(e.target.value)} onBlur={save}
            onKeyDown={e => { if (e.key === 'Escape') { setTmp(value || ''); setEditing(false); } }}
            placeholder={placeholder} rows={3}
            style={{
              flex: 1, padding: '6px 8px', borderRadius: 6,
              border: `1px solid ${t.accent}`, background: t.inputBg,
              fontSize: 13, color: t.text, outline: 'none', fontFamily: 'inherit',
              textAlign: 'right', resize: 'none',
            }}
          />
        ) : (
          <input
            autoFocus value={tmp} onChange={e => setTmp(e.target.value)} onBlur={save}
            onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { setTmp(value || ''); setEditing(false); } }}
            placeholder={placeholder} type={type}
            style={{
              flex: 1, padding: '6px 8px', borderRadius: 6,
              border: `1px solid ${t.accent}`, background: t.inputBg,
              fontSize: 13, color: t.text, outline: 'none', fontFamily: 'inherit',
              textAlign: 'right',
            }}
          />
        )
      ) : (
        <Pressable onClick={() => setEditing(true)} scale={0.99} style={{ flex: 1 }}>
          <div style={{
            fontSize: 13, color: value ? t.text : t.textTer,
            fontWeight: 500, textAlign: 'right', whiteSpace: 'pre-wrap',
            padding: '0 0 0 8px',
          }}>{value || placeholder} <span style={{ color: t.textTer, fontSize: 10, marginLeft: 4 }}>✎</span></div>
        </Pressable>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Date helpers
// ─────────────────────────────────────────────────────────────
const DE_DAYS = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
const DE_MONTHS = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const DE_MONTHS_SHORT = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'];

const fmtDateISO = (d) => {
  if (typeof d === 'string') return d;
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${dd}`;
};
const fromISO = (s) => { const [y,m,d] = s.split('-').map(Number); return new Date(y, m-1, d); };
const fmtDateDE = (s) => { const d = fromISO(s); return `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth()+1).padStart(2,'0')}.${d.getFullYear()}`; };
const fmtDateLong = (s) => { const d = fromISO(s); return `${DE_DAYS[d.getDay()]}, ${d.getDate()}. ${DE_MONTHS[d.getMonth()]}`; };
const fmtRange = (s, e) => {
  const ds = fromISO(s), de = fromISO(e);
  if (s === e) return `${ds.getDate()}. ${DE_MONTHS[ds.getMonth()]} ${ds.getFullYear()}`;
  if (ds.getMonth() === de.getMonth() && ds.getFullYear() === de.getFullYear())
    return `${ds.getDate()}.–${de.getDate()}. ${DE_MONTHS[ds.getMonth()]} ${ds.getFullYear()}`;
  return `${ds.getDate()}. ${DE_MONTHS_SHORT[ds.getMonth()]} – ${de.getDate()}. ${DE_MONTHS_SHORT[de.getMonth()]} ${de.getFullYear()}`;
};
const daysBetween = (s, e) => {
  return Math.round((fromISO(e) - fromISO(s)) / 86400000) + 1;
};
// Billable days for pricing. Defaults to the calendar span, but a rental may
// carry a `billDays` override (e.g. rent over a week, charge only 1 day).
const billableDays = (r) => {
  const bd = r && r.billDays;
  if (bd != null && bd !== '' && Number(bd) >= 1) return Math.floor(Number(bd));
  return daysBetween(r.start, r.end);
};
const todayISO = () => fmtDateISO(new Date()); // real "today"

// ─────────────────────────────────────────────────────────────
// Tab bar — Home · Kalender · Equipment · Finanzen · Akten
// ─────────────────────────────────────────────────────────────
function InteractiveTabBar({ active, onChange, dark, variant, showLabels }) {
  const t = useTheme();
  const items = [
    { id: 'home', d: 'M3 12L12 3l9 9M5 10v10h14V10' },
    { id: 'cal',  d: icons.cal },
    { id: 'eq',   img: IMG_ICONS.warenhaus },
    { id: 'eur',  d: icons.euro },
    { id: 'doc',  d: icons.doc },
  ];

  // Which visual style: floating pill vs. full-width bottom bar.
  // `variant` (from the Darstellung setting) wins; otherwise the theme decides
  // (Studio → full bar, classic → floating pill).
  const style = variant || (t.studio ? 'bar' : 'pill');

  // Full-width bottom bar (Studio look): labelled 4-tab bar — Start · Kalender · Lager · Akten.
  // Finanzen is reached from the dashboard; Mieter/Rechnungen/Schnelltexte live under Akten.
  if (style === 'bar') {
    const studioItems = t.studio ? [
      { id: 'home', label: 'Start',    d: 'M3 12L12 3l9 9M5 10v10h14V10' },
      { id: 'cal',  label: 'Kalender', d: icons.cal },
      { id: 'eq',   label: 'Lager',    d: 'M3 7l9-4 9 4v10l-9 4-9-4V7zM3 7l9 4 9-4M12 11v10' },
      { id: 'doc',  label: 'Akten',    d: icons.doc },
    ] : [
      { id: 'home', label: 'Start',    d: 'M3 12L12 3l9 9M5 10v10h14V10' },
      { id: 'cal',  label: 'Kalender', d: icons.cal },
      { id: 'eq',   label: 'Lager',    img: IMG_ICONS.warenhaus },
      { id: 'eur',  label: 'Finanzen', d: icons.euro },
      { id: 'doc',  label: 'Akten',    d: icons.doc },
    ];
    const activeCol = t.accent;
    const muteCol = t.textSec;
    return (
      <div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 30,
        background: dark ? 'rgba(21,26,31,0.86)' : 'rgba(255,255,255,0.86)',
        backdropFilter: 'blur(40px) saturate(180%)', WebkitBackdropFilter: 'blur(40px) saturate(180%)',
        borderTop: `0.5px solid ${t.sep}`, paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 6px)' }}>
        <div style={{ display: 'flex', alignItems: 'stretch', justifyContent: 'space-around', padding: '8px 6px 4px' }}>
          {studioItems.map(i => {
            const on = i.id === active;
            return (
              <Pressable key={i.id} onClick={() => onChange(i.id)} scale={0.9} style={{ flex: 1 }}>
                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '4px 0' }}>
                  {i.img
                    ? <ImgIcon light={i.img.light} dark={i.img.dark} size={22} style={{ opacity: on ? 1 : 0.4 }}/>
                    : <Icon d={i.d} color={on ? activeCol : muteCol} size={22} sw={on ? 2.2 : 1.7}/>}
                  {showLabels !== false && <span style={{ fontSize: 10.5, fontWeight: on ? 700 : 500, color: on ? activeCol : muteCol, letterSpacing: 0.2 }}>{i.label}</span>}
                </div>
              </Pressable>
            );
          })}
        </div>
      </div>
    );
  }

  return (
    <div style={{ position: 'fixed', bottom: 'calc(env(safe-area-inset-bottom, 0px) + 14px)', left: 12, right: 12, zIndex: 30 }}>
      <Glass dark={dark} radius={32} padding="8px 4px">
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-around' }}>
          {items.map(i => {
            const isActive = i.id === active;
            return (
              <Pressable key={i.id} onClick={() => onChange(i.id)} scale={0.85} style={{
                flex: 1, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>
                {i.img ? (() => {
                  const isLineArt = !!i.img.invertDark;
                  if (isLineArt) {
                    // Use mask-image so the icon renders as a flat solid color
                    // (no anti-aliased grays leaking through) — pure #000 in light, #fff in dark.
                    const src = dark ? i.img.dark : i.img.light;
                    const color = dark ? '#fff' : '#000';
                    const size = isActive ? 22 : 21;
                    return (
                      <div style={{
                        width: size, height: size,
                        background: color,
                        WebkitMask: `url(${src}) center / contain no-repeat`,
                        mask: `url(${src}) center / contain no-repeat`,
                        opacity: isActive ? 1 : 0.35,
                      }}/>
                    );
                  }
                  return (
                    <ImgIcon light={i.img.light} dark={i.img.dark} size={21}
                      style={{ opacity: isActive ? 1 : 0.35 }}/>
                  );
                })() : (
                  <Icon d={i.d} color={isActive ? t.text : (dark ? 'rgba(235,235,245,0.55)' : 'rgba(60,60,67,0.55)')} size={21} sw={isActive ? 2.4 : 1.8}/>
                )}
              </Pressable>
            );
          })}
        </div>
      </Glass>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Initial seed data (nur Startwert, falls der Schlüssel auf dem Server noch nicht existiert)
// ─────────────────────────────────────────────────────────────
const SEED_EQUIPMENT = [
  { id: 'jbl', name: 'JBL EON 712', cat: 'Aktivlautsprecher', sub: '1.300 W · 12″ · Bluetooth', kind: 'Tontechnik', qty: 2, repairQty: 0, until: 'bis Sa', price: 45, util: 78, uses: 42, ic: 'speaker',
    accessories: ['Boxenständer', 'Stromkabel 5m', 'Speakon-Kabel'],
    maint: [{ id: 'm-jbl1', type: 'check', title: 'Funktionscheck', date: '2026-03-12', done: true, note: 'Alles ok' }] },
  { id: 'tr750', name: 'Anhänger 750kg', cat: 'Kastenanhänger', sub: '2,05 × 1,10 m · ungebremst', kind: 'Anhänger', qty: 1, repairQty: 0, until: '', price: 40, util: 62, uses: 28, ic: 'trailer',
    accessories: ['Spanngurte (4×)', 'Sicherungsnetz'],
    maint: [
      { id: 'm-tr1', type: 'tuev', title: 'Nächster TÜV / HU', date: '2026-06-14', done: false, note: 'GTÜ Prüfstelle München-Süd' },
      { id: 'm-tr2', type: 'check', title: 'Reifendruck geprüft', date: '2026-04-30', done: true, note: '' },
    ] },
  { id: 'sm58', name: 'Shure SM58 ×4', cat: 'Mikrofon-Set', sub: '4× Gesangs-Mikro mit Stativen', kind: 'Tontechnik', qty: 4, repairQty: 1, until: 'bis Mi', price: 30, util: 71, uses: 56, ic: 'mic',
    accessories: ['XLR-Kabel 5m', 'Mikrofonständer', 'Popschutz'],
    maint: [{ id: 'm-sm1', type: 'defect', title: 'Mikro #3: Klinke wackelt', date: '2026-05-16', done: false, note: 'Kabelbruch vermutet – vor nächster Vermietung prüfen' }] },
  { id: 'mg10', name: 'Yamaha MG10XU', cat: 'Mischpult', sub: '10 Kanäle · USB · Effekte', kind: 'Tontechnik', qty: 1, repairQty: 1, until: '', price: 35, util: 54, uses: 31, ic: 'mixer',
    accessories: ['USB-Kabel', 'Klinke-Klinke 3m'],
    maint: [{ id: 'm-mg1', type: 'repair', title: 'Kanal 4 rauscht', date: '2026-05-10', done: false, note: 'In Reparatur bei Werkstatt Müller' }] },
  { id: 'tr1500', name: 'Anhänger 1500kg', cat: 'Pferdeanhänger', sub: 'Doppelachser · gebremst', kind: 'Anhänger', qty: 1, repairQty: 0, until: '', price: 75, util: 28, uses: 9, ic: 'trailer',
    accessories: ['Spanngurte (4×)'],
    maint: [{ id: 'm-tr15', type: 'tuev', title: 'Nächster TÜV / HU', date: '2027-02-28', done: false, note: '' }] },
  { id: 'ew100', name: 'Sennheiser EW100', cat: 'Funkmikro-Set', sub: 'Funkstrecke · 2× Handsender', kind: 'Tontechnik', qty: 2, repairQty: 0, until: '', price: 55, util: 36, uses: 14, ic: 'mic',
    accessories: ['XLR-Kabel 5m', 'Ersatz-Batterien (AA ×4)'],
    maint: [] },
];

const SEED_RENTALS = [
  { id: 'r1', status: 'aktiv',
    tenantName: 'Andreas Müller', address: 'Hauptstr. 12\n80331 München', email: 'a.mueller@example.com', phone: '+49 89 123 4567', idCard: 'L1234567',
    equipmentId: 'jbl', equipmentName: 'JBL EON 712', purpose: 'Hochzeit',
    start: '2026-05-18', end: '2026-05-22', startTime: '14:00', endTime: '11:00',
    dailyRate: 45, deposit: 200, depositStatus: 'erhalten',
    delivery: { enabled: true, address: 'Hauptstr. 12, 80331 München', km: 18, tripFactor: 4 } },
  { id: 'r6', status: 'aktiv',
    tenantName: 'Familie Bauer', address: 'Lindenweg 4\n82152 Planegg', email: 's.bauer@example.com', phone: '+49 89 700 2211', idCard: 'T2233445',
    equipmentId: 'sm58', equipmentName: 'Shure SM58 ×4', purpose: 'Gartenfest',
    start: '2026-05-19', end: '2026-05-21', dailyRate: 30, deposit: 80, depositStatus: 'erhalten' },
  { id: 'r2', status: 'reserviert',
    tenantName: 'Konstantin Klein', address: 'Birkenallee 22\n80809 München', email: 'k.klein@gmail.com', phone: '+49 89 444 1122', idCard: 'L7654321',
    equipmentId: 'jbl', equipmentName: 'PA-Komplettset', purpose: 'Geburtstag (40)',
    start: '2026-06-01', end: '2026-06-02', startTime: '16:00', endTime: '12:00',
    dailyRate: 160, deposit: 300, depositStatus: 'offen',
    delivery: { enabled: true, address: 'Birkenallee 22, 80809 München', km: 22, tripFactor: 4 } },
  { id: 'r3', status: 'abgeschlossen',
    tenantName: 'TSV Sportverein e.V.', address: 'Sportweg 5\n82110 Germering', email: 'kontakt@tsv-germering.de', phone: '+49 89 555 8800', idCard: '–',
    equipmentId: 'sm58', equipmentName: 'Shure SM58 ×4', purpose: 'Vereinsfest',
    start: '2026-04-26', end: '2026-04-27', startTime: '09:00', endTime: '18:00', dailyRate: 30, deposit: 60, depositStatus: 'zurueckgezahlt' },
  { id: 'r4', status: 'abgeschlossen',
    tenantName: 'Petra Schmidt', address: 'Ringstr. 8\n85375 Neufahrn', email: 'p.schmidt@web.de', phone: '+49 8165 123 45', idCard: 'X9876543',
    equipmentId: 'tr750', equipmentName: 'Anhänger 750kg', purpose: 'Umzug',
    start: '2026-05-15', end: '2026-05-15', startTime: '08:00', endTime: '18:00', dailyRate: 40, deposit: 100, depositStatus: 'zurueckgezahlt' },
  { id: 'r5', status: 'abgeschlossen',
    tenantName: 'Andreas Müller', address: 'Hauptstr. 12\n80331 München', email: 'a.mueller@example.com', phone: '+49 89 123 4567', idCard: 'L1234567',
    equipmentId: 'tr750', equipmentName: 'Anhänger 750kg', purpose: 'Gartenabfälle',
    start: '2026-03-07', end: '2026-03-08', dailyRate: 40, deposit: 100, depositStatus: 'zurueckgezahlt' },
];

// Protocol templates — now editable. {tenant} {equipment} {period} {start} {end} {total} {deposit} {days} {rate} {address} {today} placeholders.
const SEED_PROTOCOLS = [
  { id: 'reservation', name: 'Reservierungsbestätigung', icon: '📋', color: '#007AFF',
    desc: 'Bestätigung an den Mieter senden',
    text:
`Hallo {tenant},

vielen Dank für Ihre Reservierung!

📅 Zeitraum: {period}
🕐 Abholung: {start}, {startTime} Uhr
🕐 Rückgabe: {end}, {endTime} Uhr
🚚 {delivery}
🎤 Equipment: {equipment}
💰 Gesamtpreis: {total} €
🔒 Kaution: {deposit} €

Bitte zur Übergabe einen gültigen Ausweis mitbringen.

Bei Fragen melden Sie sich gerne.

Viele Grüße
RentFlow Manager`,
  },
  { id: 'handover', name: 'Übergabeprotokoll', icon: '📦', color: '#34C759',
    desc: 'Bei Abholung dokumentieren',
    text:
`ÜBERGABEPROTOKOLL

Datum: {today}
Mieter: {tenant}
Adresse: {address}

Equipment: {equipment}
Mietzeitraum: {period}
Abholung/Anlieferung: {start} {startTime} Uhr
Rückgabe spätestens: {end} {endTime} Uhr

Logistik: {delivery}

Zustand bei Übergabe:
☑ Vollständig und funktionsfähig
☑ Keine sichtbaren Mängel
☐ Anmerkung: ____________________

Kaution erhalten: {deposit} €

___________________   ___________________
Mieter                Vermieter`,
  },
  { id: 'return', name: 'Rückgabeprotokoll', icon: '↩️', color: '#FF9500',
    desc: 'Bei Rückgabe dokumentieren',
    text:
`RÜCKGABEPROTOKOLL

Datum: {today}
Mieter: {tenant}
Equipment: {equipment}
Mietzeitraum: {period}
Rückgabe-Modus: {delivery}

Zustand bei Rückgabe:
☑ Vollständig
☑ Funktionsfähig
☑ Keine neuen Mängel
☐ Anmerkung: ____________________

Kaution zurückerstattet: {deposit} €
Abrechnung: {total} € ({days} Tage à {rate} €)

___________________   ___________________
Mieter                Vermieter`,
  },
  { id: 'note', name: 'Erinnerung / Notiz', icon: '📝', color: '#AF52DE',
    desc: 'Kurze Nachricht zum Mieter',
    text:
`Hallo {tenant},

kurze Erinnerung: Ihr Termin für {equipment} ist am {start}.

Bei Fragen einfach melden!

Viele Grüße`,
  },
];

// ─────────────────────────────────────────────────────────────
// Seed emails (Posteingang) — incoming rental requests + normal mail
// App's fixed "today" is 2026-05-20.
// ─────────────────────────────────────────────────────────────
const SEED_EMAILS = [
  { id: 'm1', from: 'Lena Wagner', fromEmail: 'lena.wagner@gmail.com',
    subject: 'Anfrage Lautsprecher für Hochzeit', dateISO: '2026-05-20', time: '09:14',
    unread: true, starred: false, kind: 'request', replied: false, archived: false, linkedRentalId: null,
    parsed: { equipmentHint: 'JBL Lautsprecher / PA', startISO: '2026-06-13', endISO: '2026-06-14', purpose: 'Hochzeit', phone: '+49 170 2233445', deliveryAddress: 'Seestr. 9, 82211 Herrsching' },
    body: 'Hallo,\n\nwir heiraten am 13. Juni und suchen eine Musikanlage für ca. 80 Gäste im Festsaal. Zwei aktive Lautsprecher mit Bluetooth wären super. Wir würden gerne am Freitag (13.6.) abholen und am Sonntag zurückbringen.\n\nKönnt ihr auch liefern? Die Adresse wäre Seestr. 9 in Herrsching.\n\nViele Grüße\nLena Wagner\nTel. 0170 2233445' },

  { id: 'm2', from: 'Thomas Brandt', fromEmail: 't.brandt@web.de',
    subject: 'Anhänger am Wochenende frei?', dateISO: '2026-05-20', time: '08:02',
    unread: true, starred: false, kind: 'request', replied: false, archived: false, linkedRentalId: null,
    parsed: { equipmentHint: 'Anhänger 750kg', startISO: '2026-05-24', endISO: '2026-05-24', purpose: 'Umzug', phone: '+49 151 99887766', deliveryAddress: '' },
    body: 'Guten Morgen,\n\nich bräuchte für einen kleinen Umzug am Samstag, den 24.5., einen Kastenanhänger (750 kg reicht). Selbstabholung ist kein Problem.\n\nWas kostet das für einen Tag?\n\nBeste Grüße\nThomas Brandt' },

  { id: 'm3', from: 'SV Eintracht Germering', fromEmail: 'vorstand@sv-eintracht.de',
    subject: 'Tontechnik Sommerfest 18.–19. Juli', dateISO: '2026-05-19', time: '17:46',
    unread: false, starred: true, kind: 'request', replied: false, archived: false, linkedRentalId: null,
    parsed: { equipmentHint: 'PA + Mikrofone (Komplettset)', startISO: '2026-07-18', endISO: '2026-07-19', purpose: 'Vereins-Sommerfest', phone: '+49 89 880011', deliveryAddress: 'Sportplatz Germering' },
    body: 'Sehr geehrte Damen und Herren,\n\nfür unser Sommerfest am 18. und 19. Juli benötigen wir eine Beschallungsanlage inkl. 2–3 Mikrofonen für Ansagen und eine kleine Bühne. Lieferung und Aufbau auf unseren Sportplatz wären ideal.\n\nBitte um ein Angebot.\n\nMit freundlichen Grüßen\nVorstand SV Eintracht' },

  { id: 'm4', from: 'Andreas Müller', fromEmail: 'a.mueller@example.com',
    subject: 'Re: Rückgabe JBL EON 712', dateISO: '2026-05-19', time: '14:20',
    unread: false, starred: false, kind: 'normal', replied: false, archived: false, linkedRentalId: 'r1',
    parsed: null,
    body: 'Hallo,\n\npasst es, wenn ich die Boxen am Freitag erst gegen 18 Uhr zurückbringe? Komme direkt von der Arbeit.\n\nDanke & Gruß\nAndreas' },

  { id: 'm5', from: 'Petra Schmidt', fromEmail: 'p.schmidt@web.de',
    subject: 'Frage zur Rechnung RE-2026-118', dateISO: '2026-05-18', time: '11:05',
    unread: false, starred: false, kind: 'normal', replied: true, archived: false, linkedRentalId: 'r4',
    parsed: null,
    body: 'Guten Tag,\n\nich habe die Rechnung für den Anhänger erhalten – vielen Dank. Eine kurze Frage: Ist die Kaution darin schon verrechnet oder bekomme ich die separat zurück?\n\nFreundliche Grüße\nPetra Schmidt' },

  { id: 'm6', from: 'Markus Reiter', fromEmail: 'm.reiter@gmx.de',
    subject: 'Funkmikrofone für Konferenz', dateISO: '2026-05-17', time: '16:30',
    unread: false, starred: false, kind: 'request', replied: false, archived: false, linkedRentalId: null,
    parsed: { equipmentHint: 'Sennheiser EW100 Funkmikro', startISO: '2026-06-05', endISO: '2026-06-05', purpose: 'Firmen-Konferenz', phone: '', deliveryAddress: '' },
    body: 'Hallo zusammen,\n\nfür eine Tagesveranstaltung am 5. Juni bräuchten wir 2 Funkmikrofone (Handsender). Abholung am Vortag möglich?\n\nDanke\nM. Reiter' },
];

const SEED_EVENTS = [
  { id: 'g1', title: 'Werkstatt-Termin Anhänger', equipmentId: '', start: '2026-05-21', end: '2026-05-21', color: '#5856D6', note: 'TÜV-Vorbereitung 750kg' },
  { id: 'g2', title: 'Inventur Lager', equipmentId: '', start: '2026-05-25', end: '2026-05-25', color: '#FF2D55', note: '' },
  { id: 'g3', title: 'Messe AV-Technik München', equipmentId: '', start: '2026-05-28', end: '2026-05-29', color: '#AF52DE', note: 'Networking & Einkauf' },
];

const PROTOCOL_COLORS = ['#007AFF', '#34C759', '#FF9500', '#AF52DE', '#FF3B30', '#5856D6', '#FF2D55', '#30D158'];
const EVENT_COLORS    = ['#6c8388', '#4f7a5b', '#b07a3c', '#6c6f8a', '#b0474b', '#5a6b86', '#b06a52'];
const EQUIPMENT_ICONS = ['speaker', 'mic', 'cable', 'trailer'];

// Editable Schnelltexte — message + document templates (WhatsApp, E-Mail, Protokolle).
const SEED_QUICKTEXTS = [
  { id: 'wa-res', name: 'Reservierungsbestätigung (WhatsApp)',
    desc: 'Kurze Bestätigung per WhatsApp', icon: '💬',
    text: 'Hallo {tenant}, ✨\n\nIhre Reservierung ist bestätigt:\n\n• {equipment}\n• Zeitraum: {period}\n• Gesamt: {total} €\n\nBei Fragen melden Sie sich gerne. Vielen Dank!' },
  { id: 'mail-res', name: 'Reservierungsbestätigung (E-Mail)',
    desc: 'Formellere Variante per E-Mail', icon: '✉️',
    text: 'Sehr geehrte/r {tenant},\n\nvielen Dank für Ihre Reservierung. Hiermit bestätigen wir folgenden Mietvorgang:\n\nEquipment:  {equipment}\nZeitraum:   {period}\nGesamt:     {total} €\n\nBei Rückfragen stehen wir Ihnen jederzeit zur Verfügung.\n\nMit freundlichen Grüßen' },
  { id: 'reminder', name: 'Erinnerung Abholung',
    desc: 'Am Tag vor der Abholung schicken', icon: '⏰',
    text: 'Hallo {tenant}, kleine Erinnerung: Ihr {equipment} steht morgen für Sie bereit. Bis dann!' },
  { id: 'handover', name: 'Übergabeprotokoll', icon: '📦',
    desc: 'Bei Abholung dokumentieren',
    text:
`ÜBERGABEPROTOKOLL

Datum: {today}
Mieter: {tenant}
Adresse: {address}

Equipment: {equipment}
Mietzeitraum: {period}
Abholung/Anlieferung: {start} {startTime} Uhr
Rückgabe spätestens: {end} {endTime} Uhr

Logistik: {delivery}

Zustand bei Übergabe:
☑ Vollständig und funktionsfähig
☑ Keine sichtbaren Mängel
☐ Anmerkung: ____________________

Kaution erhalten: {deposit} €

___________________   ___________________
Mieter                Vermieter` },
  { id: 'return', name: 'Rückgabeprotokoll', icon: '↩️',
    desc: 'Bei Rückgabe dokumentieren',
    text:
`RÜCKGABEPROTOKOLL

Datum: {today}
Mieter: {tenant}
Equipment: {equipment}
Mietzeitraum: {period}
Rückgabe-Modus: {delivery}

Zustand bei Rückgabe:
☑ Vollständig
☑ Funktionsfähig
☑ Keine neuen Mängel
☐ Anmerkung: ____________________

Kaution zurückerstattet: {deposit} €
Abrechnung: {total} € ({days} Tage à {rate} €)

___________________   ___________________
Mieter                Vermieter` },
];

const fillTemplate = (tpl, rental, eqName) => {
  const days = billableDays(rental);
  const items = (window.getRentalItems ? window.getRentalItems(rental) : []);
  const itemsSubtotal = items.length
    ? items.reduce((s, it) => {
        return s + days * (Number(it.dailyRate) || 0) * Math.max(1, Number(it.quantity) || 1);
      }, 0)
    : days * (Number(rental.dailyRate) || 0) * Math.max(1, Number(rental.quantity) || 1);
  // Delivery cost
  const logistik = window.__rfLogistik || {};
  const d = rental.delivery || {};
  let deliveryTotal = 0, deliveryLine = '';
  if (d.enabled) {
    const calc = window.calcDelivery
      ? window.calcDelivery(d.km, d.tripFactor || logistik.tripFactor || 4, logistik.kmRate || 0.7, logistik.pauschale || 10)
      : { total: 0, totalKm: 0 };
    deliveryTotal = calc.total || 0;
    deliveryLine = deliveryTotal > 0
      ? `Anlieferung & Abholung: ${(d.address || rental.address || '').replace(/\n/g, ', ')} · ${String(calc.totalKm).replace('.', ',')} km — ${deliveryTotal.toFixed(2).replace('.', ',')} €`
      : '';
  } else {
    deliveryLine = 'Selbstabholung & Selbstrückgabe durch Mieter';
  }
  const subtotal = itemsSubtotal + deliveryTotal;
  const discount = Number(rental.discount) || 0;
  const total = Math.max(0, subtotal - discount);
  const qty = items[0] ? items[0].quantity : Math.max(1, Number(rental.quantity) || 1);
  const equipmentList = items.length
    ? items.map(it => (it.quantity > 1 ? it.quantity + ' × ' : '') + it.equipmentName).join(', ')
    : (eqName || rental.equipmentName || '');
  const accList = (rental.accessoryItems || []).map(a => (a.quantity > 1 ? a.quantity + ' × ' : '') + a.name).join(', ') || '–';
  return tpl
    .replaceAll('{tenant}', rental.tenantName)
    .replaceAll('{equipment}', equipmentList)
    .replaceAll('{zubehoer}', accList)
    .replaceAll('{period}', fmtRange(rental.start, rental.end))
    .replaceAll('{start}', fmtDateDE(rental.start))
    .replaceAll('{end}', fmtDateDE(rental.end))
    .replaceAll('{startTime}', rental.startTime || '–')
    .replaceAll('{endTime}', rental.endTime || '–')
    .replaceAll('{delivery}', deliveryLine)
    .replaceAll('{deliveryAddress}', (d.address || rental.address || '').replaceAll('\n', ', '))
    .replaceAll('{deliveryFee}', deliveryTotal.toFixed(2).replace('.', ','))
    .replaceAll('{total}', String(total))
    .replaceAll('{subtotal}', String(subtotal))
    .replaceAll('{discount}', String(discount))
    .replaceAll('{quantity}', String(qty))
    .replaceAll('{deposit}', String(rental.deposit))
    .replaceAll('{days}', String(days))
    .replaceAll('{rate}', String(rental.dailyRate))
    .replaceAll('{address}', (rental.address || '').replaceAll('\n', ', '))
    .replaceAll('{today}', todayDE());
};
const todayDE = () => new Date().toLocaleDateString('de-DE');

// ─────────────────────────────────────────────────────────────
// Status colors for rentals
// ─────────────────────────────────────────────────────────────
const statusMeta = (s) => ({
  aktiv:         { color: '#34C759', label: 'AKTIV' },
  reserviert:    { color: '#FF9500', label: 'RESERVIERT' },
  abgeschlossen: { color: 'rgba(60,60,67,0.55)', label: 'ABGESCHLOSSEN' },
}[s] || { color: 'rgba(60,60,67,0.55)', label: s.toUpperCase() });

// Auto-compute a rental's status from its date range.
// Future → 'reserviert', Past → 'abgeschlossen', Today/active span → 'aktiv'.
const autoRentalStatus = (start, end) => {
  const today = todayISO();
  if (!start || !end) return 'reserviert';
  if (today < start) return 'reserviert';
  if (today > end)   return 'abgeschlossen';
  return 'aktiv';
};

// Deposit status meta (Kaution)
const depositMeta = (s) => ({
  offen:          { color: '#FF9500', label: 'Kaution offen', short: 'Offen' },
  bezahlt:        { color: '#5856D6', label: 'Voraus bezahlt', short: 'Bezahlt' },
  erhalten:       { color: '#34C759', label: 'Kaution aktiv',   short: 'Aktiv' },
  zurueckgezahlt: { color: 'rgba(60,60,67,0.55)', label: 'Kaution zurückgezahlt', short: 'Zurückgezahlt' },
}[s || 'offen']);
const DEPOSIT_STEPS = ['offen', 'bezahlt', 'zurueckgezahlt'];

// Auto-derive Kaution status from the rental status.
// reserviert → 'offen' (unless user marked 'bezahlt' = paid-in-advance)
// aktiv      → 'erhalten' (active — locked in once the rental runs)
// abgeschlossen → 'zurueckgezahlt'
const autoDepositStatus = (rentalStatus, current) => {
  if (rentalStatus === 'aktiv') return 'erhalten';
  if (rentalStatus === 'abgeschlossen') return 'zurueckgezahlt';
  return current === 'bezahlt' ? 'bezahlt' : 'offen';
};

// Mietbetrag-Status (separate from deposit) — same logic.
// reserviert → user toggles offen / bezahlt
// aktiv / abgeschlossen → auto 'bezahlt' (the rent has been collected by then)
const PAYMENT_STEPS = ['offen', 'bezahlt'];
const paymentMeta = (s) => ({
  offen:   { color: '#FF9500', label: 'Mietbetrag offen', short: 'Offen' },
  bezahlt: { color: '#34C759', label: 'Mietbetrag bezahlt', short: 'Bezahlt' },
}[s || 'offen']);
const autoPaymentStatus = (rentalStatus, current) => {
  if (rentalStatus === 'aktiv' || rentalStatus === 'abgeschlossen') return 'bezahlt';
  return current === 'bezahlt' ? 'bezahlt' : 'offen';
};

// Maintenance record meta
const maintMeta = (type) => ({
  tuev:   { label: 'TÜV / HU', color: '#AF52DE', icon: '🛡️' },
  repair: { label: 'Reparatur', color: '#FF3B30', icon: '🔧' },
  defect: { label: 'Defekt', color: '#FF9500', icon: '⚠️' },
  check:  { label: 'Wartung', color: '#34C759', icon: '✅' },
}[type] || { label: 'Notiz', color: 'rgba(60,60,67,0.55)', icon: '📝' });
const MAINT_TYPES = ['check', 'defect', 'repair', 'tuev'];

// "Tomorrow" relative to the app's fixed today (2026-05-20)
const tomorrowISO = () => { const d = fromISO(todayISO()); d.setDate(d.getDate() + 1); return fmtDateISO(d); };

// ─────────────────────────────────────────────────────────────
// Blocked customers — global registry keyed by normalized tenant name.
// One entry per customer; status follows them across all rentals.
// Shape: { key, name, reason, since }
// ─────────────────────────────────────────────────────────────
const normalizeCustomerKey = (name) => String(name || '').trim().toLowerCase().replace(/\s+/g, ' ');
const findBlockedCustomer = (name, blocked) => {
  const key = normalizeCustomerKey(name);
  if (!key) return null;
  return (blocked || []).find(b => b.key === key) || null;
};
const upsertBlockedCustomer = (blocked, name, reason) => {
  const key = normalizeCustomerKey(name);
  if (!key) return blocked || [];
  const list = blocked || [];
  const idx = list.findIndex(b => b.key === key);
  const entry = {
    key, name: String(name || '').trim(),
    reason: String(reason || ''),
    since: (idx >= 0 ? list[idx].since : todayISO()),
  };
  if (idx >= 0) { const next = list.slice(); next[idx] = entry; return next; }
  return [...list, entry];
};
const removeBlockedCustomer = (blocked, name) => {
  const key = normalizeCustomerKey(name);
  return (blocked || []).filter(b => b.key !== key);
};

// Inclusive overlap test for two ISO date ranges
const rangesOverlap = (s1, e1, s2, e2) => s1 <= e2 && s2 <= e1;

// Stock breakdown for one piece of equipment.
// total = Anzahl, vermietet = aktive Vermietungen, reparatur = manuell rausgenommen,
// verfuegbar = Rest. Single source of truth for availability across the app.
function eqStock(e, rentals = []) {
  const total = Math.max(1, Number(e && e.qty) || 1);
  const vermietet = rentals
    .filter(r => r && r.equipmentId === (e && e.id) && r.status === 'aktiv')
    .reduce((s, r) => s + (Number(r.quantity) || 1), 0);
  const reparatur = Math.max(0, Math.min(total, Number(e && e.repairQty) || 0));
  const verfuegbar = Math.max(0, total - vermietet - reparatur);
  return { total, vermietet, reparatur, verfuegbar };
}

// Find rentals that book an accessory in [start,end] beyond its available stock.
function accessoryConflicts(accessoryId, need, start, end, accessories = [], rentals = [], excludeRentalId = null) {
  if (!accessoryId || !start || !end) return null;
  const acc = accessories.find((a) => a.id === accessoryId);
  if (!acc) return null;
  const total = Math.max(0, Number(acc.qty) || 0) - Math.max(0, Number(acc.defectQty) || 0);
  let booked = 0;
  const clashRentals = [];
  for (const r of rentals) {
    if (excludeRentalId && r.id === excludeRentalId) continue;
    if (r.status !== 'aktiv' && r.status !== 'reserviert') continue;
    if (!rangesOverlap(start, end, r.start, r.end)) continue;
    for (const a of (r.accessoryItems || [])) {
      if (a.accessoryId === accessoryId && !a.returned) {
        booked += Math.max(0, Number(a.quantity) || 0);
        clashRentals.push({ rentalId: r.id, tenantName: r.tenantName, qty: Number(a.quantity) || 0 });
      }
    }
  }
  const over = (booked + Math.max(0, need)) - total;
  if (over > 0) return { acc, total, booked, need, over, clashRentals };
  return null;
}
if (typeof window !== 'undefined') window.accessoryConflicts = accessoryConflicts;

// Find rentals (and calendar events) that block a piece of equipment in [start,end]
function equipmentConflicts(equipmentId, start, end, rentals = [], events = [], excludeRentalId = null) {
  if (!equipmentId || !start || !end) return [];
  const out = [];
  rentals.forEach(r => {
    if (r.id === excludeRentalId) return;
    if (r.status === 'abgeschlossen') return;
    // Multi-item or legacy single-equipment check
    const ids = (Array.isArray(r.items) && r.items.length) ? r.items.map(it => it.equipmentId) : [r.equipmentId];
    if (!ids.includes(equipmentId)) return;
    if (rangesOverlap(start, end, r.start, r.end)) {
      out.push({ kind: 'rental', id: r.id, title: r.tenantName, start: r.start, end: r.end, status: r.status });
    }
  });
  events.forEach(ev => {
    if (ev.equipmentId !== equipmentId) return;
    if (rangesOverlap(start, end, ev.start, ev.end)) {
      out.push({ kind: 'event', id: ev.id, title: ev.title, start: ev.start, end: ev.end, color: ev.color });
    }
  });
  return out;
}

// Build the list of due reminders (returns due tomorrow / today, overdue, TÜV soon)
function buildReminders(rentals = [], equipment = []) {
  const today = todayISO(), tmrw = tomorrowISO();
  const rem = [];
  rentals.forEach(r => {
    if (r.status === 'abgeschlossen') return;
    if (r.end === tmrw) rem.push({ id: 'ret-' + r.id, urgency: 'soon', icon: '📦', title: 'Rückgabe morgen fällig', sub: `${r.tenantName} · ${r.equipmentName}` });
    else if (r.end === today) rem.push({ id: 'ret-' + r.id, urgency: 'today', icon: '📦', title: 'Rückgabe heute fällig', sub: `${r.tenantName} · ${r.equipmentName}` });
    else if (r.end < today) rem.push({ id: 'ret-' + r.id, urgency: 'over', icon: '⏰', title: 'Rückgabe überfällig', sub: `${r.tenantName} · ${r.equipmentName}` });
    if (r.status === 'reserviert' && r.start === tmrw) rem.push({ id: 'pick-' + r.id, urgency: 'soon', icon: '🚀', title: outLabel(r) + ' morgen', sub: `${r.tenantName} · ${r.equipmentName}` });
  });
  equipment.forEach(e => {
    (e.maint || []).forEach(m => {
      if (!m.done && m.type === 'tuev') {
        const days = daysBetween(todayISO(), m.date) - 1;
        if (days >= 0 && days <= 45) rem.push({ id: 'tuev-' + m.id, urgency: days <= 14 ? 'soon' : 'info', icon: '🛡️', title: `TÜV in ${days} Tag${days === 1 ? '' : 'en'}`, sub: `${e.name} · ${fmtDateDE(m.date)}` });
      }
      if (!m.done && (m.type === 'defect' || m.type === 'repair')) {
        rem.push({ id: 'def-' + m.id, urgency: 'info', icon: '🔧', title: m.title, sub: `${e.name}` });
      }
    });
  });
  // urgency order: over, today, soon, info
  const ord = { over: 0, today: 1, soon: 2, info: 3 };
  return rem.sort((a, b) => ord[a.urgency] - ord[b.urgency]);
}

// ─────────────────────────────────────────────────────────────
// Router / App
// ─────────────────────────────────────────────────────────────
function App() {
  // Theme: 'light' | 'dark' | 'nord' | 'pastell'. Migrate the old rf-dark boolean once.
  const [themeId, setThemeId] = useLocal('rf-theme', (() => {
    try {
      const saved = localStorage.getItem('rf-theme');
      if (saved) { const v = JSON.parse(saved); if (v === 'light' || v === 'dark') return 'studio'; }
    } catch {}
    return 'studio';
  })());
  const [accents, setAccents] = useLocal('rf-accents', {}); // { themeId: '#hex' } accent overrides
  const [navLayoutRaw, setNavLayout] = useLocal('rf-navlayout', 'classic'); // classic | command | launcher
  const navLayout = navLayoutRaw === 'rail' ? 'classic' : navLayoutRaw; // 'rail' layout retired → fall back to classic
  const [navLabels, setNavLabels] = useLocal('rf-navlabels', true); // show text labels under bottom-bar icons
  const baseTheme = THEMES[themeId] || studioTheme;
  // Retire old light/dark themes — force anyone still on them into Studio.
  React.useEffect(() => {
    if (themeId === 'light' || themeId === 'dark') setThemeId('studio');
  }, [themeId]);
  const t = React.useMemo(() => applyAccent(baseTheme, accents[themeId]), [baseTheme, accents, themeId]);
  const dark = t.dark;
  // Keep the document root background + theme-color in sync with the theme so
  // the area revealed when the mobile address bar collapses matches the app.
  React.useEffect(() => {
    document.documentElement.style.background = t.pageBg;
    document.body.style.background = t.bg;
    let m = document.querySelector('meta[name="theme-color"]');
    if (!m) { m = document.createElement('meta'); m.setAttribute('name', 'theme-color'); document.head.appendChild(m); }
    m.setAttribute('content', t.pageBg);
  }, [t]);
  const [route, setRoute] = useState({ screen: 'home', params: {} });
  const [toast, setToast] = useState('');
  const [rfdbErr, clearRfdbErr] = useRfdbErrorBanner();

  // Lifted, persisted state
  const [equipment, setEquipment] = useLocal('rf-equipment-v2', []);
  const [accessories, setAccessories] = useLocal('rf-accessories', []);
  const [workflows, setWorkflows] = useLocal('rf-workflows', {});
  const [reviews, setReviews] = useLocal('rf-reviews', []);
  // Auto-merge any reviews the review site left in this browser (same-origin case).
  React.useEffect(() => {
    const dismissed = window.loadDismissedReviews ? window.loadDismissedReviews() : new Set();
    const site = (window.loadSiteReviews ? window.loadSiteReviews() : []).filter((r) => r && !dismissed.has(String(r.id)));
    if (site.length) {
      setReviews((prev) => {
        const merged = window.mergeReviews ? window.mergeReviews(prev, site) : prev;
        return merged.length !== prev.length ? merged : prev;
      });
    }
  }, []);
  const [tabSettings, setTabSettings] = useLocal('rf-tab-settings', {}); // per-tab display prefs
  const [accCats, setAccCats] = useLocal('rf-acc-cats', []);
  const [categories, setCategories] = useLocal('rf-categories', []);
  const [rentals, setRentals]       = useLocal('rf-rentals-v2', []);
  // Auto-derive each rental's status from its date range whenever rentals change
  // or the day rolls over. Payment & Kaution stay 100% user-controlled — the user
  // explicitly asked to be able to pick any value at any time.
  React.useEffect(() => {
    const normalize = (r) => {
      const wantStatus = autoRentalStatus(r.start, r.end);
      if (wantStatus === r.status) return r;
      return { ...r, status: wantStatus };
    };
    setRentals(prev => {
      let changed = false;
      const next = prev.map(r => { const n = normalize(r); if (n !== r) changed = true; return n; });
      return changed ? next : prev;
    });
    const id = setInterval(() => {
      setRentals(prev => {
        let changed = false;
        const next = prev.map(r => { const n = normalize(r); if (n !== r) changed = true; return n; });
        return changed ? next : prev;
      });
    }, 60 * 60 * 1000);
    return () => clearInterval(id);
  }, []);
  const [protocols, setProtocols]   = useLocal('rf-protocols', SEED_PROTOCOLS);
  const [quickTexts, setQuickTexts] = useLocal('rf-quicktexts-v2', SEED_QUICKTEXTS);
  const [events, setEvents]         = useLocal('rf-events-v2', []);
  const [genDocs, setGenDocs]       = useLocal('rf-gendocs', {});  // { rentalId: [...] }
  const [tasks, setTasks]           = useLocal('rf-tasks', { items: [], autoDone: {} });     // manual + auto completion
  const [logistik, setLogistik]     = useLocal('rf-logistik', window.DEFAULT_LOGISTIK || {});
  // Expose so fillTemplate (defined in module scope) can read live values
  React.useEffect(() => { window.__rfLogistik = logistik; }, [logistik]);
  const [emails, setEmails]         = useLocal('rf-emails-v1', []);
  // Finance — single source of truth, shared with Home dashboard
  const [transactions, setTransactions] = useLocal('rf-transactions', []);
  // First-boot timestamp so charts can start “here” instead of from January
  const [installedAt, setInstalledAt]   = useLocal('rf-installed-at', '');
  React.useEffect(() => {
    if (!installedAt) setInstalledAt(new Date().toISOString());
  }, []);
  const [company, setCompany]       = useLocal('rf-company', window.DEFAULT_COMPANY || {});
  const [blockedCustomers, setBlockedCustomers] = useLocal('rf-blocked-customers', []);
  const [search, setSearch]         = useState(false);             // global search overlay
  const [profileOpen, setProfileOpen] = useState(false);            // global Firmenprofil/Einstellungen
  const [quickRental, setQuickRental] = useState(false);            // new-rental sheet opened as overlay (e.g. from dashboard) — stays on current screen
  const [quickRentalPrefill, setQuickRentalPrefill] = useState(null);

  const showToast = (msg) => setToast(msg);
  const navDirRef = React.useRef(0); // -1 / +1 directional hint for the screen entrance animation
  const go = (screen, params = {}) => {
    // "Quick" new rental: open the sheet as an overlay over the CURRENT screen
    // instead of navigating into the Dokumente tab, so the user returns here on close/save.
    if (screen === 'doc' && params.newRental && params.origin === 'quick') {
      setQuickRentalPrefill(params.defaultStart ? { start: params.defaultStart, end: params.defaultStart } : null);
      setQuickRental(true);
      return;
    }
    navDirRef.current = 0; setRoute({ screen, params });
  };

  // Save handler for the overlay sheet — adds the rental without leaving the current screen.
  const addRentalQuick = (r) => {
    setRentals((prev) => [r, ...prev]);
    const its = r.items && r.items.length ? r.items : r.equipmentId ? [{ equipmentId: r.equipmentId }] : [];
    if (its.length) {
      const ids = new Set(its.map((it) => it.equipmentId));
      setEquipment((prev) => prev.map((e) => ids.has(e.id) ? { ...e, uses: (Number(e.uses) || 0) + 1 } : e));
    }
    setQuickRental(false);
    showToast(`„${r.tenantName}“ angelegt`);
  };
  // Back-compat: toggleDark flips between Hell and Dunkel (used by older entry points).
  const toggleDark = () => { setThemeId((id) => id === 'dark' ? 'light' : 'dark'); };

  // Swipe between the 5 main tabs
  const TAB_ORDER = ['home', 'cal', 'eq', 'eur', 'doc'];
  const [docHomeNonce, setDocHomeNonce] = useState(0);
  const goTab = (id) => {
    // Re-tapping the Akten tab while already there returns to its index landing.
    if (id === 'doc' && route.screen === 'doc') setDocHomeNonce((n) => n + 1);
    const cur = TAB_ORDER.indexOf(tabActive);
    const nx = TAB_ORDER.indexOf(id);
    navDirRef.current = (cur >= 0 && nx >= 0) ? Math.sign(nx - cur) : 0;
    setRoute({ screen: id, params: {} });
  };
  const swipe = React.useRef({ x: 0, y: 0, active: false, allowed: false, dir: null });
  // Block tab-swipe when the gesture starts on something that manages its own
  // horizontal gesture (calendar pills / swipe-delete rows use touch-action,
  // scrollers overflow-x) so we never hijack those.
  const swipeBlocked = (target) => {
    let el = target;
    while (el && el.nodeType === 1 && el !== document.body) {
      const cs = window.getComputedStyle(el);
      if (cs.touchAction === 'none' || cs.touchAction === 'pan-y') return true;
      if ((cs.overflowX === 'auto' || cs.overflowX === 'scroll') && el.scrollWidth > el.clientWidth + 4) return true;
      if (el.getAttribute && el.getAttribute('data-noswipe') != null) return true;
      el = el.parentElement;
    }
    return false;
  };
  const onSwipeDown = (e) => {
    if (e.pointerType === 'mouse') { swipe.current.active = false; return; }
    swipe.current = { x: e.clientX, y: e.clientY, active: true, allowed: !swipeBlocked(e.target), dir: null };
  };
  const onSwipeMove = (e) => {
    const s = swipe.current;
    if (!s.active || s.dir) return;
    const dx = e.clientX - s.x, dy = e.clientY - s.y;
    if (Math.abs(dx) > 12 || Math.abs(dy) > 12) s.dir = Math.abs(dx) > Math.abs(dy) ? 'h' : 'v';
  };
  const onSwipeUp = (e) => {
    const s = swipe.current; if (!s.active) return; s.active = false;
    if (!s.allowed || s.dir !== 'h') return;
    if (!TAB_ORDER.includes(route.screen)) return;
    const dx = e.clientX - s.x, dy = e.clientY - s.y;
    if (Math.abs(dx) < 64 || Math.abs(dy) > 90) return;
    const cur = TAB_ORDER.indexOf(route.screen);
    const next = dx < 0 ? cur + 1 : cur - 1;
    if (next < 0 || next >= TAB_ORDER.length) return;
    goTab(TAB_ORDER[next]);
  };

  const tabActive = ({
    home: 'home', detail: 'home',
    cal: 'cal', eq: 'eq', log: 'eq', eur: 'eur', doc: 'doc',
  })[route.screen] || 'home';

  let body;
  switch (route.screen) {
    case 'home':
      body = <ScreenDashboard go={go} equipment={equipment} categories={categories}
                              rentals={rentals} events={events}
                              workflows={workflows} setWorkflows={setWorkflows}
                              setRentals={setRentals} setEquipment={setEquipment}
                              accessories={accessories} setAccessories={setAccessories}
                              reviews={reviews} setReviews={setReviews}
                              tasks={tasks} setTasks={setTasks}
                              transactions={transactions} installedAt={installedAt}
                              dark={dark} toggleDark={toggleDark}
                              onSearch={() => setSearch(true)} toast={showToast} company={company}/>;
      break;
    case 'detail':
      body = <ScreenDetail id={route.params.id} go={go} equipment={equipment} rentals={rentals} toast={showToast}/>;
      break;
    case 'cal':
      body = <ScreenKalender go={go} events={events} setEvents={setEvents}
                             equipment={equipment} setEquipment={setEquipment}
                             accessories={accessories}
                             rentals={rentals} setRentals={setRentals}
                             toast={showToast}/>;
      break;
    case 'log':
    case 'eq':
      body = <ScreenEquipment equipment={equipment} setEquipment={setEquipment}
                              accessories={accessories} setAccessories={setAccessories}
                              accCats={accCats} setAccCats={setAccCats}
                              tabSettings={tabSettings} setTabSettings={setTabSettings}
                              categories={categories} setCategories={setCategories}
                              rentals={rentals} setRentals={setRentals}
                              logistik={logistik} setLogistik={setLogistik}
                              initialArea={route.screen === 'log' ? 'logistik' : 'equipment'}
                              toast={showToast} go={go}/>;
      break;
    case 'eur':
      body = <ScreenFinanzen toast={showToast} go={go}
                             transactions={transactions} setTransactions={setTransactions}
                             rentals={rentals}
                             installedAt={installedAt}/>;
      break;
    case 'doc':
      body = <ScreenDokumente equipment={equipment} setEquipment={setEquipment} rentals={rentals} setRentals={setRentals}
                              accessories={accessories} setAccessories={setAccessories}
                              workflows={workflows} setWorkflows={setWorkflows}
                              reviews={reviews} setReviews={setReviews}
                              tabSettings={tabSettings}
                              protocols={protocols} setProtocols={setProtocols}
                              quickTexts={quickTexts} setQuickTexts={setQuickTexts}
                              genDocs={genDocs} setGenDocs={setGenDocs}
                              emails={emails} setEmails={setEmails}
                              company={company} setCompany={setCompany}
                              blockedCustomers={blockedCustomers} setBlockedCustomers={setBlockedCustomers}
                              logistik={logistik} setLogistik={setLogistik}
                              dark={dark} toggleDark={toggleDark}
                              events={events} focusRentalId={route.params.rentalId}
                              initialTab={route.params.tab} openNewRental={route.params.newRental}
                              docHomeNonce={docHomeNonce}
                              origin={route.params.origin}
                              go={go} toast={showToast}/>;
      break;
    default:
      body = <ScreenDashboard go={go} equipment={equipment} categories={categories}/>;
  }

  return (
    <ThemeContext.Provider value={t}>
    <window.CompanyCtx.Provider value={{ company, openSettings: () => setProfileOpen(true) }}>
    <div style={{
      minHeight: '100dvh', background: t.pageBg, transition: 'background 0.4s ease',
    }}>
      {t.bgGradient && t.glass &&
        <div style={{ position: 'fixed', inset: 0, zIndex: 0, background: t.bgGradient, pointerEvents: 'none' }} />}
      <Phone bg={t.glass ? 'transparent' : t.bg} dark={dark}>
        <div onPointerDown={onSwipeDown} onPointerMove={onSwipeMove} onPointerUp={onSwipeUp}
             onPointerCancel={() => { swipe.current.active = false; }}
             style={{ position: 'relative', zIndex: 1 }}>
          <div key={route.screen + (route.params.id || '')} style={{
            position: 'relative',
            animation: `${navDirRef.current > 0 ? 'rfSlideR' : navDirRef.current < 0 ? 'rfSlideL' : 'fadeIn'} 0.3s cubic-bezier(0.2,0.7,0.3,1)`,
          }}>
            {body}
          </div>
        </div>

        {(navLayout === 'classic' || navLayout === 'pill' || navLayout === 'bar')
          ? <InteractiveTabBar active={tabActive} onChange={(id) => goTab(id)} dark={dark}
              variant={navLayout === 'classic' ? undefined : navLayout} showLabels={navLabels !== false}/>
          : <window.AltNav layout={navLayout} active={tabActive} dark={dark} theme={t}
              onTab={(id) => goTab(id)}
              onNew={() => { setQuickRentalPrefill(null); setQuickRental(true); }}
              onSearch={() => setSearch(true)}
              onSettings={() => setProfileOpen(true)} />}
        <Toast msg={toast} onClose={() => setToast('')}/>
        <ErrorBanner err={rfdbErr} onClose={clearRfdbErr}/>
        <SearchOverlay open={search} onClose={() => setSearch(false)}
          equipment={equipment} rentals={rentals} go={go}/>
        <window.FirmenprofilSheet open={profileOpen} onClose={() => setProfileOpen(false)}
          company={company} setCompany={setCompany} toast={showToast}
          logistik={logistik} setLogistik={setLogistik}
          dark={dark} toggleDark={toggleDark}
          themeId={themeId} setThemeId={setThemeId} accents={accents} setAccents={setAccents}
          navLayout={navLayout} setNavLayout={setNavLayout}
          navLabels={navLabels} setNavLabels={setNavLabels}
          tabSettings={tabSettings} setTabSettings={setTabSettings}/>
        {window.NewRentalSheet &&
        <window.NewRentalSheet open={quickRental} onClose={() => setQuickRental(false)}
          onSave={addRentalQuick} equipment={equipment} accessories={accessories} tabSettings={tabSettings} rentals={rentals} events={events} prefill={quickRentalPrefill}
          logistik={logistik} company={company} blockedCustomers={blockedCustomers} toast={showToast} />
        }
      </Phone>

      <style>{`
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
        @keyframes rfSlideR { from { opacity: 0.4; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } }
        @keyframes rfSlideL { from { opacity: 0.4; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } }
        @keyframes rfFade { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
        input::placeholder, textarea::placeholder { color: ${t.textTer}; }
      `}</style>
    </div>
    </window.CompanyCtx.Provider>
    </ThemeContext.Provider>
  );
}

// ── Multi-item rentals (backward-compatible) ──
function getRentalItems(r) {
  if (r && Array.isArray(r.items) && r.items.length) {
    return r.items.map(it => ({
      equipmentId: it.equipmentId,
      equipmentName: it.equipmentName || '',
      dailyRate: Number(it.dailyRate) || 0,
      quantity: Math.max(1, Number(it.quantity) || 1),
    }));
  }
  if (r && r.equipmentId) {
    return [{
      equipmentId: r.equipmentId,
      equipmentName: r.equipmentName || '',
      dailyRate: Number(r.dailyRate) || 0,
      quantity: Math.max(1, Number(r.quantity) || 1),
    }];
  }
  return [];
}
function rentalEquipmentLabel(r) {
  const its = getRentalItems(r);
  if (its.length === 0) return r.equipmentName || '';
  const head = its[0].equipmentName + (its[0].quantity > 1 ? ' ×' + its[0].quantity : '');
  return its.length > 1 ? head + ' +' + (its.length - 1) : head;
}

// Total a rental costs across all items, minus discount. Never negative.
const rentalQty = (r) => {
  const its = getRentalItems(r);
  return its[0] ? its[0].quantity : Math.max(1, Number(r.quantity) || 1);
};
const rentalSubtotal = (r) => {
  const days = billableDays(r);
  const its = getRentalItems(r);
  const items = its.length ? its.reduce((s, it) => {
    return s + days * (Number(it.dailyRate) || 0) * Math.max(1, Number(it.quantity) || 1);
  }, 0) : days * (Number(r.dailyRate) || 0) * Math.max(1, Number(r.quantity) || 1);
  return items + rentalAccessoryCost(r) + rentalDeliveryFee(r);
};

// Paid accessories add to the rental price: days × dailyRate × quantity.
// Accessories without a dailyRate (>0) are "zugehörig" and cost nothing.
const rentalAccessoryCost = (r) => {
  const days = billableDays(r);
  const accs = (r && Array.isArray(r.accessoryItems)) ? r.accessoryItems : [];
  return accs.reduce((s, a) => s + days * (Number(a.dailyRate) || 0) * Math.max(1, Number(a.quantity) || 1), 0);
};

// Delivery surcharge for a rental (0 if customer self-pickup or no km set).
const rentalDeliveryFee = (r) => {
  const d = r && r.delivery;
  if (!d || !d.enabled) return 0;
  // Manual flat fee overrides the km-based calculation when chosen.
  if (d.feeMode === 'manual') {
    const f = Number(String(d.manualFee).replace(',', '.'));
    return Math.max(0, Math.round(f || 0));
  }
  const l = window.__rfLogistik || {};
  const factor = d.tripFactor || l.tripFactor || 4;
  const rate = l.kmRate ?? 0.7;
  const pauschale = l.pauschale ?? 10;
  if (window.calcDelivery) return window.calcDelivery(d.km, factor, rate, pauschale).total || 0;
  const k = Math.max(0, Number(String(d.km).replace(',', '.')) || 0);
  if (k === 0) return 0;
  return k * factor * rate + pauschale;
};
const rentalTotal = (r) => Math.max(0, rentalSubtotal(r) - (Number(r.discount) || 0));

// Virtual finance income from rentals: every rental whose rent is paid
// (active/finished rentals auto-count; reserved ones once marked paid) is
// surfaced as a read-only "Einnahme" so the user never re-enters it manually.
const rentalIncomeTxs = (rentals = []) => {
  const out = [];
  for (const r of rentals) {
    if (!r) continue;
    const paid = (window.autoPaymentStatus
      ? window.autoPaymentStatus(r.status, r.paymentStatus)
      : (r.status === 'aktiv' || r.status === 'abgeschlossen' ? 'bezahlt' : (r.paymentStatus || 'offen'))) === 'bezahlt';
    if (!paid) continue;
    const amount = rentalTotal(r);
    if (!amount) continue;
    out.push({
      id: 'rental:' + r.id,
      kind: 'in',
      label: 'Vermietung · ' + (r.tenantName || 'Mieter'),
      sub: window.rentalEquipmentLabel ? window.rentalEquipmentLabel(r) : (r.equipmentName || ''),
      amount,
      date: (r.start || todayISO()).slice(0, 10),
      _rental: true,
      rentalId: r.id,
    });
  }
  return out;
};

// True when a rental/event uses delivery (Lieferung) instead of self-pickup (Abholung).
const isDelivery = (x) => !!(x && x.delivery && x.delivery.enabled);
// Dynamic outbound label: delivery → "Lieferung"/"Lieferzeitpunkt", else "Abholung".
const outLabel = (x, long) => isDelivery(x) ? (long ? 'Lieferzeitpunkt' : 'Lieferung') : 'Abholung';

Object.assign(window, {
  isDelivery, outLabel,
  useLocal, Pressable, Toast, Sheet, Confirm, Field, LongPressRow, RowContextMenu,
  fmtDateISO, fromISO, fmtDateDE, fmtDateLong, fmtRange, daysBetween, billableDays, todayISO, todayDE, tomorrowISO,
  DE_DAYS, DE_MONTHS, DE_MONTHS_SHORT,
  PROTOCOL_COLORS, EVENT_COLORS, EQUIPMENT_ICONS,
  fillTemplate, statusMeta, autoRentalStatus, depositMeta, autoDepositStatus, DEPOSIT_STEPS,
  paymentMeta, autoPaymentStatus, PAYMENT_STEPS, maintMeta, MAINT_TYPES,
  rangesOverlap, equipmentConflicts, buildReminders, eqStock,
  rentalTotal, rentalSubtotal, rentalDeliveryFee, rentalAccessoryCost, rentalQty, rentalIncomeTxs,
  autoRentalStatus, autoDepositStatus, autoPaymentStatus,
  getRentalItems, rentalEquipmentLabel,
  normalizeCustomerKey, findBlockedCustomer, upsertBlockedCustomer, removeBlockedCustomer,
});

// App wird NICHT hier gemountet, sondern vom letzten <script type="text/babel">
// in index.html — erst nachdem alle Screen-Dateien geladen sind UND die erste
// Server-Abfrage (window.RFDB.initPromise) erfolgreich war. Schlägt die
// Server-Verbindung fehl, wird stattdessen die Fehler-Ansicht gezeigt statt
// die App mit falschen/leeren Daten zu starten.
