Shop G1: Verdrahtung #webshop, Webshop-Kachel, Bestellseite mit key und optionaler Zusammenfassung

Dashboard-Abschnitt view-webshop, shop.css und Platzhalter shop.js, app.js (Hash, Refresher, Label, Elternansicht), Kachel in funktionen.js nach shop.verfuegbar, GET /api/funktionen liefert shop.verfuegbar, funktion-bestellen.js nimmt key/name/zusammenfassung (Standard: Terminplaner unverändert).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:57:02 +00:00
1 parent 0ebd2e6663
commit 809da80c9b
11 files changed
+333 -19

No files matched your search

+24 -15
View File
@@ -4,12 +4,15 @@
Es gibt keine Texte des Registers im Client: Knopf, Haken, Belehrung, Preis und Unterlagen kommen als `bestellung` aus
GET /api/termine/einstellungen (Server: bestellungAnsicht), und der Knopf zeigt genau bestellung.consent.knopf, denselben Text, den der
Server im Nachweis speichert. Fremdtext kommt nur über textContent in die Seite (h aus termine-dom.js), nie über innerHTML.
Zusammenfassung: zusammenfassung() aus termine-view.js. Checkout: POST /api/funktionen/termine/checkout (Server: routes/funktionen.js). */
Zusammenfassung: zusammenfassung() aus termine-view.js (Standard für key 'termine'; andere Funktionen übergeben eine eigene Funktion oder
zeigen keine). Checkout: POST /api/funktionen/<key>/checkout (Server: routes/funktionen.js), key ist standardmäßig 'termine'. */
import { fehlerKarte, h, request } from './termine-dom.js'
import { FRAGEBOGEN_SCHRITTE, zusammenfassung } from './termine-view.js'
const CHECKOUT = '/api/funktionen/termine/checkout'
const STANDARD_KEY = 'termine'
// Checkout-Pfad der Funktion; der Schlüssel ist ein Registerschlüssel (Kleinbuchstaben), alles andere fällt auf den Terminplaner zurück
const checkoutPfad = (key) => `/api/funktionen/${typeof key === 'string' && /^[a-z][a-z0-9-]{0,31}$/.test(key) ? key : STANDARD_KEY}/checkout`
const RECHTLICHES = '/rechtliches/'
const KUNDENANSICHT = 'In der Kundenansicht kann nicht bestellt werden.'
const ZAHLUNGSSEITE = 'Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuch es noch einmal oder schreib uns im Chat.'
@@ -113,12 +116,16 @@ function ladeBeiRueckkehrNeu(element) {
* 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).
* Weitere Optionen: key = Schlüssel der Funktion im Register (Standard 'termine', bestimmt den Checkout-Pfad), zusammenfassung =
* Funktion (einstellung, fragebogen, branchen) => [{ schritt, titel, zeilen }]; ohne sie zeigt nur der Terminplaner seine Zusammenfassung,
* andere Funktionen lassen den Block weg.
*/
export function renderBestellen(root, { ctx, daten, zumSchritt } = {}) {
return baueSeite(root, { ctx, daten, zumSchritt }, {})
export function renderBestellen(root, { ctx, daten, zumSchritt, key = STANDARD_KEY, zusammenfassung: gruppenFunktion } = {}) {
const gruppenFn = typeof gruppenFunktion === 'function' ? gruppenFunktion : key === STANDARD_KEY ? zusammenfassung : null
return baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, {})
}
function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
function baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, uebernahme) {
const roh = daten?.bestellung
if (!roh || !istConsent(roh.consent)) {
const karte = fehlerKarte('Die Bestellseite konnte gerade nicht geladen werden. Bitte lade die Seite neu.')
@@ -140,7 +147,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
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 },
const gruppen = (gruppenFn ? gruppenFn(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),
springt
@@ -200,8 +207,8 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
const seite = h('section', { class: 'kb-panel tm-bestellen' },
h('h3', null, 'Prüfen und bestellen'),
h('p', { class: 'tm-abschnitt-text tm-hinweis' }, 'Lies alles in Ruhe durch. Mit „Ändern“ kommst du zu dem Schritt zurück, in dem du die Angabe gemacht hast.'),
h('div', { class: 'tm-bestellen-zusammenfassung' }, gruppen),
gruppenFn ? h('p', { class: 'tm-abschnitt-text tm-hinweis' }, 'Lies alles in Ruhe durch. Mit „Ändern“ kommst du zu dem Schritt zurück, in dem du die Angabe gemacht hast.') : null,
gruppenFn ? h('div', { class: 'tm-bestellen-zusammenfassung' }, gruppen) : null,
offenBlock,
preisBlock,
belehrungBlock,
@@ -246,7 +253,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
karte.focus({ preventScroll: true })
return
}
const neueSeite = baueSeite(root, { ctx, daten: neu, zumSchritt }, {
const neueSeite = baueSeite(root, { ctx, daten: neu, zumSchritt, key, gruppenFn }, {
offen: err.code === 'bestellung_offen' ? err.body?.offen : undefined,
consent: err.code === 'fassung_veraltet' ? err.body?.consent : undefined,
})
@@ -264,7 +271,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
aktualisiere()
zeigeMeldung('')
try {
const antwort = await request(CHECKOUT, { method: 'POST', body: bestellKoerper({ projectId: ctx?.projectId, bestellung, gewaehlt }) })
const antwort = await request(checkoutPfad(key), { method: 'POST', body: bestellKoerper({ projectId: ctx?.projectId, bestellung, gewaehlt }) })
if (geheZu(antwort?.url)) return // die Seite wird verlassen, der Knopf bleibt gesperrt
zeigeMeldung(ZAHLUNGSSEITE)
} catch (err) {
@@ -288,13 +295,15 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
* 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, meldung = Text, der am Knopf „Zahlung fortsetzen“ steht (z. B. der Fehler eines Versuchs vor dem Neuzeichnen).
* key = Schlüssel der Funktion (Standard 'termine', Checkout-Pfad), name = Name der Funktion in
* den Texten (Standard 'Terminplaner'), hinweisEinrichtung = Satz unter den Fakten im Zustand setup (Standard: Einstellungen gesperrt).
* 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, meldung } = {}, { weiter } = {}) {
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht, meldung, key = STANDARD_KEY, name = 'Terminplaner', hinweisEinrichtung } = {}, { weiter } = {}) {
const bestellt = bestellung?.state === 'ordered'
const datum = datumTag(bestellung?.orderedAt)
const ticket = bestellung?.ticketWoid
@@ -317,7 +326,7 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
fortsetzenKnopf.disabled = true
meldungEl.textContent = ''
try {
const antwort = await request(CHECKOUT, { method: 'POST', body: fortsetzenKoerper(projekt?.projectId) })
const antwort = await request(checkoutPfad(key), { method: 'POST', body: fortsetzenKoerper(projekt?.projectId) })
if (geheZu(antwort?.url)) return // die Seite wird verlassen
meldungEl.textContent = ZAHLUNGSSEITE
} catch (err) {
@@ -341,12 +350,12 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
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'),
h('p', null, bestellt ? 'Sobald deine Zahlung bei uns ankommt, legen wir mit der Einrichtung los.' : 'Sobald die Einrichtung fertig ist, schalten wir deinen Terminplaner für dich frei.'),
h('h3', null, bestellt ? 'Deine Bestellung wartet auf die Zahlung' : `Wir richten deinen ${text(name) || 'Terminplaner'} ein`),
h('p', null, bestellt ? 'Sobald deine Zahlung bei uns ankommt, legen wir mit der Einrichtung los.' : `Sobald die Einrichtung fertig ist, schalten wir deinen ${text(name) || 'Terminplaner'} für dich frei.`),
fakten.length > 0 ? h('dl', { class: 'tm-fakten' }, fakten) : null,
bestellt
? h('p', { class: 'tm-hinweis' }, 'Zahlst du per SEPA-Lastschrift, kann es einige Tage dauern, bis die Zahlung bei uns ankommt.')
: h('p', { class: 'tm-hinweis' }, 'Deine Einstellungen sind jetzt gesperrt, Änderungen laufen über den Chat.'),
: h('p', { class: 'tm-hinweis' }, text(hinweisEinrichtung) || 'Deine Einstellungen sind jetzt gesperrt, Änderungen laufen über den Chat.'),
meldungEl,
bestellt && kundenansicht === true ? h('p', { class: 'tm-hinweis' }, KUNDENANSICHT) : null,
h('div', { class: 'tm-aktionen' },