- public/termine-fragebogen.js: Schritt 4 Zeiten (je Person/Platz wochenplanFeld, Bundesland ohne Vorauswahl mit leerer erster Option, "An Feiertagen geschlossen" im PATCH, danach PUT mit bundeslandGewaehlt; darunter abwesenheiten und sonderoeffnungen mit eigenem Container und eigenem zeichne, damit ungespeicherte Zeiten bleiben), Schritt 5 Regeln (Sammelknopf Alle sofort buchbar / Alle nur als Anfrage mit buchungsartFuerAlle und buchungsartLage, regeln, storno mit Muster nach Absagefrist, rechtliches, zahlung), Schritt 6 Gastfelder, Schritt 7 mit gemeinsamem leseAbschnitte, Schritt 8 Website (betrieb nur Telefon und vier Links, Einbauwuensche Modus/Knopftext/Stelle/Gestaltung/Bemerkung im PUT, Hinweis auf Mehrwuensche nach Stunden); danach renderBestellen. Platzhalter-Schritt entfernt. - termine.css: Klassen der Schritte 4 bis 8 - Tests: termineView (Importe, PATCH/PUT nur an erlaubten Stellen, Bundesland ohne Vorauswahl im Quelltext), termineFragebogenView (Verhalten mit Fake-DOM fuer die Schritte 4 bis 8, Schluesselmengen, Bestellseite nach Schritt 8) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
729 lines
40 KiB
JavaScript
729 lines
40 KiB
JavaScript
/* Terminplaner im Kundenbereich: Fragebogen vor der Bestellung (TP2). renderFragebogen(root, ctx) zeigt in der Huelle (termine.js) die acht Schritte
|
|
und danach die Bestellseite (funktion-bestellen.js). ctx ist der Kontext der Reiter (baueCtx in termine.js, hinweise() bleibt leer); ctx.daten ist die
|
|
Antwort von GET /api/termine/einstellungen im Zustand off (einstellung, version, fragebogen, branchen, betriebVorschlag, kundenansicht, offen, bestellung).
|
|
Aufbau: Fortschrittsleiste (<ol>, aria-current="step"; am Handy dazu „Schritt n von 8“), darunter der Schritt. Der Start ist der erste offene
|
|
Schritt (alles bestaetigt: die Bestellseite); die Leiste erlaubt freie Navigation, nur ohne Entwurf fuehrt sie allein in die Branchenwahl.
|
|
Jeder Schritt steht in BAU: key ⇒ (t) ⇒ Element mit t = { ctx, key, nr, titel, gehe(key), neuZeichnen(), weiter(), zurueck }. Ein Schritt mit
|
|
Feldern ist ein <form>; „Weiter“ (verdrahte) macht genau EINEN PATCH ueber den neuerSpeicher des Schritts (termine-speichern.js, der einzige
|
|
PATCH-Weg; nur wenn der Schritt Felder hat) und danach PUT /fragebogen mit dem Schritt in bestaetigt (bestaetige, der einzige PUT). Fehler stehen am
|
|
Feld (setzeFehler); beim Wechsel bekommt die Ueberschrift des Schritts den Fokus. Kundenansicht (daten.kundenansicht): PATCH bleibt moeglich, es gibt
|
|
keinen PUT, denn Schritte bestaetigt nur der Kunde selbst. Die Schritte bauen auf den Abschnitten der Reiter auf (wochenplanFeld, abwesenheiten,
|
|
sonderoeffnungen, betrieb, darstellung, regeln, gastFelder, erinnerung, storno, rechtliches, zahlung); hier steht nur, was der Fragebogen daraus macht.
|
|
Nach Schritt 8 kommt die Bestellseite (renderBestellen). Texte der Kunden kommen nur ueber value/textContent in die Seite (h aus termine-dom.js),
|
|
nie als HTML. */
|
|
|
|
import {
|
|
BUNDESLAENDER, FRAGEBOGEN_SCHRITTE, LIMITS, MAX, buchungsartFuerAlle, buchungsartLage, ersterOffenerSchritt, preisAnzeige, ressourcenAusNamen, schrittStatus,
|
|
} from './termine-view.js'
|
|
import { abschnitt, allgemeinerFehler, feld, frage, h, hakenFeld, setzeFehler, zeigeFehler } from './termine-dom.js'
|
|
import { neuerSpeicher, stelleVon } from './termine-speichern.js'
|
|
import { render as renderLeistungen } from './termine-tab-leistungen.js'
|
|
import { wochenplanFeld } from './termine-tab-team.js'
|
|
import { abwesenheiten, sonderoeffnungen } from './termine-team-ausnahmen.js'
|
|
import { betrieb, darstellung, erinnerung, gastFelder, rechtliches, regeln, storno, zahlung } from './termine-tab-einstellungen.js'
|
|
import { renderBestellen } from './funktion-bestellen.js'
|
|
|
|
const BESTELLEN = 'bestellen'
|
|
const TITEL = new Map([...FRAGEBOGEN_SCHRITTE, [BESTELLEN, 'Prüfen und bestellen']])
|
|
const AUFBEWAHRUNG = 'Wir speichern deinen Entwurf bis zu 90 Tage nach deiner letzten Änderung.'
|
|
const KUNDENANSICHT = 'Du siehst den Fragebogen in der Kundenansicht. Angaben kannst du ändern, Schritte bestätigen und bestellen kann nur der Kunde selbst.'
|
|
const GESUNDHEIT_TEXT = 'Für den Gesundheitsbereich ist der Terminplaner nicht verfügbar. Schon der Name einer Leistung oder ein Terminwunsch kann etwas über die Gesundheit deiner Gäste verraten, '
|
|
+ 'und solche Angaben sind besonders geschützt. Gehört dein Angebot nicht zum Gesundheitsbereich, wähle die Branche, die am besten passt. Fragen dazu beantworten wir dir im Chat.'
|
|
const WECHSEL_TEXT = 'Das ersetzt deine Leistungen, Zeiten und Regeln durch die Vorschläge der neuen Branche. Alle Bestätigungen gehen zurück, du bestätigst die Schritte danach noch einmal. '
|
|
+ 'Deine Angaben zum Betrieb bleiben erhalten.'
|
|
const NACH_VORLAGE_FEHLT = 'Deine Branche ist angelegt, aber nicht alle Angaben konnten gespeichert werden. Bitte prüfe sie und klick auf „Weiter“.'
|
|
const NACH_VORLAGE_LADEN = 'Deine Branche ist angelegt. Wir konnten den Stand aber nicht laden. Bitte lade die Seite neu.'
|
|
|
|
// Betriebsangaben von Schritt 1, ohne Entwurf und mit Entwurf
|
|
const BETRIEB_NUR = ['name', 'adresse', 'kleinunternehmer']
|
|
// POST vorlage uebernimmt aus betrieb nur diese Felder (server/routes/termine.js) ...
|
|
const KONTAKT_FELDER = ['name', 'adresse', 'telefon', 'email', 'websiteUrl']
|
|
// ... diese bleiben dabei auf der Strecke; der Client traegt sie mit einem PATCH nach (betriebNachtrag)
|
|
const NACHTRAG_FELDER = ['kleinunternehmer', 'impressumUrl', 'datenschutzUrl', 'agbUrl']
|
|
|
|
const WER_ARTEN = [
|
|
['einzel', 'Einzelperson', 'Eine Person wird gebucht, zum Beispiel du selbst.'],
|
|
['team', 'Team', 'Mehrere Personen können gebucht werden, jede mit eigenen Zeiten.'],
|
|
['plaetze', 'Plätze', 'Gebucht werden Plätze oder Räume, zum Beispiel Stühle oder Zimmer.'],
|
|
]
|
|
|
|
// Schritt 4: ohne Wahl des Bundeslands geht es nicht weiter (der Serverstandard RP im Entwurf ist keine Wahl)
|
|
const LAND_FEHLT = 'Bitte wähle das Bundesland deines Betriebs. Danach richten sich die Feiertage.'
|
|
|
|
// Schritt 5: Lage der Buchungsarten aller Leistungen (Schluessel wie buchungsartLage) und die zwei Sammelknoepfe
|
|
const LAGE_TEXT = {
|
|
sofort: 'Alle Leistungen sind sofort buchbar.',
|
|
anfrage: 'Alle Leistungen sind nur als Anfrage buchbar.',
|
|
gemischt: 'Die Leistungen sind gemischt: je Leistung sofort buchbar oder nur als Anfrage.',
|
|
}
|
|
const SAMMEL_ARTEN = [['sofort', 'Alle sofort buchbar'], ['anfrage', 'Alle nur als Anfrage']]
|
|
|
|
// Schritt 8: Einbauwuensche (Werte und Grenzen wie normalisiereFragebogen auf dem Server)
|
|
const EINBAU_MODI = [
|
|
['inline', 'Direkt auf der Seite', 'Der Terminplaner steht offen an der Stelle, die du uns nennst.'],
|
|
['knopf', 'Hinter einem Knopf', 'Gäste klicken auf einen Knopf, dann öffnet sich der Terminplaner.'],
|
|
]
|
|
const EINBAU_MAX = { knopfText: 40, stelle: 300, gestaltung: 1000, bemerkung: 1000 }
|
|
const MODUS_FEHLT = 'Bitte wähle, ob der Terminplaner direkt auf deiner Seite oder hinter einem Knopf erscheint.'
|
|
const KNOPFTEXT_FEHLT = 'Bitte gib an, was auf dem Knopf stehen soll, hinter dem sich der Terminplaner öffnet.'
|
|
const MEHRWUENSCHE = 'Der Einbau umfasst den Terminplaner an einer Stelle deiner Website. Ein zweites Widget, Knöpfe für einzelne Leistungen und ein Sonderdesign gehen nach Stunden.'
|
|
const WEBSITE_NUR = ['telefon', 'impressumUrl', 'datenschutzUrl', 'agbUrl', 'websiteUrl']
|
|
|
|
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
|
|
|
|
/* ---------------------------------------------------------------- Gerüst --- */
|
|
export function renderFragebogen(root, ctx) {
|
|
const stand = h('p', { class: 'tm-schritt-stand' })
|
|
const liste = h('ol', { class: 'tm-schritte' })
|
|
const inhalt = h('div', { class: 'tm-fb-inhalt' })
|
|
root.replaceChildren(h('div', { class: 'tm-fragebogen' },
|
|
ctx.daten.kundenansicht === true ? h('div', { class: 'tm-box tm-box-info', role: 'note' }, h('span', null, KUNDENANSICHT)) : null,
|
|
h('nav', { class: 'tm-fortschritt', 'aria-label': 'Fortschritt im Fragebogen' }, stand, liste),
|
|
inhalt,
|
|
h('p', { class: 'tm-hinweis tm-fb-aufbewahrung' }, AUFBEWAHRUNG)))
|
|
let angezeigt = null // Schluessel des Schritts, der gerade steht
|
|
|
|
// ohne Entwurf gibt es nur die Branchenwahl; alle anderen Schritte brauchen eine Einstellung
|
|
const darfHin = (key) => key === 'branche' || Boolean(ctx.daten.einstellung)
|
|
|
|
function zeichneLeiste() {
|
|
const eintraege = [...schrittStatus(ctx.daten.fragebogen), { key: BESTELLEN, titel: TITEL.get(BESTELLEN), erledigt: false }]
|
|
liste.replaceChildren(...eintraege.map(({ key, titel, erledigt }, i) => h('li', {
|
|
class: 'tm-schritt', 'data-schritt': key, 'data-erledigt': erledigt ? 'true' : null, 'aria-current': key === angezeigt ? 'step' : null,
|
|
}, h('button', { type: 'button', class: 'tm-schritt-knopf', disabled: !darfHin(key), onclick: () => { if (key !== angezeigt) zeige(key) } },
|
|
h('span', { class: 'tm-schritt-nr', 'aria-hidden': 'true' }, erledigt ? '✓' : String(i + 1)),
|
|
h('span', { class: 'tm-schritt-titel' }, titel),
|
|
erledigt ? h('span', { class: 'tm-sr' }, ' (erledigt)') : null))))
|
|
const nr = FRAGEBOGEN_SCHRITTE.findIndex(([key]) => key === angezeigt)
|
|
stand.textContent = nr < 0 ? TITEL.get(BESTELLEN) : `Schritt ${nr + 1} von ${FRAGEBOGEN_SCHRITTE.length}`
|
|
}
|
|
|
|
function bestellseite() {
|
|
const seite = h('div', { class: 'tm-fb-bestellen' })
|
|
renderBestellen(seite, { ctx, daten: ctx.daten, zumSchritt: (key) => zeige(key) })
|
|
return seite
|
|
}
|
|
|
|
/** Schritt zeigen (=> sein Element). fokus: die Ueberschrift bekommt den Fokus, so wie bei jedem Wechsel; der erste Aufruf nimmt keinen. */
|
|
function zeige(key, { fokus = true } = {}) {
|
|
const ziel = TITEL.has(key) && darfHin(key) ? key : 'branche'
|
|
angezeigt = ziel
|
|
zeichneLeiste()
|
|
const el = ziel === BESTELLEN ? bestellseite() : BAU[ziel](baueT(ziel))
|
|
inhalt.replaceChildren(el)
|
|
const kopf = fokus ? el.querySelector('h3') : null
|
|
if (kopf) {
|
|
kopf.setAttribute('tabindex', '-1')
|
|
kopf.focus()
|
|
}
|
|
return el
|
|
}
|
|
|
|
function baueT(key) {
|
|
const nr = FRAGEBOGEN_SCHRITTE.findIndex(([k]) => k === key)
|
|
return {
|
|
ctx,
|
|
key,
|
|
nr,
|
|
titel: TITEL.get(key),
|
|
gehe: (ziel) => zeige(ziel),
|
|
// nur solange der Schritt noch steht: eine spaete Antwort holt den Kunden nicht aus einem anderen Schritt zurueck
|
|
neuZeichnen: () => (angezeigt === key ? zeige(key, { fokus: false }) : null),
|
|
weiter: () => zeige(FRAGEBOGEN_SCHRITTE[nr + 1]?.[0] ?? BESTELLEN),
|
|
zurueck: nr > 0 ? () => zeige(FRAGEBOGEN_SCHRITTE[nr - 1][0]) : null,
|
|
}
|
|
}
|
|
|
|
zeige(ersterOffenerSchritt(ctx.daten.fragebogen), { fokus: false })
|
|
}
|
|
|
|
/* ------------------------------------------------------------ Bausteine --- */
|
|
/** Rahmen eines Schritts: Ueberschrift (Fokusziel beim Wechsel) und ein erklaerender Satz; der Schritt haengt seinen Inhalt an. */
|
|
function rahmen(t, intro) {
|
|
return h('section', { class: 'tm-fb-schritt', 'data-schritt': t.key },
|
|
h('h3', { id: `tm-fb-${t.key}`, tabindex: '-1' }, t.titel),
|
|
intro ? h('p', { class: 'tm-hinweis tm-abschnitt-text' }, intro) : null)
|
|
}
|
|
|
|
const schrittForm = (t, ...kinder) => h('form', { class: 'tm-fb-form', novalidate: true, 'aria-labelledby': `tm-fb-${t.key}` }, ...kinder)
|
|
|
|
/** Zurueck (ohne zu speichern) und Weiter (sendet das Formular). */
|
|
function knoepfe(t, { mitWeiter = true } = {}) {
|
|
return h('div', { class: 'tm-frage-knoepfe' },
|
|
t.zurueck ? h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueck', onclick: () => t.zurueck() }, 'Zurück') : null,
|
|
mitWeiter ? h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'weiter' }, 'Weiter') : null)
|
|
}
|
|
|
|
/**
|
|
* PUT /fragebogen: der Schritt kommt in bestaetigt, aenderung(fragebogen) setzt weitere Angaben (z. B. preiseGeprueft). Gesendet wird der ganze Fragebogen
|
|
* (PUT ersetzt ihn) auf dem Stand von ctx.daten; die Antwort gilt danach in ctx.daten (fragebogen und die offenen Punkte der Bestellung).
|
|
* In der Kundenansicht passiert nichts: Schritte bestaetigt nur der Kunde selbst.
|
|
*/
|
|
async function bestaetige(t, aenderung) {
|
|
const { ctx } = t
|
|
if (ctx.daten.kundenansicht === true) return
|
|
const fragebogen = structuredClone(istObjekt(ctx.daten.fragebogen) ? ctx.daten.fragebogen : {})
|
|
aenderung?.(fragebogen)
|
|
fragebogen.bestaetigt = [...new Set([...(Array.isArray(fragebogen.bestaetigt) ? fragebogen.bestaetigt : []), t.key])]
|
|
const antwort = await ctx.api.send('PUT', '/fragebogen', { fragebogen })
|
|
if (!ctx.aktuell()) return
|
|
if (istObjekt(antwort?.fragebogen)) ctx.daten.fragebogen = antwort.fragebogen
|
|
if (antwort?.offen !== undefined) ctx.daten.offen = antwort.offen
|
|
}
|
|
|
|
/**
|
|
* „Weiter“ eines Schritts: pruefe() (Fehler ⇒ { feld: text } am Feld, nichts wird gesendet), aendere(entwurf) (ein PATCH ueber den Speicher des
|
|
* Schritts; false = der Schritt hat selbst Fehler gezeigt; fehlt aendere, gibt es keinen PATCH), dann PUT mit dem Schritt (fragebogen(f) setzt weitere
|
|
* Angaben) und der naechste Schritt. Nur ein Durchlauf zur Zeit; verlaesst der Kunde den Schritt oder die Ansicht, fasst nichts mehr die Seite an.
|
|
*/
|
|
function verdrahte(t, form, sp, { pruefe, aendere, fragebogen } = {}) {
|
|
const weiter = form.querySelector('[data-aktion="weiter"]')
|
|
let laeuft = false
|
|
form.addEventListener('submit', async (ev) => {
|
|
ev.preventDefault()
|
|
if (laeuft) return
|
|
laeuft = true
|
|
const hatteFokus = document.activeElement === weiter
|
|
weiter.disabled = true
|
|
try {
|
|
setzeFehler(form, {})
|
|
const fehler = pruefe?.()
|
|
if (fehler) {
|
|
zeigeFehler(form, { fehler })
|
|
return
|
|
}
|
|
if (aendere && !(await sp.speichere(aendere, { container: form }))) return
|
|
if (!sp.nochHier()) return
|
|
try {
|
|
await bestaetige(t, fragebogen)
|
|
} catch (err) {
|
|
if (sp.nochHier()) zeigeFehler(form, err)
|
|
return
|
|
}
|
|
if (sp.nochHier()) t.weiter()
|
|
} finally {
|
|
laeuft = false
|
|
weiter.disabled = false
|
|
// ein gesperrter Knopf verliert im Browser den Fokus: bleibt der Schritt stehen und hat ihn niemand sonst (ein Feld mit Fehler), kehrt er zurueck
|
|
const jetzt = document.activeElement
|
|
if (hatteFokus && weiter.isConnected && (!jetzt || jetzt === document.body || jetzt === weiter)) weiter.focus()
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Schritt neu zeichnen und im neuen Formular einen allgemeinen Fehler zeigen (nach einem Teilerfolg, den die alte Ansicht nicht kennt). */
|
|
function zeigeNeu(t, text) {
|
|
const form = t.neuZeichnen()?.querySelector('form')
|
|
if (form) setzeFehler(form, { _: text })
|
|
}
|
|
|
|
/**
|
|
* aendere() fuer Schritte aus Abschnitten der Einstellungen (jeder ist { el, lese(entwurf, fehler) }): jeder liest seine Felder in den Entwurf.
|
|
* Eingaben, die kein Feld versteht, stehen danach am Feld und nichts wird gesendet (false). Das Pruefen im Einzelnen bleibt Sache des Servers.
|
|
*/
|
|
function leseAbschnitte(form, abschnitte) {
|
|
return (entwurf) => {
|
|
const fehler = {}
|
|
for (const a of abschnitte) a.lese(entwurf, fehler)
|
|
if (Object.keys(fehler).length === 0) return true
|
|
zeigeFehler(form, { fehler })
|
|
return false
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------- Schritt 1: Branche und Betrieb --- */
|
|
/**
|
|
* Radiogruppe der Branchen aus daten.branchen. Der Gesundheitsbereich zeigt den Erklaertext und KEINEN Knopf (es geht nie eine Anfrage raus), jede andere
|
|
* Wahl den Knopf (type submit, data-aktion branche-starten). => { liste, hinweis, aktion, gewaehlt() }: der Aufrufer setzt die drei Elemente.
|
|
*/
|
|
function brancheWahl(daten, { name, knopf }) {
|
|
let gewaehlt = null
|
|
const hinweis = h('div', { class: 'tm-fb-branchenhinweis' })
|
|
const aktion = h('div', { class: 'tm-frage-knoepfe' })
|
|
const liste = h('fieldset', { class: 'tm-vorlagen-liste tm-fb-branchen' },
|
|
h('legend', { class: 'tm-sr' }, 'Branche'),
|
|
(daten.branchen || []).map((b) => {
|
|
const radio = h('input', { type: 'radio', name, value: b.key })
|
|
radio.addEventListener('change', () => { gewaehlt = b; zeichne() })
|
|
const beschreibung = (daten.vorlagen || []).find((v) => v.key === b.key)?.beschreibung
|
|
return h('label', { class: 'tm-vorlage' }, radio, h('span', null, h('strong', null, b.name), beschreibung ? h('small', null, beschreibung) : null))
|
|
}))
|
|
function zeichne() {
|
|
if (gewaehlt?.gesundheit) {
|
|
hinweis.replaceChildren(h('div', { class: 'tm-box tm-box-warn', role: 'note', 'data-hinweis': 'gesundheit' },
|
|
h('span', null, GESUNDHEIT_TEXT),
|
|
h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Chat öffnen')))
|
|
aktion.replaceChildren()
|
|
return
|
|
}
|
|
hinweis.replaceChildren()
|
|
aktion.replaceChildren(h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'branche-starten', disabled: gewaehlt === null }, knopf))
|
|
}
|
|
zeichne()
|
|
return { liste, hinweis, aktion, gewaehlt: () => gewaehlt }
|
|
}
|
|
|
|
/** Nur die nichtleeren Texte der Felder, die POST vorlage aus betrieb uebernimmt. */
|
|
const kontaktVon = (angaben) => Object.fromEntries(KONTAKT_FELDER
|
|
.filter((name) => typeof angaben?.[name] === 'string' && angaben[name].trim() !== '')
|
|
.map((name) => [name, angaben[name]]))
|
|
|
|
/** Was der Kunde angegeben hatte und POST vorlage nicht uebernommen hat (Haken gesetzt, Adresse eingetragen), im Vergleich zum neuen Entwurf. */
|
|
function betriebNachtrag(gewuenscht, neu) {
|
|
const nachtrag = {}
|
|
for (const name of NACHTRAG_FELDER) {
|
|
const wunsch = gewuenscht?.[name]
|
|
if (wunsch && wunsch !== neu?.[name]) nachtrag[name] = wunsch
|
|
}
|
|
return nachtrag
|
|
}
|
|
|
|
/**
|
|
* Nach einem erfolgreichen POST vorlage: Daten neu laden (Einstellung, Version, Fragebogen) und nachtragen, was der Server aus betrieb nicht uebernimmt
|
|
* (ein PATCH, nur wenn etwas fehlt). => true, wenn der Entwurf danach vollstaendig steht.
|
|
*/
|
|
async function nachVorlage(t, sp, container, gewuenscht) {
|
|
const { ctx } = t
|
|
try {
|
|
await ctx.neuLaden()
|
|
} catch {
|
|
return false
|
|
}
|
|
if (!sp.nochHier() || !ctx.daten.einstellung) return false
|
|
const nachtrag = betriebNachtrag(gewuenscht, ctx.daten.einstellung.betrieb)
|
|
if (Object.keys(nachtrag).length === 0) return true
|
|
return sp.speichere((entwurf) => { entwurf.betrieb = { ...entwurf.betrieb, ...nachtrag }; return true }, { container })
|
|
}
|
|
|
|
function schrittBranche(t) {
|
|
return t.ctx.daten.einstellung ? brancheMitEntwurf(t) : brancheStart(t)
|
|
}
|
|
|
|
/** Ohne Entwurf: Branche waehlen und die Betriebsangaben pruefen; „Mit dieser Branche starten“ legt den Entwurf an und bestaetigt den Schritt. */
|
|
function brancheStart(t) {
|
|
const { ctx } = t
|
|
const wurzel = rahmen(t, 'Such dir die Branche aus, die am besten zu deinem Betrieb passt. Daraus legen wir Leistungen, Zeiten und Regeln als Vorschlag an. Du kannst danach alles ändern.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const angaben = betrieb(ctx, { betrieb: ctx.daten.betriebVorschlag ?? {} }, { nur: BETRIEB_NUR })
|
|
const wahl = brancheWahl(ctx.daten, { name: 'termine-branche', knopf: 'Mit dieser Branche starten' })
|
|
const form = schrittForm(t, h('div', { class: 'kb-panel' }, wahl.liste, wahl.hinweis), angaben.el, allgemeinerFehler(), wahl.aktion)
|
|
|
|
let laeuft = false
|
|
form.addEventListener('submit', async (ev) => {
|
|
ev.preventDefault()
|
|
const branche = wahl.gewaehlt()
|
|
if (laeuft || branche === null || branche.gesundheit) return // der Gesundheitsbereich sendet nie etwas
|
|
laeuft = true
|
|
const knopf = form.querySelector('[data-aktion="branche-starten"]')
|
|
if (knopf) knopf.disabled = true
|
|
try {
|
|
setzeFehler(form, {})
|
|
const gewuenscht = angaben.lese()
|
|
// die Website des Projekts kennt nur der Vorschlag (der Server nimmt Name, Anschrift, E-Mail und Telefon selbst aus dem Konto): sie geht mit
|
|
const website = typeof ctx.daten.betriebVorschlag?.websiteUrl === 'string' ? ctx.daten.betriebVorschlag.websiteUrl.trim() : ''
|
|
try {
|
|
await ctx.api.send('POST', '/einstellungen/vorlage', { branche: branche.key, betrieb: website ? { ...gewuenscht, websiteUrl: website } : gewuenscht })
|
|
} catch (err) {
|
|
if (sp.nochHier()) zeigeFehler(form, err)
|
|
return
|
|
}
|
|
// ab hier gibt es den Entwurf: was schiefgeht, zeigt den Schritt neu (mit Entwurf) statt noch einmal anzulegen
|
|
const vollstaendig = await nachVorlage(t, sp, form, gewuenscht)
|
|
if (!sp.nochHier()) return
|
|
if (!vollstaendig) {
|
|
if (ctx.daten.einstellung) zeigeNeu(t, NACH_VORLAGE_FEHLT)
|
|
else zeigeFehler(form, { fehler: { _: NACH_VORLAGE_LADEN } })
|
|
return
|
|
}
|
|
try {
|
|
await bestaetige(t)
|
|
} catch (err) {
|
|
if (sp.nochHier()) zeigeNeu(t, err.message)
|
|
return
|
|
}
|
|
if (!sp.nochHier()) return
|
|
ctx.meldung(`Die Branche „${branche.name}“ ist angelegt. Alles darin ist ein Vorschlag, den du anpassen kannst.`)
|
|
t.weiter()
|
|
} finally {
|
|
laeuft = false
|
|
if (knopf) knopf.disabled = false
|
|
}
|
|
})
|
|
|
|
wurzel.append(sp.konflikt, form)
|
|
return wurzel
|
|
}
|
|
|
|
/** Mit Entwurf: gewaehlte Branche (wechseln nur mit Rueckfrage), Betriebsangaben, „Weiter“ speichert sie und bestaetigt den Schritt. */
|
|
function brancheMitEntwurf(t) {
|
|
const { ctx } = t
|
|
const e = ctx.daten.einstellung
|
|
const branche = (ctx.daten.branchen || []).find((b) => b.key === ctx.daten.fragebogen?.branche)
|
|
const wurzel = rahmen(t, 'Prüfe die Angaben zu deinem Betrieb. Die Branche bestimmt, welche Vorschläge für Leistungen, Zeiten und Regeln du siehst.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const angaben = betrieb(ctx, e, { nur: BETRIEB_NUR })
|
|
const wechsel = wechselFrage(t, sp, angaben, branche !== undefined)
|
|
const form = schrittForm(t, angaben.el, allgemeinerFehler(), knoepfe(t))
|
|
verdrahte(t, form, sp, {
|
|
pruefe: () => (branche ? null : { _: 'Bitte wähle zuerst deine Branche.' }),
|
|
aendere: (entwurf) => { angaben.lese(entwurf); return true },
|
|
})
|
|
wurzel.append(sp.konflikt,
|
|
h('div', { class: 'tm-fb-branche' }, h('p', null, 'Deine Branche: ', h('strong', null, branche?.name ?? 'noch nicht gewählt')), wechsel.knopf),
|
|
wechsel.box,
|
|
form)
|
|
return wurzel
|
|
}
|
|
|
|
/**
|
|
* „Branche wechseln“ mit Rueckfrage in der Seite (kein confirm()): ersetzt Leistungen, Zeiten und Regeln und setzt alle Bestaetigungen zurueck
|
|
* (POST vorlage mit ersetzen: true und der gelesenen version). Die Betriebsangaben gehen mit; was der Server dabei nicht uebernimmt, wird nachgetragen.
|
|
* Danach steht Schritt 1 neu da, ein PUT gibt es nicht. => { knopf, box }: der Aufrufer setzt beide NEBEN das Formular des Schritts.
|
|
*/
|
|
function wechselFrage(t, sp, angaben, hatBranche) {
|
|
const { ctx } = t
|
|
const box = h('div', { class: 'tm-frage' })
|
|
const knopf = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'branche-wechseln' }, hatBranche ? 'Branche wechseln' : 'Branche wählen')
|
|
const zu = () => { box.replaceChildren(); knopf.focus() }
|
|
knopf.addEventListener('click', () => {
|
|
const wahl = brancheWahl(ctx.daten, { name: 'termine-branche-neu', knopf: 'Ja, Branche wechseln' })
|
|
const abbrechen = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'abbrechen', onclick: zu }, 'Abbrechen')
|
|
const rueckfrage = frage('Branche wechseln?', h('p', null, WECHSEL_TEXT), wahl.liste, wahl.hinweis, allgemeinerFehler(), h('div', { class: 'tm-frage-knoepfe' }, wahl.aktion, abbrechen))
|
|
let laeuft = false
|
|
rueckfrage.addEventListener('submit', async () => {
|
|
const neu = wahl.gewaehlt()
|
|
if (laeuft || neu === null || neu.gesundheit) return
|
|
laeuft = true
|
|
const ok = rueckfrage.querySelector('[data-aktion="branche-starten"]')
|
|
if (ok) ok.disabled = true
|
|
try {
|
|
setzeFehler(rueckfrage, {})
|
|
const gewuenscht = { ...ctx.daten.einstellung.betrieb, ...angaben.lese() }
|
|
try {
|
|
await ctx.api.send('POST', '/einstellungen/vorlage', { branche: neu.key, betrieb: kontaktVon(gewuenscht), ersetzen: true, version: ctx.daten.version })
|
|
} catch (err) {
|
|
if (!sp.nochHier()) return
|
|
if (err.code === 'EINSTELLUNG_VERALTET') {
|
|
box.replaceChildren()
|
|
sp.konflikt.hidden = false // „Neu laden“ des Speichers
|
|
sp.konflikt.scrollIntoView({ block: 'nearest' })
|
|
} else zeigeFehler(rueckfrage, err)
|
|
return
|
|
}
|
|
const vollstaendig = await nachVorlage(t, sp, rueckfrage, gewuenscht)
|
|
if (!sp.nochHier()) return
|
|
if (vollstaendig) t.gehe('branche')
|
|
else zeigeNeu(t, NACH_VORLAGE_FEHLT)
|
|
} finally {
|
|
laeuft = false
|
|
if (ok) ok.disabled = false
|
|
}
|
|
})
|
|
box.replaceChildren(rueckfrage)
|
|
rueckfrage.focus()
|
|
})
|
|
return { knopf, box }
|
|
}
|
|
|
|
/* ------------------------------------------------------ Schritt 2: Wer wird gebucht --- */
|
|
/** Vorbelegung der Art: nur Plaetze ⇒ Plaetze, mehr als ein Eintrag ⇒ Team, sonst Einzelperson. */
|
|
function vorgabeArt(ressourcen) {
|
|
if (ressourcen.length > 0 && ressourcen.every((r) => r.typ === 'platz')) return 'plaetze'
|
|
return ressourcen.length > 1 ? 'team' : 'einzel'
|
|
}
|
|
|
|
function schrittWer(t) {
|
|
const { ctx } = t
|
|
const wurzel = rahmen(t, 'Trag ein, wer oder was bei dir gebucht werden kann. Neue Personen oder Plätze können zunächst alle Leistungen anbieten, das änderst du im nächsten Schritt.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const ressourcen = ctx.daten.einstellung.ressourcen || []
|
|
const vorgabe = vorgabeArt(ressourcen)
|
|
const radios = WER_ARTEN.map(([wert, name, beschreibung]) => h('label', { class: 'tm-vorlage' },
|
|
h('input', { type: 'radio', name: 'termine-wer-art', value: wert, checked: wert === vorgabe }),
|
|
h('span', null, h('strong', null, name), h('small', null, beschreibung))))
|
|
const namen = h('textarea', { rows: '4', maxlength: String(LIMITS.ressourcen * (MAX.ressourcenName + 1)), autocomplete: 'off' }, ressourcen.map((r) => r.name).join('\n'))
|
|
const form = schrittForm(t,
|
|
h('div', { class: 'kb-panel' },
|
|
h('fieldset', { class: 'tm-vorlagen-liste tm-fb-branchen' }, h('legend', { class: 'tm-sr' }, 'Wer wird gebucht'), radios),
|
|
feld('ressourcen', 'Namen (einer pro Zeile)', namen, `Höchstens ${LIMITS.ressourcen} Einträge mit je höchstens ${MAX.ressourcenName} Zeichen. Bei einer Einzelperson trägst du nur einen Namen ein.`)),
|
|
allgemeinerFehler(),
|
|
knoepfe(t))
|
|
|
|
let gelesen = null
|
|
const art = () => form.querySelector('input[name="termine-wer-art"]:checked')?.getAttribute('value') ?? vorgabe
|
|
verdrahte(t, form, sp, {
|
|
pruefe: () => {
|
|
const zeilen = namen.value.split(/\r?\n/)
|
|
const gefuellt = zeilen.filter((zeile) => zeile.trim() !== '')
|
|
let fehler = null
|
|
if (gefuellt.length === 0) fehler = 'Bitte trag mindestens einen Namen ein.'
|
|
else if (art() === 'einzel' && gefuellt.length > 1) fehler = 'Für eine Einzelperson trägst du nur einen Namen ein. Wähle „Team“, wenn mehrere Personen gebucht werden können.'
|
|
else if (gefuellt.length > LIMITS.ressourcen) fehler = `Es sind höchstens ${LIMITS.ressourcen} Einträge möglich.`
|
|
else if (gefuellt.some((zeile) => [...zeile.trim()].length > MAX.ressourcenName)) fehler = `Ein Name darf höchstens ${MAX.ressourcenName} Zeichen lang sein.`
|
|
if (fehler) return { ressourcen: fehler }
|
|
gelesen = ressourcenAusNamen({ bisher: ctx.daten.einstellung.ressourcen, typ: art() === 'plaetze' ? 'platz' : 'person', namen: zeilen })
|
|
return null
|
|
},
|
|
// neue Eintraege bieten alle Leistungen an (der Validator verlangt eine aktive Ressource), entfernte verschwinden aus Leistungen und Abwesenheiten
|
|
aendere: (entwurf) => {
|
|
const { ressourcen: neu, entfernt, neu: hinzu } = gelesen
|
|
entwurf.ressourcen = neu
|
|
for (const leistung of entwurf.leistungen || []) leistung.ressourcen = [...(leistung.ressourcen || []).filter((id) => !entfernt.includes(id)), ...hinzu]
|
|
entwurf.abwesenheiten = (entwurf.abwesenheiten || []).filter((a) => !entfernt.includes(a.ressourceId))
|
|
return true
|
|
},
|
|
})
|
|
wurzel.append(sp.konflikt, form)
|
|
return wurzel
|
|
}
|
|
|
|
/* ---------------------------------------------------------- Schritt 3: Leistungen --- */
|
|
/** Leistungen, die Gaeste sehen: aktiv und mit mindestens einer aktiven Person oder einem aktiven Platz (wie sichtbareLeistungen auf dem Server). */
|
|
function sichtbareLeistungen(einstellung) {
|
|
const aktiv = new Set((einstellung?.ressourcen || []).filter((r) => r?.aktiv).map((r) => r.id))
|
|
return (einstellung?.leistungen || []).filter((l) => l?.aktiv && (l.ressourcen || []).some((id) => aktiv.has(id)))
|
|
}
|
|
|
|
const hatPreis = (leistung) => preisAnzeige(leistung) !== 'ohne Preis'
|
|
|
|
function schrittLeistungen(t) {
|
|
const { ctx } = t
|
|
const wurzel = rahmen(t, 'Prüfe die Leistungen, die deine Gäste buchen können, und ändere sie, wie du sie brauchst. Änderungen an einer Leistung werden sofort gespeichert.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
// der Reiter Leistungen speichert selbst; sein Platz ist ein eigenes Element, das Formular des Schritts steht daneben (kein verschachteltes Formular)
|
|
const leistungen = h('div', { class: 'tm-fb-leistungen' })
|
|
renderLeistungen(leistungen, ctx)
|
|
const haken = h('input', { type: 'checkbox', checked: ctx.daten.fragebogen?.preiseGeprueft === true })
|
|
const form = schrittForm(t,
|
|
hakenFeld('preiseGeprueft', haken, 'Ich habe die Preise geprüft (die Preise der Vorlage sind Platzhalter)'),
|
|
h('p', { class: 'tm-fehler', 'data-fehler': 'leistungen', role: 'alert' }),
|
|
allgemeinerFehler(),
|
|
knoepfe(t))
|
|
verdrahte(t, form, sp, {
|
|
pruefe: () => {
|
|
const sichtbar = sichtbareLeistungen(ctx.daten.einstellung)
|
|
if (sichtbar.length === 0) return { leistungen: 'Bitte lege mindestens eine aktive Leistung an, die einer aktiven Person oder einem aktiven Platz zugeordnet ist. Nur solche Leistungen sehen Gäste.' }
|
|
if (sichtbar.some(hatPreis) && !haken.checked) return { preiseGeprueft: 'Bitte bestätige, dass du die Preise geprüft hast.' }
|
|
return null
|
|
},
|
|
fragebogen: (f) => { f.preiseGeprueft = haken.checked },
|
|
})
|
|
wurzel.append(sp.konflikt, leistungen, form)
|
|
return wurzel
|
|
}
|
|
|
|
/* ---------------------------------------------------------- Schritt 4: Zeiten --- */
|
|
/**
|
|
* Je Person und Platz ein Wochenplan (wochenplanFeld), dazu Bundesland OHNE Vorauswahl (leere erste Option; gewaehlt ist nur, was der Kunde schon
|
|
* ausdruecklich gewaehlt hatte) und „An Feiertagen geschlossen“: alles im PATCH dieses Schritts, danach der PUT mit bundeslandGewaehlt.
|
|
* Darunter Abwesenheiten und Sonderoeffnungen aus dem Reiter „Team & Plaetze“: sie speichern selbst und haben deshalb einen eigenen Container mit
|
|
* eigenem zeichne, damit das Neuzeichnen nach dem Speichern die ungespeicherten Zeiten im Formular darueber nicht verliert. Der Speicher ist derselbe
|
|
* wie im Schritt (ein Speichern zur Zeit, eine Version).
|
|
*/
|
|
function schrittZeiten(t) {
|
|
const { ctx } = t
|
|
const e = ctx.daten.einstellung
|
|
const wurzel = rahmen(t, 'Wann kann bei dir gebucht werden? Trag für jede Person und jeden Platz die Zeiten ein und wähle dein Bundesland. Urlaub und besondere Öffnungstage trägst du weiter unten ein, sie werden sofort gespeichert.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const plaene = (e.ressourcen || []).map((r, i) => ({ r, plan: wochenplanFeld(r, `ressourcen.${i}`) }))
|
|
const vorgabe = ctx.daten.fragebogen?.bundeslandGewaehlt === true ? e.bundesland : ''
|
|
const land = h('select', null,
|
|
h('option', { value: '' }, 'Bitte wählen'),
|
|
BUNDESLAENDER.map(([key, name]) => h('option', { value: key, selected: key === vorgabe }, name)))
|
|
const geschlossen = h('input', { type: 'checkbox', checked: e.feiertageGeschlossen !== false })
|
|
const form = schrittForm(t,
|
|
plaene.length > 0
|
|
? plaene.map(({ r, plan }) => h('div', { class: 'kb-panel tm-fb-person', 'data-ressource': r.id }, h('h4', null, `${r.name}${r.aktiv === false ? ' (nicht aktiv)' : ''}`), plan.el))
|
|
: h('p', { class: 'tm-leer' }, 'Es gibt noch keine Person und keinen Platz. Lege sie im Schritt „Wer wird gebucht“ an.'),
|
|
abschnitt('Bundesland und Feiertage', null,
|
|
feld('bundesland', 'Bundesland', land, 'Danach richten sich die gesetzlichen Feiertage. Bitte wähle es ausdrücklich aus.'),
|
|
hakenFeld('feiertageGeschlossen', geschlossen, 'An Feiertagen geschlossen', 'Dann kann an gesetzlichen Feiertagen niemand buchen. Eine Sonderöffnung geht vor.')),
|
|
allgemeinerFehler(),
|
|
knoepfe(t))
|
|
|
|
let gelesen = []
|
|
verdrahte(t, form, sp, {
|
|
pruefe: () => {
|
|
const fehler = {}
|
|
const plaeneGelesen = []
|
|
for (const { r, plan } of plaene) {
|
|
const ergebnis = plan.lese()
|
|
if (ergebnis.fehler) Object.assign(fehler, ergebnis.fehler)
|
|
else plaeneGelesen.push([r.id, ergebnis.plan])
|
|
}
|
|
if (land.value === '') fehler.bundesland = LAND_FEHLT
|
|
if (Object.keys(fehler).length > 0) return fehler
|
|
gelesen = plaeneGelesen
|
|
return null
|
|
},
|
|
aendere: (entwurf) => {
|
|
for (const [id, plan] of gelesen) {
|
|
const stelle = stelleVon(entwurf.ressourcen, id, form, 'Diese Person oder dieser Platz')
|
|
if (stelle < 0) return false
|
|
entwurf.ressourcen[stelle].plan = plan
|
|
}
|
|
entwurf.bundesland = land.value
|
|
entwurf.feiertageGeschlossen = geschlossen.checked
|
|
return true
|
|
},
|
|
fragebogen: (f) => { f.bundeslandGewaehlt = true },
|
|
})
|
|
|
|
const ausnahmen = h('div', { class: 'tm-fb-ausnahmen' })
|
|
const at = { ctx, sp, wurzel: ausnahmen, zeichne: () => ausnahmen.replaceChildren(abwesenheiten(at), sonderoeffnungen(at)) }
|
|
at.zeichne()
|
|
wurzel.append(sp.konflikt, form, ausnahmen)
|
|
return wurzel
|
|
}
|
|
|
|
/* --------------------------------------------------------- Schritt 5: Regeln --- */
|
|
/**
|
|
* Sammelknopf „Alle sofort buchbar“ / „Alle nur als Anfrage“: zeigt die Lage der Leistungen (buchungsartLage) und merkt sich die Wahl; geschrieben wird
|
|
* sie mit „Weiter“ in den PATCH des Schritts (buchungsartFuerAlle auf dem frischen Stand), ein Klick allein ruft nichts auf. => { el, lese(entwurf) }.
|
|
*/
|
|
function buchungsartSammel(e) {
|
|
const leistungen = e.leistungen || []
|
|
let gewaehlt = null // null: der Kunde hat nichts gewaehlt, die Leistungen bleiben, wie sie sind
|
|
const lage = h('p', { class: 'tm-hinweis', role: 'status' })
|
|
const tasten = SAMMEL_ARTEN.map(([art, text]) => h('button', {
|
|
type: 'button', class: 'kb-btn-outline', 'data-aktion': `alle-${art}`, onclick: () => { gewaehlt = art; zeige() },
|
|
}, text))
|
|
function zeige() {
|
|
const jetzt = buchungsartLage(gewaehlt === null ? leistungen : buchungsartFuerAlle(leistungen, gewaehlt))
|
|
lage.setAttribute('data-lage', jetzt)
|
|
lage.textContent = `${LAGE_TEXT[jetzt]}${gewaehlt === null ? '' : ' Gespeichert wird mit „Weiter“.'}`
|
|
SAMMEL_ARTEN.forEach(([art], i) => tasten[i].setAttribute('aria-pressed', String(jetzt === art)))
|
|
}
|
|
zeige()
|
|
const el = abschnitt('Buchungsart',
|
|
'Sollen deine Gäste sofort buchen können oder nur eine Anfrage stellen, die du bestätigst? Das gilt für alle Leistungen zugleich. Einzelne Leistungen änderst du im Schritt „Leistungen“.',
|
|
lage,
|
|
h('div', { class: 'tm-fb-sammel', role: 'group', 'aria-label': 'Buchungsart für alle Leistungen' }, tasten))
|
|
return { el, lese(entwurf) { if (gewaehlt !== null) entwurf.leistungen = buchungsartFuerAlle(entwurf.leistungen, gewaehlt) } }
|
|
}
|
|
|
|
function schrittRegeln(t) {
|
|
const { ctx } = t
|
|
const e = ctx.daten.einstellung
|
|
const wurzel = rahmen(t, 'Lege fest, wie deine Gäste buchen, was bei Absagen gilt und wie sie bezahlen.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const sammel = buchungsartSammel(e)
|
|
const regelnAbschnitt = regeln(e)
|
|
// das Muster der Stornobedingungen folgt der Frist, die im Feld „Selbst absagen bis“ steht (auch ungespeichert)
|
|
const abschnitte = [regelnAbschnitt, storno(ctx, e, regelnAbschnitt.absage), rechtliches(e), zahlung(e)]
|
|
const form = schrittForm(t, sammel.el, abschnitte.map((a) => a.el), allgemeinerFehler(), knoepfe(t))
|
|
verdrahte(t, form, sp, { aendere: leseAbschnitte(form, [sammel, ...abschnitte]) })
|
|
wurzel.append(sp.konflikt, form)
|
|
return wurzel
|
|
}
|
|
|
|
/* ----------------------------------------------------- Schritt 6: Gastfelder --- */
|
|
function schrittGastfelder(t) {
|
|
const { ctx } = t
|
|
const wurzel = rahmen(t, 'Was möchtest du von deinen Gästen wissen? Name und E-Mail-Adresse brauchen wir immer.')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const gast = gastFelder(ctx, ctx.daten.einstellung)
|
|
const form = schrittForm(t, gast.el, allgemeinerFehler(), knoepfe(t))
|
|
verdrahte(t, form, sp, { aendere: leseAbschnitte(form, [gast]) })
|
|
wurzel.append(sp.konflikt, form)
|
|
return wurzel
|
|
}
|
|
|
|
/* ------------------------------------------------- Schritt 7: Benachrichtigung --- */
|
|
function schrittBenachrichtigung(t) {
|
|
const { ctx } = t
|
|
const wurzel = rahmen(t, 'Wohin sollen die Nachrichten zu Terminen gehen, wie sprichst du deine Gäste an und bekommen Gäste eine Erinnerung?')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
const e = ctx.daten.einstellung
|
|
// aus dem Betrieb nur die E-Mail-Adresse: sie ist die einzige Adresse fuer Nachrichten und auch Reply-To
|
|
const abschnitte = [betrieb(ctx, e, { nur: ['email'], titel: 'Deine E-Mail-Adresse', hinweis: null }), darstellung(e), erinnerung(e)]
|
|
const form = schrittForm(t, abschnitte.map((a) => a.el), allgemeinerFehler(), knoepfe(t))
|
|
verdrahte(t, form, sp, { aendere: leseAbschnitte(form, abschnitte) })
|
|
wurzel.append(sp.konflikt, form)
|
|
return wurzel
|
|
}
|
|
|
|
/* ---------------------------------------------------------- Schritt 8: Website --- */
|
|
/**
|
|
* Einbauwuensche: Modus (direkt auf der Seite / hinter einem Knopf, ohne Vorauswahl), Knopftext (nur bei Knopf), Stelle, Gestaltung, Bemerkung und der
|
|
* Hinweis, was nach Stunden geht. einbau = fragebogen.einbau (darf fehlen). => { el, pruefe(), wert() }: pruefe() => null oder { feld: text }, wert() =>
|
|
* das Objekt fuer fragebogen.einbau (bei „direkt auf der Seite“ ohne Knopftext). Die Texte kuerzt und bereinigt der Server.
|
|
*/
|
|
function einbauWuensche(einbau) {
|
|
const alt = istObjekt(einbau) ? einbau : {}
|
|
const textVon = (name) => (typeof alt[name] === 'string' ? alt[name] : '')
|
|
const eingaben = []
|
|
const radios = EINBAU_MODI.map(([wert, name, beschreibung]) => {
|
|
const radio = h('input', { type: 'radio', name: 'termine-einbau-modus', value: wert, checked: alt.modus === wert })
|
|
radio.addEventListener('change', zeigeKnopfFeld)
|
|
eingaben.push(radio)
|
|
return h('label', { class: 'tm-vorlage' }, radio, h('span', null, h('strong', null, name), h('small', null, beschreibung)))
|
|
})
|
|
const gewaehlt = () => eingaben.find((radio) => radio.checked)?.value ?? null
|
|
const knopfText = h('input', { type: 'text', maxlength: String(EINBAU_MAX.knopfText), autocomplete: 'off', value: textVon('knopfText') })
|
|
const knopfFeld = feld('einbau.knopfText', 'Text auf dem Knopf', knopfText, 'Zum Beispiel „Termin buchen“.')
|
|
const mehrzeilig = (name) => h('textarea', { rows: '3', maxlength: String(EINBAU_MAX[name]), autocomplete: 'off' }, textVon(name))
|
|
const stelle = mehrzeilig('stelle')
|
|
const gestaltung = mehrzeilig('gestaltung')
|
|
const bemerkung = mehrzeilig('bemerkung')
|
|
function zeigeKnopfFeld() { knopfFeld.hidden = gewaehlt() !== 'knopf' }
|
|
zeigeKnopfFeld()
|
|
|
|
const el = abschnitt('Einbau auf deiner Website', 'Wir bauen den Terminplaner für dich in deine Website ein. Sag uns, wie und wo er erscheinen soll.',
|
|
h('fieldset', { class: 'tm-gruppe-felder', 'data-feld': 'einbau.modus', tabindex: '-1' },
|
|
h('legend', { class: 'tm-label' }, 'So erscheint der Terminplaner'),
|
|
h('div', { class: 'tm-vorlagen-liste' }, radios),
|
|
h('p', { class: 'tm-fehler', 'data-fehler': 'einbau.modus', role: 'alert' })),
|
|
knopfFeld,
|
|
feld('einbau.stelle', 'Wo auf deiner Website soll er stehen?', stelle, 'Beschreibe die Stelle in Worten, zum Beispiel „auf der Seite Kontakt unter dem Text“.'),
|
|
feld('einbau.gestaltung', 'Wünsche zur Gestaltung (optional)', gestaltung, 'Farben, Schrift, Stil: Wir passen den Terminplaner an deine Website an.'),
|
|
feld('einbau.bemerkung', 'Bemerkung (optional)', bemerkung),
|
|
h('p', { class: 'tm-hinweis', 'data-hinweis': 'einbau-mehr' }, MEHRWUENSCHE))
|
|
el.classList.add('tm-fb-einbau')
|
|
|
|
return {
|
|
el,
|
|
pruefe() {
|
|
const modus = gewaehlt()
|
|
if (modus === null) return { 'einbau.modus': MODUS_FEHLT }
|
|
if (modus === 'knopf' && knopfText.value.trim() === '') return { 'einbau.knopfText': KNOPFTEXT_FEHLT }
|
|
return null
|
|
},
|
|
wert() {
|
|
const modus = gewaehlt()
|
|
return { modus, knopfText: modus === 'knopf' ? knopfText.value.trim() : '', stelle: stelle.value, gestaltung: gestaltung.value, bemerkung: bemerkung.value }
|
|
},
|
|
}
|
|
}
|
|
|
|
function schrittWebsite(t) {
|
|
const { ctx } = t
|
|
const wurzel = rahmen(t, 'Zum Schluss: Wie erreichen dich Gäste, und wie soll der Terminplaner auf deiner Website erscheinen?')
|
|
const sp = neuerSpeicher(ctx, wurzel, t.neuZeichnen)
|
|
// aus dem Betrieb Telefon (Ausweichweg fuer Gaeste) und die Links; Name, Anschrift und E-Mail stehen in den Schritten 1 und 7
|
|
const angaben = betrieb(ctx, ctx.daten.einstellung, {
|
|
nur: WEBSITE_NUR,
|
|
titel: 'Telefon und Links',
|
|
hinweis: 'Das Telefon ist der Ausweichweg für Gäste. Impressum und Datenschutzerklärung brauchen wir, weil der Terminplaner Gäste darauf verweist.',
|
|
})
|
|
const einbau = einbauWuensche(ctx.daten.fragebogen?.einbau)
|
|
const form = schrittForm(t, angaben.el, einbau.el, allgemeinerFehler(), knoepfe(t))
|
|
let gewuenscht = null
|
|
verdrahte(t, form, sp, {
|
|
pruefe: () => {
|
|
const fehler = einbau.pruefe()
|
|
if (fehler === null) gewuenscht = einbau.wert()
|
|
return fehler
|
|
},
|
|
aendere: (entwurf) => { angaben.lese(entwurf); return true },
|
|
fragebogen: (f) => { f.einbau = gewuenscht },
|
|
})
|
|
wurzel.append(sp.konflikt, form)
|
|
return wurzel
|
|
}
|
|
|
|
const BAU = {
|
|
branche: schrittBranche, wer: schrittWer, leistungen: schrittLeistungen, zeiten: schrittZeiten, regeln: schrittRegeln,
|
|
gastfelder: schrittGastfelder, benachrichtigung: schrittBenachrichtigung, website: schrittWebsite,
|
|
}
|