import React, { useEffect, useState, useRef } from "react"; function App() { const [datum, setDatum] = useState("2025-05-14"); const [data, setData] = useState({ abwesenheiten: [], zeitslots: [] }); const [startzeit, setStartzeit] = useState(null); const [endzeit, setEndzeit] = useState(null); const [manualEndzeit, setManualEndzeit] = useState(""); const [showEndzeitInput, setShowEndzeitInput] = useState(false); // Gemeinsames Formular für neue Einträge const [showForm, setShowForm] = useState(false); const [isAbwesenheit, setIsAbwesenheit] = useState(true); const [newEntry, setNewEntry] = useState({ titel: "", start: "", ende: "", farbe: "#ccffcc" }); // Ref für das neue Eintragsformular (Hülle) const newEntryRef = useRef(null); // Ref für das Texteingabefeld im neuen Eintrag const newEntryInputRef = useRef(null); // Fokussieren des neuen Eintragsformulars und Scrollen bei Anzeige useEffect(() => { if (showForm && newEntryInputRef.current) { newEntryInputRef.current.focus(); newEntryInputRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); } }, [showForm]); const [editAbwesenheitId, setEditAbwesenheitId] = useState(null); const [editZeitslotId, setEditZeitslotId] = useState(null); const [editForm, setEditForm] = useState({ titel: "", start: "", ende: "" }); const zeitachseRef = useRef(null); const [pixelsPerMinute, setPixelsPerMinute] = useState(1); const [manualStartzeit, setManualStartzeit] = useState(""); const [showStartzeitInput, setShowStartzeitInput] = useState(false); useEffect(() => { setPixelsPerMinute(1); }, []); const startEdit = (eintrag, typ) => { setEditForm({ titel: eintrag.titel, start: eintrag.start, ende: eintrag.ende }); if (typ === "abwesenheit") setEditAbwesenheitId(eintrag.id); if (typ === "zeitslot") setEditZeitslotId(eintrag.id); }; // Speichern-Funktionen für Edit und Neu const handleSaveEdit = async (typ) => { const url = typ === "abwesenheit" ? `http://localhost:3001/api/abwesenheit/${editAbwesenheitId}` : `http://localhost:3001/api/zeitslot/${editZeitslotId}`; await fetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(editForm), }); setEditAbwesenheitId(null); setEditZeitslotId(null); setEditForm({ titel: "", start: "", ende: "" }); reloadData(); }; const handleSaveNew = async () => { const endpoint = isAbwesenheit ? "abwesenheit" : "zeitslot"; await fetch(`http://localhost:3001/api/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ datum, ...newEntry }), }); setNewEntry({ titel: "", start: "", ende: "", farbe: "#ccffcc" }); setShowForm(false); reloadData(); }; const deleteEntry = async (id, typ) => { const url = `http://localhost:3001/api/${typ}/${id}`; await fetch(url, { method: "DELETE" }); reloadData(); if (typ === "abwesenheit") setEditAbwesenheitId(null); else setEditZeitslotId(null); }; // Daten laden useEffect(() => { fetch(`http://localhost:3001/api/data/${datum}`) .then((res) => res.json()) .then((data) => { setData(data); setStartzeit(data.startzeit); setEndzeit(data.endzeit); }); }, [datum]); const reloadData = () => { fetch(`http://localhost:3001/api/data/${datum}`) .then((res) => res.json()) .then((data) => { setData(data); setStartzeit(data.startzeit); setEndzeit(data.endzeit); }); }; const beginSetEndzeit = () => { const now = new Date(); const h = String(now.getHours()).padStart(2, "0"); const m = String(Math.floor(now.getMinutes() / 15) * 15).padStart(2, "0"); setManualEndzeit(`${h}:${m}`); setShowEndzeitInput(true); }; const saveManualEndzeit = async () => { await fetch("http://localhost:3001/api/endzeit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ datum, zeit: manualEndzeit }), }); setShowEndzeitInput(false); reloadData(); }; const beginSetStartzeit = () => { const now = new Date(); const h = String(now.getHours()).padStart(2, "0"); const m = String(Math.floor(now.getMinutes() / 15) * 15).padStart(2, "0"); setManualStartzeit(`${h}:${m}`); setShowStartzeitInput(true); }; const saveManualStartzeit = async () => { await fetch("http://localhost:3001/api/startzeit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ datum, zeit: manualStartzeit }), }); setShowStartzeitInput(false); reloadData(); }; return (

My Day

{new Date(datum).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "2-digit", })}
setDatum(e.target.value)} /> {!showStartzeitInput ? ( ) : ( setManualStartzeit(e.target.value)} /> )} {/* Endzeit Button und Eingabefeld */} {!showEndzeitInput ? ( ) : ( setManualEndzeit(e.target.value)} /> )}
{/* Abwesenheiten */}
{ if (e.target.closest('[data-entry]') || e.target.closest('[data-form]')) return; const containerTop = e.currentTarget.getBoundingClientRect().top; const clickY = e.clientY - containerTop; const rawMin = Math.floor(clickY / pixelsPerMinute + 300 - 24); const clickedMin = Math.round(rawMin / 15) * 15; const startH = String(Math.floor(clickedMin / 60)).padStart(2, "0"); const startM = String(clickedMin % 60).padStart(2, "0"); const endMin = clickedMin + 30; const endH = String(Math.floor(endMin / 60)).padStart(2, "0"); const endM = String(endMin % 60).padStart(2, "0"); setIsAbwesenheit(true); setNewEntry({ titel: "", start: `${startH}:${startM}`, ende: `${endH}:${endM}` }); setShowForm(true); }} > {data.abwesenheiten.map((a) => { const startParts = a.start.split(":"); const startMin = parseInt(startParts[0]) * 60 + parseInt(startParts[1]); if (editAbwesenheitId === a.id) { return (
{ if (e.key === "Enter") handleSaveEdit("abwesenheit"); if (e.key === "Escape") { setEditAbwesenheitId(null); setEditForm({ titel: "", start: "", ende: "" }); } }} > setEditForm({ ...editForm, titel: e.target.value })} />
setEditForm({ ...editForm, start: e.target.value })} /> setEditForm({ ...editForm, ende: e.target.value })} />
); } const endParts = a.ende.split(":"); const endMin = parseInt(endParts[0]) * 60 + parseInt(endParts[1]); const top = (startMin - 300 + 24) * pixelsPerMinute; // 5:00 = 300 min, +24 min Offset const height = (endMin - startMin) * pixelsPerMinute; return (
startEdit(a, "abwesenheit")} > {a.titel} {a.start}–{a.ende} ({height / pixelsPerMinute > 120 ? `${(height / pixelsPerMinute / 60).toFixed(1)} Std` : `${height / pixelsPerMinute} Min`})
); })} {/* Formular für neuen Eintrag in Abwesenheiten-Spalte anzeigen, nur wenn isAbwesenheit === true */} {showForm && isAbwesenheit && newEntry.start && (() => { const startParts = newEntry.start.split(":"); const startMin = parseInt(startParts[0]) * 60 + parseInt(startParts[1]); const top = (startMin - 300 + 24) * pixelsPerMinute; return (
{ if (e.key === "Enter") { handleSaveNew(); } if (e.key === "Escape") { setShowForm(false); setNewEntry({ titel: "", start: "", ende: "" }); } }} style={{ position: "absolute", top: `${top}px`, height: "60px", minHeight: "60px", left: 0, right: 0, background: "#ffe5e5", boxShadow: "inset 0 0 0 2px #999", boxSizing: "border-box", padding: "0.25rem", zIndex: 5 }} > setNewEntry({ ...newEntry, titel: e.target.value })} />
setNewEntry({ ...newEntry, start: e.target.value })} /> setNewEntry({ ...newEntry, ende: e.target.value })} />
); })()}
{/* Zeitachse */}
{Array.from({ length: 17 }, (_, i) => { const hour = i + 5; return (
{hour}:00
); })}
{/* Zeitslots */}
{/* Abwesenheiten als Sperrflächen */} {data.abwesenheiten.map((a) => { const startParts = a.start.split(":"); const endParts = a.ende.split(":"); const startMin = parseInt(startParts[0]) * 60 + parseInt(startParts[1]); const endMin = parseInt(endParts[0]) * 60 + parseInt(endParts[1]); const top = (startMin - 300 + 24) * pixelsPerMinute; const height = (endMin - startMin) * pixelsPerMinute; return (
); })} {(() => { const today = new Date().toISOString().split("T")[0]; if (datum !== today) return null; const now = new Date(); const nowMin = now.getHours() * 60 + now.getMinutes(); if (nowMin < 300 || nowMin > 1260) return null; // außerhalb des sichtbaren Zeitbereichs const top = (nowMin - 300 + 24) * pixelsPerMinute; return (
); })()} {startzeit && (() => { const startzeitMin = parseInt(startzeit.split(":")[0]) * 60 + parseInt(startzeit.split(":")[1]); const startzeitTop = (startzeitMin - 300 + 24) * pixelsPerMinute; const top = 0; const height = startzeitTop; return (
); })()} {endzeit && (() => { const endzeitMin = parseInt(endzeit.split(":")[0]) * 60 + parseInt(endzeit.split(":")[1]); const endzeitTop = (endzeitMin - 300 + 24) * pixelsPerMinute; const bottomMin = 1260; // 21:00 Uhr const height = (bottomMin - endzeitMin) * pixelsPerMinute; return (
); })()} {data.zeitslots .sort((a, b) => { const aStart = parseInt(a.start.split(":")[0]) * 60 + parseInt(a.start.split(":")[1]); const bStart = parseInt(b.start.split(":")[0]) * 60 + parseInt(b.start.split(":")[1]); return aStart - bStart; }) .flatMap((z, index, arr) => { const startParts = z.start.split(":"); const endParts = z.ende.split(":"); const startMin = parseInt(startParts[0]) * 60 + parseInt(startParts[1]); const endMin = parseInt(endParts[0]) * 60 + parseInt(endParts[1]); const startTotalMin = parseInt(startzeit?.split(":")[0]) * 60 + parseInt(startzeit?.split(":")[1]); const endTotalMin = parseInt(endzeit?.split(":")[0]) * 60 + parseInt(endzeit?.split(":")[1]); if (startMin < startTotalMin || endMin > endTotalMin) return []; const top = (startMin - 300 + 24) * pixelsPerMinute; const height = (endMin - startMin) * pixelsPerMinute; if (editZeitslotId === z.id) { // Edit-Modus: Formular an exakter Slot-Position anzeigen return [
{ if (e.key === "Enter") handleSaveEdit("zeitslot"); if (e.key === "Escape") { setEditZeitslotId(null); setEditForm({ titel: "", start: "", ende: "" }); } }} > setEditForm({ ...editForm, titel: e.target.value })} />
setEditForm({ ...editForm, start: e.target.value })} /> setEditForm({ ...editForm, ende: e.target.value })} />

]; } const entry = (
startEdit(z, "zeitslot")} > {z.titel} {z.start}–{z.ende} ({height / pixelsPerMinute > 120 ? `${(height / pixelsPerMinute / 60).toFixed(1)} Std` : `${height / pixelsPerMinute} Min`})
); // Block für freie Zeit zwischen Startzeit und erstem Zeitslot if (index === 0 && startzeit) { const startzeitMin = parseInt(startzeit.split(":")[0]) * 60 + parseInt(startzeit.split(":")[1]); const gapStart = startMin - startzeitMin; if (gapStart > 0) { const gapTop = (startzeitMin - 300 + 24) * pixelsPerMinute; const gapHeight = gapStart * pixelsPerMinute; return [
{gapStart} Min frei
, entry ]; } } if (index === 0) return [entry]; return [entry]; })} {/* Freie Zeitblöcke zwischen Sperrbereichen und Zeitslots */} {(() => { const combined = [ ...data.abwesenheiten.map((a) => ({ id: a.id, start: a.start, ende: a.ende, typ: "sperre" })), ...data.zeitslots.map((z) => ({ id: z.id, start: z.start, ende: z.ende, typ: "zeitslot" })) ]; const startzeitMin = parseInt(startzeit?.split(":")[0]) * 60 + parseInt(startzeit?.split(":")[1]); const endzeitMin = parseInt(endzeit?.split(":")[0]) * 60 + parseInt(endzeit?.split(":")[1]); const sorted = combined .map((e) => ({ ...e, startMin: parseInt(e.start.split(":")[0]) * 60 + parseInt(e.start.split(":")[1]), endMin: parseInt(e.ende.split(":")[0]) * 60 + parseInt(e.ende.split(":")[1]) })) .filter((e) => e.startMin >= startzeitMin && e.endMin <= endzeitMin) .sort((a, b) => a.startMin - b.startMin); const gaps = []; // Lücke zwischen Startzeit und erstem Eintrag anzeigen if (sorted.length > 0 && startzeitMin < sorted[0].startMin) { const first = sorted[0]; const gap = first.startMin - startzeitMin; if (gap > 0) { const gapTop = (startzeitMin - 300 + 24) * pixelsPerMinute; const gapHeight = gap * pixelsPerMinute; gaps.push(
{ setIsAbwesenheit(false); setShowForm(true); const startStunde = String(Math.floor(startzeitMin / 60)).padStart(2, "0"); const startMinute = String(startzeitMin % 60).padStart(2, "0"); const endStunde = String(Math.floor(first.startMin / 60)).padStart(2, "0"); const endMinute = String(first.startMin % 60).padStart(2, "0"); setNewEntry({ titel: "", start: `${startStunde}:${startMinute}`, ende: `${endStunde}:${endMinute}` }); }} style={{ position: "absolute", top: `${gapTop}px`, height: `${gapHeight}px`, left: 0, right: 0, textAlign: "center", fontSize: "0.6rem", color: "#999", pointerEvents: "auto", zIndex: 2, cursor: "pointer", background: "rgba(200, 255, 200, 0.05)" }} > {gap} Min frei
); } } for (let i = 0; i < sorted.length - 1; i++) { const curr = sorted[i]; const next = sorted[i + 1]; const gap = next.startMin - curr.endMin; if (gap > 0) { const gapTop = (curr.endMin - 300 + 24) * pixelsPerMinute; const gapHeight = gap * pixelsPerMinute; gaps.push(
{ setIsAbwesenheit(false); setShowForm(true); const startStunde = Math.floor(curr.endMin / 60).toString().padStart(2, "0"); const startMinute = (curr.endMin % 60).toString().padStart(2, "0"); const endStunde = Math.floor(next.startMin / 60).toString().padStart(2, "0"); const endMinute = (next.startMin % 60).toString().padStart(2, "0"); setNewEntry({ titel: "", start: `${startStunde}:${startMinute}`, ende: `${endStunde}:${endMinute}` }); }} style={{ position: "absolute", top: `${gapTop}px`, height: `${gapHeight}px`, left: 0, right: 0, textAlign: "center", fontSize: "0.6rem", color: "#999", pointerEvents: "auto", zIndex: 2, cursor: "pointer", background: "rgba(200, 255, 200, 0.05)" }} > {gap} Min frei
); } } // Lücke nach dem letzten Eintrag bis zur Endzeit anzeigen if (sorted.length > 0 && endzeitMin) { const last = sorted[sorted.length - 1]; const gap = endzeitMin - last.endMin; if (gap > 0) { const gapTop = (last.endMin - 300 + 24) * pixelsPerMinute; const gapHeight = gap * pixelsPerMinute; gaps.push(
{ setIsAbwesenheit(false); setShowForm(true); const startStunde = Math.floor(last.endMin / 60).toString().padStart(2, "0"); const startMinute = (last.endMin % 60).toString().padStart(2, "0"); const endStunde = Math.floor(endzeitMin / 60).toString().padStart(2, "0"); const endMinute = (endzeitMin % 60).toString().padStart(2, "0"); setNewEntry({ titel: "", start: `${startStunde}:${startMinute}`, ende: `${endStunde}:${endMinute}` }); }} style={{ position: "absolute", top: `${gapTop}px`, height: `${gapHeight}px`, left: 0, right: 0, textAlign: "center", fontSize: "0.6rem", color: "#999", pointerEvents: "auto", zIndex: 2, cursor: "pointer", background: "rgba(200, 255, 200, 0.05)" }} > {gap} Min frei
); } } return gaps; })()} {/* Formular für neuen Eintrag an passender Stelle anzeigen (nur wenn isAbwesenheit === false) */} {showForm && !isAbwesenheit && newEntry.start && (() => { const startParts = newEntry.start.split(":"); const startMin = parseInt(startParts[0]) * 60 + parseInt(startParts[1]); const top = (startMin - 300 + 24) * pixelsPerMinute; return (
{ if (e.key === "Enter") { handleSaveNew(); } if (e.key === "Escape") { setShowForm(false); setNewEntry({ titel: "", start: "", ende: "", farbe: "#ccffcc" }); } }} style={{ position: "absolute", top: `${top}px`, height: "60px", minHeight: "60px", left: 0, right: 0, // background: "#e0f7ff", background: newEntry.farbe || "#e0f7ff", boxShadow: "inset 0 0 0 2px #999", boxSizing: "border-box", padding: "0.25rem", zIndex: 5 }} > setNewEntry({ ...newEntry, titel: e.target.value })} />
setNewEntry({ ...newEntry, start: e.target.value })} /> setNewEntry({ ...newEntry, ende: e.target.value })} />

); })()}
); } export default App;