You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

903 lines
34 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useEffect, useState, useRef } from "react";
function App() {
const API_BASE = process.env.REACT_APP_API_URL || "myday.samuelzielke.de";
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"
? `${API_BASE}/api/abwesenheit/${editAbwesenheitId}`
: `${API_BASE}/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(`${API_BASE}/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 = `${API_BASE}/api/${typ}/${id}`;
await fetch(url, { method: "DELETE" });
reloadData();
if (typ === "abwesenheit") setEditAbwesenheitId(null);
else setEditZeitslotId(null);
};
// Daten laden
useEffect(() => {
fetch(`${API_BASE}/api/data/${datum}`)
.then((res) => res.json())
.then((data) => {
setData(data);
setStartzeit(data.startzeit);
setEndzeit(data.endzeit);
});
}, [datum]);
const reloadData = () => {
fetch(`${API_BASE}/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(`${API_BASE}/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(`${API_BASE}/api/startzeit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ datum, zeit: manualStartzeit }),
});
setShowStartzeitInput(false);
reloadData();
};
return (
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
<div style={{ marginBottom: "1rem" }}>
<h1 style={{ fontSize: "2.5rem", fontWeight: "lighter", margin: 0 }}>
<span style={{ color: "#333" }}>My</span>
<span style={{ color: "#007acc" }}>Day</span>
</h1>
<div style={{ fontSize: "1.3rem", color: "#666" }}>
{new Date(datum).toLocaleDateString("de-DE", {
day: "2-digit",
month: "long",
year: "2-digit",
})}
</div>
</div>
<button onClick={() => {
const d = new Date(datum);
d.setDate(d.getDate() - 1);
setDatum(d.toISOString().split("T")[0]);
}} style={{ marginRight: "0.5rem" }}></button>
<input type="date" value={datum} onChange={(e) => setDatum(e.target.value)} />
<button onClick={() => {
const d = new Date(datum);
d.setDate(d.getDate() + 1);
setDatum(d.toISOString().split("T")[0]);
}} style={{ marginLeft: "0.5rem" }}></button>
{!showStartzeitInput ? (
<button onClick={beginSetStartzeit} style={{ marginLeft: "1rem" }}>
Startzeit setzen
</button>
) : (
<span style={{ marginLeft: "1rem" }}>
<input
type="time"
value={manualStartzeit}
onChange={(e) => setManualStartzeit(e.target.value)}
/>
<button onClick={saveManualStartzeit}>Speichern</button>
</span>
)}
{/* Endzeit Button und Eingabefeld */}
{!showEndzeitInput ? (
<button onClick={beginSetEndzeit} style={{ marginLeft: "1rem" }}>
Endzeit setzen
</button>
) : (
<span style={{ marginLeft: "1rem" }}>
<input
type="time"
value={manualEndzeit}
onChange={(e) => setManualEndzeit(e.target.value)}
/>
<button onClick={saveManualEndzeit}>Speichern</button>
</span>
)}
<button
onClick={() => {
const now = new Date();
now.setMinutes(Math.ceil(now.getMinutes() / 15) * 15);
const h = String(now.getHours()).padStart(2, "0");
const m = String(now.getMinutes()).padStart(2, "0");
const start = `${h}:${m}`;
const endDate = new Date(now.getTime() + 30 * 60000);
const eh = String(endDate.getHours()).padStart(2, "0");
const em = String(endDate.getMinutes()).padStart(2, "0");
const ende = `${eh}:${em}`;
setIsAbwesenheit(false);
setNewEntry({ titel: "", start, ende });
setShowForm(true);
}}
style={{ marginLeft: "1rem" }}
>
Eintrag hinzufügen
</button>
<div style={{ display: "grid", gridTemplateColumns: "1fr 60px 1fr", marginTop: "2rem" }}>
{/* Abwesenheiten */}
<div
style={{ padding: "1rem", borderRight: "1px solid #ccc", position: "relative" }}
onClick={(e) => {
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 (
<div
key={a.id}
data-entry
style={{ background: "#ffe5e5", padding: "0.5rem", margin: "0.5rem 0" }}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveEdit("abwesenheit");
if (e.key === "Escape") {
setEditAbwesenheitId(null);
setEditForm({ titel: "", start: "", ende: "" });
}
}}
>
<input
type="text"
value={editForm.titel}
onChange={(e) => setEditForm({ ...editForm, titel: e.target.value })}
/>
<br />
<input
type="time"
value={editForm.start}
onChange={(e) => setEditForm({ ...editForm, start: e.target.value })}
/>
<input
type="time"
value={editForm.ende}
onChange={(e) => setEditForm({ ...editForm, ende: e.target.value })}
/>
<br />
<button onClick={() => handleSaveEdit("abwesenheit")}>Speichern</button>
<button onClick={() => deleteEntry(a.id, "abwesenheit")}>Löschen</button>
</div>
);
}
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 (
<div
key={a.id}
data-entry
title={`${a.titel} (${a.start}${a.ende})`}
style={{
position: "absolute",
top: `${top}px`,
height: `${height}px`,
left: 0,
right: 0,
background: "#ffcccc",
boxShadow: "inset 0 0 0 2px #999",
boxSizing: "border-box",
padding: "0.25rem",
margin: "0.1rem",
cursor: "pointer",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontSize: height < 20 ? "0.6rem" : "0.8rem",
lineHeight: "1.2"
}}
onClick={() => startEdit(a, "abwesenheit")}
>
<strong>{a.titel}</strong> {a.start}{a.ende} ({height / pixelsPerMinute > 120
? `${(height / pixelsPerMinute / 60).toFixed(1)} Std`
: `${height / pixelsPerMinute} Min`})
</div>
);
})}
{/* 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 (
<div
ref={newEntryRef}
data-form
tabIndex={0}
onKeyDown={(e) => {
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
}}
>
<input
ref={newEntryInputRef}
autoFocus
type="text"
placeholder="Titel"
value={newEntry.titel}
onChange={(e) => setNewEntry({ ...newEntry, titel: e.target.value })}
/>
<br />
<input
type="time"
value={newEntry.start}
onChange={(e) => setNewEntry({ ...newEntry, start: e.target.value })}
/>
<input
type="time"
value={newEntry.ende}
onChange={(e) => setNewEntry({ ...newEntry, ende: e.target.value })}
/>
<br />
<button onClick={handleSaveNew}>Speichern</button>
</div>
);
})()}
</div>
{/* Zeitachse */}
<div
ref={zeitachseRef}
style={{
height: "1020px",
padding: "1rem",
borderLeft: "1px solid #aaa",
borderRight: "1px solid #aaa",
textAlign: "right",
position: "relative"
}}
>
{Array.from({ length: 17 }, (_, i) => {
const hour = i + 5;
return (
<div key={hour} style={{ height: "60px", fontSize: "12px", color: "#666" }}>
{hour}:00
</div>
);
})}
</div>
{/* Zeitslots */}
<div ref={zeitachseRef} style={{ padding: "1rem", position: "relative", height: "100%" }}>
{/* 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 (
<div
key={`sperre-${a.id}`}
style={{
position: "absolute",
top: `${top}px`,
height: `${height}px`,
left: 0,
right: 0,
background: "rgba(255, 200, 200, 0.4)",
pointerEvents: "none",
zIndex: 1,
}}
/>
);
})}
{(() => {
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 (
<div
style={{
position: "absolute",
top: `${top}px`,
left: 0,
right: 0,
height: "2px",
backgroundColor: "lightblue",
zIndex: 10
}}
/>
);
})()}
{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 (
<div
style={{
position: "absolute",
top: `${top}px`,
height: `${height}px`,
left: 0,
right: 0,
background: "#eee",
borderBottom: "2px solid red",
boxSizing: "border-box",
}}
/>
);
})()}
{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 (
<div
style={{
position: "absolute",
top: `${endzeitTop}px`,
height: `${height}px`,
left: 0,
right: 0,
background: "#eee",
borderTop: "2px solid red",
boxSizing: "border-box",
}}
/>
);
})()}
{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 [
<div
key={z.id}
style={{
position: "absolute",
top: `${top}px`,
height: `${height}px`,
minHeight: "60px",
left: 0,
right: 0,
background: editForm.farbe || "#e5ffe5",
boxShadow: "inset 0 0 0 2px #999",
boxSizing: "border-box",
padding: "0.25rem",
margin: "0.1rem",
fontSize: height < 20 ? "0.6rem" : "0.8rem",
lineHeight: "1.2",
overflow: "auto",
zIndex: 5
}}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveEdit("zeitslot");
if (e.key === "Escape") {
setEditZeitslotId(null);
setEditForm({ titel: "", start: "", ende: "" });
}
}}
>
<input
type="text"
value={editForm.titel}
onChange={(e) => setEditForm({ ...editForm, titel: e.target.value })}
/>
<br />
<input
type="time"
value={editForm.start}
onChange={(e) => setEditForm({ ...editForm, start: e.target.value })}
/>
<input
type="time"
value={editForm.ende}
onChange={(e) => setEditForm({ ...editForm, ende: e.target.value })}
/>
<div style={{ marginTop: "0.5rem" }}>
<button
onClick={() => setEditForm({ ...editForm, farbe: "#ccffcc" })}
style={{
background: z.farbe || "#ccffcc",
border: "1px solid #999",
marginRight: "0.5rem",
width: "20px",
height: "20px",
cursor: "pointer"
}}
/>
<button
onClick={() => setEditForm({ ...editForm, farbe: "#ccccff" })}
style={{
background: "#ccccff",
border: "1px solid #999",
width: "20px",
height: "20px",
cursor: "pointer"
}}
/>
</div>
<br />
<button onClick={() => handleSaveEdit("zeitslot")}>Speichern</button>
<button onClick={() => deleteEntry(z.id, "zeitslot")}>Löschen</button>
</div>
];
}
const entry = (
<div
key={z.id}
title={`${z.titel} (${z.start}${z.ende})`}
style={{
position: "absolute",
top: `${top}px`,
height: `${height}px`,
left: 0,
right: 0,
background: z.farbe || "#ccffcc",
boxShadow: "inset 0 0 0 2px #999",
boxSizing: "border-box",
padding: "0.25rem",
margin: "0.1rem",
cursor: "pointer",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontSize: height < 20 ? "0.6rem" : "0.8rem",
lineHeight: "1.2"
}}
onClick={() => startEdit(z, "zeitslot")}
>
<strong>{z.titel}</strong> {z.start}{z.ende} ({height / pixelsPerMinute > 120
? `${(height / pixelsPerMinute / 60).toFixed(1)} Std`
: `${height / pixelsPerMinute} Min`})
</div>
);
// 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 [
<div
key={`gap-start-${z.id}`}
style={{
position: "absolute",
top: `${gapTop}px`,
height: `${gapHeight}px`,
left: 0,
right: 0,
textAlign: "center",
fontSize: "0.6rem",
color: "#999",
pointerEvents: "none"
}}
>
{gapStart} Min frei
</div>,
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(
<div
key={`gap-before-first-${first.id}`}
onClick={() => {
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
</div>
);
}
}
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(
<div
key={`gap-combined-${curr.id}-${next.id}`}
onClick={() => {
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
</div>
);
}
}
// 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(
<div
key={`gap-after-last-${last.id}`}
onClick={() => {
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
</div>
);
}
}
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 (
<div
ref={newEntryRef}
tabIndex={0}
onKeyDown={(e) => {
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
}}
>
<input
ref={newEntryInputRef}
autoFocus
type="text"
placeholder="Titel"
value={newEntry.titel}
onChange={(e) => setNewEntry({ ...newEntry, titel: e.target.value })}
/>
<br />
<input
type="time"
value={newEntry.start}
onChange={(e) => setNewEntry({ ...newEntry, start: e.target.value })}
/>
<input
type="time"
value={newEntry.ende}
onChange={(e) => setNewEntry({ ...newEntry, ende: e.target.value })}
/>
<div style={{ marginTop: "0.5rem" }}>
<button
onClick={() => setNewEntry({ ...newEntry, farbe: "#ccffcc" })}
style={{
background: "#ccffcc",
border: "1px solid #999",
marginRight: "0.5rem",
width: "20px",
height: "20px",
cursor: "pointer"
}}
/>
<button
onClick={() => setNewEntry({ ...newEntry, farbe: "#ccccff" })}
style={{
background: "#ccccff",
border: "1px solid #999",
width: "20px",
height: "20px",
cursor: "pointer"
}}
/>
</div>
<br />
<button
onClick={handleSaveNew}
>
Speichern
</button>
</div>
);
})()}
</div>
</div>
</div>
);
}
export default App;

Powered by TurnKey Linux.