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:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 06:35:42 +00:00
1 parent 2fc48df915
commit 5f99f08a39
8 files changed
+314 -39

No files matched your search

+39 -9
View File
@@ -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
}