TP2 Abschlussprüfung: Fragebogen und Bestellseite (veralteter Tab, Abo-Hinweis, Zurück aus Stripe)
- PUT /fragebogen: ein Tab mit veralteter Branche bekommt 409 EINSTELLUNG_VERALTET (die Branche wechselt nur die Vorlage), bestaetigt wird mit dem gespeicherten Stand vereinigt; die ungeprüften Vorlagenpreise können nicht mehr als geprüft gelten - GET /einstellungen liefert in off aboHinweis (aboMeldung, auch bei nicht lesbarem Abo); die Hinweiskarte zeigt ihn immer und hat „Neu laden“ - Bestellseite und Statuskarte laden bei pageshow (persisted) neu, wenn der Browser sie nach der Zahlungsseite aus dem bfcache holt - renderBestellen zeichnet „Ändern“ und „Zum Schritt“ nur mit zumSchritt; nach einem 409 bekommt die Meldung den Fokus - Zusammenfassung nennt „Kleinunternehmer: ja/nein“ Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2fc48df915
commit
5f99f08a39
8 files changed
+314
-39
No files matched your search
@@ -84,13 +84,32 @@ const offeneListe = (wert) => (Array.isArray(wert) ? wert.filter((punkt) => punk
|
||||
|
||||
const chatKnopf = () => h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Frage dazu? Chat öffnen')
|
||||
|
||||
/* ----------------------------------------------- Rückkehr von der Zahlungsseite --- */
|
||||
/**
|
||||
* Der Browser-Zurück-Knopf der Zahlungsseite stellt die Seite oft aus dem Speicher (bfcache) so wieder her, wie sie verlassen wurde: der Knopf bleibt
|
||||
* gesperrt, es steht keine Meldung da. Darum laden Bestellseite und Statuskarte bei pageshow mit persisted neu (wie app.js beim Wechsel-Knopf).
|
||||
* Gilt nur, solange das Element noch im Baum steht; danach entfernt sich der Handler beim nächsten pageshow selbst.
|
||||
*/
|
||||
function ladeBeiRueckkehrNeu(element) {
|
||||
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return
|
||||
const beiAnzeige = (ev) => {
|
||||
if (!element.isConnected) {
|
||||
window.removeEventListener('pageshow', beiAnzeige)
|
||||
return
|
||||
}
|
||||
if (ev?.persisted === true) window.location.reload()
|
||||
}
|
||||
window.addEventListener('pageshow', beiAnzeige)
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Bestellseite --- */
|
||||
/**
|
||||
* „Prüfen und bestellen“ in root zeichnen und die Seite zurückgeben. Reihenfolge: Zusammenfassung der Angaben (je Gruppe „Ändern“),
|
||||
* offene Punkte (je Punkt „Zum Schritt“), Preis und Einrichtungsdauer, die vollständige Widerrufsbelehrung samt Muster-Formular als Text
|
||||
* (ein scrollbarer Bereich, nie eingeklappt), die Unterlagen als Links, die Zustimmungen und zuletzt der Knopf.
|
||||
* ctx = Kontext der Ansicht (projectId, aktuell(), neuLaden()), daten = Antwort von GET /api/termine/einstellungen im Zustand off
|
||||
* (einstellung, fragebogen, branchen, offen, kundenansicht, bestellung), zumSchritt(key) springt in einen Schritt des Fragebogens.
|
||||
* (einstellung, fragebogen, branchen, offen, kundenansicht, bestellung), zumSchritt(key) springt in einen Schritt des Fragebogens (ist es keine Funktion,
|
||||
* zeichnet die Seite weder „Ändern“ noch „Zum Schritt“).
|
||||
* Der Knopf ist gesperrt, solange ein Haken fehlt, offene Punkte bleiben oder die Kundenansicht läuft. Ein Klick sperrt ihn, sendet
|
||||
* bestellKoerper und geht zur Zahlungsseite. Bei 409 lädt die Seite die Daten neu und setzt die Haken zurück (bestellung_offen zeigt die
|
||||
* Punkte aus der Antwort, fassung_veraltet die neue Fassung aus der Antwort, ein Abo-Code den Text der Antwort).
|
||||
@@ -116,15 +135,19 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
const gewaehlt = new Set()
|
||||
let laeuft = false
|
||||
|
||||
const gehe = (schritt) => { if (typeof zumSchritt === 'function') zumSchritt(schritt) }
|
||||
// „Ändern“ und „Zum Schritt“ gibt es nur, wo es ein Ziel gibt: ohne zumSchritt zeichnet die Seite keine Knöpfe, die nichts tun
|
||||
const springt = typeof zumSchritt === 'function'
|
||||
const gehe = (schritt) => { if (springt) zumSchritt(schritt) }
|
||||
|
||||
// 1 Zusammenfassung: je Gruppe die Angaben als Text und „Ändern“
|
||||
const gruppen = zusammenfassung(daten.einstellung, daten.fragebogen, daten.branchen).map((gruppe) => h('div', { class: 'tm-bestellen-gruppe', 'data-schritt': gruppe.schritt },
|
||||
h('div', { class: 'tm-bestellen-gruppe-kopf' },
|
||||
h('h4', null, gruppe.titel),
|
||||
h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'aendern', 'data-schritt': gruppe.schritt, 'aria-label': `Ändern: ${gruppe.titel}`, onclick: () => gehe(gruppe.schritt),
|
||||
}, 'Ändern')),
|
||||
springt
|
||||
? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'aendern', 'data-schritt': gruppe.schritt, 'aria-label': `Ändern: ${gruppe.titel}`, onclick: () => gehe(gruppe.schritt),
|
||||
}, 'Ändern')
|
||||
: null),
|
||||
gruppe.zeilen.length > 0 ? h('ul', null, gruppe.zeilen.map((zeile) => h('li', null, zeile))) : h('p', { class: 'tm-hinweis' }, 'Noch keine Angaben.')))
|
||||
|
||||
// 2 offene Punkte: „Zum Schritt“ nur dort, wo es einen Schritt des Fragebogens gibt (der Abo-Punkt gehört zu dieser Seite)
|
||||
@@ -133,7 +156,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
h('p', { class: 'tm-hinweis' }, 'Erledige diese Punkte, dann kannst du bestellen.'),
|
||||
h('ul', null, offen.map((punkt) => h('li', null,
|
||||
h('span', null, punkt.text),
|
||||
SCHRITT_TITEL.has(punkt.schritt)
|
||||
springt && SCHRITT_TITEL.has(punkt.schritt)
|
||||
? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'zum-schritt', 'data-schritt': punkt.schritt, 'aria-label': `Zum Schritt „${SCHRITT_TITEL.get(punkt.schritt)}“`, onclick: () => gehe(punkt.schritt),
|
||||
}, 'Zum Schritt')
|
||||
@@ -160,7 +183,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
|
||||
// 6 Zustimmungen in Registerreihenfolge: Einträge mit haken: true sind Haken, die anderen reiner Text
|
||||
const knopf = h('button', { type: 'button', class: 'kb-btn-primary tm-bestellen-knopf', 'data-aktion': 'bestellen', disabled: true, onclick: () => bestelle() }, text(bestellung.consent.knopf))
|
||||
const meldungEl = h('p', { class: 'tm-fehler tm-bestellen-meldung', role: 'alert', 'data-meldung': 'bestellen' })
|
||||
const meldungEl = h('p', { class: 'tm-fehler tm-bestellen-meldung', role: 'alert', 'data-meldung': 'bestellen', tabindex: '-1' })
|
||||
const hinweisEl = h('p', { class: 'tm-hinweis', 'data-hinweis': 'bestellen' })
|
||||
const hakenZeile = (eintrag) => {
|
||||
const feld = h('input', { type: 'checkbox', 'data-haken': eintrag.id })
|
||||
@@ -186,6 +209,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
zustimmungBlock,
|
||||
h('div', { class: 'tm-bestellen-aktion' }, meldungEl, knopf, hinweisEl))
|
||||
root.replaceChildren(seite)
|
||||
ladeBeiRueckkehrNeu(seite)
|
||||
|
||||
function zeigeMeldung(meldung) {
|
||||
meldungEl.textContent = meldung
|
||||
@@ -210,13 +234,16 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
}
|
||||
if (neu === null || typeof neu !== 'object') {
|
||||
zeigeMeldung(err.message)
|
||||
meldungEl.focus({ preventScroll: true })
|
||||
laeuft = false
|
||||
aktualisiere()
|
||||
return
|
||||
}
|
||||
if (typeof ctx.aktuell === 'function' && !ctx.aktuell()) return
|
||||
if (!neu.bestellung) {
|
||||
root.replaceChildren(h('div', { class: 'kb-panel tm-info', role: 'status' }, h('p', null, err.message), h('p', null, 'Lade die Seite neu, dann siehst du den aktuellen Stand.')))
|
||||
const karte = h('div', { class: 'kb-panel tm-info', role: 'status', tabindex: '-1' }, h('p', null, err.message), h('p', null, 'Lade die Seite neu, dann siehst du den aktuellen Stand.'))
|
||||
root.replaceChildren(karte)
|
||||
karte.focus({ preventScroll: true })
|
||||
return
|
||||
}
|
||||
const neueSeite = baueSeite(root, { ctx, daten: neu, zumSchritt }, {
|
||||
@@ -226,6 +253,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
const neueMeldung = neueSeite.querySelector('[data-meldung="bestellen"]')
|
||||
if (neueMeldung) {
|
||||
neueMeldung.textContent = err.message
|
||||
neueMeldung.focus({ preventScroll: true })
|
||||
neueMeldung.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
}
|
||||
@@ -310,7 +338,7 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
|
||||
fortsetzenKnopf.disabled = false
|
||||
}
|
||||
|
||||
return h('section', { class: 'kb-panel tm-statuskarte', 'data-state': bestellt ? 'ordered' : 'setup' },
|
||||
const karte = h('section', { class: 'kb-panel tm-statuskarte', 'data-state': bestellt ? 'ordered' : 'setup' },
|
||||
danke ? h('p', { class: 'tm-danke', role: 'status', 'data-danke': true }, DANKE) : null,
|
||||
h('span', { class: 'tm-pille tm-pille-wait' }, bestellt ? 'Zahlung offen' : 'In Einrichtung'),
|
||||
h('h3', null, bestellt ? 'Deine Bestellung wartet auf die Zahlung' : 'Wir richten deinen Terminplaner ein'),
|
||||
@@ -325,4 +353,6 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
|
||||
fortsetzenKnopf,
|
||||
!bestellt && rechnung ? h('a', { class: 'kb-btn-outline', href: sicherLink(rechnung), target: '_blank', rel: 'noopener noreferrer' }, 'Rechnung ansehen') : null,
|
||||
chatKnopf()))
|
||||
ladeBeiRueckkehrNeu(karte)
|
||||
return karte
|
||||
}
|
||||
@@ -655,6 +655,7 @@ export function zusammenfassung(einstellung, fragebogen, branchen) {
|
||||
zeile('branche', istText(branche?.name) && `Branche: ${branche.name}`)
|
||||
zeile('branche', istText(betrieb.name) && `Betrieb: ${betrieb.name}`)
|
||||
zeile('branche', istText(betrieb.adresse) && `Anschrift: ${betrieb.adresse}`)
|
||||
zeile('branche', typeof betrieb.kleinunternehmer === 'boolean' && `Kleinunternehmer: ${betrieb.kleinunternehmer ? 'ja' : 'nein'}`)
|
||||
|
||||
// 2. Wer wird gebucht: Personen und Plaetze getrennt, in der Reihenfolge ihres ersten Auftretens
|
||||
const nachTyp = new Map()
|
||||
|
||||
+14
-8
@@ -176,13 +176,16 @@ const neueMeldung = () => h('div', { class: 'tm-meldung', role: 'status', 'aria-
|
||||
|
||||
/* ----------------------------------------------- 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.
|
||||
* Ohne laufendes Wartungs-Abo gibt es keinen Fragebogen, nur den Hinweis. Der Text kommt ausschliesslich vom Server, nie aus einer Kopie im
|
||||
* Browser: aboHinweis aus GET /api/termine/einstellungen (derselbe Aufruf, der den Zustand liefert, also auch dann gefuellt, wenn das Abo nicht
|
||||
* lesbar war oder sich seit dem Laden der Liste geaendert hat), ersatzweise termine.hinweis aus GET /api/funktionen (beide: aboMeldung in
|
||||
* server/services/featureOrder.js). Fehlt der Text auch dort, wird die Liste noch einmal geholt. Bei kein_abo kommt der Link zu den
|
||||
* Vertraegen dazu (Quelle: vertraege-link.js). „Neu laden“ holt Daten und Ansicht neu, damit der Kunde nach einer Aenderung des Abos nicht
|
||||
* an der Karte haengen bleibt.
|
||||
*/
|
||||
async function zeigeAboHinweis(s, { aktuell }) {
|
||||
if (typeof s.termine?.hinweis !== 'string' || s.termine.hinweis === '') {
|
||||
const textVon = (wert) => (typeof wert === 'string' ? wert : '')
|
||||
if (textVon(s.daten?.aboHinweis) === '' && textVon(s.termine?.hinweis) === '') {
|
||||
try {
|
||||
await ladeFunktionen(s)
|
||||
} catch {
|
||||
@@ -190,13 +193,16 @@ async function zeigeAboHinweis(s, { aktuell }) {
|
||||
}
|
||||
if (!aktuell()) return
|
||||
}
|
||||
const hinweis = typeof s.termine?.hinweis === 'string' ? s.termine.hinweis : ''
|
||||
const link = s.termine?.bestellbar === false && s.termine.grund === 'kein_abo'
|
||||
const hinweis = textVon(s.daten?.aboHinweis) || textVon(s.termine?.hinweis)
|
||||
const keinAbo = s.daten?.abo?.grund === 'kein_abo' || (s.termine?.bestellbar === false && s.termine.grund === 'kein_abo')
|
||||
const link = keinAbo
|
||||
? 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))
|
||||
hinweis || link ? h('p', null, hinweis, hinweis && link ? ' ' : null, link) : null,
|
||||
h('div', { class: 'tm-aktionen' },
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'neu-laden', onclick: () => zeigeProjekt(s) }, 'Neu laden'))))
|
||||
}
|
||||
|
||||
/** Der Fragebogen mit der Bestellseite danach (termine-fragebogen.js); die Meldung ueber ihm ist s.meldungEl (ctx.meldung). */
|
||||
|
||||
Reference in new issue
Block a user