Terminplaner TP2: Reiter-Exporte und wochenplanFeld fuer den Fragebogen
- termine-tab-einstellungen.js: die acht Abschnitte (betrieb, darstellung, regeln,
gastFelder, erinnerung, storno, rechtliches, zahlung) sind Exporte; betrieb nimmt
{ nur, titel, hinweis } (nur die gewaehlten Felder, lese schreibt nur diese)
- termine-tab-team.js: wochenplanFeld(r, pfad) => { el, lese() => { plan } | { fehler } },
der Editor einer Person benutzt es selbst
- termine-speichern.js: fehlt kommt aus der Antwort des Servers
- termine.js: baueCtx aus zeigeReiter herausgeloest (Verhalten gleich)
- test/support/fakeDom.js: Minimal-DOM und Fake-fetch fuer die Client-Tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0090332c70
commit
747be432c5
7 files changed
+1032
-59
No files matched your search
@@ -69,48 +69,79 @@ export function render(panel, ctx) {
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- Betrieb --- */
|
||||
function betrieb(ctx, e) {
|
||||
const textFeld = (wert, max, extra) => h('input', { type: 'text', maxlength: max, autocomplete: 'off', value: wert ?? '', ...extra })
|
||||
const urlFeld = (wert) => textFeld(wert, MAX.url, { inputmode: 'url', placeholder: 'https://' })
|
||||
|
||||
/** Die Angaben zum Betrieb in der Reihenfolge der Seite: Name, Mailadresse des Betriebs, Telefon, Anschrift, Kleinunternehmer, vier Links. */
|
||||
const BETRIEB_FELDER = {
|
||||
name: { eingabe: (b) => textFeld(b.name, MAX.betriebName), zeige: (i) => feld('betrieb.name', 'Name des Betriebs', i) },
|
||||
email: {
|
||||
eingabe: (b) => h('input', { type: 'email', maxlength: '254', autocomplete: 'off', value: b.email ?? '' }),
|
||||
zeige: (i) => feld('betrieb.email', 'E-Mail des Betriebs', i, 'Hierhin schicken wir dir Nachrichten zu Terminen. Gäste können auf ihre Mails hier antworten.'),
|
||||
},
|
||||
telefon: {
|
||||
eingabe: (b) => textFeld(b.telefon, MAX.telefon),
|
||||
zeige: (i) => feld('betrieb.telefon', 'Telefon (Pflicht)', i, 'Gäste sehen die Nummer als Ausweichweg und in ihren Mails.'),
|
||||
},
|
||||
adresse: {
|
||||
eingabe: (b) => textFeld(b.adresse, MAX.adresse),
|
||||
zeige: (i) => feld('betrieb.adresse', 'Anschrift', i, 'Pflicht, wenn Privatpersonen bei dir buchen oder anfragen: Die Anschrift steht in der Widerrufsbelehrung.'),
|
||||
},
|
||||
kleinunternehmer: {
|
||||
eingabe: (b) => h('input', { type: 'checkbox', checked: b.kleinunternehmer === true }),
|
||||
zeige: (i) => hakenFeld('betrieb.kleinunternehmer', i, 'Ich bin Kleinunternehmer nach § 19 UStG', 'Dann steht bei den Preisen „kein Ausweis der USt nach § 19 UStG“ statt „inkl. USt“.'),
|
||||
},
|
||||
impressumUrl: { eingabe: (b) => urlFeld(b.impressumUrl), zeige: (i) => feld('betrieb.impressumUrl', 'Link zum Impressum', i, 'Vollständige Adresse mit https://') },
|
||||
datenschutzUrl: { eingabe: (b) => urlFeld(b.datenschutzUrl), zeige: (i) => feld('betrieb.datenschutzUrl', 'Link zur Datenschutzerklärung', i, 'Vollständige Adresse mit https://') },
|
||||
agbUrl: { eingabe: (b) => urlFeld(b.agbUrl), zeige: (i) => feld('betrieb.agbUrl', 'Link zu deinen AGB (optional)', i, 'Vollständige Adresse mit https://') },
|
||||
websiteUrl: { eingabe: (b) => urlFeld(b.websiteUrl), zeige: (i) => feld('betrieb.websiteUrl', 'Link zu deiner Website', i, 'Vollständige Adresse mit https://') },
|
||||
}
|
||||
/** Wie die Felder auf der Seite zusammenstehen: Zeilen im Raster, das Ankreuzfeld steht allein. */
|
||||
const BETRIEB_ZEILEN = [
|
||||
{ felder: ['name', 'email'], raster: true },
|
||||
{ felder: ['telefon', 'adresse'], raster: true },
|
||||
{ felder: ['kleinunternehmer'], raster: false },
|
||||
{ felder: ['impressumUrl', 'datenschutzUrl', 'agbUrl', 'websiteUrl'], raster: true },
|
||||
]
|
||||
const BETRIEB_TITEL = 'Dein Betrieb'
|
||||
const BETRIEB_HINWEIS = 'Diese Angaben sehen deine Gäste beim Buchen und in ihren E-Mails. Die meisten haben wir aus deinem Kundenkonto übernommen.'
|
||||
|
||||
/**
|
||||
* Abschnitt „Dein Betrieb“. Ohne Optionen alle neun Felder wie bisher. Der Fragebogen zeigt in einem Schritt nur einen Teil:
|
||||
* nur = Teilmenge der Felder (name, email, telefon, adresse, kleinunternehmer, impressumUrl, datenschutzUrl, agbUrl, websiteUrl); es entstehen nur
|
||||
* diese Felder (in der Reihenfolge der Seite) und lese schreibt nur diese in den Entwurf. titel und hinweis ersetzen Ueberschrift und Erklaersatz
|
||||
* (hinweis: null laesst ihn weg). lese(entwurf) schreibt in entwurf.betrieb und gibt die gelesenen Felder zurueck (nur die gezeigten).
|
||||
* Der Hinweis auf fehlende Pflichtangaben nennt mit nur nur Angaben, die dabei zu sehen sind.
|
||||
*/
|
||||
export function betrieb(ctx, e, { nur, titel = BETRIEB_TITEL, hinweis = BETRIEB_HINWEIS } = {}) {
|
||||
const b = e.betrieb || {}
|
||||
const text = (wert, max, extra) => h('input', { type: 'text', maxlength: max, autocomplete: 'off', value: wert ?? '', ...extra })
|
||||
const url = (wert) => text(wert, MAX.url, { inputmode: 'url', placeholder: 'https://' })
|
||||
const f = {
|
||||
name: text(b.name, MAX.betriebName), email: h('input', { type: 'email', maxlength: '254', autocomplete: 'off', value: b.email ?? '' }), telefon: text(b.telefon, MAX.telefon),
|
||||
adresse: text(b.adresse, MAX.adresse), impressumUrl: url(b.impressumUrl), datenschutzUrl: url(b.datenschutzUrl), agbUrl: url(b.agbUrl), websiteUrl: url(b.websiteUrl),
|
||||
kleinunternehmer: h('input', { type: 'checkbox', checked: b.kleinunternehmer === true }),
|
||||
}
|
||||
const fehlt = ctx.daten.fehlt || []
|
||||
for (const name of nur || []) if (!Object.hasOwn(BETRIEB_FELDER, name)) throw new Error(`Unbekanntes Feld im Betrieb: ${name}`)
|
||||
const gezeigt = Object.keys(BETRIEB_FELDER).filter((name) => !nur || nur.includes(name))
|
||||
const f = Object.fromEntries(gezeigt.map((name) => [name, BETRIEB_FELDER[name].eingabe(b)]))
|
||||
const fehlt = (ctx.daten.fehlt || []).filter((pfad) => !nur || gezeigt.includes(pfad.replace(/^betrieb\./, '')))
|
||||
const warnung = fehlt.length
|
||||
? h('div', { class: 'tm-box tm-box-warn', role: 'note' }, `Ohne Telefon/Anschrift ist der Terminplaner auf deiner Website nicht buchbar. Es fehlt noch: ${fehltText(fehlt) || 'eine Angabe zu deinem Betrieb'}.`)
|
||||
: null
|
||||
const el = abschnitt('Dein Betrieb',
|
||||
'Diese Angaben sehen deine Gäste beim Buchen und in ihren E-Mails. Die meisten haben wir aus deinem Kundenkonto übernommen.',
|
||||
warnung,
|
||||
h('div', { class: 'tm-raster' },
|
||||
feld('betrieb.name', 'Name des Betriebs', f.name),
|
||||
feld('betrieb.email', 'E-Mail des Betriebs', f.email, 'Hierhin schicken wir dir Nachrichten zu Terminen. Gäste können auf ihre Mails hier antworten.')),
|
||||
h('div', { class: 'tm-raster' },
|
||||
feld('betrieb.telefon', 'Telefon (Pflicht)', f.telefon, 'Gäste sehen die Nummer als Ausweichweg und in ihren Mails.'),
|
||||
feld('betrieb.adresse', 'Anschrift', f.adresse, 'Pflicht, wenn Privatpersonen bei dir buchen oder anfragen: Die Anschrift steht in der Widerrufsbelehrung.')),
|
||||
hakenFeld('betrieb.kleinunternehmer', f.kleinunternehmer, 'Ich bin Kleinunternehmer nach § 19 UStG', 'Dann steht bei den Preisen „kein Ausweis der USt nach § 19 UStG“ statt „inkl. USt“.'),
|
||||
h('div', { class: 'tm-raster' },
|
||||
feld('betrieb.impressumUrl', 'Link zum Impressum', f.impressumUrl, 'Vollständige Adresse mit https://'),
|
||||
feld('betrieb.datenschutzUrl', 'Link zur Datenschutzerklärung', f.datenschutzUrl, 'Vollständige Adresse mit https://'),
|
||||
feld('betrieb.agbUrl', 'Link zu deinen AGB (optional)', f.agbUrl, 'Vollständige Adresse mit https://'),
|
||||
feld('betrieb.websiteUrl', 'Link zu deiner Website', f.websiteUrl, 'Vollständige Adresse mit https://')))
|
||||
const zeilen = BETRIEB_ZEILEN
|
||||
.map(({ felder, raster }) => {
|
||||
const els = felder.filter((name) => f[name]).map((name) => BETRIEB_FELDER[name].zeige(f[name]))
|
||||
if (!els.length) return null
|
||||
return raster ? h('div', { class: 'tm-raster' }, els) : els
|
||||
})
|
||||
.filter(Boolean)
|
||||
const el = abschnitt(titel, hinweis, warnung, ...zeilen)
|
||||
return {
|
||||
el,
|
||||
lese(entwurf) {
|
||||
entwurf.betrieb = {
|
||||
...entwurf.betrieb,
|
||||
name: f.name.value, email: f.email.value, telefon: f.telefon.value, adresse: f.adresse.value, kleinunternehmer: f.kleinunternehmer.checked,
|
||||
impressumUrl: f.impressumUrl.value, datenschutzUrl: f.datenschutzUrl.value, agbUrl: f.agbUrl.value, websiteUrl: f.websiteUrl.value,
|
||||
}
|
||||
lese(entwurf = {}) {
|
||||
const gelesen = Object.fromEntries(gezeigt.map((name) => [name, name === 'kleinunternehmer' ? f[name].checked : f[name].value]))
|
||||
entwurf.betrieb = { ...entwurf.betrieb, ...gelesen }
|
||||
return gelesen
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Anrede und Farbe --- */
|
||||
function darstellung(e) {
|
||||
export function darstellung(e) {
|
||||
const anrede = h('select', null, wahl(ANREDE_WAHL, e.anrede ?? 'sie'))
|
||||
const farbe = h('input', { type: 'color', value: /^#[0-9a-f]{6}$/.test(e.farbe) ? e.farbe : '#7c3aed' })
|
||||
const el = abschnitt('Anrede und Farbe', 'So treten die Buchungsseite und die E-Mails an deine Gäste auf.',
|
||||
@@ -121,7 +152,7 @@ function darstellung(e) {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ Regeln --- */
|
||||
function regeln(e) {
|
||||
export function regeln(e) {
|
||||
const r = e.regeln || {}
|
||||
const raster = h('select', null, wahl(RASTER_MIN.map((n) => [n, `${n} Minuten`]), r.raster ?? 15))
|
||||
const vorlauf = zahlenFeld('regeln.vorlaufMin', 'Mindestvorlauf (Minuten)', r.vorlaufMin, { min: 0, max: 20160, hinweis: 'Wie kurzfristig darf gebucht werden? 120 sind 2 Stunden, höchstens 20160 (14 Tage).' })
|
||||
@@ -146,7 +177,7 @@ function regeln(e) {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------- Angaben von Gaesten --- */
|
||||
function gastFelder(ctx, e) {
|
||||
export function gastFelder(ctx, e) {
|
||||
const g = e.gastFelder || {}
|
||||
const telefon = h('select', null, wahl(GAST_FELD_WAHL, g.telefon ?? 'optional'))
|
||||
const nachricht = h('select', null, wahl(GAST_FELD_WAHL, g.nachricht ?? 'aus'))
|
||||
@@ -210,7 +241,7 @@ function gastFelder(ctx, e) {
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Erinnerung --- */
|
||||
function erinnerung(e) {
|
||||
export function erinnerung(e) {
|
||||
const r = e.erinnerung || {}
|
||||
const anbieten = h('input', { type: 'checkbox', checked: r.anbieten !== false })
|
||||
const stunden = h('select', null, wahl(ERINNERUNG_STUNDEN.map((n) => [n, `${n} Stunden vorher`]), r.stundenVorher ?? 24))
|
||||
@@ -221,7 +252,7 @@ function erinnerung(e) {
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- Stornobedingungen --- */
|
||||
function storno(ctx, e, fristFeld) {
|
||||
export function storno(ctx, e, fristFeld) {
|
||||
const s = e.storno || {}
|
||||
const textfeld = h('textarea', { rows: '6', maxlength: String(MAX.storno) }, s.text ?? '')
|
||||
// das Muster setzt die Frist ein, die im Feld „Selbst absagen bis“ steht (auch ungespeichert)
|
||||
@@ -243,7 +274,7 @@ function storno(ctx, e, fristFeld) {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Rechtliches --- */
|
||||
function rechtliches(e) {
|
||||
export function rechtliches(e) {
|
||||
const r = e.recht || {}
|
||||
const gaeste = h('select', null, wahl([['verbraucher', 'Privatpersonen'], ['unternehmer', 'Unternehmen'], ['gemischt', 'Beides']], r.gaesteSind ?? 'verbraucher'))
|
||||
const freizeit = h('input', { type: 'checkbox', checked: r.freizeit === true })
|
||||
@@ -255,7 +286,7 @@ function rechtliches(e) {
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- Bezahlung --- */
|
||||
function zahlung(e) {
|
||||
export function zahlung(e) {
|
||||
const z = e.zahlung || {}
|
||||
const arten = ZAHLARTEN.map(([key, name]) => ({ key, name, input: h('input', { type: 'checkbox', checked: (z.vorOrtArten || []).includes(key) }) }))
|
||||
const el = abschnitt('Bezahlung', 'Bezahlt wird bei dir vor Ort. Welche Zahlarten nimmst du an? Gäste sehen das vor der Buchung.',
|
||||
|
||||
Reference in new issue
Block a user