206 lines
8.4 KiB
JavaScript
206 lines
8.4 KiB
JavaScript
(function () {
|
|
const loginView = document.getElementById("loginView");
|
|
const adminView = document.getElementById("adminView");
|
|
const loginForm = document.getElementById("loginForm");
|
|
const passwordInput = document.getElementById("passwordInput");
|
|
const loginMessage = document.getElementById("loginMessage");
|
|
const addDateMessage = document.getElementById("addDateMessage");
|
|
const datesList = document.getElementById("datesList");
|
|
const dateCount = document.getElementById("dateCount");
|
|
const emptyDates = document.getElementById("emptyDates");
|
|
const logoutBtn = document.getElementById("logoutBtn");
|
|
const monthLabel = document.getElementById("monthLabel");
|
|
const weekdayRow = document.getElementById("weekdayRow");
|
|
const dayGrid = document.getElementById("dayGrid");
|
|
const prevMonthBtn = document.getElementById("prevMonthBtn");
|
|
const nextMonthBtn = document.getElementById("nextMonthBtn");
|
|
const saveDatesBtn = document.getElementById("saveDatesBtn");
|
|
const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
|
const months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
let dates = [];
|
|
let bookedDates = new Set();
|
|
let draftDates = new Map();
|
|
let viewYear = today.getFullYear();
|
|
let viewMonth = today.getMonth();
|
|
|
|
function setMessage(element, message, success) {
|
|
element.textContent = message;
|
|
element.classList.toggle("is-success", Boolean(success));
|
|
}
|
|
|
|
function toDateKey(date) {
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
}
|
|
|
|
function formatDate(date) {
|
|
return new Date(`${date}T00:00:00`).toLocaleDateString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit", year: "numeric" });
|
|
}
|
|
|
|
async function request(path, options) {
|
|
const response = await fetch(path, { ...options, headers: { "Content-Type": "application/json", ...(options && options.headers) } });
|
|
const data = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const error = new Error(data.message || "Die Anfrage ist fehlgeschlagen.");
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function showLogin(message) {
|
|
adminView.classList.add("hidden");
|
|
loginView.classList.remove("hidden");
|
|
if (message) setMessage(loginMessage, message, false);
|
|
passwordInput.focus();
|
|
}
|
|
|
|
function renderDates() {
|
|
const plannedDates = Array.from(draftDates, ([date, type]) => ({ date, type })).sort((a, b) => a.date.localeCompare(b.date));
|
|
datesList.innerHTML = "";
|
|
dateCount.textContent = `${plannedDates.length} ${plannedDates.length === 1 ? "Termin" : "Termine"}`;
|
|
emptyDates.classList.toggle("hidden", plannedDates.length !== 0);
|
|
plannedDates.forEach((date) => {
|
|
const row = document.createElement("li");
|
|
row.className = "date-row";
|
|
const label = document.createElement("span");
|
|
label.className = "date-label";
|
|
label.textContent = formatDate(date.date);
|
|
const typeLabel = document.createElement("span");
|
|
typeLabel.className = "date-type-label";
|
|
typeLabel.textContent = date.type === "short" ? "Kurz" : "Lang";
|
|
const deleteButton = document.createElement("button");
|
|
deleteButton.className = "delete-date-btn";
|
|
deleteButton.type = "button";
|
|
deleteButton.textContent = "Entfernen";
|
|
if (bookedDates.has(date.date)) {
|
|
deleteButton.disabled = true;
|
|
deleteButton.classList.add("is-booked");
|
|
deleteButton.title = "Gebuchte Termine können nicht entfernt werden.";
|
|
} else {
|
|
deleteButton.addEventListener("click", () => {
|
|
draftDates.delete(date.date);
|
|
renderCalendar();
|
|
renderDates();
|
|
setMessage(addDateMessage, "Änderung vorgemerkt. Noch nicht gespeichert.", false);
|
|
});
|
|
}
|
|
row.append(label, typeLabel, deleteButton);
|
|
datesList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function renderCalendar() {
|
|
monthLabel.textContent = `${months[viewMonth]} ${viewYear}`;
|
|
weekdayRow.innerHTML = "";
|
|
weekdays.forEach((day) => {
|
|
const span = document.createElement("span");
|
|
span.textContent = day;
|
|
weekdayRow.appendChild(span);
|
|
});
|
|
dayGrid.innerHTML = "";
|
|
const firstOfMonth = new Date(viewYear, viewMonth, 1);
|
|
const leadingBlanks = (firstOfMonth.getDay() + 6) % 7;
|
|
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
for (let index = 0; index < leadingBlanks; index += 1) {
|
|
const blank = document.createElement("span");
|
|
blank.className = "day-cell is-empty";
|
|
dayGrid.appendChild(blank);
|
|
}
|
|
for (let day = 1; day <= daysInMonth; day += 1) {
|
|
const cellDate = new Date(viewYear, viewMonth, day);
|
|
const key = toDateKey(cellDate);
|
|
const type = draftDates.get(key);
|
|
const isBooked = bookedDates.has(key);
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "day-cell";
|
|
button.textContent = String(day);
|
|
if (cellDate <= today) {
|
|
button.disabled = true;
|
|
button.classList.add("is-disabled");
|
|
} else if (isBooked) {
|
|
button.disabled = true;
|
|
button.classList.add("is-booked");
|
|
} else if (type) {
|
|
button.classList.add(type === "short" ? "is-short" : "is-long");
|
|
}
|
|
if (cellDate.getTime() === today.getTime()) button.classList.add("is-today");
|
|
button.setAttribute("aria-label", `${formatDate(key)}${isBooked ? ", gebucht" : type ? `, ${type === "short" ? "kurz" : "lang"}` : ", nicht verfügbar"}`);
|
|
if (!isBooked && cellDate > today) button.addEventListener("click", () => {
|
|
const nextType = type === undefined ? "short" : type === "short" ? "long" : undefined;
|
|
if (nextType) draftDates.set(key, nextType); else draftDates.delete(key);
|
|
renderCalendar();
|
|
renderDates();
|
|
setMessage(addDateMessage, "Änderung vorgemerkt. Noch nicht gespeichert.", false);
|
|
});
|
|
dayGrid.appendChild(button);
|
|
}
|
|
prevMonthBtn.disabled = viewYear === today.getFullYear() && viewMonth === today.getMonth();
|
|
}
|
|
|
|
async function loadDates() {
|
|
try {
|
|
const [loadedDates, loadedBookedDates] = await Promise.all([
|
|
request("/api/admin/dates", { method: "GET" }),
|
|
request(`/api/booked-dates?t=${Date.now()}`, { method: "GET" }),
|
|
]);
|
|
dates = loadedDates;
|
|
bookedDates = new Set((loadedBookedDates || []).filter((date) => typeof date === "string"));
|
|
dates.sort((a, b) => a.date.localeCompare(b.date));
|
|
draftDates = new Map(dates.map((date) => [date.date, date.type]));
|
|
loginView.classList.add("hidden");
|
|
adminView.classList.remove("hidden");
|
|
renderCalendar();
|
|
renderDates();
|
|
} catch (error) {
|
|
if (error.status === 401) showLogin();
|
|
else showLogin("Die Termine konnten nicht geladen werden.");
|
|
}
|
|
}
|
|
|
|
loginForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
setMessage(loginMessage, "", false);
|
|
try {
|
|
await request("/api/admin/login", { method: "POST", body: JSON.stringify({ password: passwordInput.value }) });
|
|
passwordInput.value = "";
|
|
await loadDates();
|
|
} catch (error) {
|
|
setMessage(loginMessage, error.message, false);
|
|
}
|
|
});
|
|
|
|
saveDatesBtn.addEventListener("click", async () => {
|
|
saveDatesBtn.disabled = true;
|
|
try {
|
|
await request("/api/admin/dates/bulk", { method: "POST", body: JSON.stringify(Array.from(draftDates, ([date, type]) => ({ date, type }))) });
|
|
dates = Array.from(draftDates, ([date, type]) => ({ date, type }));
|
|
renderDates();
|
|
setMessage(addDateMessage, "Änderungen gespeichert.", true);
|
|
} catch (error) {
|
|
if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen.");
|
|
else setMessage(addDateMessage, error.message, false);
|
|
} finally {
|
|
saveDatesBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
prevMonthBtn.addEventListener("click", () => {
|
|
viewMonth -= 1;
|
|
if (viewMonth < 0) { viewMonth = 11; viewYear -= 1; }
|
|
renderCalendar();
|
|
});
|
|
nextMonthBtn.addEventListener("click", () => {
|
|
viewMonth += 1;
|
|
if (viewMonth > 11) { viewMonth = 0; viewYear += 1; }
|
|
renderCalendar();
|
|
});
|
|
logoutBtn.addEventListener("click", async () => {
|
|
try { await request("/api/admin/logout", { method: "POST" }); } finally { showLogin(); }
|
|
});
|
|
|
|
loadDates();
|
|
})();
|