diff --git a/css/leistungen.css b/css/leistungen.css
index 96aba5f..638f941 100644
--- a/css/leistungen.css
+++ b/css/leistungen.css
@@ -86,7 +86,6 @@
/* Empfehlung kommt aus der JSON-Datei; die Auswahlklasse setzt leistungen.js beim Klick. */
.pakete .paket-karte--empfohlen {
- border-color: 1px solid rgba(248, 250, 252, 0.08);
box-shadow: 0 0 0 1px var(--accent-soft);
}
diff --git a/css/terminbuchung.css b/css/terminbuchung.css
index 739f884..c575129 100644
--- a/css/terminbuchung.css
+++ b/css/terminbuchung.css
@@ -37,6 +37,10 @@
position: relative;
box-sizing: border-box;
width: min(560px, 100%);
+ /* Bei kleinen Fenstern bleibt der gesamte Dialog erreichbar. */
+ max-height: calc(100dvh - 48px);
+ overflow-y: auto;
+ overscroll-behavior: contain;
background-color: #111a1e;
border: 2px solid rgb(255 255 255 / 9%);
border-radius: 16px;
@@ -1145,4 +1149,4 @@
.tb-overlay * {
transition-duration: 0.01ms !important;
}
-}
\ No newline at end of file
+}
diff --git a/js/terminbuchung-form.js b/js/terminbuchung-form.js
index 71499c3..858d4e9 100644
--- a/js/terminbuchung-form.js
+++ b/js/terminbuchung-form.js
@@ -34,66 +34,6 @@
});
})();
-// ----- Termin-Zusammenfassung aus der URL -----
-(function () {
- const params = new URLSearchParams(window.location.search);
- const beraterId = params.get("berater");
- const datum = params.get("datum");
- const zeit = params.get("zeit");
-
- const elBerater = document.getElementById("tb-berater");
- const elDatum = document.getElementById("tb-datum");
- const elZeit = document.getElementById("tb-zeit");
- const zusammenfassung = document.getElementById("termin-zusammenfassung");
- if (!zusammenfassung) return;
-
- // Ohne Termin-Parameter (z. B. direkter Seitenaufruf): Hinweis statt Zusammenfassung.
- if (!beraterId || !datum || !zeit) {
- zusammenfassung.classList.add("tb-summary--fehlend");
- zusammenfassung.innerHTML = `
-
-
Kein Termin ausgewählt
-
- Du hast noch keinen Termin im Kalender gewählt.
- Gehe bitte zurück zur Startseite und öffne dort über einen
- Berater die Terminbuchung.
-
-
- `;
- return;
- }
-
- // Datum lesbar formatieren, z. B. "Montag, 05.10.2026"
- const [jahr, monat, tag] = datum.split("-").map(Number);
- if (!jahr || !monat || !tag) return;
- const datumObjekt = new Date(jahr, monat - 1, tag);
- const formatiert = new Intl.DateTimeFormat("de-DE", {
- weekday: "long",
- day: "2-digit",
- month: "2-digit",
- year: "numeric"
- }).format(datumObjekt);
-
- elDatum.textContent = formatiert;
- elZeit.textContent = zeit.replace(":", ".") + " Uhr";
-
- // Berater-Namen aus json/berater.json auflösen (ID stammt aus dem Kalender).
- fetch("../json/berater.json")
- .then(function (response) {
- if (!response.ok) throw new Error("HTTP " + response.status);
- return response.json();
- })
- .then(function (daten) {
- const treffer = (daten.berater || []).find(function (b) {
- return String(b.id) === String(beraterId);
- });
- elBerater.textContent = treffer ? String(treffer.name).trim() : "Unbekannter Berater";
- })
- .catch(function () {
- elBerater.textContent = "Unbekannter Berater";
- });
-})();
-
// ----- Anamnese-Tags (Allergien & Unverträglichkeiten) -----
(function () {
const input = document.getElementById("tag-input");
@@ -234,6 +174,7 @@
function allValid() {
return (
+ terminIstGueltig() &&
fields.art.value !== "" &&
fields.format.value !== "" &&
fields.gespraech.value !== "" &&
@@ -266,25 +207,75 @@
});
});
- // Termin-Angaben aus der URL für das E-Mail übernehmen.
+ // Nur ein eingetragener, noch zukünftiger Termin darf angefragt werden.
const params = new URLSearchParams(window.location.search);
- const datumRoh = params.get("datum") || "";
- const zeitRoh = params.get("zeit") || "";
- let terminText = "–";
- if (datumRoh && zeitRoh) {
- const [jahr, monat, tag] = datumRoh.split("-").map(Number);
- if (jahr && monat && tag) {
- const formatiert = new Intl.DateTimeFormat("de-DE", {
- weekday: "long",
- day: "2-digit",
- month: "2-digit",
- year: "numeric"
- }).format(new Date(jahr, monat - 1, tag));
- terminText = formatiert + " um " + zeitRoh.replace(":", ".") + " Uhr";
- }
+ const beraterId = params.get("berater");
+ const datum = params.get("datum");
+ const zeit = params.get("zeit");
+ let termin = null;
+
+ function terminIstGueltig() {
+ return termin !== null && termin.zeitpunkt > Date.now();
}
- const elBerater = document.getElementById("tb-berater");
+ function terminFehlerAnzeigen(text) {
+ termin = null;
+ const zusammenfassung = document.getElementById("termin-zusammenfassung");
+ zusammenfassung.classList.add("tb-summary--fehlend");
+ zusammenfassung.innerHTML = `
+
+
Bitte einen gültigen Termin wählen
+
+ Wähle auf der Startseite einen Berater,
+ einen verfügbaren Tag und eine Uhrzeit.
+
+
+ `;
+ pruefBereich.hidden = true;
+ formularBereich.hidden = false;
+ sendenLink.removeAttribute("href");
+ showStatus("error", text);
+ updateSubmitState();
+ }
+
+ async function terminLaden() {
+ if (!beraterId || !datum || !zeit) {
+ terminFehlerAnzeigen("Bitte wähle zuerst einen Termin im Kalender.");
+ return;
+ }
+ try {
+ const [beraterDaten, terminDaten] = await Promise.all(
+ ["../json/berater.json", "../json/terminbuchung.json"].map(async function (url) {
+ const response = await fetch(url);
+ if (!response.ok) throw new Error("HTTP " + response.status);
+ return response.json();
+ })
+ );
+ const berater = beraterDaten.berater.find(function (eintrag) {
+ return String(eintrag.id) === beraterId;
+ });
+ const zeiten = terminDaten[beraterId]?.[datum];
+ const zeitpunkt = new Date(`${datum}T${zeit}:00`).getTime();
+ if (!berater || !Array.isArray(zeiten) || !zeiten.includes(zeit) || !(zeitpunkt > Date.now())) {
+ terminFehlerAnzeigen("Dieser Termin ist nicht verfügbar oder bereits vergangen. Bitte wähle einen neuen Termin.");
+ return;
+ }
+ const datumText = new Intl.DateTimeFormat("de-DE", {
+ weekday: "long", day: "2-digit", month: "2-digit", year: "numeric"
+ }).format(new Date(zeitpunkt));
+ termin = {
+ zeitpunkt,
+ berater: String(berater.name).trim(),
+ text: datumText + " um " + zeit.replace(":", ".") + " Uhr"
+ };
+ document.getElementById("tb-berater").textContent = termin.berater;
+ document.getElementById("tb-datum").textContent = datumText;
+ document.getElementById("tb-zeit").textContent = zeit.replace(":", ".") + " Uhr";
+ updateSubmitState();
+ } catch (error) {
+ terminFehlerAnzeigen("Der Termin konnte nicht geprüft werden. Bitte lade die Seite erneut.");
+ }
+ }
function labelVon(key) {
const select = fields[key];
@@ -310,6 +301,11 @@
sendenLink.addEventListener("click", function (event) {
event.preventDefault();
+ if (!terminIstGueltig()) {
+ terminFehlerAnzeigen("Der Termin ist nicht mehr gültig. Bitte wähle einen neuen Termin.");
+ formularBereich.scrollIntoView({ block: "start" });
+ return;
+ }
bestaetigungDialog.showModal();
window.location.href = sendenLink.href;
});
@@ -317,6 +313,10 @@
form.addEventListener("submit", function (event) {
event.preventDefault();
form.dataset.touched = "true";
+ if (!terminIstGueltig()) {
+ terminFehlerAnzeigen("Bitte wähle zuerst einen verfügbaren, zukünftigen Termin.");
+ return;
+ }
const okArt = validateSelect("art");
const okFormat = validateSelect("format");
@@ -330,7 +330,8 @@
}
const tagListe = window.TerminbuchungTags ? window.TerminbuchungTags.holen() : [];
- const beraterText = elBerater ? elBerater.textContent.trim() : "–";
+ const beraterText = termin.berater;
+ const terminText = termin.text;
const anfrageDaten = [
["Berater/in", beraterText],
["Termin", terminText],
@@ -379,4 +380,5 @@
}
updateSubmitState();
+ terminLaden();
})();
diff --git a/js/terminbuchung.js b/js/terminbuchung.js
index 5f35e54..69c022a 100644
--- a/js/terminbuchung.js
+++ b/js/terminbuchung.js
@@ -77,18 +77,17 @@
const tabelle = terminDaten[String(beraterId)];
if (!tabelle || typeof tabelle !== "object") return [];
const zeiten = tabelle[schluessel];
- return Array.isArray(zeiten) ? zeiten : [];
+ // Auch bereits vergangene Uhrzeiten am heutigen Tag ausblenden.
+ return Array.isArray(zeiten)
+ ? zeiten.filter((zeit) => new Date(`${schluessel}T${zeit}:00`).getTime() > Date.now())
+ : [];
}
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]));
- });
+ return Object.keys(tabelle).some((schluessel) => zeitenFuer(beraterId, schluessel).length > 0);
}
function istVerfuegbar(jahr, monat, tag) {
@@ -181,7 +180,32 @@
if (event.target === overlay) schliessen();
});
document.addEventListener("keydown", (event) => {
- if (istGeoeffnet && event.key === "Escape") schliessen();
+ if (!istGeoeffnet) return;
+ if (event.key === "Escape") {
+ event.preventDefault();
+ schliessen();
+ return;
+ }
+ if (event.key !== "Tab") return;
+
+ // Tab und Shift+Tab bleiben zwischen den sichtbaren Dialog-Buttons.
+ const buttons = [...overlay.querySelectorAll("button:not(:disabled)")]
+ .filter((button) => button.getClientRects().length > 0);
+ const erster = buttons[0];
+ const letzter = buttons[buttons.length - 1];
+ const fokus = document.activeElement;
+ if (!overlay.contains(fokus) || (event.shiftKey && fokus === erster)) {
+ event.preventDefault();
+ (event.shiftKey ? letzter : erster).focus();
+ } else if (!event.shiftKey && fokus === letzter) {
+ event.preventDefault();
+ erster.focus();
+ }
+ });
+ document.addEventListener("focusin", (event) => {
+ if (istGeoeffnet && !overlay.contains(event.target)) {
+ overlay.querySelector(".tb-close").focus();
+ }
});
// Buchen
@@ -273,9 +297,14 @@
kalenderZeichnen();
zeitenZeichnen();
buchButtonAktualisieren();
+ overlay.querySelector(".tb-day--ausgewaehlt")?.focus();
}
function zeitKlicken(zeit) {
+ if (!zeitenFuer(zustand.beraterId, zustand.datum).includes(zeit)) {
+ auswahlAktualisieren();
+ return;
+ }
zustand.zeit = zeit;
overlay.querySelectorAll(".tb-time-chip").forEach((chip) => {
chip.classList.toggle("is-active", chip.dataset.zeit === zeit);
@@ -285,6 +314,11 @@
function buchen() {
if (!zustand.datum || !zustand.zeit || zustand.beraterId == null) return;
+ // Die Seite kann seit der Auswahl offen geblieben sein.
+ if (!zeitenFuer(zustand.beraterId, zustand.datum).includes(zustand.zeit)) {
+ auswahlAktualisieren();
+ return;
+ }
const ziel =
ZIELSEITE +
"?berater=" + encodeURIComponent(zustand.beraterId) +
@@ -293,6 +327,15 @@
window.location.href = ziel;
}
+ function auswahlAktualisieren() {
+ zustand.zeit = null;
+ kalenderZeichnen();
+ zeitenZeichnen();
+ legendeAktualisieren();
+ buchButtonAktualisieren();
+ overlay.querySelector(".tb-close").focus();
+ }
+
// ----- Zeichnen -----
function kalenderZeichnen() {