Files
date-wizard/public/admin.js
T

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