Ö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>
This commit is contained in:
1 parent
242fbb01e5
commit
b3e96d4a56
8 files changed
+1684
-13
No files matched your search
@@ -9,7 +9,7 @@
|
||||
„geschlossen“, ein „bitte prüfen“-Tag (Heiligabend, Silvester, nur ortsuebliche Feiertage) folgt dem Wochenplan.
|
||||
Ausnahmen: Betriebsurlaub (von–bis, Text) und Sondertag (Datum, geschlossen oder Zeiten), Geltung „alle Pläne“ oder ein Plan. Nach dem Speichern
|
||||
eines Urlaubs ab 2 Tagen fragt der Editor „Hinweis-Leiste dazu anlegen?“ (ctx.hinweisFrage); „Ja“ legt den Vorschlag (hinweisVorschlag) in
|
||||
daten.hinweise an und speichert. */
|
||||
daten.hinweise an und speichert (danach werden Ausnahmen und die Hinweis-Liste neu gezeichnet). */
|
||||
|
||||
import { BUNDESLAENDER, datumKurz, datumLang, heuteBerlin, istTag } from './termine-view.js'
|
||||
import { abschnitt, allgemeinerFehler, editorZeile, feld, frage, gesperrt, h, neuerEintrag, zeigeFehler } from './termine-dom.js'
|
||||
@@ -196,7 +196,7 @@ function hinweisFrage(t) {
|
||||
}, { container: form })
|
||||
if (!ok || !sp.nochHier()) return
|
||||
ctx.hinweisFrage = null
|
||||
nachSpeichern(t, `Die Hinweis-Leiste „${v.text}“ ist angelegt.`, '[data-werkzeug="urlaub-neu"]', () => t.zeichne(['ausnahmen']))
|
||||
nachSpeichern(t, `Die Hinweis-Leiste „${v.text}“ ist angelegt.`, '[data-werkzeug="urlaub-neu"]', () => t.zeichne(['ausnahmen', 'hinweise']))
|
||||
})
|
||||
return form
|
||||
}
|
||||
|
||||
+96
-5
@@ -4,9 +4,14 @@
|
||||
root.dataset.state = Zustand der Funktion (setup, active, disabled): disabled heisst nur lesen (alles gesperrt, PUT antwortet mit 403).
|
||||
ctx von zeiten.js: { projectId, kundenansicht, neuZeichnen() } – neuZeichnen holt die Liste der Funktionen neu und zeichnet den Kopf der
|
||||
Ansicht (Statuskarte); der Editor ruft es einmal nach dem ersten Speichern (zeitenLeer wechselt), nie sonst.
|
||||
Der Editor-ctx t.ctx der Abschnitte (aus Aufgabe 29; Aufgabe 30 haengt ihre Abschnitte an ABSCHNITTE und die Live-Vorschau in den Kopf):
|
||||
Der Editor-ctx t.ctx der Abschnitte:
|
||||
{ projectId, kundenansicht, nurLesen, stand, entwurfsplan, offen, hinweisFrage, aktuell(), neuLaden(), meldung(text, art), uebernimm(antwort, { erstes }),
|
||||
leereDaten(), leer() }
|
||||
beiStand(fn), leereDaten(), leer() }
|
||||
Die sechs Abschnitte (ABSCHNITTE): Zeitplaene, Feiertage, Ausnahmen (zeiten-plaene.js, zeiten-ausnahmen.js), Hinweis-Leiste, Kontakt-Leiste (zeiten-leisten.js) und
|
||||
Google (zeiten-google.js). Oben im Kopfbereich steht die Live-Vorschau „So steht es gerade auf deiner Seite: …“ (Status aus GET /api/zeiten bzw. der Antwort des
|
||||
letzten Speicherns) mit dem Regler „Uhrzeit testen“ (Wochentag und Uhrzeit, GET /api/zeiten/<projectId>/vorschau?zeit=JJJJ-MM-TTTHH:MM, dieselbe Serverlogik; vor dem
|
||||
ersten Speichern antwortet der Server mit 409 keine_zeiten: das ist ein Hinweis, kein Fehler). Abgeschaltet (nur lesen) gibt es keine Vorschau: die Website zeigt dann nichts mehr.
|
||||
ctx.beiStand(fn): fn() laeuft nach jedem Speichern, wenn ctx.stand neu ist (Vorschau, Google-Text); gibt fn false zurueck, wird sie nicht mehr gerufen (Abschnitt weg).
|
||||
stand = Antwort des Servers { state, daten (null vor dem ersten Speichern), version, kundenansicht, vorschau, feiertage, googleText }; sie wird bei jedem
|
||||
Speichern und bei „Neu laden“ ersetzt. Ein Abschnitt ist eine Funktion (t) => Element mit t = { ctx, sp, zeichne, wurzel }.
|
||||
t.zeichne() zeichnet alles neu (nur nach „Neu laden“, wenn die Daten vom Server neu sind); t.zeichne(['ausnahmen', …]) zeichnet nur die genannten Abschnitte
|
||||
@@ -16,11 +21,13 @@
|
||||
kontakt.whatsapp.bestaetigtAt (R12, das tut Aufgabe 30 mit dem Haken des Kunden). */
|
||||
|
||||
import { fehlerKarte, h, request } from './termine-dom.js'
|
||||
import { istTag } from './termine-view.js'
|
||||
import { leererPlan } from './zeiten-view.js'
|
||||
import { WOCHENTAGE_PLAN, datumLang, heuteBerlin, istTag } from './termine-view.js'
|
||||
import { leererPlan, naechstesVorkommen, wochentagVon } from './zeiten-view.js'
|
||||
import { neuerZeitenSpeicher } from './zeiten-speichern.js'
|
||||
import { plaene } from './zeiten-plaene.js'
|
||||
import { ausnahmen, feiertage, feiertageUebernehmen } from './zeiten-ausnahmen.js'
|
||||
import { hinweise, kontakt } from './zeiten-leisten.js'
|
||||
import { google } from './zeiten-google.js'
|
||||
|
||||
/**
|
||||
* Abschnitte in der Reihenfolge der Seite (Spec 3.4). mitDaten: erst nach dem ersten Speichern. uebernehmen(alt, neu): wird gerufen, wenn der Abschnitt allein
|
||||
@@ -30,6 +37,9 @@ const ABSCHNITTE = [
|
||||
{ key: 'plaene', bauen: plaene, mitDaten: false },
|
||||
{ key: 'feiertage', bauen: feiertage, mitDaten: true, uebernehmen: feiertageUebernehmen },
|
||||
{ key: 'ausnahmen', bauen: ausnahmen, mitDaten: true },
|
||||
{ key: 'hinweise', bauen: hinweise, mitDaten: true },
|
||||
{ key: 'kontakt', bauen: kontakt, mitDaten: true },
|
||||
{ key: 'google', bauen: google, mitDaten: true },
|
||||
]
|
||||
|
||||
const NICHT_GELADEN = 'Deine Öffnungszeiten konnten gerade nicht geladen werden. Bitte versuch es noch einmal.'
|
||||
@@ -38,6 +48,10 @@ const KUNDENANSICHT = 'Kundenansicht: Was du hier speicherst, ändert die Öffnu
|
||||
const LEER = 'Trag deine Öffnungszeiten ein. Sobald du speicherst, kennt deine Website sie.'
|
||||
const SPAETER = 'Feiertage, Ausnahmen und alles Weitere kannst du eintragen, sobald deine Öffnungszeiten gespeichert sind.'
|
||||
|
||||
const VORSCHAU_LEER = 'Noch keine Öffnungszeiten gespeichert.'
|
||||
const VORSCHAU_ERST_SPEICHERN = 'Speichere zuerst deine Öffnungszeiten, dann kannst du Uhrzeiten testen.'
|
||||
const VORSCHAU_KEINE_ANZEIGE = 'Dazu gibt es gerade keine Anzeige.'
|
||||
|
||||
// Welcher Aufruf von renderEditor gehoert zu einem Platz? Ein neuer Aufruf macht den alten ungueltig (seine Antworten fassen nichts mehr an).
|
||||
const LAEUFE = new WeakMap()
|
||||
|
||||
@@ -58,6 +72,78 @@ function standAus(antwort) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Berliner Uhrzeit 'HH:MM' (Vorgabe des Reglers). */
|
||||
const berlinUhrzeit = (jetzt = new Date()) => jetzt.toLocaleTimeString('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
|
||||
/**
|
||||
* Live-Vorschau oben im Editor: der Status, wie ihn die Website jetzt zeigt (ec.stand.vorschau = { offen, text, kurz }), und der Regler „Uhrzeit testen“. Der
|
||||
* Regler fragt den Server mit dem naechsten Vorkommen des gewaehlten Wochentags (ab heute, Berliner Kalendertag) und der Uhrzeit (Berliner Wanduhr); die Logik ist
|
||||
* die der Website. Nur die letzte Anfrage zaehlt. Nach jedem Speichern (ec.beiStand) zeigt die Vorschau den neuen Status, und eine schon gestellte Frage wird mit
|
||||
* den neuen Daten wiederholt.
|
||||
*/
|
||||
function vorschauKarte(ec) {
|
||||
const jetzt = h('strong', { class: 'zt-vorschau-text', 'data-vorschau': 'jetzt' })
|
||||
const zeigeJetzt = () => {
|
||||
const v = ec.stand.vorschau
|
||||
const gueltig = v !== null && typeof v === 'object' && typeof v.text === 'string' && v.text !== ''
|
||||
jetzt.textContent = gueltig ? v.text : VORSCHAU_LEER
|
||||
jetzt.setAttribute('data-offen', gueltig ? String(v.offen === true || v.offen === false ? v.offen : null) : 'leer')
|
||||
}
|
||||
zeigeJetzt()
|
||||
|
||||
const heute = heuteBerlin()
|
||||
const tag = h('select', { id: 'zt-regler-tag', 'data-regler': 'tag' }, WOCHENTAGE_PLAN.map(([nr, name]) => h('option', { value: nr, selected: Number(nr) === wochentagVon(heute) }, name)))
|
||||
const zeit = h('input', { id: 'zt-regler-zeit', type: 'time', 'data-regler': 'zeit', value: berlinUhrzeit() })
|
||||
const ergebnis = h('p', { class: 'tm-hinweis zt-regler-ergebnis', 'data-vorschau': 'test', 'aria-live': 'polite' })
|
||||
const karte = h('section', { class: 'kb-panel zt-vorschau', 'aria-label': 'Vorschau deiner Öffnungszeiten' },
|
||||
h('p', { class: 'zt-vorschau-zeile' }, 'So steht es gerade auf deiner Seite: ', jetzt),
|
||||
h('fieldset', { class: 'zt-regler' },
|
||||
h('legend', null, 'Uhrzeit testen'),
|
||||
h('div', { class: 'zt-regler-felder' },
|
||||
h('div', { class: 'tm-feld' }, h('label', { for: 'zt-regler-tag' }, 'Wochentag'), tag),
|
||||
h('div', { class: 'tm-feld' }, h('label', { for: 'zt-regler-zeit' }, 'Uhrzeit'), zeit)),
|
||||
ergebnis))
|
||||
|
||||
let lauf = 0
|
||||
let benutzt = false
|
||||
async function frage() {
|
||||
const wann = zeit.value
|
||||
const nr = ++lauf
|
||||
if (!/^\d{2}:\d{2}$/.test(wann)) {
|
||||
ergebnis.textContent = ''
|
||||
ergebnis.removeAttribute('role')
|
||||
return
|
||||
}
|
||||
benutzt = true
|
||||
const datum = naechstesVorkommen(heuteBerlin(), Number(tag.value))
|
||||
const ort = () => nr === lauf && karte.isConnected && ec.aktuell() // eine veraltete Antwort oder ein anderer Editor zaehlt nicht
|
||||
try {
|
||||
const antwort = await request(`/api/zeiten/${encodeURIComponent(ec.projectId)}/vorschau?zeit=${encodeURIComponent(`${datum}T${wann}`)}`)
|
||||
if (!ort()) return
|
||||
ergebnis.removeAttribute('role')
|
||||
ergebnis.textContent = typeof antwort?.text === 'string' && antwort.text !== '' ? `${datumLang(datum)}, ${wann} Uhr: ${antwort.text}` : VORSCHAU_KEINE_ANZEIGE
|
||||
} catch (err) {
|
||||
if (!ort()) return
|
||||
if (err.code === 'keine_zeiten') {
|
||||
ergebnis.removeAttribute('role') // vor dem ersten Speichern gibt es nichts zu testen: ein Hinweis, kein Fehler
|
||||
ergebnis.textContent = VORSCHAU_ERST_SPEICHERN
|
||||
return
|
||||
}
|
||||
ergebnis.setAttribute('role', 'alert')
|
||||
ergebnis.textContent = err.message
|
||||
}
|
||||
}
|
||||
tag.addEventListener('change', frage)
|
||||
zeit.addEventListener('change', frage)
|
||||
ec.beiStand(() => {
|
||||
if (!karte.isConnected) return false
|
||||
zeigeJetzt()
|
||||
if (benutzt) frage()
|
||||
return true
|
||||
})
|
||||
return karte
|
||||
}
|
||||
|
||||
export async function renderEditor(root, ctx = {}) {
|
||||
const token = {}
|
||||
LAEUFE.set(root, token)
|
||||
@@ -78,6 +164,7 @@ export async function renderEditor(root, ctx = {}) {
|
||||
const nurLesen = ctx.nurLesen === true || root.dataset.state === 'disabled'
|
||||
const meldungEl = h('div', { class: 'tm-meldung', 'data-meldung': 'editor', role: 'status', 'aria-live': 'polite', tabindex: '-1' })
|
||||
const entwurfsplan = leererPlan({ name: 'Öffnungszeiten', kurz: 'oeffnungszeiten', haupt: true })
|
||||
const beobachter = new Set() // Funktionen, die nach jedem Speichern laufen (beiStand)
|
||||
const ec = {
|
||||
projectId: ctx.projectId,
|
||||
kundenansicht: ctx.kundenansicht === true,
|
||||
@@ -87,6 +174,7 @@ export async function renderEditor(root, ctx = {}) {
|
||||
offen: new Set([entwurfsplan.id, ...(stand.daten ? stand.daten.plaene.filter((p) => p?.haupt === true).map((p) => p.id) : [])]), // aufgeklappte Plaene
|
||||
hinweisFrage: null, // { vorschlag } nach einem Urlaub ab 2 Tagen
|
||||
aktuell,
|
||||
beiStand: (fn) => { beobachter.add(fn) },
|
||||
leer: () => ec.stand.daten === null,
|
||||
/** Daten fuer das erste Speichern: der leere Hauptplan; die Kontaktdaten setzt der Server auf seine Vorgabe. */
|
||||
leereDaten: () => ({ land: 'RP', plaene: [ec.entwurfsplan], feiertage: {}, ausnahmen: [], hinweise: [] }),
|
||||
@@ -109,6 +197,9 @@ export async function renderEditor(root, ctx = {}) {
|
||||
/** Antwort auf ein erfolgreiches Speichern. Das erste Speichern aendert, was die Huelle zeigt (zeitenLeer): sie zeichnet ihren Kopf neu. */
|
||||
uebernimm(antwort, { erstes = false } = {}) {
|
||||
ec.stand = standAus(antwort)
|
||||
for (const fn of [...beobachter]) {
|
||||
if (fn() === false) beobachter.delete(fn)
|
||||
}
|
||||
if (erstes && typeof ctx.neuZeichnen === 'function') {
|
||||
try {
|
||||
Promise.resolve(ctx.neuZeichnen()).catch(() => {}) // scheitert das, zeigt der Kopf den alten Stand bis zum naechsten Besuch
|
||||
@@ -150,7 +241,7 @@ export async function renderEditor(root, ctx = {}) {
|
||||
if (ec.kundenansicht || ec.stand.kundenansicht) teile.push(h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'kundenansicht' }, h('span', null, KUNDENANSICHT)))
|
||||
if (nurLesen) teile.push(h('div', { class: 'tm-box tm-box-warn', role: 'note', 'data-hinweis': 'nurlesen' }, h('span', null, NUR_LESEN)))
|
||||
else if (leer) teile.push(h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'leer' }, h('span', null, LEER)))
|
||||
teile.push(h('div', { class: 'zt-kopfbereich' })) // Platz der Live-Vorschau (Aufgabe 30)
|
||||
teile.push(h('div', { class: 'zt-kopfbereich' }, nurLesen ? null : vorschauKarte(ec))) // Live-Vorschau; abgeschaltet zeigt die Website nichts mehr
|
||||
for (const abschnitt of ABSCHNITTE) {
|
||||
if (abschnitt.mitDaten && leer) continue
|
||||
teile.push(baueAbschnitt(abschnitt))
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/* Oeffnungszeiten live im Kundenbereich: Editor, Bereich 6 „Google“ (Spec 3.4). google(t) ist ein Abschnitt des Editors (t = { ctx, sp, zeichne, wurzel }
|
||||
aus zeiten-editor.js). Der Text fuer das Google-Profil kommt vom Server (ctx.stand.googleText aus GET/PUT /api/zeiten: Montag bis Sonntag, danach die
|
||||
Sonderzeiten der naechsten 60 Tage) und wird hier nie selbst gebaut. „Für Google kopieren“ legt ihn in die Zwischenablage; fehlt die Schnittstelle oder
|
||||
lehnt der Browser ab, bleibt der Text im (schreibgeschuetzten) Textfeld markiert und eine Meldung sagt, wie der Kunde ihn selbst kopiert. Nach jedem Speichern
|
||||
folgt die Anzeige dem neuen Text (ctx.beiStand). Das Google-Profil selbst oeffnet der Link in einem neuen Fenster (noopener); wir schreiben dort nichts. */
|
||||
|
||||
import { abschnitt, h } from './termine-dom.js'
|
||||
|
||||
const GOOGLE_PROFIL = 'https://business.google.com/'
|
||||
const KOPIERT = 'Kopiert. Füge den Text jetzt in dein Google-Profil ein.'
|
||||
const KEIN_TEXT = 'Es gibt noch keinen Text zum Kopieren. Speichere zuerst deine Öffnungszeiten.'
|
||||
const NICHT_KOPIERT = 'Das Kopieren hat nicht geklappt. Der Text ist unten markiert: Kopiere ihn selbst (Strg+C, am Handy: lange drücken und „Kopieren“ wählen).'
|
||||
|
||||
export function google(t) {
|
||||
const { ctx } = t
|
||||
const text = () => (typeof ctx.stand.googleText === 'string' ? ctx.stand.googleText : '')
|
||||
const feld = h('textarea', { class: 'zt-google-text', readonly: true, 'aria-label': 'Öffnungszeiten für dein Google-Profil', 'data-google': 'text' })
|
||||
const meldung = h('p', { class: 'tm-hinweis zt-google-meldung', role: 'status', 'aria-live': 'polite', 'data-meldung': 'google' })
|
||||
const knopf = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'google-kopieren' }, 'Für Google kopieren')
|
||||
|
||||
const zeige = () => {
|
||||
feld.value = text()
|
||||
feld.setAttribute('rows', String(Math.min(Math.max(text().split('\n').length + 1, 4), 16)))
|
||||
}
|
||||
zeige()
|
||||
|
||||
knopf.addEventListener('click', async () => {
|
||||
const inhalt = text() // der Stand zum Zeitpunkt des Klicks, nicht der beim Zeichnen
|
||||
if (inhalt === '') {
|
||||
meldung.textContent = KEIN_TEXT
|
||||
return
|
||||
}
|
||||
let kopiert = false
|
||||
try {
|
||||
const zwischenablage = typeof navigator !== 'undefined' ? navigator.clipboard : undefined
|
||||
if (zwischenablage && typeof zwischenablage.writeText === 'function') {
|
||||
await zwischenablage.writeText(inhalt)
|
||||
kopiert = true
|
||||
}
|
||||
} catch {
|
||||
kopiert = false // nicht erlaubt: weiter mit dem markierten Text
|
||||
}
|
||||
if (kopiert) {
|
||||
meldung.textContent = KOPIERT
|
||||
return
|
||||
}
|
||||
feld.focus()
|
||||
feld.select()
|
||||
if (typeof feld.setSelectionRange === 'function') feld.setSelectionRange(0, feld.value.length) // iOS markiert sonst nichts
|
||||
meldung.textContent = NICHT_KOPIERT
|
||||
})
|
||||
|
||||
const el = abschnitt('Google',
|
||||
'Die Google-Daten im Seitencode laufen automatisch – dafür musst du nichts tun. Dein Google-Unternehmensprofil pflegst du selbst: Kopiere hier den Text mit deinen Öffnungszeiten (auch die Sonderzeiten der nächsten 60 Tage) und füge ihn dort ein.',
|
||||
feld,
|
||||
h('div', { class: 'zt-google-aktionen' },
|
||||
knopf,
|
||||
h('a', { class: 'kb-btn-outline', href: GOOGLE_PROFIL, target: '_blank', rel: 'noopener noreferrer', 'data-google': 'profil' }, 'Google-Profil öffnen')),
|
||||
meldung)
|
||||
ctx.beiStand(() => {
|
||||
if (!el.isConnected) return false // der Abschnitt wurde neu gezeichnet: diese Anzeige gibt es nicht mehr
|
||||
zeige()
|
||||
return true
|
||||
})
|
||||
return el
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
/* 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)
|
||||
}
|
||||
@@ -209,6 +209,18 @@ export function planLoeschenGrund(daten, planId) {
|
||||
return null
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- Wochentage --- */
|
||||
/** Wochentag eines Kalendertags 'JJJJ-MM-TT': 1 = Montag … 7 = Sonntag. */
|
||||
export function wochentagVon(tag) {
|
||||
const nr = new Date(`${tag}T00:00:00Z`).getUTCDay() // 0 = Sonntag
|
||||
return nr === 0 ? 7 : nr
|
||||
}
|
||||
|
||||
/** Der erste Tag ab heute (heute eingeschlossen), der auf den Wochentag (1 = Montag … 7 = Sonntag) fällt: Tag für den Regler „Uhrzeit testen“. */
|
||||
export function naechstesVorkommen(heute, wochentag) {
|
||||
return tagPlus(heute, (wochentag - wochentagVon(heute) + 7) % 7)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ Hinweis-Leiste --- */
|
||||
const VORLAUF_TAGE = 7
|
||||
const tageZwischen = (von, bis) => Math.round((Date.parse(`${bis}T00:00:00Z`) - Date.parse(`${von}T00:00:00Z`)) / 86400000)
|
||||
|
||||
+40
-2
@@ -1,4 +1,5 @@
|
||||
/* Öffnungszeiten live im Kundenbereich (zeiten.js, zeiten-bestellen.js und der Editor: zeiten-editor.js, zeiten-plaene.js, zeiten-ausnahmen.js).
|
||||
/* Öffnungszeiten live im Kundenbereich (zeiten.js, zeiten-bestellen.js und der Editor: zeiten-editor.js, zeiten-plaene.js, zeiten-ausnahmen.js,
|
||||
zeiten-leisten.js, zeiten-google.js).
|
||||
Farben und Abstände aus den Tokens von kundenbereich.css (.kb-page). Die Bausteine der Bestell- und Statusseiten (tm-bestellen, tm-statuskarte,
|
||||
tm-rechtstext, tm-haken, tm-pille, tm-box, tm-meldung, tm-info …) stehen in termine.css und gelten hier mit; neu sind nur Klassen mit zt-
|
||||
und die Regeln für #view-zeiten. Handy: bis 880px wie die App-Ansicht, Tippflächen mindestens 44px. */
|
||||
@@ -66,11 +67,48 @@
|
||||
.zt-tag > .tm-fehler { margin-top: 8px; }
|
||||
.zt-feiertag .zt-tag { margin-top: 10px; padding: 0; border: 0; background: transparent; }
|
||||
|
||||
/* Editor II: Live-Vorschau mit Regler „Uhrzeit testen“, Hinweis-Leiste, Kontakt-Leiste, Google (zeiten-editor.js, zeiten-leisten.js, zeiten-google.js) */
|
||||
.zt-editor [hidden] { display: none; }
|
||||
.zt-vorschau { padding: 16px 18px; }
|
||||
.zt-vorschau-zeile { margin: 0; font-size: 15px; line-height: 1.5; overflow-wrap: anywhere; }
|
||||
.zt-vorschau-text { font-size: 17px; }
|
||||
.zt-vorschau-text::before { content: ''; display: inline-block; width: 10px; height: 10px; margin-right: 8px; border-radius: 50%; background: var(--kb-muted); }
|
||||
.zt-vorschau-text[data-offen='true']::before { background: #4ade80; }
|
||||
.zt-vorschau-text[data-offen='false']::before { background: #f87171; }
|
||||
.zt-vorschau-text[data-offen='null']::before { background: #fbbf24; }
|
||||
.zt-regler { min-width: 0; margin: 14px 0 0; padding: 0; border: 0; }
|
||||
.zt-regler legend { padding: 0; margin-bottom: 2px; font-size: 13px; font-weight: 600; }
|
||||
.zt-regler-felder { display: flex; flex-wrap: wrap; gap: 0 14px; }
|
||||
.zt-regler-felder .tm-feld { margin-top: 8px; }
|
||||
.zt-regler-ergebnis { margin-top: 10px; font-size: 14px; line-height: 1.5; color: inherit; overflow-wrap: anywhere; }
|
||||
.zt-regler-ergebnis:empty { display: none; }
|
||||
.zt-regler-ergebnis[role='alert'] { color: #fecaca; }
|
||||
.zt-regler .tm-feld select, .zt-regler .tm-feld input { min-height: 40px; }
|
||||
.zt-art { display: inline-block; padding: 1px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.zt-art-info { background: #e0f2fe; color: #0c4a6e; }
|
||||
.zt-art-aktion { background: #dcfce7; color: #14532d; }
|
||||
.zt-art-urlaub { background: #fef3c7; color: #78350f; }
|
||||
.zt-art-wichtig { background: #b91c1c; color: #ffffff; }
|
||||
.zt-muster { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px 12px; margin: 14px 0 0; padding: 10px 14px; border-radius: 10px; font-size: 14px; line-height: 1.4; text-align: center; }
|
||||
.zt-muster-text { overflow-wrap: anywhere; }
|
||||
.zt-muster-knopf { padding: 3px 12px; border-radius: 999px; border: 1.5px solid currentColor; font-size: 13px; font-weight: 700; }
|
||||
.zt-knopf-felder { margin-top: 0; }
|
||||
.zt-hinweis-form .zt-tag { margin-top: 0; }
|
||||
.zt-whatsapp { margin-top: 10px; padding: 0 14px 10px; border-radius: 12px; border: 1px solid var(--kb-border); background: rgba(255, 255, 255, 0.02); }
|
||||
.zt-google-text {
|
||||
display: block; width: 100%; box-sizing: border-box; padding: 10px 12px; border-radius: 12px; font: inherit; font-size: 14px; line-height: 1.6; resize: vertical; white-space: pre; overflow: auto;
|
||||
border: 1px solid var(--kb-border-strong); background: rgba(255, 255, 255, 0.05); color: inherit;
|
||||
}
|
||||
.zt-google-aktionen { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 12px; }
|
||||
.zt-google-aktionen a { display: inline-flex; align-items: center; text-decoration: none; }
|
||||
.zt-google-meldung { margin-top: 10px; font-size: 13.5px; line-height: 1.5; color: inherit; }
|
||||
.zt-google-meldung:empty { display: none; }
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) {
|
||||
#view-zeiten button, #view-zeiten .kb-btn-outline, #view-zeiten .kb-btn-primary { min-height: 44px; }
|
||||
#view-zeiten .tm-haken { align-items: center; min-height: 44px; margin-top: 0; margin-bottom: 0; }
|
||||
#view-zeiten .tm-haken input { margin-top: 0; }
|
||||
#view-zeiten .zt-zeit input, #view-zeiten .zt-tag select, #view-zeiten .zt-feiertag select { min-height: 44px; }
|
||||
#view-zeiten .zt-zeit input, #view-zeiten .zt-tag select, #view-zeiten .zt-feiertag select, #view-zeiten .zt-regler .tm-feld select, #view-zeiten .zt-regler .tm-feld input { min-height: 44px; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.zt-fenster-name { min-width: 100%; }
|
||||
|
||||
Reference in new issue
Block a user