Ö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
+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))
|
||||
|
||||
Reference in new issue
Block a user