(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 activitiesList = document.getElementById("activitiesList"); const emptyActivities = document.getElementById("emptyActivities"); const activityMessage = document.getElementById("activityMessage"); const activityAddRow = document.getElementById("activityAddRow"); const bookingsList = document.getElementById("bookingsList"); const emptyBookings = document.getElementById("emptyBookings"); const newActivityIcon = document.getElementById("newActivityIcon"); const newActivityName = document.getElementById("newActivityName"); const newActivityType = document.getElementById("newActivityType"); const activityIcons = ["🧺", "🍸", "💆", "🥾", "🎬", "🍣", "🎨", "🎢", "🌲", "🍒", "🍜", "🏊", "🛋️", "🏸", "🍛", "🖼️", "🎮", "✏️", "🍕", "🍔", "☕", "🍰", "🍽️", "🎵", "🎤", "🎭", "📚", "🧩", "🎲", "🚲", "🚗", "✈️", "🏖️", "⛺", "🌅", "🌸", "🐶", "🐱", "📷", "🎸", "🧘", "🏋️", "⚽", "🎳", "🛍️", "💌", "❤️", "⭐", "🌈"]; 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(); let activities = []; let bookings = []; 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(); } function renderBookings() { bookingsList.innerHTML = ""; emptyBookings.classList.toggle("hidden", bookings.length > 0); bookings.forEach((booking) => { const row = document.createElement("li"); row.className = "booking-row"; const date = document.createElement("strong"); date.className = "booking-date"; date.textContent = formatDate(booking.date); row.appendChild(date); if (booking.activities && booking.activities.length > 0) { const activityList = document.createElement("div"); activityList.className = "booking-activities"; booking.activities.forEach((activity) => { const activityItem = document.createElement("span"); activityItem.className = "booking-activity"; activityItem.textContent = `${activity.icon || ""} ${activity.name}`.trim(); activityList.appendChild(activityItem); }); row.appendChild(activityList); } const metadata = document.createElement("div"); metadata.className = "booking-metadata"; if (booking.name && booking.name.trim()) { const name = document.createElement("span"); name.textContent = booking.name; metadata.appendChild(name); } if (booking.bookedAt) { const created = document.createElement("time"); created.dateTime = booking.bookedAt; created.textContent = `Gebucht am ${new Date(booking.bookedAt).toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" })}`; metadata.appendChild(created); } if (metadata.childElementCount > 0) row.appendChild(metadata); const deleteButton = document.createElement("button"); deleteButton.className = "delete-booking-btn"; deleteButton.type = "button"; deleteButton.textContent = "Termin löschen"; deleteButton.addEventListener("click", () => deleteBooking(booking, deleteButton)); row.appendChild(deleteButton); bookingsList.appendChild(row); }); } async function deleteBooking(booking, deleteButton) { if (!window.confirm(`Den gebuchten Termin am ${formatDate(booking.date)} wirklich löschen?`)) return; deleteButton.disabled = true; try { await request("/api/admin/bookings", { method: "DELETE", body: JSON.stringify({ date: booking.date }) }); bookedDates.delete(booking.date); await loadBookings(); renderCalendar(); renderDates(); } catch (error) { deleteButton.disabled = false; if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen."); else window.alert(error.message); } } async function loadBookings() { bookings = await request("/api/admin/bookings", { method: "GET" }); renderBookings(); } 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"); await Promise.all([loadActivities(), loadBookings()]); renderCalendar(); renderDates(); } catch (error) { if (error.status === 401) showLogin(); else showLogin("Die Termine konnten nicht geladen werden."); } } function renderActivities() { activitiesList.innerHTML = ""; emptyActivities.classList.toggle("hidden", activities.length > 0); const orderedActivities = [...activities].sort((first, second) => { return (first.type === "custom") - (second.type === "custom"); }); orderedActivities.forEach((activity) => { const row = document.createElement("li"); row.className = `activity-row${activity.disabled ? " is-disabled" : ""}`; const identity = document.createElement("div"); identity.className = "activity-identity"; const icon = document.createElement("button"); icon.className = "activity-row-icon"; icon.type = "button"; icon.textContent = activity.icon; icon.title = "Icon ändern"; icon.setAttribute("aria-label", `${activity.name}: Icon ändern`); icon.disabled = activity.type === "custom"; icon.addEventListener("click", (event) => { event.stopPropagation(); toggleIconPicker(activity, identity); }); const iconPicker = createIconPicker(activity); const name = document.createElement("span"); name.className = "activity-row-name"; name.textContent = activity.name; const meta = document.createElement("small"); meta.textContent = `${activity.type === "custom" ? "Benutzerdefiniert" : activity.type === "short" ? "Kurz" : "Lang"}${activity.disabled ? " · Deaktiviert" : ""}`; identity.append(icon, name, meta); const actions = document.createElement("div"); actions.className = "activity-actions"; const renameButton = createRowAction("✎", "Aktivität umbenennen", () => renameActivity(activity, identity, name, renameButton)); const typeButton = createRowAction("", activity.type === "long" ? "Auf kurz ändern" : "Auf lang ändern", () => changeType(activity)); typeButton.classList.add("type-action", activity.type === "long" ? "is-long" : "is-short"); const durationIndicator = document.createElement("span"); durationIndicator.className = "duration-indicator"; typeButton.appendChild(durationIndicator); typeButton.disabled = activity.type === "custom"; const disableButton = document.createElement("button"); disableButton.className = "row-action icon-action"; disableButton.type = "button"; disableButton.textContent = activity.disabled ? "▶" : "⏸"; disableButton.title = activity.disabled ? "Aktivität aktivieren" : "Aktivität deaktivieren"; disableButton.setAttribute("aria-label", `${activity.name}: ${activity.disabled ? "Aktivität aktivieren" : "Aktivität deaktivieren"}`); disableButton.addEventListener("click", () => toggleActivity(activity)); if (activity.type !== "custom") { const deleteButton = createRowAction("🗑", "Aktivität löschen", () => deleteActivity(activity)); deleteButton.classList.add("is-danger"); actions.append(renameButton, typeButton, disableButton, deleteButton); } else { actions.append(renameButton, disableButton); } identity.append(iconPicker); row.append(identity, actions); activitiesList.appendChild(row); }); } function createRowAction(symbol, label, action) { const button = document.createElement("button"); button.className = "row-action icon-action"; button.type = "button"; button.textContent = symbol; button.title = label; button.setAttribute("aria-label", label); button.addEventListener("click", action); return button; } function createIconPicker(activity, onSelect) { const picker = document.createElement("div"); picker.className = "activity-icon-picker hidden"; picker.setAttribute("role", "dialog"); picker.setAttribute("aria-label", `${activity.name}: Icon auswählen`); activityIcons.forEach((icon) => { const option = document.createElement("button"); option.type = "button"; option.className = "icon-option"; option.textContent = icon; option.title = `Icon ${icon}`; option.setAttribute("aria-label", `Icon ${icon} auswählen`); option.classList.toggle("is-selected", icon === activity.icon); option.addEventListener("click", (event) => { event.stopPropagation(); picker.classList.add("hidden"); if (onSelect) onSelect(icon); else if (icon !== activity.icon) updateActivity(activity, { icon }); }); picker.appendChild(option); }); return picker; } function toggleIconPicker(activity, identity) { const picker = identity.querySelector(".activity-icon-picker"); if (!picker) return; document.querySelectorAll(".activity-icon-picker").forEach((item) => { if (item !== picker) item.classList.add("hidden"); }); picker.classList.toggle("hidden"); } async function updateActivity(activity, changes) { try { await request("/api/admin/activities", { method: "PUT", body: JSON.stringify({ originalName: activity.name, ...activity, ...changes }) }); await loadActivities(); setMessage(activityMessage, "Aktivität gespeichert.", true); } catch (error) { if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen."); else setMessage(activityMessage, error.message, false); } } function renameActivity(activity, identity, nameElement, renameButton) { const existingInput = identity.querySelector(".rename-input"); if (existingInput) return; const input = document.createElement("input"); input.className = "activity-name-input rename-input"; input.type = "text"; input.maxLength = 100; input.value = activity.name; input.setAttribute("aria-label", `${activity.name}: neuer Name`); nameElement.replaceWith(input); const cancelButton = createRowAction("×", "Umbenennen abbrechen", () => renderActivities()); renameButton.textContent = "✓"; renameButton.title = "Namen speichern"; renameButton.setAttribute("aria-label", "Namen speichern"); renameButton.onclick = async () => { const nextName = input.value.trim(); if (!nextName || nextName === activity.name) { renderActivities(); return; } await updateActivity(activity, { name: nextName }); }; renameButton.parentElement.insertBefore(cancelButton, renameButton.nextSibling); input.addEventListener("keydown", (event) => { if (event.key === "Enter") renameButton.click(); if (event.key === "Escape") cancelButton.click(); }); input.focus(); input.select(); } function changeType(activity) { updateActivity(activity, { type: activity.type === "long" ? "short" : "long" }); } async function loadActivities() { try { activities = await request("/api/admin/activities", { method: "GET" }); renderActivities(); } catch (error) { if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen."); else setMessage(activityMessage, error.message, false); } } async function toggleActivity(activity) { try { await request("/api/admin/activities", { method: "PUT", body: JSON.stringify({ originalName: activity.name, ...activity, disabled: !activity.disabled }) }); await loadActivities(); setMessage(activityMessage, "Aktivitätsstatus gespeichert.", true); } catch (error) { setMessage(activityMessage, error.message, false); } } async function deleteActivity(activity) { if (!window.confirm(`Aktivität „${activity.name}“ wirklich löschen?`)) return; try { await request("/api/admin/activities", { method: "DELETE", body: JSON.stringify({ name: activity.name }) }); await loadActivities(); setMessage(activityMessage, "Aktivität gelöscht.", true); } catch (error) { setMessage(activityMessage, error.message, false); } } function createPickerForNewActivity() { const draft = { name: "Neue Aktivität", icon: newActivityIcon.textContent, type: newActivityType.classList.contains("is-long") ? "long" : "short" }; const picker = createIconPicker(draft, (icon) => { newActivityIcon.textContent = icon; }); picker.classList.remove("hidden"); picker.classList.add("activity-add-picker"); newActivityIcon.parentElement.appendChild(picker); return picker; } 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; } }); newActivityIcon.addEventListener("click", (event) => { event.stopPropagation(); const existingPicker = newActivityIcon.parentElement.querySelector(".activity-add-picker"); if (existingPicker) { existingPicker.remove(); return; } createPickerForNewActivity(); }); newActivityType.addEventListener("click", () => { const isLong = newActivityType.classList.toggle("is-long"); newActivityType.classList.toggle("is-short", !isLong); newActivityType.title = isLong ? "Auf kurz ändern" : "Auf lang ändern"; newActivityType.setAttribute("aria-label", `Neue Aktivität: ${isLong ? "auf kurz" : "auf lang"} ändern`); }); activityAddRow.addEventListener("submit", async (event) => { event.preventDefault(); const activity = { name: newActivityName.value.trim(), icon: newActivityIcon.textContent, type: newActivityType.classList.contains("is-long") ? "long" : "short" }; try { await request("/api/admin/activities", { method: "POST", body: JSON.stringify(activity) }); newActivityName.value = ""; newActivityIcon.textContent = "🧺"; newActivityType.classList.remove("is-long"); newActivityType.classList.add("is-short"); await loadActivities(); setMessage(activityMessage, "Aktivität hinzugefügt.", true); } catch (error) { if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen."); else setMessage(activityMessage, error.message, false); } }); document.querySelectorAll(".admin-nav-btn").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll(".admin-nav-btn").forEach((item) => item.classList.toggle("is-active", item === button)); document.querySelectorAll(".admin-panel").forEach((panel) => panel.classList.toggle("hidden", panel.id !== button.dataset.view)); }); }); document.addEventListener("click", () => { document.querySelectorAll(".activity-icon-picker").forEach((picker) => picker.classList.add("hidden")); }); 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(); })();