trihub-ernaehrung-advinters/js/terminbuchung.js

401 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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">&times;</button>
<span class="tb-kicker">Online-Terminbuchung</span>
<h2 class="tb-title" id="tb-title">Trainingsplan &amp; 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">&#8249;</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">&#8250;</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
};
})();