401 lines
16 KiB
JavaScript
401 lines
16 KiB
JavaScript
// Online-Terminbuchung – Kalender-Pop-up auf der Startseite (index.html).
|
||
// Daten: json/berater.json (Trainer mit ID) und json/terminbuchung.json
|
||
// (Verfügbarkeit pro Trainer-ID: Datum JJJJ-MM-TT → Array von Uhrzeiten HH:MM).
|
||
|
||
(function () {
|
||
"use strict";
|
||
|
||
const BERATER_URL = "json/berater.json";
|
||
const TERMINE_URL = "json/terminbuchung.json";
|
||
const ZIELSEITE = "html/terminbuchung.html";
|
||
|
||
const MONATE = [
|
||
"Januar", "Februar", "März", "April", "Mai", "Juni",
|
||
"Juli", "August", "September", "Oktober", "November", "Dezember"
|
||
];
|
||
const WOCHE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||
|
||
let beraterDaten = []; // [{id, name, ...}]
|
||
let terminDaten = {}; // { "1": { "2026-10-05": ["09:00", ...] }, ... }
|
||
let geladen = null; // Promise, die auf das Laden beider JSON-Dateien wartet
|
||
|
||
let overlay = null; // DOM: Pop-up (wird einmalig erzeugt)
|
||
let istGeoeffnet = false;
|
||
let fokusVorher = null; // Element, das vor dem Öffnen Fokus hatte
|
||
|
||
const zustand = {
|
||
beraterId: null,
|
||
jahr: 0,
|
||
monat: 0, // 0-basiert
|
||
datum: null, // "JJJJ-MM-TT"
|
||
zeit: null // "HH:MM"
|
||
};
|
||
|
||
// ----- Daten laden -----
|
||
|
||
function ladeDaten() {
|
||
if (geladen) return geladen;
|
||
geladen = Promise.all([
|
||
fetch(BERATER_URL).then((r) => (r.ok ? r.json() : Promise.reject(new Error("berater.json: HTTP " + r.status)))),
|
||
fetch(TERMINE_URL).then((r) => (r.ok ? r.json() : Promise.reject(new Error("terminbuchung.json: HTTP " + r.status))))
|
||
])
|
||
.then(([berater, termine]) => {
|
||
beraterDaten = Array.isArray(berater.berater) ? berater.berater : [];
|
||
terminDaten = termine && typeof termine === "object" ? termine : {};
|
||
})
|
||
.catch((error) => {
|
||
console.error("Terminbuchung: Daten konnten nicht geladen werden.", error);
|
||
});
|
||
return geladen;
|
||
}
|
||
|
||
// ----- Hilfsfunktionen -----
|
||
|
||
function pad(n) {
|
||
return String(n).padStart(2, "0");
|
||
}
|
||
|
||
function datumSchluessel(jahr, monat, tag) {
|
||
return `${jahr}-${pad(monat + 1)}-${pad(tag)}`;
|
||
}
|
||
|
||
function heutigerTag() {
|
||
const jetzt = new Date();
|
||
return { jahr: jetzt.getFullYear(), monat: jetzt.getMonth(), tag: jetzt.getDate() };
|
||
}
|
||
|
||
function istVergangen(jahr, monat, tag) {
|
||
const heute = heutigerTag();
|
||
return (
|
||
jahr < heute.jahr ||
|
||
(jahr === heute.jahr && monat < heute.monat) ||
|
||
(jahr === heute.jahr && monat === heute.monat && tag < heute.tag)
|
||
);
|
||
}
|
||
|
||
function zeitenFuer(beraterId, schluessel) {
|
||
const tabelle = terminDaten[String(beraterId)];
|
||
if (!tabelle || typeof tabelle !== "object") return [];
|
||
const zeiten = tabelle[schluessel];
|
||
return Array.isArray(zeiten) ? zeiten : [];
|
||
}
|
||
|
||
function hatFreieTermine(beraterId) {
|
||
const tabelle = terminDaten[String(beraterId)];
|
||
if (!tabelle || typeof tabelle !== "object") return false;
|
||
|
||
return Object.entries(tabelle).some(([schluessel, zeiten]) => {
|
||
const datum = /^(\d{4})-(\d{2})-(\d{2})$/.exec(schluessel);
|
||
if (!datum || !Array.isArray(zeiten) || zeiten.length === 0) return false;
|
||
return !istVergangen(Number(datum[1]), Number(datum[2]) - 1, Number(datum[3]));
|
||
});
|
||
}
|
||
|
||
function istVerfuegbar(jahr, monat, tag) {
|
||
if (istVergangen(jahr, monat, tag)) return false;
|
||
return zeitenFuer(zustand.beraterId, datumSchluessel(jahr, monat, tag)).length > 0;
|
||
}
|
||
|
||
function deutscherTagText(schluessel) {
|
||
const [jahr, monat, tag] = schluessel.split("-").map(Number);
|
||
const datum = new Date(jahr, monat - 1, tag);
|
||
return (
|
||
WOCHE[(datum.getDay() + 6) % 7] + ", " +
|
||
pad(tag) + "." + pad(monat) + "." + jahr
|
||
);
|
||
}
|
||
|
||
function beraterName(beraterId) {
|
||
const treffer = beraterDaten.find((b) => Number(b.id) === Number(beraterId));
|
||
return treffer ? String(treffer.name).trim() : "Berater/in";
|
||
}
|
||
|
||
// ----- Pop-up aufbauen (einmalig) -----
|
||
|
||
function bauPopUp() {
|
||
if (overlay) return overlay;
|
||
|
||
const div = document.createElement("div");
|
||
div.innerHTML = `
|
||
<div class="tb-overlay" hidden>
|
||
<div class="tb-dialog" role="dialog" aria-modal="true" aria-labelledby="tb-title">
|
||
<button type="button" class="tb-close" aria-label="Terminbuchung schließen">×</button>
|
||
<span class="tb-kicker">Online-Terminbuchung</span>
|
||
<h2 class="tb-title" id="tb-title">Trainingsplan & Beratung – Termin wählen</h2>
|
||
<p class="tb-subtitle">Wähle deinen Berater, einen verfügbaren Tag und eine Uhrzeit.</p>
|
||
|
||
<span class="tb-trainer-label">Berater/in wählen:</span>
|
||
<div class="tb-trainer"></div>
|
||
|
||
<div class="tb-cal-head">
|
||
<button type="button" class="tb-cal-nav" data-richtung="-1" aria-label="Vorheriger Monat">‹</button>
|
||
<span class="tb-cal-month" aria-live="polite"></span>
|
||
<button type="button" class="tb-cal-nav" data-richtung="1" aria-label="Nächster Monat">›</button>
|
||
</div>
|
||
|
||
<div class="tb-cal-weekdays" aria-hidden="true">
|
||
${WOCHE.map((w) => `<span>${w}</span>`).join("")}
|
||
</div>
|
||
<div class="tb-cal-grid"></div>
|
||
|
||
<ul class="tb-legend" aria-live="polite">
|
||
<li><span class="tb-dot" aria-hidden="true"></span><span class="tb-legend-label"></span></li>
|
||
</ul>
|
||
|
||
<div class="tb-times" hidden>
|
||
<p class="tb-times-label"></p>
|
||
<div class="tb-time-chips"></div>
|
||
</div>
|
||
|
||
<button type="button" class="tb-book" disabled>Weiter zur Buchung</button>
|
||
<p class="tb-hinweis">Nach der Auswahl wirst du zur Buchungsseite weitergeleitet, wo du die Details zu Beratung und Kontakt angibst.</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
overlay = div.firstElementChild;
|
||
document.body.appendChild(overlay);
|
||
|
||
// Trainer-Buttons füllen
|
||
const trainerBox = overlay.querySelector(".tb-trainer");
|
||
beraterDaten.forEach((berater) => {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "tb-trainer-btn";
|
||
btn.textContent = String(berater.name).trim();
|
||
btn.dataset.beraterId = berater.id;
|
||
btn.addEventListener("click", () => trainerWaehlen(Number(berater.id)));
|
||
trainerBox.appendChild(btn);
|
||
});
|
||
|
||
// Monatsnavigation
|
||
overlay.querySelectorAll(".tb-cal-nav").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
verschiebeMonat(Number(btn.dataset.richtung));
|
||
});
|
||
});
|
||
|
||
// Schließen: Knopf, Klick auf den Schleier, Escape
|
||
overlay.querySelector(".tb-close").addEventListener("click", schliessen);
|
||
overlay.addEventListener("click", (event) => {
|
||
if (event.target === overlay) schliessen();
|
||
});
|
||
document.addEventListener("keydown", (event) => {
|
||
if (istGeoeffnet && event.key === "Escape") schliessen();
|
||
});
|
||
|
||
// Buchen
|
||
overlay.querySelector(".tb-book").addEventListener("click", buchen);
|
||
|
||
return overlay;
|
||
}
|
||
|
||
// ----- Öffnen / Schließen -----
|
||
|
||
async function oeffnen(beraterId) {
|
||
await ladeDaten();
|
||
|
||
// Ohne Berater-Daten gibt es nichts auszuwählen – Pop-up bleibt geschlossen.
|
||
if (!beraterDaten.length) {
|
||
console.error("Terminbuchung: Keine Berater geladen – Pop-up wird nicht geöffnet.");
|
||
return;
|
||
}
|
||
|
||
bauPopUp();
|
||
|
||
const heute = heutigerTag();
|
||
zustand.jahr = heute.jahr;
|
||
zustand.monat = heute.monat;
|
||
trainerWaehlen(beraterId != null ? Number(beraterId) : beraterDaten[0].id);
|
||
|
||
overlay.hidden = false;
|
||
istGeoeffnet = true;
|
||
fokusVorher = document.activeElement;
|
||
document.body.style.overflow = "hidden";
|
||
overlay.querySelector(".tb-close").focus();
|
||
}
|
||
|
||
function schliessen() {
|
||
if (!istGeoeffnet) return;
|
||
overlay.hidden = true;
|
||
istGeoeffnet = false;
|
||
document.body.style.overflow = "";
|
||
if (fokusVorher && typeof fokusVorher.focus === "function") {
|
||
fokusVorher.focus();
|
||
}
|
||
}
|
||
|
||
// ----- Auswahl-Logik -----
|
||
|
||
function trainerWaehlen(beraterId) {
|
||
zustand.beraterId = beraterId;
|
||
zustand.datum = null;
|
||
zustand.zeit = null;
|
||
|
||
overlay.querySelectorAll(".tb-trainer-btn").forEach((btn) => {
|
||
const aktiv = Number(btn.dataset.beraterId) === Number(beraterId);
|
||
btn.classList.toggle("is-active", aktiv);
|
||
btn.setAttribute("aria-pressed", aktiv ? "true" : "false");
|
||
});
|
||
|
||
legendeAktualisieren();
|
||
zeitenBereichZuruecksetzen();
|
||
kalenderZeichnen();
|
||
buchButtonAktualisieren();
|
||
}
|
||
|
||
function legendeAktualisieren() {
|
||
const dot = overlay.querySelector(".tb-legend .tb-dot");
|
||
const label = overlay.querySelector(".tb-legend-label");
|
||
const verfuegbar = hatFreieTermine(zustand.beraterId);
|
||
dot.className = "tb-dot " + (verfuegbar ? "tb-dot--verfuegbar" : "tb-dot--nicht");
|
||
label.textContent = verfuegbar ? "Verfügbar" : "Nicht verfügbar";
|
||
}
|
||
|
||
function verschiebeMonat(richtung) {
|
||
const heute = heutigerTag();
|
||
let ziel = zustand.jahr * 12 + zustand.monat + richtung;
|
||
if (ziel < heute.jahr * 12 + heute.monat) return; // nicht vor dem aktuellen Monat
|
||
|
||
zustand.jahr = Math.floor(ziel / 12);
|
||
zustand.monat = ziel % 12;
|
||
zustand.datum = null;
|
||
zustand.zeit = null;
|
||
zeitenBereichZuruecksetzen();
|
||
kalenderZeichnen();
|
||
buchButtonAktualisieren();
|
||
}
|
||
|
||
function tagKlicken(jahr, monat, tag) {
|
||
if (!istVerfuegbar(jahr, monat, tag)) return;
|
||
zustand.datum = datumSchluessel(jahr, monat, tag);
|
||
zustand.zeit = null;
|
||
kalenderZeichnen();
|
||
zeitenZeichnen();
|
||
buchButtonAktualisieren();
|
||
}
|
||
|
||
function zeitKlicken(zeit) {
|
||
zustand.zeit = zeit;
|
||
overlay.querySelectorAll(".tb-time-chip").forEach((chip) => {
|
||
chip.classList.toggle("is-active", chip.dataset.zeit === zeit);
|
||
});
|
||
buchButtonAktualisieren();
|
||
}
|
||
|
||
function buchen() {
|
||
if (!zustand.datum || !zustand.zeit || zustand.beraterId == null) return;
|
||
const ziel =
|
||
ZIELSEITE +
|
||
"?berater=" + encodeURIComponent(zustand.beraterId) +
|
||
"&datum=" + encodeURIComponent(zustand.datum) +
|
||
"&zeit=" + encodeURIComponent(zustand.zeit);
|
||
window.location.href = ziel;
|
||
}
|
||
|
||
// ----- Zeichnen -----
|
||
|
||
function kalenderZeichnen() {
|
||
const grid = overlay.querySelector(".tb-cal-grid");
|
||
const monatsLabel = overlay.querySelector(".tb-cal-month");
|
||
const navVorher = overlay.querySelector('.tb-cal-nav[data-richtung="-1"]');
|
||
|
||
monatsLabel.textContent = `${MONATE[zustand.monat]} ${zustand.jahr}`;
|
||
|
||
// Vor dem aktuellen Monat kann nicht zurückgeblättert werden.
|
||
const heute = heutigerTag();
|
||
navVorher.disabled = zustand.jahr * 12 + zustand.monat <= heute.jahr * 12 + heute.monat;
|
||
|
||
const ersterTag = new Date(zustand.jahr, zustand.monat, 1);
|
||
const versatz = (ersterTag.getDay() + 6) % 7; // Montag als erste Spalte
|
||
const tageImMonat = new Date(zustand.jahr, zustand.monat + 1, 0).getDate();
|
||
const heuteInfo = heutigerTag();
|
||
|
||
grid.innerHTML = "";
|
||
for (let i = 0; i < versatz; i++) {
|
||
const leer = document.createElement("span");
|
||
leer.className = "tb-day tb-day--leer";
|
||
grid.appendChild(leer);
|
||
}
|
||
|
||
for (let tag = 1; tag <= tageImMonat; tag++) {
|
||
const verfuegbar = istVerfuegbar(zustand.jahr, zustand.monat, tag);
|
||
const schluessel = datumSchluessel(zustand.jahr, zustand.monat, tag);
|
||
const vergangen = istVergangen(zustand.jahr, zustand.monat, tag);
|
||
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
let klassen = "tb-day ";
|
||
if (verfuegbar) {
|
||
klassen += "tb-day--verfuegbar";
|
||
} else {
|
||
klassen += "tb-day--neutral";
|
||
}
|
||
if (
|
||
zustand.jahr === heuteInfo.jahr &&
|
||
zustand.monat === heuteInfo.monat &&
|
||
tag === heuteInfo.tag
|
||
) {
|
||
klassen += " tb-day--heute";
|
||
}
|
||
if (zustand.datum === schluessel) {
|
||
klassen += " tb-day--ausgewaehlt";
|
||
}
|
||
btn.className = klassen;
|
||
btn.textContent = tag;
|
||
btn.disabled = !verfuegbar;
|
||
const status = verfuegbar ? "verfügbar" : vergangen ? "vergangen" : "keine Termine eingetragen";
|
||
btn.setAttribute("aria-label", `${deutscherTagText(schluessel)} – ${status}`);
|
||
if (verfuegbar) {
|
||
btn.addEventListener("click", () => tagKlicken(zustand.jahr, zustand.monat, tag));
|
||
}
|
||
grid.appendChild(btn);
|
||
}
|
||
}
|
||
|
||
function zeitenBereichZuruecksetzen() {
|
||
const bereich = overlay.querySelector(".tb-times");
|
||
bereich.hidden = true;
|
||
overlay.querySelector(".tb-time-chips").innerHTML = "";
|
||
overlay.querySelector(".tb-times-label").textContent = "";
|
||
}
|
||
|
||
function zeitenZeichnen() {
|
||
const bereich = overlay.querySelector(".tb-times");
|
||
const label = overlay.querySelector(".tb-times-label");
|
||
const chips = overlay.querySelector(".tb-time-chips");
|
||
const zeiten = zeitenFuer(zustand.beraterId, zustand.datum);
|
||
|
||
if (!zeiten.length) {
|
||
bereich.hidden = true;
|
||
return;
|
||
}
|
||
|
||
label.innerHTML = `Wähle eine Uhrzeit für <strong>${deutscherTagText(zustand.datum)}</strong> bei ${beraterName(zustand.beraterId)}:`;
|
||
chips.innerHTML = "";
|
||
zeiten.forEach((zeit) => {
|
||
const chip = document.createElement("button");
|
||
chip.type = "button";
|
||
chip.className = "tb-time-chip" + (zustand.zeit === zeit ? " is-active" : "");
|
||
chip.dataset.zeit = zeit;
|
||
chip.textContent = zeit.replace(":", ".") + " Uhr";
|
||
chip.addEventListener("click", () => zeitKlicken(zeit));
|
||
chips.appendChild(chip);
|
||
});
|
||
bereich.hidden = false;
|
||
}
|
||
|
||
function buchButtonAktualisieren() {
|
||
const btn = overlay.querySelector(".tb-book");
|
||
btn.disabled = !(zustand.datum && zustand.zeit && zustand.beraterId != null);
|
||
}
|
||
|
||
// ----- Öffentliche Schnittstelle (wird von berater.js genutzt) -----
|
||
|
||
window.Terminbuchung = {
|
||
oeffnen,
|
||
schliessen,
|
||
istGeoeffnet: () => istGeoeffnet
|
||
};
|
||
})();
|