(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(); })();