const { useState, useEffect, useCallback, useRef } = React; const { createRoot } = ReactDOM; const { Bus, Clock, MapPin, LogOut, Plus, Check, Unlock, ArrowLeft, ChevronUp, ChevronDown, Pencil, Trash2, Save, StickyNote, Building2, Phone, RefreshCw, CalendarDays, List, Sun, Moon, Clapperboard, } = Icons; const API = "storage.php"; const ITEMS_POLL_MS = 6000; // "alle paar Sekunden" – Ablauf eines offenen Stops const STOPS_POLL_MS = 15000; // Liste aller Stops (neue Termine etc.) // ---------- API-Helfer ---------- function headers(withEdit) { const h = { "Content-Type": "application/json", "X-View-Pin": window.pinStorage.getView() }; if (withEdit) h["X-Edit-Pin"] = window.pinStorage.getEdit(); return h; } async function apiGet(action, params = {}) { const qs = new URLSearchParams({ action, ...params }).toString(); const r = await fetch(`${API}?${qs}`, { headers: headers(false) }); if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || "Fehler beim Laden"); return r.json(); } async function apiPost(body, withEdit = false) { const r = await fetch(API, { method: "POST", headers: headers(withEdit), body: JSON.stringify(body) }); if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || "Fehler beim Speichern"); return r.json(); } async function login(pin) { const r = await fetch(API, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "login", pin }) }); return r.json(); } // ---------- Datum-Helfer ---------- const WEEKDAYS = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; const MONTHS = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; function parseDate(dateStr) { const [y, m, d] = dateStr.split("-").map(Number); return new Date(y, m - 1, d); } function dateBadge(dateStr) { const d = parseDate(dateStr); return { day: String(d.getDate()).padStart(2, "0"), month: MONTHS[d.getMonth()] }; } function headerDate(dateStr) { const d = parseDate(dateStr); return `${WEEKDAYS[d.getDay()]}, ${String(d.getDate()).padStart(2, "0")}. ${MONTHS[d.getMonth()]} ${d.getFullYear()}`; } function todayISO() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } // Einträge ohne Startzeit werden ans Ende sortiert; bei gleicher Zeit bleibt // die bisherige Reihenfolge erhalten (stabile Sortierung) – so wirkt sich // ein manuelles Verschieben per Pfeil bei Zeitgleichheit weiterhin aus. function sortByTime(items) { return [...items].sort((a, b) => { const at = a.startTime || "99:99"; const bt = b.startTime || "99:99"; return at.localeCompare(bt); }); } // ---------- URL-Deeplink (?date=YYYY-MM-DD) ---------- function getUrlDate() { return new URLSearchParams(window.location.search).get("date") || ""; } function setUrlDate(date) { const url = new URL(window.location.href); if (date) url.searchParams.set("date", date); else url.searchParams.delete("date"); window.history.replaceState({}, "", url.toString()); } // ---------- Theme ---------- function getStoredTheme() { return localStorage.getItem("hope-ablauf-theme") || "dark"; } function setStoredTheme(t) { localStorage.setItem("hope-ablauf-theme", t); } // ---------- PIN-Screen (einziger Ort, an dem der Bearbeitungsmodus vergeben wird) ---------- function PinScreen({ onSuccess }) { const [pin, setPin] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const submit = async () => { if (!pin) return; setBusy(true); setError(""); try { const res = await login(pin); if (res.view) { window.pinStorage.setView(pin); if (res.edit) window.pinStorage.setEdit(pin); onSuccess(!!res.edit); } else { setError("Falscher PIN."); } } catch { setError("Verbindung fehlgeschlagen."); } finally { setBusy(false); } }; return (

hope Ablauf

Tagesablauf der Tournee 2027 — bitte PIN eingeben

setPin(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submit()} autoFocus /> {error &&

{error}

}

Der Bearbeiten-PIN schaltet direkt beim Login den Bearbeitungsmodus frei — danach nur noch über Abmelden & neu einloggen wechselbar.

); } // ---------- Topbar ---------- function Topbar({ title, sub, onBack, isEditor, theme, onToggleTheme, onLogout }) { return (
{onBack ? ( ) : (
)}
{title} {sub && {sub}}
{isEditor && Bearbeiten}
); } // ---------- Stop-Liste ---------- function StopList({ stops, onSelect }) { if (!stops.length) return

Noch keine Tourstopps vorhanden.

; return (
{stops.map((s) => { const b = dateBadge(s.date); const isToday = s.date === todayISO(); return ( ); })}
); } // ---------- Schedule-Item-Sheet ---------- function ItemSheet({ initial, onClose, onSave }) { const [title, setTitle] = useState(initial?.title || ""); const [startTime, setStartTime] = useState(initial?.startTime || ""); const [endTime, setEndTime] = useState(initial?.endTime || ""); const [iconType, setIconType] = useState(initial?.iconType || "clock"); const [note, setNote] = useState(initial?.note || ""); const [tags, setTags] = useState(initial?.tags || ""); const save = () => { if (!title.trim()) return; onSave({ ...initial, title: title.trim(), startTime, endTime, iconType, note, tags }); onClose(); }; return (
e.stopPropagation()}>

{initial?.id ? "Eintrag bearbeiten" : "Neuer Eintrag"}

setTitle(e.target.value)} autoFocus />
setStartTime(e.target.value)} />
setEndTime(e.target.value)} />