|
|
|
@ -0,0 +1,902 @@
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
|
|
|
<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;
|