Öffnungszeiten: Speichern zeichnet nur neu, was sich geändert hat

Ungespeicherte Eingaben in anderen Formularen bleiben stehen: nach dem Speichern
wird nur die Plankarte, die Planliste (mit den Eingaben der übrigen Karten),
Feiertage oder Ausnahmen neu gezeichnet, „Nein, danke“ entfernt nur die Frage.
Während des Speicherns sind auch die Eingabefelder gesperrt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 06:18:21 +00:00
1 parent 04a3525f79
commit f01ed15247
6 files changed
+409 -58

No files matched your search

+100 -9
View File
@@ -1,6 +1,9 @@
/* Oeffnungszeiten live im Kundenbereich: Editor, Bereich 1 „Zeitplaene“ (Spec 3.4) und die Eingabe eines Tages, die auch Feiertage und
Sondertage nutzen (tagFelder). plaene(t) ist ein Abschnitt des Editors; t = { ctx, sp, zeichne, wurzel } kommt aus zeiten-editor.js
(sp = Speicher des Editors, zeichne() zeichnet alle Abschnitte neu). Jede Aenderung speichert die ganzen Daten (zeiten-speichern.js).
(sp = Speicher des Editors, zeichne(['ausnahmen', …]) zeichnet einzelne Abschnitte neu). Jede Aenderung speichert die ganzen Daten (zeiten-speichern.js).
Nach einem Speichern wird nur neu gezeichnet, was sich geaendert hat: die Karte des gespeicherten Plans (ersetzePlanKarte), die Liste der Plaene bei
Hinzufuegen, Loeschen und Hauptplan (zeichnePlanListe, mit den ungespeicherten Eingaben der anderen Karten), die Ausnahmen bei neuem oder geloeschtem Plan
und bei einem neuen Namen. Alles andere in der Seite bleibt mit seinen ungespeicherten Eingaben stehen.
Ein Plan: Name, Kurzname (nur Anzeige: der Server vergibt ihn beim ersten Speichern endgueltig und nennt ihn in der Antwort, R33), je
Wochentag der Haken „geöffnet“ mit zwei Zeitpaaren (<input type="time" step="300">) und der Wahl „zu festen Zeiten“, „rund um die Uhr“,
„nach Vereinbarung“; Knoepfe Speichern, Zum Hauptplan machen, Plan löschen (nicht der einzige, nicht der Hauptplan).
@@ -32,6 +35,9 @@ const zeitWert = (wert) => (wert === '24:00' ? '00:00' : typeof wert === 'string
let idZaehler = 0
/** Gibt es in der Auswahl einen Wert wert? (Eine Eingabe aus einer anderen Liste, z. B. nach einem Wechsel des Bundeslands, passt eventuell nicht mehr.) */
const hatOption = (auswahl, wert) => [...auswahl.querySelectorAll('option')].some((o) => o.value === wert)
/**
* Eingabe eines Tages (Wochentag, Feiertag, Sondertag): Betriebsart und bis zu zwei Zeitpaare.
* haken: Wochentag – Haken „geöffnet“ (ohne Haken: geschlossen) und daneben die Wahl der Art; sonst eine Auswahl der Betriebsart (wahl: Liste
@@ -39,8 +45,9 @@ let idZaehler = 0
* pfad: Name des Feldes (data-feld) und des Fehlers (data-fehler) – so findet ihn die Antwort des Servers; fensterPfad: Pfad des Fehlers bei den
* Zeitfenstern, wenn der Server fuer Betriebsart und Fenster getrennte Pfade nennt (Sondertag: …modus und …fenster).
* sichtbar: die Beschriftung der Auswahl steht in der Seite (sonst nur fuer Screenreader).
* => { el, lies }: lies() liefert { modus, fenster } oder { fehler: { [pfad]: Text } } (ein Paar nur halb ausgefuellt). Die Regeln der Fenster pruefen
* der Aufrufer (tagFehler/wochenFehler) und der Server.
* => { el, lies, merke, stelleWieder }: lies() liefert { modus, fenster } oder { fehler: { [pfad]: Text } } (ein Paar nur halb ausgefuellt). Die Regeln der
* Fenster pruefen der Aufrufer (tagFehler/wochenFehler) und der Server. merke() haelt die Eingaben so fest, wie sie im Formular stehen (auch Halbfertiges),
* stelleWieder(merke()) setzt sie in einem neu gebauten Feld wieder ein: so gehen ungespeicherte Eingaben nicht verloren, wenn die Karte neu gezeichnet wird.
*/
export function tagFelder({ pfad, fensterPfad = pfad, label, modus = 'zu', fenster = [], haken = false, wahl = MODUS_WAHL, sichtbar = false }) {
const start = typeof modus === 'string' && (MODI.includes(modus) || modus === '') ? modus : 'zu'
@@ -56,6 +63,7 @@ export function tagFelder({ pfad, fensterPfad = pfad, label, modus = 'zu', fenst
let felder
let modusWert
let zeige
let setzeModus
if (haken) {
const geoeffnet = h('input', { type: 'checkbox', checked: start !== 'zu' && start !== '' })
geoeffnet.dataset.feld = pfad
@@ -63,6 +71,10 @@ export function tagFelder({ pfad, fensterPfad = pfad, label, modus = 'zu', fenst
kopf = h('div', { class: 'tm-feld tm-feld-haken' }, h('label', { class: 'tm-haken' }, geoeffnet, h('span', null, h('strong', null, label), ' geöffnet')))
felder = h('div', { class: 'zt-tag-felder' }, art, zeiten)
modusWert = () => (geoeffnet.checked ? art.value : 'zu')
setzeModus = (wert) => {
geoeffnet.checked = wert.an === true
if (hatOption(art, wert.art)) art.value = wert.art
}
zeige = () => {
felder.hidden = !geoeffnet.checked
zeiten.hidden = !(geoeffnet.checked && art.value === 'zeiten')
@@ -76,6 +88,7 @@ export function tagFelder({ pfad, fensterPfad = pfad, label, modus = 'zu', fenst
kopf = h('div', { class: 'tm-feld' }, h('label', { for: id, class: sichtbar ? 'tm-label' : 'tm-sr' }, sichtbar ? 'Öffnung' : `${label}: Öffnung`), auswahl)
felder = h('div', { class: 'zt-tag-felder' }, zeiten)
modusWert = () => auswahl.value
setzeModus = (wert) => { if (hatOption(auswahl, wert.wahl)) auswahl.value = wert.wahl }
zeige = () => { zeiten.hidden = auswahl.value !== 'zeiten' }
auswahl.addEventListener('change', zeige)
}
@@ -98,7 +111,22 @@ export function tagFelder({ pfad, fensterPfad = pfad, label, modus = 'zu', fenst
}
return { modus: art, fenster: gelesen }
}
return { el, lies }
const merke = () => ({
an: haken ? modusWert() !== 'zu' : undefined,
art: haken ? (el.querySelector('select')?.value ?? '') : undefined,
wahl: haken ? undefined : modusWert(),
zeiten: paare.flatMap((paar) => [paar.von.value, paar.bis.value]),
})
const stelleWieder = (gemerkt) => {
if (!gemerkt) return
setzeModus(gemerkt)
paare.forEach((paar, i) => {
paar.von.value = gemerkt.zeiten?.[2 * i] ?? ''
paar.bis.value = gemerkt.zeiten?.[2 * i + 1] ?? ''
})
zeige()
}
return { el, lies, merke, stelleWieder }
}
/* ----------------------------------------------------------------- Zeitplaene --- */
@@ -107,6 +135,12 @@ const LOESCHEN_WARNUNG = 'Steht dieser Plan auf deiner Website, zeigt sie dort w
/** Der Plan mit dieser ID im aktuellen Stand (nach dem Speichern: mit dem Kurznamen des Servers). */
const planMitId = (ctx, id) => (ctx.stand.daten?.plaene || []).find((p) => p?.id === id)
const planSelektor = (id, rest) => `details[data-plan="${CSS.escape(id)}"] ${rest}`
const grenzText = `Mehr als ${ZEITEN_LIMITS.plaene} Zeitpläne sind nicht möglich.`
// Pro Karte, wie man ihre Eingaben festhaelt und wieder einsetzt (merke/stelleWieder): die Liste der Plaene wird bei „Plan hinzufügen“, „Löschen“
// und „Zum Hauptplan machen“ neu gezeichnet; was in den anderen Karten ungespeichert steht, geht dabei nicht verloren.
const KARTEN = new WeakMap()
const kartenIn = (t) => [...(t.wurzel.querySelector('.zt-plaene')?.children ?? [])]
export function plaene(t) {
const { ctx } = t
@@ -114,16 +148,51 @@ export function plaene(t) {
const liste = daten ? daten.plaene : [ctx.entwurfsplan]
const neu = daten ? neuerEintrag({
nurLesen: ctx.nurLesen, text: '+ Plan hinzufügen', werkzeug: 'plan-neu',
voll: liste.length >= ZEITEN_LIMITS.plaene, grenzText: `Mehr als ${ZEITEN_LIMITS.plaene} Zeitpläne sind nicht möglich.`,
baue: (schliessen) => neuFormular(t, { schliessen }),
}) : null
const voll = liste.length >= ZEITEN_LIMITS.plaene
const knopf = neu?.leiste?.querySelector('button')
if (knopf) knopf.disabled = voll
return abschnitt('Zeitpläne',
'Wann hast du geöffnet? Der Hauptplan steuert den Live-Status und die Leisten auf deiner Website. Weitere Pläne (zum Beispiel für Küche oder Lieferdienst) bekommen einen eigenen Kurznamen, unter dem das Team sie auf deiner Website einbauen kann.',
neu?.leiste,
neu ? h('p', { class: 'tm-hinweis zt-grenze', 'data-grenze': '' }, voll ? grenzText : '') : null,
neu?.box,
h('div', { class: 'zt-plaene' }, liste.map((plan) => planKarte(t, plan))))
}
/** Nach dem Speichern eines Plans: nur seine Karte neu zeichnen (aus dem Stand des Servers); alle anderen Formulare bleiben, wie sie sind. */
function ersetzePlanKarte(t, id) {
const plan = planMitId(t.ctx, id)
const alt = kartenIn(t).find((karte) => karte.getAttribute('data-plan') === id)
if (!plan || !alt) return
alt.parentNode.insertBefore(planKarte(t, plan), alt)
alt.remove()
}
/**
* Nach „Plan hinzufügen“, „Plan löschen“ und „Zum Hauptplan machen“: die Karten neu zeichnen (Hauptplan-Marke, Knoepfe, neue und entfernte Plaene)
* und den Knopf „+ Plan hinzufügen“ auf den Stand bringen. Die Eingaben der Karten, die es noch gibt, werden wieder eingesetzt (ungespeichert, wie sie waren);
* das Formular „Plan hinzufügen“ (neben der Liste) bleibt unberuehrt.
*/
function zeichnePlanListe(t) {
const { ctx } = t
const behaelter = t.wurzel.querySelector('.zt-plaene')
if (!behaelter || !ctx.stand.daten) return t.zeichne(['plaene'])
const gemerkt = new Map(kartenIn(t).map((karte) => [karte.getAttribute('data-plan'), KARTEN.get(karte)?.merke()]))
const karten = ctx.stand.daten.plaene.map((plan) => {
const karte = planKarte(t, plan)
if (gemerkt.get(plan.id)) KARTEN.get(karte)?.stelleWieder(gemerkt.get(plan.id))
return karte
})
behaelter.replaceChildren(...karten)
const voll = ctx.stand.daten.plaene.length >= ZEITEN_LIMITS.plaene
const knopf = t.wurzel.querySelector('[data-werkzeug="plan-neu"]')
if (knopf) knopf.disabled = voll
const grenze = t.wurzel.querySelector('[data-grenze]')
if (grenze) grenze.textContent = voll ? grenzText : ''
}
/** Formular „Plan hinzufügen“: Name und die Vorschau des Kurznamens (wie ihn der Server bildet; endgueltig ist der Wert aus der Antwort). */
function neuFormular(t, { schliessen }) {
const { ctx, sp } = t
@@ -156,7 +225,11 @@ function neuFormular(t, { schliessen }) {
if (!ok || !sp.nochHier()) return
ctx.offen.add(neuerPlan.id)
const gespeichert = planMitId(ctx, neuerPlan.id) // Name und Kurzname gelten so, wie der Server sie gespeichert hat
nachSpeichern(t, `Der Zeitplan „${gespeichert?.name ?? gewaehlt}“ ist angelegt, sein Kurzname ist „${gespeichert?.kurz ?? neuerPlan.kurz}“. Trage jetzt seine Zeiten ein.`, planSelektor(neuerPlan.id, 'summary'))
nachSpeichern(t, `Der Zeitplan „${gespeichert?.name ?? gewaehlt}“ ist angelegt, sein Kurzname ist „${gespeichert?.kurz ?? neuerPlan.kurz}“. Trage jetzt seine Zeiten ein.`, planSelektor(neuerPlan.id, 'summary'), () => {
zeichnePlanListe(t)
schliessen() // das Formular hat seine Arbeit getan
t.zeichne(['ausnahmen']) // „Gilt für“ kennt den neuen Plan
})
})
return form
}
@@ -219,7 +292,10 @@ function planKarte(t, plan) {
},
nachher: () => {
ctx.offen.delete(plan.id)
nachSpeichern(t, `Der Zeitplan „${plan.name}“ ist gelöscht.`, '[data-werkzeug="plan-neu"]')
nachSpeichern(t, `Der Zeitplan „${plan.name}“ ist gelöscht.`, '[data-werkzeug="plan-neu"]', () => {
zeichnePlanListe(t)
t.zeichne(['ausnahmen']) // die Ausnahmen dieses Plans sind mit weg, „Gilt für“ kennt den Plan nicht mehr
})
},
}) : null
@@ -251,7 +327,15 @@ function planKarte(t, plan) {
return true
}, { container: form })
if (!ok || !sp.nochHier()) return
nachSpeichern(t, `Die Zeiten von „${planMitId(ctx, plan.id)?.name ?? plan.name}“ sind gespeichert.`, planSelektor(plan.id, '[data-aktion="speichern"]'))
const gespeichert = planMitId(ctx, plan.id)
nachSpeichern(t, `Die Zeiten von „${gespeichert?.name ?? plan.name}“ sind gespeichert.`, planSelektor(plan.id, '[data-aktion="speichern"]'), () => {
if (!daten) { // das erste Speichern: jetzt gibt es auch Feiertage und Ausnahmen, der ganze Editor ist neu (es gibt sonst noch kein Formular)
t.zeichne()
return
}
ersetzePlanKarte(t, plan.id)
if (gespeichert && gespeichert.name !== plan.name) t.zeichne(['ausnahmen']) // die Liste der Ausnahmen nennt den Plan beim Namen
})
})
hauptKnopf?.addEventListener('click', async () => {
@@ -261,9 +345,16 @@ function planKarte(t, plan) {
return true
}, { container: form })
if (!ok || !sp.nochHier()) return
nachSpeichern(t, `Der Zeitplan „${planMitId(ctx, plan.id)?.name ?? plan.name}“ ist jetzt der Hauptplan. Er steuert den Live-Status und die Leisten auf deiner Website.`, planSelektor(plan.id, 'summary'))
nachSpeichern(t, `Der Zeitplan „${planMitId(ctx, plan.id)?.name ?? plan.name}“ ist jetzt der Hauptplan. Er steuert den Live-Status und die Leisten auf deiner Website.`, planSelektor(plan.id, 'summary'), () => zeichnePlanListe(t))
})
KARTEN.set(details, {
merke: () => ({ name: name.value, tage: tage.map(({ tag }) => tag.merke()) }),
stelleWieder: (gemerkt) => {
name.value = gemerkt.name
tage.forEach(({ tag }, i) => tag.stelleWieder(gemerkt.tage[i]))
},
})
details.append(
h('summary', { class: 'zt-plan-kopf' },
h('strong', { class: 'tm-name' }, plan.name),