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:
1 parent
52456dac76
commit
87d267e51a
13 files changed
+1077
-71
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
Reference in new issue
Block a user