Terminplaner TP2: Ansicht nach Zustand und Rueckkehr von Stripe

Die Ansicht #termine folgt dem Zustand aus GET /api/termine/einstellungen:
off mit laufendem Abo zeigt den Fragebogen samt Bestellseite, ohne Abo den
Hinweis des Servers (termine.hinweis, R111) und bei kein_abo den Link zu
Vertraege aus einer gemeinsamen Quelle (vertraege-link.js); ordered und setup
zeigen die Statuskarte und die Reiter Leistungen, Team & Plaetze und
Einstellungen nur zum Lesen (ohne Reiter Termine); active und disabled
bleiben wie bisher. Rueckkehr von Stripe: #termine?bestellt=1&projekt=<id>
und #termine?projekt=<id> waehlen das Projekt, "Danke!" nur mit bestellt=1,
die Adresse wird einmal gelesen und bereinigt.

R110: GET /einstellungen liefert bestellung und offen auch in ordered (ohne
Abo-Punkt, abo bleibt null); scheitert "Zahlung fortsetzen", zeichnet die
Huelle Liste und Daten neu (bei 409 zustimmung_fehlt mit dem
Bestellformular, sonst nach dem neuen Zustand, die Meldung bleibt am Knopf,
solange die Bestellung offen ist).

Dazu: "null" als Text im lesbaren Reiter Leistungen entfernt, Export und
Status der Einstellungen erst ab der Freischaltung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 23:10:54 +00:00
1 parent 52456dac76
commit 87d267e51a
13 files changed
+1077 -71

No files matched your search

+15 -8
View File
@@ -259,10 +259,14 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
* Karte für eine Bestellung im Zustand ordered („Zahlung offen“) oder setup („In Einrichtung“).
* projekt = Eintrag des Projekts aus GET /api/funktionen (projectId), bestellung = projekt.features.termine (state, orderedAt, ticketWoid,
* invoiceUrl), einrichtung = Text der Zeiteinschätzung (Register: setupEstimate), danke = Rückkehr von Stripe, kundenansicht = Sitzung in der
* Kundenansicht. weiter() wird gerufen, wenn „Zahlung fortsetzen“ mit 409 zustimmung_fehlt antwortet (keine offene Sitzung mit Nachweis): es
* lädt die Seite neu und zeigt wieder das Bestellformular. Fehlt weiter, steht die Meldung des Servers am Knopf.
* Kundenansicht, meldung = Text, der am Knopf „Zahlung fortsetzen“ steht (z. B. der Fehler eines Versuchs vor dem Neuzeichnen).
* weiter(fall) wird gerufen, wenn „Zahlung fortsetzen“ scheitert (Ruling R110): fall = { bestellen, meldung }, bestellen ist wahr bei 409
* zustimmung_fehlt (keine offene Sitzung mit Nachweis: die Ansicht zeigt wieder das Bestellformular), meldung der Fehlertext der Antwort.
* weiter() lädt neu und zeichnet die Ansicht nach dem Zustand von neuem, damit keine veraltete Karte stehen bleibt (z. B. wenn die Bestellung
* inzwischen bezahlt ist und der Server 400 antwortet); wirft es, bleibt die Karte stehen und der Fehler steht am Knopf. Fehlt weiter,
* steht die Meldung des Servers am Knopf.
*/
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht } = {}, { weiter } = {}) {
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht, meldung } = {}, { weiter } = {}) {
const bestellt = bestellung?.state === 'ordered'
const datum = datumTag(bestellung?.orderedAt)
const ticket = bestellung?.ticketWoid
@@ -275,7 +279,7 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
!bestellt && text(einrichtung) ? zeile('Zeiteinschätzung', einrichtung) : null,
].filter(Boolean)
const meldungEl = h('p', { class: 'tm-fehler', role: 'alert', 'data-meldung': 'fortsetzen' })
const meldungEl = h('p', { class: 'tm-fehler', role: 'alert', 'data-meldung': 'fortsetzen' }, text(meldung) || null)
const fortsetzenKnopf = bestellt
? h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'fortsetzen', disabled: kundenansicht === true, onclick: () => fortsetzen() }, 'Zahlung fortsetzen')
: null
@@ -289,15 +293,18 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
if (geheZu(antwort?.url)) return // die Seite wird verlassen
meldungEl.textContent = ZAHLUNGSSEITE
} catch (err) {
if (err?.status === 409 && err.code === 'zustimmung_fehlt' && typeof weiter === 'function') {
const nachricht = err?.message || ZAHLUNGSSEITE
const zustimmungFehlt = err?.status === 409 && err.code === 'zustimmung_fehlt'
if (typeof weiter === 'function') {
try {
await weiter()
await weiter({ bestellen: zustimmungFehlt, meldung: nachricht })
return // die Ansicht zeichnet sich neu
} catch (fehler) {
meldungEl.textContent = fehler?.message || ZAHLUNGSSEITE
// Neuladen gescheitert: bei fehlender Zustimmung ist das der Grund, der den Kunden betrifft, sonst bleibt der Fehler des Versuchs
meldungEl.textContent = zustimmungFehlt ? fehler?.message || ZAHLUNGSSEITE : nachricht
}
} else {
meldungEl.textContent = err?.message || ZAHLUNGSSEITE
meldungEl.textContent = nachricht
}
}
fortsetzenKnopf.disabled = false
+4 -2
View File
@@ -5,6 +5,7 @@
import { bannerColor, BANNER_PREVIEW_TEXT, controllerLines, chartModel, barRows, countryName, flagEmoji, refLabel, DEVICE_LABELS, CONTACT_LABELS } from '/analyse-view.js'
import { HEAT_DEVICES, deviceHtml, heatCaption } from '/geraete.js'
import { ZU_VERTRAEGE } from './vertraege-link.js'
const PRICE = '30 €' // Rueckfall, wenn die Antwort von /api/funktionen kein features.news (Register) enthaelt
const LEISTUNGSBESCHREIBUNG = '/vertragsunterlagen/funktionen-leistungsbeschreibung.html'
@@ -112,8 +113,9 @@ function newsTile(projects, price = PRICE) {
</article>`
}
// Link in den Hinweisen ohne Wartungs-Abo (kein_abo): Analyse-Kachel, Analyse-Seite und Terminplaner-Kachel teilen ihn
const ZU_VERTRAEGE_LINK = '<button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button>'
// Link in den Hinweisen ohne Wartungs-Abo (kein_abo): Analyse-Kachel, Analyse-Seite und Terminplaner-Kachel teilen ihn, und die Ansicht
// #termine (termine.js) nimmt Text, Ziel und Klasse aus derselben Quelle (vertraege-link.js)
const ZU_VERTRAEGE_LINK = `<button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>`
/* Analyse: Kachel je nach Zustand der Websites (comingSoon = Schalter aus) */
const analyseOf = (project) => project.analyse || { comingSoon: true }
+2
View File
@@ -341,6 +341,8 @@ function einbau(ctx) {
/* ------------------------------------------------------------ Export, Status --- */
function exportUndStatus(ctx) {
// vor der Freischaltung (off, ordered, setup) gibt es weder Termine zum Exportieren noch ein „Aktiv“; null: der Aufrufer laesst den Abschnitt weg
if (['off', 'ordered', 'setup'].includes(ctx.daten.state)) return null
const seit = aktivSeitText(ctx.daten.aktivSeit)
const abgeschaltet = ctx.daten.state === 'disabled'
return abschnitt('Export und Status', null,
+4 -2
View File
@@ -92,7 +92,8 @@ export function render(panel, ctx) {
})
aktualisiere()
wurzel.replaceChildren(
// neu.leiste ist null, solange die Funktion nur lesbar ist: replaceChildren wuerde sonst den Text "null" in die Seite schreiben
wurzel.replaceChildren(...[
sp.konflikt,
h('div', { class: 'tm-box tm-box-info', role: 'note' },
h('span', null, h('strong', null, GESUNDHEIT_HINWEIS), ' Namen und Beschreibungen deiner Leistungen liest jeder, der auf deiner Website bucht. Schreib dort zum Beispiel keine Krankheiten oder Beschwerden hinein.')),
@@ -100,7 +101,8 @@ export function render(panel, ctx) {
neu.box,
leiste,
kopfFehler,
leistungen.length ? liste : h('p', { class: 'tm-leer' }, 'Noch keine Leistungen. Ohne Leistung kann niemand buchen.'))
leistungen.length ? liste : h('p', { class: 'tm-leer' }, 'Noch keine Leistungen. Ohne Leistung kann niemand buchen.'),
].filter(Boolean))
}
zeichne()
}
+190 -29
View File
@@ -1,10 +1,16 @@
/* Terminplaner im Kundenbereich (ES-Modul; app.js laedt es erst beim Oeffnen der Ansicht #termine). Das ist die Huelle:
initTermine(rootEl, { projectId }): Projektauswahl, Vorlagenauswahl beim ersten Oeffnen, Reiter-Geruest und die Meldung ueber den Reitern.
initTermine(rootEl, { projectId }): Projektauswahl und je nach Zustand der Funktion (GET /api/termine/einstellungen, state) die Ansicht:
off: ohne laufendes Wartungs-Abo (daten.abo.ok) der Hinweis wie in der Kachel (Text vom Server, GET /api/funktionen termine.hinweis; kein
Fragebogen), sonst der Fragebogen mit der Bestellseite (termine-fragebogen.js); ordered und setup: die Statuskarte (funktion-bestellen.js)
und darunter die Reiter Leistungen, Team & Plaetze und Einstellungen nur zum Lesen (der Reiter Termine entfaellt, der Server antwortet dort
mit 409); active: die Reiter, ohne Einstellung die Vorlagenauswahl; disabled: der Banner der Abschaltung (abschaltText) und die Reiter nur
zum Lesen. Rueckkehr von Stripe: #termine?bestellt=1&projekt=<id> (Zahlung) und #termine?projekt=<id> (Abbruch) waehlen das Projekt,
„Danke!“ gibt es nur mit bestellt=1; beides wird einmal gelesen und aus der Adresse genommen.
Die Reiter stehen in REITER: jeder ist ein Modul mit render(panel, ctx) => void | Promise (termine-tab-termine.js, termine-tab-leistungen.js,
termine-tab-team.js, termine-tab-einstellungen.js; die drei letzten speichern ueber termine-speichern.js).
ctx = { s, projectId, daten, nurLesen, auswahl, aktuell(), api, meldung, hinweise, zumReiter, neuLaden } (baueCtx): Zustand und Pruefung "bin ich noch
aktuell?" bekommen die Reiter nur ueber ctx, nie ueber Modulvariablen. daten ist die Antwort von GET /api/termine/einstellungen
(die Reiter, die speichern, halten daten.einstellung und daten.version aktuell), nurLesen gilt fuer abgeschaltete Funktionen,
(die Reiter, die speichern, halten daten.einstellung und daten.version aktuell), nurLesen gilt fuer alles ausser off und active,
hinweise() zeichnet die Hinweisleiste ueber den Reitern neu (z. B. wenn Telefon und Anschrift nach dem Speichern nicht mehr fehlen). Server: server/routes/termine.js. Tage sind Berliner Kalendertage 'JJJJ-MM-TT';
gerechnet wird in termine-view.js. Helfer ohne Zustand: termine-dom.js. Styles: termine.css. */
@@ -14,19 +20,40 @@ import { render as renderTermine } from './termine-tab-termine.js'
import { render as renderLeistungen } from './termine-tab-leistungen.js'
import { render as renderTeam } from './termine-tab-team.js'
import { render as renderEinstellungen } from './termine-tab-einstellungen.js'
import { renderFragebogen } from './termine-fragebogen.js'
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
import { ZU_VERTRAEGE } from './vertraege-link.js'
// Auswahl bleibt beim Wechsel zwischen Funktionen und Terminplaner erhalten (der Tag nicht: er beginnt immer bei heute)
const auswahl = { projectId: '', reiter: 'termine', ansicht: 'tag', filter: 'alle' }
let S = null // aktuelle Sitzung der Ansicht; eine alte Antwort darf eine neuere nicht ueberschreiben (S !== s)
/* --------------------------------------------------------- Einstieg --- */
/**
* Rueckkehr von Stripe aus der Adresse lesen und die Adresse bereinigen (wie initNews): #termine?bestellt=1&projekt=<id> nach der Zahlung
* (success_url) und #termine?projekt=<id> nach dem Abbruch (cancel_url). Gelesen wird einmal, danach steht nur noch #termine da.
*/
function leseRueckkehr() {
const hash = window.location.hash || ''
if (!hash.startsWith('#termine?')) return { bestellt: false, projekt: '' }
const query = new URLSearchParams(hash.slice(hash.indexOf('?') + 1))
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#termine`)
return { bestellt: query.get('bestellt') === '1', projekt: query.get('projekt') || '' }
}
const zustandVon = (projekt) => projekt?.features?.termine?.state || 'off'
const istBestellt = (zustand) => zustand === 'ordered' || zustand === 'setup'
// Entwurf: Funktionsdokument im Zustand Aus mit siteKey (wie die Kachel in funktionen.js)
const hatEntwurf = (projekt) => zustandVon(projekt) === 'off' && Boolean(projekt?.features?.termine?.siteKey)
export async function initTermine(root, { projectId } = {}) {
if (!root) return
const wunsch = projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
const rueckkehr = leseRueckkehr()
const wunsch = rueckkehr.projekt || projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
delete root.dataset.wunschProjekt
if (wunsch) auswahl.projectId = wunsch
const { projects = [], termine } = await request('/api/funktionen')
const s = { root, projects, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null }
const s = { root, projects, termine: termine || null, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null, danke: false }
S = s
if (!termine?.verfuegbar) {
root.replaceChildren(infoKarte('Der Terminplaner ist noch nicht verfügbar', 'Sobald er für dich bereit ist, findest du ihn hier.'))
@@ -36,10 +63,13 @@ export async function initTermine(root, { projectId } = {}) {
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
return
}
const zustand = (p) => p.features?.termine?.state
// Wahl wie bei der Kachel: das gewuenschte Projekt, dann aktiv, bestellt, abgeschaltet, Entwurf, das erste
s.projekt = projects.find((p) => p.projectId === auswahl.projectId)
|| projects.find((p) => zustand(p) === 'active') || projects.find((p) => zustand(p) === 'disabled') || projects[0]
|| projects.find((p) => zustandVon(p) === 'active') || projects.find((p) => istBestellt(zustandVon(p)))
|| projects.find((p) => zustandVon(p) === 'disabled') || projects.find(hatEntwurf) || projects[0]
auswahl.projectId = s.projekt.projectId
// „Danke!“ nur nach der Zahlung (bestellt=1) und nur fuer das Projekt, fuer das bezahlt wurde
s.danke = rueckkehr.bestellt && (!rueckkehr.projekt || rueckkehr.projekt === s.projekt.projectId)
s.buehne = h('div', { id: 'termine-buehne' })
root.replaceChildren(...(projects.length > 1 ? [projektwahl(s)] : []), s.buehne)
await zeigeProjekt(s)
@@ -55,6 +85,7 @@ function projektwahl(s) {
auswahl.projectId = select.value
s.projekt = s.projects.find((p) => p.projectId === select.value)
s.tag = heuteBerlin()
s.danke = false // der Dank gilt dem Projekt, mit dem man von Stripe zurueckkam
zeigeProjekt(s)
})
return h('div', { class: 'fn-project kb-panel' }, h('label', { for: 'termine-project' }, 'Website'), select)
@@ -62,6 +93,18 @@ function projektwahl(s) {
const ladeDaten = (projectId) => request(`/api/termine/einstellungen?projectId=${encodeURIComponent(projectId)}`)
/**
* Liste der Funktionen neu holen (GET /api/funktionen): Projekte, das aktuelle Projekt darin, Preis, Einrichtungsdauer und Abo-Hinweis des
* Servers. Wirft bei einem Fehler und aendert dann nichts.
*/
async function ladeFunktionen(s) {
const antwort = await request('/api/funktionen')
const projects = Array.isArray(antwort.projects) ? antwort.projects : []
s.projects = projects
s.termine = antwort.termine || null
s.projekt = projects.find((p) => p.projectId === s.projekt?.projectId) || s.projekt
}
/**
* Hinweis fuer abgeschaltete Funktionen: Grund, Loeschdatum und (ausser vor der Einrichtung) der Export-Link, der einzige Link im Hinweis.
* abgeschaltet: { grund, seit, loeschungAm } aus GET /einstellungen (fehlt bei einer aelteren Antwort: dann der bisherige Satz).
@@ -73,19 +116,13 @@ function hinweisAbgeschaltet(projectId, abgeschaltet) {
}
/**
* Je nach Zustand der Funktion: nicht freigeschaltet, Vorlage waehlen oder die Reiter. Jeder Aufruf zaehlt s.lauf hoch: die Antwort
* Je nach Zustand der Funktion die Ansicht zeigen (siehe Kopf der Datei). Jeder Aufruf zaehlt s.lauf hoch: die Antwort
* einer frueheren Website (langsam, dann zur anderen gewechselt) wird verworfen und erscheint nie unter der neuen.
*/
async function zeigeProjekt(s) {
async function zeigeProjekt(s, optionen = {}) {
const projekt = s.projekt
const lauf = ++s.lauf
const aktuell = () => S === s && s.lauf === lauf
const zustand = projekt.features?.termine?.state || 'off'
s.nurLesen = zustand !== 'active'
if (zustand !== 'active' && zustand !== 'disabled') {
s.buehne.replaceChildren(infoKarte('Der Terminplaner ist für diese Website noch nicht freigeschaltet', 'Wir schalten den Terminplaner für dich frei.', { chat: true }))
return
}
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let daten
try {
@@ -95,8 +132,39 @@ async function zeigeProjekt(s) {
return
}
if (!aktuell()) return
await zeichneAnsicht(s, { daten, aktuell, ...optionen })
}
/**
* Ansicht nach einer gescheiterten Aktion der Statuskarte neu aufbauen (Ruling R110): Liste und Daten frisch holen und erst dann zeichnen.
* Scheitert das Laden, wirft die Funktion und die Karte bleibt mit ihrer Meldung stehen. optionen: bestellformular (ordered zeigt das
* Bestellformular statt der Karte), meldung ({ text, art }, nur im Zustand ordered noch von Belang).
*/
async function ladeNeu(s, optionen = {}) {
const lauf = s.lauf
await ladeFunktionen(s)
const daten = await ladeDaten(s.projekt.projectId)
if (S !== s || s.lauf !== lauf) return // inzwischen hat ein Wechsel der Website die Ansicht ersetzt
const nr = ++s.lauf
await zeichneAnsicht(s, { daten, aktuell: () => S === s && s.lauf === nr, ...optionen })
}
/** Die Ansicht zum Zustand aus den frisch geladenen Daten (daten.state; faellt auf den Eintrag der Liste zurueck). */
async function zeichneAnsicht(s, { daten, aktuell, bestellformular = false, meldung = null }) {
const projekt = s.projekt
s.daten = daten
if (daten.einstellung) zeigeReiter(s, { projekt, aktuell })
const zustand = daten.state || zustandVon(projekt)
s.nurLesen = zustand !== 'active' && zustand !== 'off'
if (zustand === 'off') {
if (daten.abo?.ok === true) zeigeFragebogen(s, { projekt, aktuell })
else await zeigeAboHinweis(s, { aktuell })
return
}
if (istBestellt(zustand)) {
await zeigeBestellung(s, { zustand, aktuell, bestellformular, meldung })
return
}
if (daten.einstellung) zeigeReiter(s, { projekt, aktuell, zustand })
else if (s.nurLesen) {
s.buehne.replaceChildren(
hinweisAbgeschaltet(projekt.projectId, daten.abgeschaltet),
@@ -104,6 +172,82 @@ async function zeigeProjekt(s) {
} else zeigeVorlagen(s, { projekt, aktuell })
}
const neueMeldung = () => h('div', { class: 'tm-meldung', role: 'status', 'aria-live': 'polite', tabindex: '-1' })
/* ----------------------------------------------- Zustand off: Hinweis, Fragebogen --- */
/**
* Ohne laufendes Wartungs-Abo gibt es keinen Fragebogen, nur den Hinweis wie in der Kachel. Der Text kommt ausschliesslich vom Server
* (GET /api/funktionen, termine.hinweis = aboMeldung in server/services/featureOrder.js), nie aus einer Kopie im Browser; bei
* kein_abo kommt der Link zu den Vertraegen dazu (Quelle: vertraege-link.js). Fehlt der Text, weil sich das Abo seit dem Laden der Liste
* geaendert hat, wird die Liste noch einmal geholt.
*/
async function zeigeAboHinweis(s, { aktuell }) {
if (typeof s.termine?.hinweis !== 'string' || s.termine.hinweis === '') {
try {
await ladeFunktionen(s)
} catch {
// ohne Text bleibt der Hinweis bei der Ueberschrift (und dem Link bei kein_abo)
}
if (!aktuell()) return
}
const hinweis = typeof s.termine?.hinweis === 'string' ? s.termine.hinweis : ''
const link = s.termine?.bestellbar === false && s.termine.grund === 'kein_abo'
? h('button', { type: 'button', class: ZU_VERTRAEGE.klasse, 'data-view': ZU_VERTRAEGE.view }, ZU_VERTRAEGE.text)
: null
s.buehne.replaceChildren(h('div', { class: 'kb-panel tm-info', 'data-hinweis': 'abo' },
h('h3', null, 'Terminplaner einrichten'),
hinweis || link ? h('p', null, hinweis, hinweis && link ? ' ' : null, link) : null))
}
/** Der Fragebogen mit der Bestellseite danach (termine-fragebogen.js); die Meldung ueber ihm ist s.meldungEl (ctx.meldung). */
function zeigeFragebogen(s, { projekt, aktuell }) {
s.meldungEl = neueMeldung()
const wurzel = h('div', { class: 'tm-fb-wurzel' })
s.buehne.replaceChildren(s.meldungEl, wurzel)
renderFragebogen(wurzel, baueCtx(s, { projekt, aktuell }))
}
/* ----------------------------------------------- Zustand ordered, setup --- */
const GESPERRT = 'Deine Angaben sind seit der Bestellung gesperrt. Änderungen laufen über den Chat.'
/**
* Statuskarte (Zahlung offen | In Einrichtung) und darunter die Reiter zum Lesen. Der Eintrag der Liste liefert Bestelldatum, Ticketnummer und
* Rechnung; stimmt sein Zustand nicht mit dem der Daten ueberein (die Liste ist aelter, z. B. nach der Zahlung), wird sie neu geholt.
* Die Karte meldet ueber weiter(), wenn „Zahlung fortsetzen“ scheitert: ladeNeu zeichnet die Ansicht dann neu (bei 409 zustimmung_fehlt
* mit dem Bestellformular statt der Karte). Das Bestellformular (daten.bestellung, R110) zeigt dieselbe Seite wie der Fragebogen.
*/
async function zeigeBestellung(s, { zustand, aktuell, bestellformular, meldung }) {
if (s.projekt.features?.termine?.state !== zustand) {
try {
await ladeFunktionen(s)
} catch {
// es gilt der Zustand der Daten; was nur die Liste kennt (Ticketnummer, Rechnung), fehlt dann in der Karte
}
if (!aktuell()) return
}
const projekt = s.projekt
const eintrag = { ...projekt.features?.termine, state: zustand }
const danke = s.danke
s.danke = false // nur beim ersten Zeichnen nach der Rueckkehr
const formular = bestellformular && zustand === 'ordered' && Boolean(s.daten.bestellung)
const kopf = (ctx) => {
if (formular) {
const seite = h('div', { class: 'tm-fb-bestellen' })
renderBestellen(seite, { ctx, daten: s.daten, zumSchritt: () => ctx.meldung(GESPERRT, 'fehler') })
return seite
}
return statusKarte({
projekt,
bestellung: eintrag,
einrichtung: s.termine?.einrichtung,
danke,
kundenansicht: s.daten.kundenansicht === true,
meldung: zustand === 'ordered' && meldung ? meldung.text : '', // die Meldung gilt nur, solange die Bestellung noch offen ist
}, { weiter: (fall) => ladeNeu(s, { bestellformular: fall?.bestellen === true, meldung: fall?.meldung ? { text: fall.meldung } : null }) })
}
zeigeReiter(s, { projekt, aktuell, zustand, kopf, meldung: formular && meldung ? { text: meldung.text, art: 'ok' } : null })
}
/* ---------------------------------------------------- Vorlage wählen --- */
function zeigeVorlagen(s, { projekt, aktuell }) {
const fehler = h('p', { class: 'tm-fehler', role: 'alert' })
@@ -201,19 +345,27 @@ export function baueCtx(s, { projekt, aktuell, hinweise = () => {}, zumReiter =
return ctx
}
function zeigeReiter(s, { projekt, aktuell }) {
/**
* Die Reiter (Hinweisleiste, Meldung, Reiterleiste, Panel). zustand steuert, was dazugehoert: disabled hat den Banner der Abschaltung,
* active den Hinweis auf fehlende Pflichtangaben, ordered und setup keinen der beiden und keinen Reiter „Termine“ (der Server antwortet
* dort mit 409). kopf(ctx) liefert ein Element, das ueber der Meldung steht (die Statuskarte), meldung ({ text, art }) wird nach dem
* ersten Zeichnen angezeigt. Ohne Einstellung gibt es keine Reiter, dann bleibt es beim Kopf.
*/
function zeigeReiter(s, { projekt, aktuell, zustand = s.nurLesen ? 'disabled' : 'active', kopf = null, meldung = null }) {
const projectId = projekt.projectId
const nurLesen = s.nurLesen
s.meldungEl = h('div', { class: 'tm-meldung', role: 'status', 'aria-live': 'polite', tabindex: '-1' })
s.meldungEl = neueMeldung()
const ctx = baueCtx(s, { projekt, aktuell, hinweise: zeichneHinweise, zumReiter: waehleReiter })
const reiterListe = istBestellt(zustand) ? REITER.filter((r) => r.key !== 'termine') : REITER
const kopfEl = kopf ? kopf(ctx) : null
// Hinweisleiste: abgeschaltet, oder Pflichtangaben fuer Gaeste fehlen (GET /einstellungen: fehlt). Sie folgt s.daten.
const hinweisBox = h('div', { class: 'tm-hinweise' })
function zeichneHinweise() {
const hinweise = []
if (nurLesen) {
if (zustand === 'disabled') {
hinweise.push(hinweisAbgeschaltet(projectId, s.daten.abgeschaltet))
} else if ((s.daten.fehlt || []).length) {
} else if (!nurLesen && (s.daten.fehlt || []).length) {
hinweise.push(h('div', { class: 'tm-box tm-box-warn', role: 'note' },
h('span', null, `Noch nicht buchbar: Es fehlt ${fehltText(s.daten.fehlt) || 'eine Angabe zu deinem Betrieb'}. Solange das fehlt, können Gäste über deine Website nicht buchen.`),
h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => waehleReiter('einstellungen') }, 'Zu den Einstellungen')))
@@ -222,24 +374,33 @@ function zeigeReiter(s, { projekt, aktuell }) {
}
zeichneHinweise()
if (!s.daten.einstellung) {
s.buehne.replaceChildren(...[hinweisBox, kopfEl, s.meldungEl].filter(Boolean))
if (meldung) zeigeMeldung(s, meldung.text, meldung.art)
return
}
const panel = h('div', { class: 'tm-panel', role: 'tabpanel', id: 'tm-panel', tabindex: '0' })
const knoepfe = REITER.map((r) => h('button', {
let aktiv = null // Schluessel des gezeigten Reiters
const knoepfe = reiterListe.map((r) => h('button', {
type: 'button', role: 'tab', class: 'tm-reiter', id: `tm-tab-${r.key}`, 'aria-controls': 'tm-panel', 'data-reiter': r.key,
onclick: () => waehleReiter(r.key),
}, r.label))
const leiste = h('div', { class: 'tm-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Terminplaners' }, knoepfe)
leiste.addEventListener('keydown', (ev) => {
const i = REITER.findIndex((r) => r.key === auswahl.reiter)
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
const i = reiterListe.findIndex((r) => r.key === aktiv)
const ziel = { ArrowRight: (i + 1) % reiterListe.length, ArrowLeft: (i + reiterListe.length - 1) % reiterListe.length, Home: 0, End: reiterListe.length - 1 }[ev.key]
if (ziel === undefined) return
ev.preventDefault()
waehleReiter(REITER[ziel].key)
waehleReiter(reiterListe[ziel].key)
knoepfe[ziel].focus()
})
function waehleReiter(key) {
const reiter = REITER.find((r) => r.key === key) || REITER[0]
auswahl.reiter = reiter.key
function waehleReiter(key, { merken = true } = {}) {
// ein Reiter, den es in diesem Zustand nicht gibt (Termine in ordered und setup), wird nicht gemerkt: die Auswahl gilt fuer die naechste Website weiter
const reiter = reiterListe.find((r) => r.key === key) || reiterListe[0]
if (merken && reiter.key === key) auswahl.reiter = reiter.key
aktiv = reiter.key
for (const k of knoepfe) {
const an = k.dataset.reiter === reiter.key
k.setAttribute('aria-selected', String(an))
@@ -253,7 +414,7 @@ function zeigeReiter(s, { projekt, aktuell }) {
.catch((err) => { if (aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
}
s.buehne.replaceChildren(hinweisBox, s.meldungEl, leiste, panel)
waehleReiter(auswahl.reiter)
s.buehne.replaceChildren(...[hinweisBox, kopfEl, s.meldungEl, leiste, panel].filter(Boolean)) // replaceChildren(null) schriebe „null“ in die Seite
waehleReiter(auswahl.reiter, { merken: false })
if (meldung) zeigeMeldung(s, meldung.text, meldung.art)
}
+5
View File
@@ -0,0 +1,5 @@
/* Der Link „Zu Verträge“ in den Hinweisen ohne Wartungs-Abo (Grund kein_abo): die einzige Quelle für Text, Ziel und Klasse. Gelesen von
funktionen.js (Kachel Analyse, Analyse-Seite, Kachel Terminplaner; baut daraus HTML) und von termine.js (Hinweis in der Ansicht #termine;
baut daraus ein Element). Reine Daten ohne Import: beide laden das Modul relativ, funktionen.js behält seine zwei absoluten Importe.
Die Hinweistexte selbst stehen nicht hier: sie kommen vom Server (aboMeldung in server/services/featureOrder.js). */
export const ZU_VERTRAEGE = Object.freeze({ text: 'Zu Verträge', view: 'vertraege', klasse: 'kb-link' })