Files
Webklar-Kundenbereich/public/zeiten-leisten.js
T
ClaudeandClaude Opus 5.5 b3e96d4a56 Öffnungszeiten: Editor für Hinweis- und Kontakt-Leiste, Google und Vorschau
Hinweis-Leiste (Liste, „Hinweis-Leiste anlegen“, Art, Knopf, Zeitraum, Preis-Hinweis),
Kontakt-Leiste (Telefon, Adresse, WhatsApp mit Datenschutz-Haken, R12 in der Kundenansicht),
Google (Für Google kopieren, Profil-Link), Live-Vorschau mit Regler „Uhrzeit testen“
und UI-Prüfung test/ui/zeiten-portal.ui.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:43:22 +00:00

313 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Oeffnungszeiten live im Kundenbereich: Editor, Bereiche 4 „Hinweis-Leiste“ und 5 „Kontakt-Leiste (Handy)“ (Spec 3.4). Jeder Bereich ist eine Funktion
(t) => Element; t = { ctx, sp, zeichne, wurzel } kommt aus zeiten-editor.js (sp = Speicher des Editors, zeichne([…]) zeichnet einzelne Abschnitte neu).
Jede Aenderung speichert die ganzen Daten (zeiten-speichern.js); nach einem Speichern wird nur der eigene Abschnitt neu gezeichnet, ungespeicherte Eingaben
in anderen Formularen bleiben stehen. Abgeschaltete Funktion (ctx.nurLesen): dieselben Felder gesperrt, ohne Knoepfe zum Aendern. Stil: zeiten-ausnahmen.js.
Hinweis-Leiste: Liste der laufenden und geplanten Hinweise (Vergangenes eingeklappt), „Hinweis-Leiste anlegen“ (dieser Text ist fest: die Relaunch-Station
tippt den Knopf an) und je Hinweis Text (<= 140), Art (Info, Urlaub, Aktion, Wichtig mit festen Farben wie auf der Website), optionaler Knopf (Link
https oder Anrufen) und Zeitraum von–bis (Kalendertage, beide optional; „Aktion“ verlangt ein Enddatum). Der Browser prueft frueh (Aktion ohne Ende, Ende vor
Beginn, leerer Text) und zeigt bei Link und Rufnummer nur einen Hinweis (linkOk): entschieden wird auf dem Server, sein Fehler steht am Feld.
Kontakt-Leiste: Ein/Aus, Telefon, Adresse, WhatsApp (Nummer, Starttext, Haken „Datenschutzerklaerung“). Der Server speichert Rufnummern als Ziffern mit
Laendervorwahl; der Editor zeigt die gespeicherte Nummer aus der Antwort (+49 …) im Feld und in der Meldung, damit der Kunde sieht, was gilt.
R12: den Zeitpunkt der WhatsApp-Bestaetigung (kontakt.whatsapp.bestaetigtAt) setzt nur der Kunde selbst. In der Kundenansicht ist der Haken gesperrt und der
gespeicherte Wert geht unveraendert zurueck (ohne gespeicherten kein Speichern mit WhatsApp); beim Kunden geht ein frischer Zeitpunkt nur hinaus, wenn der Haken
neu gesetzt wurde (der Server ersetzt ihn durch seine Zeit), sonst der gespeicherte Wert. */
import { datumKurz, heuteBerlin, istTag } from './termine-view.js'
import { abschnitt, allgemeinerFehler, editorZeile, feld, gesperrt, h, hakenFeld, neuerEintrag, zeigeFehler } from './termine-dom.js'
import { HINWEIS_ARTEN, ZEITEN_LIMITS, ZEITEN_MAX, linkOk, neueZeitenId, preisHinweisNoetig } from './zeiten-view.js'
import { entfernenKnopf, nachSpeichern, stelleVon } from './zeiten-speichern.js'
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
/* ----------------------------------------------------------- Hinweis-Leiste --- */
const ART_NAME = Object.freeze({ info: 'Info', urlaub: 'Urlaub', aktion: 'Aktion', wichtig: 'Wichtig' })
const KNOPF_WAHL = Object.freeze([['', 'Kein Knopf'], ['link', 'Link'], ['anruf', 'Anrufen']])
const PREIS_HINWEIS = 'Bei Preisangaben gelten Endpreise. Bei Waren nennst du dazu den niedrigsten Preis der letzten 30 Tage.'
const LINK_HINWEIS = 'Bitte gib eine vollständige Adresse mit https:// an.'
const ANRUF_HINWEIS = 'Bitte nur Ziffern, Leerzeichen, + / und - (höchstens 30 Zeichen).'
const KNOPF_ARTEN = Object.freeze(['link', 'anruf'])
/** Rufnummer eines Anruf-Knopfs wie auf dem Server: nur Ziffern, Leerzeichen, / und -, ein + nur vorn, mindestens eine Ziffer, hoechstens 30 Zeichen. */
const anrufOk = (ziel) => {
const s = typeof ziel === 'string' ? ziel.trim() : ''
return s !== '' && [...s].length <= ZEITEN_MAX.telefon && /^\+?[0-9 /-]+$/.test(s) && /\d/.test(s)
}
/** 'JJJJ-MM-TT' oder '' (fehlt, null oder kein Kalendertag). */
const tagOder = (wert) => (typeof wert === 'string' && istTag(wert) ? wert : '')
/** „12.10.2026 – 26.10.2026“, „ab …“, „bis …“ oder „immer sichtbar“. */
function zeitraumText(x) {
const von = tagOder(x.von)
const bis = tagOder(x.bis)
if (von && bis) return von === bis ? datumKurz(von) : `${datumKurz(von)} – ${datumKurz(bis)}`
if (von) return `ab ${datumKurz(von)}`
if (bis) return `bis ${datumKurz(bis)}`
return 'immer sichtbar'
}
const nachBeginn = (a, b) => (tagOder(a.von) < tagOder(b.von) ? -1 : tagOder(a.von) > tagOder(b.von) ? 1 : 0)
export function hinweise(t) {
const { ctx } = t
const alle = (Array.isArray(ctx.stand.daten.hinweise) ? ctx.stand.daten.hinweise : []).filter((x) => istObjekt(x) && typeof x.id === 'string')
const heute = heuteBerlin()
const vorbei = (x) => tagOder(x.bis) !== '' && x.bis < heute
const kommend = alle.filter((x) => !vorbei(x)).sort(nachBeginn)
const alt = alle.filter(vorbei).sort(nachBeginn).reverse()
const voll = alle.length >= ZEITEN_LIMITS.hinweise
const neu = neuerEintrag({
nurLesen: ctx.nurLesen, text: 'Hinweis-Leiste anlegen', werkzeug: 'hinweis-neu', primaer: true, voll, grenzText: `Mehr als ${ZEITEN_LIMITS.hinweise} Hinweise sind nicht möglich.`,
baue: (schliessen) => formular(t, { a: null, schliessen }),
})
const reihe = (x) => {
const art = HINWEIS_ARTEN.includes(x.art) ? x.art : 'info'
const knopf = istObjekt(x.knopf) && KNOPF_ARTEN.includes(x.knopf.art) ? x.knopf : null
const von = tagOder(x.von)
const zustand = vorbei(x) ? ['vorbei', 'off'] : von && von > heute ? ['geplant', 'wait'] : ['läuft', 'ok']
const text = typeof x.text === 'string' ? x.text : ''
return editorZeile({
attrs: { 'data-id': x.id },
nurLesen: ctx.nurLesen,
label: `Hinweis ${text}`,
haupt: [
h('strong', { class: 'tm-name' }, text),
h('span', { class: 'tm-meta' },
h('span', { class: `zt-art zt-art-${art}` }, ART_NAME[art]),
` · ${zeitraumText(x)}${knopf ? ` · Knopf „${typeof knopf.text === 'string' ? knopf.text : ''}“ (${knopf.art === 'anruf' ? 'Anrufen' : 'Link'})` : ''}`),
],
pille: h('span', { class: `tm-pille tm-pille-${zustand[1]}` }, zustand[0]),
baue: (schliessen) => formular(t, { a: x, schliessen }),
})
}
return abschnitt('Hinweis-Leiste',
'Eine Leiste ganz oben auf deiner Website – zum Beispiel für Betriebsurlaub, eine Aktion oder etwas Wichtiges. Die Leiste erscheint nur in dem Zeitraum, den du einstellst. Gelten mehrere gleichzeitig, zeigt die Website nur einen: zuerst „Wichtig“, dann „Urlaub“, „Aktion“ und „Info“.',
h('div', { class: 'zt-werkzeuge' }, neu.leiste),
neu.box,
kommend.length
? h('ul', { class: 'tm-eintraege', 'aria-label': 'Laufende und geplante Hinweise' }, kommend.map(reihe))
: h('p', { class: 'tm-leer' }, 'Keine Hinweise eingetragen.'),
alt.length
? h('details', { class: 'tm-details' }, h('summary', null, `Vergangene Hinweise (${alt.length})`), h('ul', { class: 'tm-eintraege' }, alt.map(reihe)))
: null)
}
/**
* Formular eines neuen (a null) oder bestehenden Hinweises. Die Felder heissen nach dem Pfad der Fehler des Servers (hinweise.<ID>.text, .art, .knopf.art,
* .knopf.text, .knopf.ziel, .von, .bis); Absenden sucht den Hinweis per ID.
*/
function formular(t, { a, schliessen }) {
const { ctx, sp } = t
const neu = a === null
const alt = a || { id: neueZeitenId('h'), art: 'info', text: '', knopf: null, von: null, bis: null }
const p = `hinweise.${alt.id}`
const knopfAlt = istObjekt(alt.knopf) && KNOPF_ARTEN.includes(alt.knopf.art) ? alt.knopf : null
const text = h('input', { type: 'text', maxlength: ZEITEN_MAX.hinweisText, autocomplete: 'off', placeholder: 'z. B. Heute geschlossen – morgen wieder für dich da', value: typeof alt.text === 'string' ? alt.text : '' })
const art = h('select', null, HINWEIS_ARTEN.map((k) => h('option', { value: k, selected: k === (HINWEIS_ARTEN.includes(alt.art) ? alt.art : 'info') }, ART_NAME[k])))
const knopfArt = h('select', null, KNOPF_WAHL.map(([wert, name]) => h('option', { value: wert, selected: wert === (knopfAlt?.art ?? '') }, name)))
const knopfText = h('input', { type: 'text', maxlength: ZEITEN_MAX.knopfText, autocomplete: 'off', placeholder: 'z. B. Mehr erfahren', value: typeof knopfAlt?.text === 'string' ? knopfAlt.text : '' })
const knopfZiel = h('input', { type: 'url', maxlength: ZEITEN_MAX.link, autocomplete: 'off', value: typeof knopfAlt?.ziel === 'string' ? knopfAlt.ziel : '' })
const von = h('input', { type: 'date', value: tagOder(alt.von) })
const bis = h('input', { type: 'date', value: tagOder(alt.bis) })
const preisHinweis = h('p', { class: 'tm-hinweis', 'data-hinweis': 'preis', hidden: true }, PREIS_HINWEIS)
const zielHinweis = h('p', { class: 'tm-hinweis', 'data-hinweis': 'knopf-ziel', hidden: true }, LINK_HINWEIS)
const zielFeld = feld(`${p}.knopf.ziel`, 'Link', knopfZiel)
zielFeld.insertBefore(zielHinweis, zielFeld.querySelector('[data-fehler]'))
const textFeld = feld(`${p}.text`, 'Text', text, `Höchstens ${ZEITEN_MAX.hinweisText} Zeichen.`)
textFeld.insertBefore(preisHinweis, textFeld.querySelector('[data-fehler]'))
const knopfFelder = h('div', { class: 'tm-raster zt-knopf-felder', 'data-knopf-felder': '' },
feld(`${p}.knopf.text`, 'Beschriftung des Knopfs', knopfText, `Höchstens ${ZEITEN_MAX.knopfText} Zeichen.`),
zielFeld)
// Muster: so sieht die Leiste in der Farbe der Art aus (feste Farben wie auf der Website)
const musterText = h('span', { class: 'zt-muster-text' })
const musterKnopf = h('span', { class: 'zt-muster-knopf' })
const muster = h('div', { class: 'zt-muster', 'aria-hidden': 'true', 'data-muster': '' }, musterText, musterKnopf)
const aktualisiere = () => {
preisHinweis.hidden = !preisHinweisNoetig(text.value)
const wahl = knopfArt.value
knopfFelder.hidden = wahl === ''
knopfZiel.type = wahl === 'anruf' ? 'tel' : 'url'
knopfZiel.placeholder = wahl === 'anruf' ? 'z. B. 06301 123456' : 'https://'
zielFeld.querySelector('label').textContent = wahl === 'anruf' ? 'Telefonnummer' : 'Link'
const ziel = knopfZiel.value.trim()
zielHinweis.textContent = wahl === 'anruf' ? ANRUF_HINWEIS : LINK_HINWEIS
zielHinweis.hidden = wahl === '' || ziel === '' || (wahl === 'anruf' ? anrufOk(ziel) : linkOk(ziel))
muster.className = `zt-muster zt-art-${art.value}`
musterText.textContent = text.value.trim() || 'Dein Hinweistext'
musterKnopf.textContent = knopfText.value.trim()
musterKnopf.hidden = wahl === '' || knopfText.value.trim() === ''
}
for (const steuer of [text, art, knopfArt, knopfText, knopfZiel]) {
steuer.addEventListener('input', aktualisiere)
steuer.addEventListener('change', aktualisiere)
}
aktualisiere()
const entfernen = neu || ctx.nurLesen ? null : entfernenKnopf(sp, {
titel: 'Hinweis entfernen?',
text: 'Die Leiste verschwindet von deiner Website.',
aendere: (entwurf) => { entwurf.hinweise = (Array.isArray(entwurf.hinweise) ? entwurf.hinweise : []).filter((x) => x?.id !== alt.id) },
nachher: () => nachSpeichern(t, 'Hinweis entfernt.', '[data-werkzeug="hinweis-neu"]', () => t.zeichne(['hinweise'])),
})
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'hinweis-speichern' }, neu ? 'Hinweis speichern' : 'Speichern')
const form = h('form', { class: 'kb-panel tm-formular zt-hinweis-form', novalidate: true, 'aria-label': neu ? 'Neuer Hinweis' : 'Hinweis bearbeiten' },
h('h3', null, neu ? 'Neuer Hinweis' : 'Hinweis'),
gesperrt(ctx.nurLesen, [
textFeld,
feld(`${p}.art`, 'Art', art, 'Jede Art hat ihre feste Farbe.'),
muster,
feld(`${p}.knopf.art`, 'Knopf (optional)', knopfArt, 'Ein Knopf in der Leiste: ein Link auf eine Seite oder ein Anruf.'),
knopfFelder,
h('div', { class: 'tm-raster' },
feld(`${p}.von`, 'Sichtbar ab (optional)', von),
feld(`${p}.bis`, 'Sichtbar bis (bei einer Aktion nötig)', bis)),
h('p', { class: 'tm-hinweis' }, 'Ohne Datum gilt der Hinweis immer. Bei einer Aktion brauchst du ein Enddatum.'),
]),
allgemeinerFehler(),
h('div', { class: 'tm-frage-knoepfe' },
ctx.nurLesen ? null : speichern,
h('button', { type: 'button', class: 'kb-btn-outline', onclick: schliessen }, ctx.nurLesen ? 'Schließen' : 'Abbrechen'),
entfernen?.knopf))
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
const ok = await sp.speichere((entwurf) => {
const fehler = {}
const vonWert = von.value.trim()
const bisWert = bis.value.trim()
const hinweisText = text.value.trim()
if (hinweisText === '') fehler[`${p}.text`] = 'Bitte gib einen Text ein.'
if (vonWert !== '' && !istTag(vonWert)) fehler[`${p}.von`] = 'Bitte gib ein gültiges Datum an.'
if (bisWert !== '' && !istTag(bisWert)) fehler[`${p}.bis`] = 'Bitte gib ein gültiges Datum an.'
if (!fehler[`${p}.von`] && !fehler[`${p}.bis`]) {
if (vonWert !== '' && bisWert !== '' && bisWert < vonWert) fehler[`${p}.bis`] = 'Das Ende darf nicht vor dem Beginn liegen.'
else if (art.value === 'aktion' && bisWert === '') fehler[`${p}.bis`] = 'Eine Aktion braucht ein Enddatum.'
}
if (Object.keys(fehler).length) {
zeigeFehler(form, { fehler })
return false
}
// ein Knopf ohne Beschriftung und Ziel ist kein Knopf (wie auf dem Server)
const ziel = knopfZiel.value.trim()
const beschriftung = knopfText.value.trim()
const knopf = knopfArt.value !== '' && (ziel !== '' || beschriftung !== '') ? { art: knopfArt.value, ziel, text: beschriftung } : null
const eintrag = { id: alt.id, art: art.value, text: hinweisText, knopf, von: vonWert || null, bis: bisWert || null }
if (neu) {
entwurf.hinweise = [...(Array.isArray(entwurf.hinweise) ? entwurf.hinweise : []), eintrag]
return true
}
const stelle = stelleVon(entwurf.hinweise, alt.id, form, 'Dieser Hinweis')
if (stelle < 0) return false
entwurf.hinweise[stelle] = eintrag
return true
}, { container: form })
if (!ok || !sp.nochHier()) return
nachSpeichern(t, 'Hinweis gespeichert.', neu ? '[data-werkzeug="hinweis-neu"]' : `li[data-id="${CSS.escape(alt.id)}"] [data-aktion="bearbeiten"]`, () => t.zeichne(['hinweise']))
})
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
}
/* ------------------------------------------------------------ Kontakt-Leiste --- */
const WHATSAPP_HAKEN = 'Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen'
const HAKEN_FEHLT = 'Bitte bestätige, dass du den WhatsApp-Hinweis in deine Datenschutzerklärung aufgenommen hast.'
const KUNDE_BESTAETIGT = 'Der Kunde muss den WhatsApp-Hinweis selbst bestätigen – in der Kundenansicht geht das nicht.'
/** Gespeicherte Rufnummer (Ziffern mit Laendervorwahl, z. B. 491711234567) als „+49 1711234567“; alles andere bleibt, wie es gespeichert ist. */
export function nummerAnzeige(wert) {
const s = typeof wert === 'string' ? wert.trim() : ''
if (!/^\d{6,15}$/.test(s)) return s
return s.startsWith('49') ? `+49 ${s.slice(2)}` : `+${s}`
}
export function kontakt(t) {
const { ctx, sp } = t
const nurLesen = ctx.nurLesen
const kundenansicht = ctx.kundenansicht === true || ctx.stand.kundenansicht === true
const k = istObjekt(ctx.stand.daten.kontakt) ? ctx.stand.daten.kontakt : {}
const wa = istObjekt(k.whatsapp) ? k.whatsapp : null
const gespeichertAt = typeof wa?.bestaetigtAt === 'string' ? wa.bestaetigtAt : ''
const telefonAlt = typeof k.telefon === 'string' ? k.telefon : ''
const nummerAlt = typeof wa?.nummer === 'string' ? wa.nummer : ''
const telefonAnzeige = nummerAnzeige(telefonAlt)
const nummerAnz = nummerAnzeige(nummerAlt)
const an = h('input', { type: 'checkbox', checked: k.an === true })
const telefon = h('input', { type: 'tel', maxlength: ZEITEN_MAX.telefon, autocomplete: 'off', placeholder: 'z. B. 06301 123456', value: telefonAnzeige })
const adresse = h('input', { type: 'text', maxlength: ZEITEN_MAX.adresse, autocomplete: 'off', placeholder: 'z. B. Hauptstraße 1, 67722 Winnweiler', value: typeof k.adresse === 'string' ? k.adresse : '' })
const waAn = h('input', { type: 'checkbox', checked: wa !== null })
const nummer = h('input', { type: 'tel', maxlength: ZEITEN_MAX.telefon, autocomplete: 'off', placeholder: 'z. B. 0171 1234567', value: nummerAnz })
const starttext = h('textarea', { rows: 3, maxlength: ZEITEN_MAX.starttext, placeholder: 'z. B. Hallo, ich habe eine Frage.' })
starttext.value = typeof wa?.starttext === 'string' ? wa.starttext : ''
const bestaetigt = h('input', { type: 'checkbox', checked: gespeichertAt !== '', disabled: kundenansicht })
const waBlock = h('div', { class: 'zt-whatsapp', 'data-whatsapp': '', hidden: wa === null },
feld('kontakt.whatsapp.nummer', 'WhatsApp-Nummer', nummer, 'Mit Vorwahl. Wir speichern sie mit Ländervorwahl, zum Beispiel +49 …'),
feld('kontakt.whatsapp.starttext', 'Vorgeschriebener Text (optional)', starttext, `Dieser Text steht schon im Nachrichtenfeld, wenn jemand auf den WhatsApp-Knopf tippt. Höchstens ${ZEITEN_MAX.starttext} Zeichen.`),
hakenFeld('kontakt.whatsapp.bestaetigtAt', bestaetigt, WHATSAPP_HAKEN, 'Beim Tippen öffnet sich WhatsApp (ein Dienst von Meta). Das gehört in die Datenschutzerklärung deiner Website – frag uns im Chat, wenn du den passenden Absatz brauchst.'),
kundenansicht ? h('p', { class: 'tm-hinweis', 'data-hinweis': 'whatsapp-kundenansicht' }, 'Kundenansicht: Den Haken zur Datenschutzerklärung kann nur der Kunde selbst setzen.') : null)
waAn.addEventListener('change', () => { waBlock.hidden = !waAn.checked })
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'kontakt-speichern' }, 'Kontakt-Leiste speichern')
const form = h('form', { novalidate: true, 'aria-label': 'Kontakt-Leiste' },
gesperrt(nurLesen, [
hakenFeld('kontakt.an', an, 'Kontakt-Leiste auf dem Handy anzeigen', 'Eine Leiste unten auf deiner Website, nur auf dem Handy: Anrufen, Route und WhatsApp – je nachdem, was du einträgst.'),
feld('kontakt.telefon', 'Telefon', telefon, 'Für „Anrufen“. Wir speichern die Nummer mit Ländervorwahl (+49 für Deutschland).'),
feld('kontakt.adresse', 'Adresse für „Route“', adresse, `Höchstens ${ZEITEN_MAX.adresse} Zeichen.`),
hakenFeld('kontakt.whatsapp', waAn, 'WhatsApp-Knopf anbieten'),
waBlock,
]),
allgemeinerFehler(),
nurLesen ? null : h('div', { class: 'tm-frage-knoepfe' }, speichern))
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
const ok = await sp.speichere((entwurf) => {
const fehler = {}
let whatsapp = null
if (waAn.checked) {
const nummerWert = nummer.value === nummerAnz ? nummerAlt : nummer.value.trim()
if (nummerWert === '') fehler['kontakt.whatsapp.nummer'] = 'Bitte gib die Nummer an.'
let bestaetigtAt
if (kundenansicht) {
// R12: im Namen des Kunden gibt es nie einen neuen Zeitpunkt; ein gespeicherter geht unveraendert zurueck
if (gespeichertAt !== '') bestaetigtAt = gespeichertAt
else fehler['kontakt.whatsapp.bestaetigtAt'] = KUNDE_BESTAETIGT
} else if (bestaetigt.checked) {
bestaetigtAt = gespeichertAt !== '' ? gespeichertAt : new Date().toISOString() // der Server ersetzt einen neuen Zeitpunkt durch seine Zeit
} else {
fehler['kontakt.whatsapp.bestaetigtAt'] = HAKEN_FEHLT
}
whatsapp = { nummer: nummerWert, starttext: starttext.value.trim(), bestaetigtAt }
}
if (Object.keys(fehler).length) {
zeigeFehler(form, { fehler })
return false
}
entwurf.kontakt = {
...(istObjekt(entwurf.kontakt) ? entwurf.kontakt : {}),
an: an.checked,
telefon: telefon.value === telefonAnzeige ? telefonAlt : telefon.value.trim(),
adresse: adresse.value.trim(),
whatsapp,
}
return true
}, { container: form })
if (!ok || !sp.nochHier()) return
// was der Server gespeichert hat, steht danach im Feld (nur dieser Abschnitt wird neu gezeichnet) und in der Meldung
const neu = istObjekt(ctx.stand.daten.kontakt) ? ctx.stand.daten.kontakt : {}
const nummern = [neu.telefon ? `Telefon ${nummerAnzeige(neu.telefon)}` : '', neu.whatsapp?.nummer ? `WhatsApp ${nummerAnzeige(neu.whatsapp.nummer)}` : ''].filter(Boolean)
nachSpeichern(t, `Kontakt-Leiste gespeichert.${nummern.length ? ` Gespeichert als ${nummern.join(', ')}.` : ''}`, 'form[aria-label="Kontakt-Leiste"] button[type="submit"]', () => t.zeichne(['kontakt']))
})
return abschnitt('Kontakt-Leiste (Handy)',
'Auf dem Handy erscheint unten auf deiner Website eine Leiste mit Knöpfen zum Anrufen, für die Route und für WhatsApp. Auf größeren Bildschirmen erscheint sie nicht.',
form)
}