Ö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:
1 parent
04a3525f79
commit
f01ed15247
6 files changed
+409
-58
No files matched your search
@@ -1,7 +1,9 @@
|
||||
/* Oeffnungszeiten live im Kundenbereich: Editor, Bereiche 2 „Feiertage“ und 3 „Ausnahmen“ (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 alle Abschnitte
|
||||
neu). Jede Aenderung speichert die ganzen Daten (zeiten-speichern.js). Abgeschaltete Funktion (ctx.nurLesen): dieselben Felder gesperrt,
|
||||
(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). Abgeschaltete Funktion (ctx.nurLesen): dieselben Felder gesperrt,
|
||||
ohne Knoepfe zum Aendern. Stil: termine-team-ausnahmen.js (Abwesenheiten, Feiertage, Sonderoeffnungen).
|
||||
Nach einem Speichern wird nur neu gezeichnet, was sich geaendert hat (nachSpeichern mit neuZeichnen): ungespeicherte Eingaben in den anderen Abschnitten
|
||||
und Formularen bleiben stehen.
|
||||
Feiertage: Bundesland (ein eigenes Speichern; die Liste kommt vom Server und gilt danach fuer das neue Land) und die Feiertage der naechsten
|
||||
12 Monate (ctx.stand.feiertage). Gespeichert werden nur Abweichungen von der Vorgabe (daten.feiertage): ein gesetzlicher Feiertag ist
|
||||
„geschlossen“, ein „bitte prüfen“-Tag (Heiligabend, Silvester, nur ortsuebliche Feiertage) folgt dem Wochenplan.
|
||||
@@ -22,6 +24,17 @@ const zeitraum = (a) => (a.von === a.bis ? datumKurz(a.von) : `${datumKurz(a.von
|
||||
const FEIERTAG_WAHL = Object.freeze([['', 'geschlossen (Vorgabe)'], ...MODUS_WAHL.filter(([wert]) => wert !== 'zu')])
|
||||
const PRUEFEN_WAHL = Object.freeze([['', 'wie im Wochenplan (Vorgabe)'], ...MODUS_WAHL])
|
||||
|
||||
// Pro Abschnitt „Feiertage“ die Tageseingaben je Datum: nach einem Wechsel des Bundeslands bekommen Tage, die es auch im neuen Land gibt, ihre Eingaben zurueck
|
||||
const ZEILEN = new WeakMap()
|
||||
|
||||
/** Wird nach dem Neuzeichnen des Abschnitts (Bundesland gewechselt) mit dem alten und dem neuen Abschnitt gerufen: ungespeicherte Eingaben wandern mit. */
|
||||
export function feiertageUebernehmen(alt, neu) {
|
||||
const frueher = ZEILEN.get(alt)
|
||||
const jetzt = ZEILEN.get(neu)
|
||||
if (!frueher || !jetzt) return
|
||||
for (const [tag, eingabe] of jetzt) if (frueher.has(tag)) eingabe.stelleWieder(frueher.get(tag).merke())
|
||||
}
|
||||
|
||||
export function feiertage(t) {
|
||||
const { ctx, sp } = t
|
||||
const daten = ctx.stand.daten
|
||||
@@ -39,7 +52,8 @@ export function feiertage(t) {
|
||||
const ok = await sp.speichere((entwurf) => { entwurf.land = land.value }, { container: landForm })
|
||||
if (!ok || !sp.nochHier()) return
|
||||
const name = BUNDESLAENDER.find(([key]) => key === land.value)?.[1] ?? land.value
|
||||
nachSpeichern(t, `Bundesland gespeichert: ${name}. Die Feiertage unten gelten jetzt dafür.`, 'form[aria-label="Bundesland"] button[type="submit"]')
|
||||
// die Liste darunter gilt jetzt fuer das neue Land: nur dieser Abschnitt wird neu gezeichnet
|
||||
nachSpeichern(t, `Bundesland gespeichert: ${name}. Die Feiertage unten gelten jetzt dafür.`, 'form[aria-label="Bundesland"] button[type="submit"]', () => t.zeichne(['feiertage']))
|
||||
})
|
||||
|
||||
const zeilen = ctx.stand.feiertage.map((f) => {
|
||||
@@ -92,14 +106,17 @@ export function feiertage(t) {
|
||||
return true
|
||||
}, { container: form })
|
||||
if (!ok || !sp.nochHier()) return
|
||||
nachSpeichern(t, 'Feiertage gespeichert.', 'form[aria-label="Feiertage"] button[type="submit"]')
|
||||
// die Liste zeigt schon, was gespeichert ist; nichts neu zeichnen (das Bundesland darueber koennte noch ungespeichert gewaehlt sein)
|
||||
nachSpeichern(t, 'Feiertage gespeichert.', 'form[aria-label="Feiertage"] button[type="submit"]', () => {})
|
||||
})
|
||||
|
||||
return abschnitt('Feiertage',
|
||||
const el = abschnitt('Feiertage',
|
||||
'An gesetzlichen Feiertagen ist deine Website als „geschlossen“ eingetragen. Willst du an einem Feiertag öffnen, wähle eigene Zeiten. „Bitte prüfen“ sind Tage, die nicht überall oder nicht für jeden frei sind (zum Beispiel Heiligabend, Silvester oder Fronleichnam): Hier gilt als Vorgabe dein Wochenplan.',
|
||||
landForm,
|
||||
h('p', { class: 'tm-hinweis' }, 'Gemeinde-Feiertage sind nicht enthalten. Trage sie bei Bedarf unten als Sondertag ein.'),
|
||||
form)
|
||||
ZEILEN.set(el, new Map(zeilen.map(({ f, tag }) => [f.tag, tag])))
|
||||
return el
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Ausnahmen --- */
|
||||
@@ -169,7 +186,7 @@ function hinweisFrage(t) {
|
||||
h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'hinweis-ja' }, 'Ja, anlegen'),
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'hinweis-nein', onclick: () => {
|
||||
ctx.hinweisFrage = null
|
||||
t.zeichne()
|
||||
form.remove() // nur die Frage verschwindet, alles andere in der Seite bleibt
|
||||
t.wurzel.querySelector('[data-werkzeug="urlaub-neu"]')?.focus()
|
||||
} }, 'Nein, danke')))
|
||||
form.dataset.frage = 'hinweis'
|
||||
@@ -179,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"]')
|
||||
nachSpeichern(t, `Die Hinweis-Leiste „${v.text}“ ist angelegt.`, '[data-werkzeug="urlaub-neu"]', () => t.zeichne(['ausnahmen']))
|
||||
})
|
||||
return form
|
||||
}
|
||||
@@ -212,7 +229,7 @@ function formular(t, { a, art, schliessen }) {
|
||||
titel: `${wort} entfernen?`,
|
||||
text: urlaub ? 'In dieser Zeit gilt danach wieder der Wochenplan.' : 'An diesem Tag gilt danach wieder der Wochenplan (und der Feiertag, falls einer ist).',
|
||||
aendere: (entwurf) => { entwurf.ausnahmen = (entwurf.ausnahmen || []).filter((x) => x?.id !== alt.id) },
|
||||
nachher: () => nachSpeichern(t, `${wort} entfernt.`, '[data-werkzeug="urlaub-neu"]'),
|
||||
nachher: () => nachSpeichern(t, `${wort} entfernt.`, '[data-werkzeug="urlaub-neu"]', () => t.zeichne(['ausnahmen'])),
|
||||
})
|
||||
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'speichern' }, neu ? `${wort} eintragen` : 'Speichern')
|
||||
const form = h('form', { class: 'kb-panel tm-formular', novalidate: true, 'aria-label': neu ? `${wort} eintragen` : `${wort} bearbeiten` },
|
||||
@@ -274,7 +291,7 @@ function formular(t, { a, art, schliessen }) {
|
||||
const geaendert = neu || alt.von !== eintrag.von || alt.bis !== eintrag.bis
|
||||
ctx.hinweisFrage = vorschlag && geaendert && !gibtHinweis(ctx.stand.daten.hinweise, vorschlag) ? { vorschlag } : null
|
||||
}
|
||||
nachSpeichern(t, neu ? `${wort} eingetragen.` : `${wort} gespeichert.`, `li[data-id="${CSS.escape(alt.id)}"] [data-aktion="bearbeiten"]`)
|
||||
nachSpeichern(t, neu ? `${wort} eingetragen.` : `${wort} gespeichert.`, `li[data-id="${CSS.escape(alt.id)}"] [data-aktion="bearbeiten"]`, () => t.zeichne(['ausnahmen']))
|
||||
})
|
||||
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
|
||||
}
|
||||
+35
-7
@@ -9,6 +9,8 @@
|
||||
leereDaten(), leer() }
|
||||
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
|
||||
neu und laesst alle anderen Formulare mit ihren ungespeicherten Eingaben stehen – das ist der Weg nach einem Speichern.
|
||||
Vor dem ersten Speichern (stand.daten null) gibt es nur den leeren Hauptplan; die anderen Abschnitte (mitDaten) erscheinen danach, denn der
|
||||
Server verlangt mindestens einen Plan. Die Kontaktdaten (daten.kontakt) gehen unveraendert zurueck: der Editor setzt nie ein neues
|
||||
kontakt.whatsapp.bestaetigtAt (R12, das tut Aufgabe 30 mit dem Haken des Kunden). */
|
||||
@@ -18,12 +20,15 @@ import { istTag } from './termine-view.js'
|
||||
import { leererPlan } from './zeiten-view.js'
|
||||
import { neuerZeitenSpeicher } from './zeiten-speichern.js'
|
||||
import { plaene } from './zeiten-plaene.js'
|
||||
import { ausnahmen, feiertage } from './zeiten-ausnahmen.js'
|
||||
import { ausnahmen, feiertage, feiertageUebernehmen } from './zeiten-ausnahmen.js'
|
||||
|
||||
/** Abschnitte in der Reihenfolge der Seite (Spec 3.4). mitDaten: erst nach dem ersten Speichern. */
|
||||
/**
|
||||
* Abschnitte in der Reihenfolge der Seite (Spec 3.4). mitDaten: erst nach dem ersten Speichern. uebernehmen(alt, neu): wird gerufen, wenn der Abschnitt allein
|
||||
* neu gezeichnet wird, und nimmt ungespeicherte Eingaben aus dem alten Abschnitt in den neuen mit (optional).
|
||||
*/
|
||||
const ABSCHNITTE = [
|
||||
{ key: 'plaene', bauen: plaene, mitDaten: false },
|
||||
{ key: 'feiertage', bauen: feiertage, mitDaten: true },
|
||||
{ key: 'feiertage', bauen: feiertage, mitDaten: true, uebernehmen: feiertageUebernehmen },
|
||||
{ key: 'ausnahmen', bauen: ausnahmen, mitDaten: true },
|
||||
]
|
||||
|
||||
@@ -114,8 +119,33 @@ export async function renderEditor(root, ctx = {}) {
|
||||
const sp = neuerZeitenSpeicher(ec, root, zeichne)
|
||||
const t = { ctx: ec, sp, zeichne, wurzel: root }
|
||||
|
||||
function zeichne() {
|
||||
let leerGezeichnet = null
|
||||
const abschnittEl = new Map() // key => das gezeichnete Element des Abschnitts
|
||||
|
||||
function baueAbschnitt(abschnitt) {
|
||||
const el = abschnitt.bauen(t)
|
||||
el.dataset.abschnitt = abschnitt.key
|
||||
abschnittEl.set(abschnitt.key, el)
|
||||
return el
|
||||
}
|
||||
|
||||
/** Alles neu (welche fehlt) oder nur die Abschnitte in welche (Liste von Schluesseln). Aenderte sich leer/nicht leer, muss alles neu. */
|
||||
function zeichne(welche) {
|
||||
const leer = ec.leer()
|
||||
if (Array.isArray(welche) && welche.length && leer === leerGezeichnet) {
|
||||
for (const key of welche) {
|
||||
const abschnitt = ABSCHNITTE.find((a) => a.key === key)
|
||||
const alt = abschnittEl.get(key)
|
||||
if (!abschnitt || !alt?.parentNode) continue
|
||||
const neu = baueAbschnitt(abschnitt)
|
||||
abschnitt.uebernehmen?.(alt, neu)
|
||||
alt.parentNode.insertBefore(neu, alt)
|
||||
alt.remove()
|
||||
}
|
||||
return
|
||||
}
|
||||
leerGezeichnet = leer
|
||||
abschnittEl.clear()
|
||||
const teile = [meldungEl, sp.konflikt]
|
||||
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)))
|
||||
@@ -123,9 +153,7 @@ export async function renderEditor(root, ctx = {}) {
|
||||
teile.push(h('div', { class: 'zt-kopfbereich' })) // Platz der Live-Vorschau (Aufgabe 30)
|
||||
for (const abschnitt of ABSCHNITTE) {
|
||||
if (abschnitt.mitDaten && leer) continue
|
||||
const el = abschnitt.bauen(t)
|
||||
el.dataset.abschnitt = abschnitt.key
|
||||
teile.push(el)
|
||||
teile.push(baueAbschnitt(abschnitt))
|
||||
}
|
||||
if (leer && !nurLesen) teile.push(h('p', { class: 'tm-hinweis zt-spaeter' }, SPAETER))
|
||||
root.replaceChildren(...teile)
|
||||
|
||||
+100
-9
@@ -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),
|
||||
|
||||
+32
-33
@@ -8,8 +8,11 @@
|
||||
'feiertage.2026-12-25'); Pfade ohne Platz im container erscheinen allgemein, mit dem Namen des Eintrags davor. Rueckgabe false.
|
||||
- 409 (ZEITEN_VERALTET, jemand hat inzwischen gespeichert): Hinweis mit Knopf „Neu laden“ oben im Editor. Rueckgabe false.
|
||||
- alles andere (429 zu viele Speichervorgaenge, 403, 500, kein Netz): Meldung ueber dem Editor (ctx.meldung). Rueckgabe false.
|
||||
- Es laeuft immer nur ein Speichern (PUT und das Uebernehmen): solange es laeuft, ist der Editor aria-busy, alle seine Knoepfe sind gesperrt, und
|
||||
ein weiterer Aufruf von speichere (z. B. ein Enter im Formular) wird abgewiesen (Rueckgabe false), statt mit einer veralteten Version zu gehen.
|
||||
- Es laeuft immer nur ein Speichern (PUT und das Uebernehmen): solange der PUT laeuft, ist der Editor aria-busy, alle seine Knoepfe UND Eingabefelder
|
||||
sind gesperrt (waehrenddessen Getipptes ginge sonst verloren), und ein weiterer Aufruf von speichere (z. B. ein Enter im Formular) wird abgewiesen
|
||||
(Rueckgabe false), statt mit einer veralteten Version zu gehen. Der Fokus geht danach zum ersten Feld mit Fehler, sonst dorthin zurueck, wo er war.
|
||||
- Nach einem Erfolg zeichnet der Abschnitt nur neu, was das Speichern geaendert hat (nachSpeichern mit neuZeichnen): ungespeicherte Eingaben in den
|
||||
anderen Formularen bleiben stehen. Nur „Neu laden“ nach einem 409 zeichnet den ganzen Editor neu.
|
||||
Der Abschnitt ruft speichere nur, wenn ctx.nurLesen nicht gesetzt ist. Texte des Servers kommen nur als Text in die Seite (h, textContent).
|
||||
ctx (baut zeiten-editor.js): { projectId, stand, aktuell(), neuLaden(), meldung(text, art), uebernimm(antwort, { erstes }) }. */
|
||||
|
||||
@@ -64,7 +67,7 @@ export function beschrifteFehler(pfad, text, quellen) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Fehler des Servers am Feld; was im container keinen Platz hat, steht allgemein (mit dem Namen des Eintrags davor). */
|
||||
/** Fehler des Servers am Feld; was im container keinen Platz hat, steht allgemein (mit dem Namen des Eintrags davor). Den Fokus setzt speichere nach dem Entsperren. */
|
||||
function zeigeFelder(container, fehler, quellen) {
|
||||
const amFeld = {}
|
||||
const rest = []
|
||||
@@ -73,13 +76,12 @@ function zeigeFelder(container, fehler, quellen) {
|
||||
else rest.push(beschrifteFehler(pfad, text, quellen))
|
||||
}
|
||||
setzeFehler(container, rest.length ? { ...amFeld, _: rest.join(' ') } : amFeld)
|
||||
container.querySelector('[aria-invalid="true"]')?.focus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Speicher des Editors. wurzel = das Element des Editors (verschwindet beim Wechsel der Website: danach fasst nichts mehr die Seite an),
|
||||
* neuZeichnen() zeichnet den Editor aus ctx.stand neu (nach „Neu laden“).
|
||||
* => { konflikt, speichere, exklusiv, nochHier }: konflikt ist das Hinweis-Element (der Editor setzt es an den Anfang), nochHier() sagt, ob der
|
||||
* => { konflikt, speichere, nochHier }: konflikt ist das Hinweis-Element (der Editor setzt es an den Anfang), nochHier() sagt, ob der
|
||||
* Editor noch angezeigt wird (vor jedem Anfassen der Seite nach einem await pruefen).
|
||||
*/
|
||||
export function neuerZeitenSpeicher(ctx, wurzel, neuZeichnen) {
|
||||
@@ -102,44 +104,36 @@ export function neuerZeitenSpeicher(ctx, wurzel, neuZeichnen) {
|
||||
}
|
||||
})
|
||||
|
||||
// Ein Speichern zur Zeit: waehrenddessen aria-busy am Editor und alle Knoepfe darin gesperrt (danach wieder frei, der Fokus kehrt zurueck).
|
||||
// Ein Speichern zur Zeit: waehrenddessen aria-busy am Editor und alle Knoepfe und Eingabefelder darin gesperrt (danach wieder frei, der Fokus kehrt zurueck).
|
||||
let laeuft = false
|
||||
function sperre(an, gesperrt) {
|
||||
if (an) {
|
||||
wurzel.setAttribute('aria-busy', 'true')
|
||||
const knoepfe = [...wurzel.querySelectorAll('button:not(:disabled)')]
|
||||
const steuerungen = [...wurzel.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled)')]
|
||||
const fokus = document.activeElement
|
||||
for (const knopf of knoepfe) knopf.disabled = true
|
||||
return { knoepfe, fokus }
|
||||
for (const steuerung of steuerungen) steuerung.disabled = true
|
||||
return { steuerungen, fokus }
|
||||
}
|
||||
wurzel.removeAttribute('aria-busy')
|
||||
for (const knopf of gesperrt.knoepfe) knopf.disabled = false
|
||||
// Der Fokus war auf einem gesperrten Knopf und ist weg: zurueck dorthin; hat der Abschnitt ihn inzwischen anders gesetzt (z. B. auf ein Feld mit Fehler), bleibt das so
|
||||
for (const steuerung of gesperrt.steuerungen) steuerung.disabled = false
|
||||
// Der Fokus war auf einer gesperrten Steuerung und ist weg: zurueck dorthin; hat der Abschnitt ihn inzwischen anders gesetzt (z. B. auf die Meldung), bleibt das so
|
||||
const jetzt = document.activeElement
|
||||
if (gesperrt.knoepfe.includes(gesperrt.fokus) && gesperrt.fokus.isConnected && (!jetzt || jetzt === document.body || jetzt === gesperrt.fokus)) gesperrt.fokus.focus()
|
||||
if (gesperrt.steuerungen.includes(gesperrt.fokus) && gesperrt.fokus.isConnected && (!jetzt || jetzt === document.body || jetzt === gesperrt.fokus)) gesperrt.fokus.focus()
|
||||
return null
|
||||
}
|
||||
|
||||
/** arbeit() exklusiv ausfuehren; laeuft schon etwas, passiert nichts (Rueckgabe undefined). */
|
||||
async function exklusiv(arbeit) {
|
||||
if (laeuft) return undefined
|
||||
laeuft = true
|
||||
const gesperrt = sperre(true)
|
||||
try {
|
||||
return await arbeit()
|
||||
} finally {
|
||||
laeuft = false
|
||||
sperre(false, gesperrt)
|
||||
}
|
||||
}
|
||||
|
||||
async function speichere(aendere, { container }) {
|
||||
const ergebnis = await exklusiv(async () => {
|
||||
if (laeuft) return false // ein Speichern zur Zeit
|
||||
laeuft = true
|
||||
let gesperrt = null
|
||||
let amFeld = false
|
||||
try {
|
||||
setzeFehler(container, {})
|
||||
konflikt.hidden = true
|
||||
const bisher = ctx.stand.daten
|
||||
const entwurf = structuredClone(bisher ?? ctx.leereDaten())
|
||||
if (aendere(entwurf) === false) return false // der Abschnitt hat selbst Fehler am Feld gezeigt
|
||||
gesperrt = sperre(true)
|
||||
try {
|
||||
const antwort = await request(`/api/zeiten/${encodeURIComponent(ctx.projectId)}`, { method: 'PUT', body: { version: ctx.stand.version, daten: entwurf } })
|
||||
if (!ctx.aktuell()) return false
|
||||
@@ -153,16 +147,20 @@ export function neuerZeitenSpeicher(ctx, wurzel, neuZeichnen) {
|
||||
konflikt.scrollIntoView({ block: 'nearest' })
|
||||
} else if (err.status === 400 && Object.keys(err.fehler || {}).length) {
|
||||
zeigeFelder(container, err.fehler, [bisher, entwurf])
|
||||
amFeld = true
|
||||
} else {
|
||||
ctx.meldung(err.message, 'fehler')
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
return ergebnis === true
|
||||
} finally {
|
||||
laeuft = false
|
||||
if (gesperrt) sperre(false, gesperrt)
|
||||
if (amFeld && nochHier()) container.querySelector('[aria-invalid="true"]')?.focus() // gesperrte Felder nehmen den Fokus nicht: erst nach dem Entsperren
|
||||
}
|
||||
}
|
||||
|
||||
return { konflikt, speichere, exklusiv, nochHier }
|
||||
return { konflikt, speichere, nochHier }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,11 +174,12 @@ export function stelleVon(liste, id, form, was) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach einem erfolgreichen Speichern: Abschnitte neu zeichnen, Meldung zeigen, Fokus auf das Element mit selektor (Regel: nach dem Speichern
|
||||
* auf den Knopf der gespeicherten Zeile, nach dem Entfernen auf den „+“-Knopf der Liste).
|
||||
* Nach einem erfolgreichen Speichern: neu zeichnen, was das Speichern geaendert hat (neuZeichnen(); ohne Angabe der ganze Editor – die Abschnitte
|
||||
* rufen es immer mit der kleinsten noetigen Aenderung auf, damit ungespeicherte Eingaben in anderen Formularen stehen bleiben), Meldung zeigen,
|
||||
* Fokus auf das Element mit selektor (Regel: nach dem Speichern auf den Knopf der gespeicherten Zeile, nach dem Entfernen auf den „+“-Knopf der Liste).
|
||||
*/
|
||||
export function nachSpeichern(t, meldung, selektor) {
|
||||
t.zeichne()
|
||||
export function nachSpeichern(t, meldung, selektor, neuZeichnen = () => t.zeichne()) {
|
||||
neuZeichnen()
|
||||
t.ctx.meldung(meldung)
|
||||
t.wurzel.querySelector(selektor)?.focus()
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
.zt-plan > .tm-editor { margin: 0; padding: 0 16px 16px; }
|
||||
.zt-plan-form .tm-frage-knoepfe { margin-top: 16px; }
|
||||
.zt-plan-form > .tm-hinweis { margin-top: 10px; }
|
||||
.zt-grenze { margin: 0 0 12px; }
|
||||
.zt-grenze:empty { display: none; }
|
||||
.zt-werkzeuge { display: flex; flex-wrap: wrap; gap: 0 10px; }
|
||||
.zt-werkzeuge:empty { display: none; }
|
||||
.zt-editor .tm-abschnitt > .tm-frage-box { margin: 12px 0 16px; }
|
||||
|
||||
@@ -626,6 +626,220 @@ test('Ausnahmen: bei 100 Einträgen sind beide Knöpfe gesperrt', mitDom({}, asy
|
||||
assert.match(fx.text(abschnitt(fx, root, 'ausnahmen')), /Mehr als 100 Ausnahmen sind nicht möglich/)
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Nur neu zeichnen, was sich geändert hat
|
||||
const kurzerUrlaub = (id, text = '') => ({ id, art: 'urlaub', von: tagPlus(HEUTE, 10), bis: tagPlus(HEUTE, 10), text, modus: 'zu', fenster: [], plan: null })
|
||||
/** Einen eintägigen Betriebsurlaub über das Formular eintragen (ohne Frage nach der Hinweis-Leiste); die Antwort kommt als nächste aus der Warteschlange. */
|
||||
async function urlaubEintragen(fx, root, antwortDaten, version) {
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||||
const form = urlaubsformular(fx, root)
|
||||
await fx.setze(feldIn(fx, form, 'von'), tagPlus(HEUTE, 10))
|
||||
await fx.setze(feldIn(fx, form, 'bis'), tagPlus(HEUTE, 10))
|
||||
fx.antworten.push(antwort(antwortDaten, version))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
}
|
||||
|
||||
test('Plan speichern zeichnet nur diese Karte: ungespeicherte Eingaben in einem anderen Plan und in anderen Abschnitten bleiben stehen', mitDom({}, async (fx) => {
|
||||
const zwei = daten({ plaene: [plan(P1), plan(P2)] })
|
||||
const { root } = await oeffne(fx, { start: stand(zwei) })
|
||||
const karteA = planKarte(fx, root, P1)
|
||||
const karteB = planKarte(fx, root, P2)
|
||||
const nameB = fx.finde(karteB, `[data-feld="plaene.${P2}.name"]`)
|
||||
await fx.setze(nameB, 'Küche Entwurf')
|
||||
await fx.klick(haken(fx, tagZeile(fx, root, P2, '7'))) // Sonntag im zweiten Plan geöffnet, nicht gespeichert
|
||||
await fx.setze(fx.finde(root, 'select[data-feld="land"]'), 'BY') // Bundesland gewählt, nicht gespeichert
|
||||
await fx.setze(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select'), 'rund')
|
||||
await fx.setze(zeitFelder(fx, tagZeile(fx, root, P1, '2'))[0], '08:15') // Dienstag in A: wird gleich gespeichert
|
||||
fx.antworten.push(antwort(daten({ plaene: [plan(P1, { tage: { ...werktags(), 2: { modus: 'zeiten', fenster: [['08:15', '18:00']] } } }), plan(P2)] }), 4))
|
||||
await fx.klick(speichernKnopf(fx, root, P1))
|
||||
const gesendet = puts(fx)[0].body.daten
|
||||
assert.deepEqual(gesendet.plaene[0].tage['2'].fenster, [['08:15', '18:00']])
|
||||
assert.equal(gesendet.plaene[1].name, 'Küche', 'der Entwurf des anderen Plans wird nicht mitgespeichert')
|
||||
assert.equal(gesendet.plaene[1].tage['7'].modus, 'zu')
|
||||
assert.notEqual(planKarte(fx, root, P1), karteA, 'die gespeicherte Karte ist neu gezeichnet')
|
||||
assert.equal(planKarte(fx, root, P2), karteB, 'die andere Karte ist dieselbe geblieben')
|
||||
assert.equal(fx.finde(planKarte(fx, root, P2), `[data-feld="plaene.${P2}.name"]`), nameB)
|
||||
assert.equal(nameB.value, 'Küche Entwurf')
|
||||
assert.equal(haken(fx, tagZeile(fx, root, P2, '7')).checked, true)
|
||||
assert.equal(fx.finde(root, 'select[data-feld="land"]').value, 'BY', 'das ungespeicherte Bundesland bleibt')
|
||||
assert.equal(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select').value, 'rund')
|
||||
assert.deepEqual(zeitFelder(fx, tagZeile(fx, root, P1, '2')).slice(0, 2).map((f) => f.value), ['08:15', '18:00'], 'die gespeicherte Karte zeigt den gespeicherten Stand')
|
||||
}))
|
||||
|
||||
test('Betriebsurlaub speichern zeichnet nur „Ausnahmen“: eine ungespeicherte Änderung am Wochenplan bleibt', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const montag = tagZeile(fx, root, P1, '1')
|
||||
const feld = zeitFelder(fx, montag)[0]
|
||||
await fx.setze(feld, '07:45')
|
||||
const karte = planKarte(fx, root, P1)
|
||||
const feiertage = abschnitt(fx, root, 'feiertage')
|
||||
const ausnahmenVorher = abschnitt(fx, root, 'ausnahmen')
|
||||
await urlaubEintragen(fx, root, daten({ ausnahmen: [kurzerUrlaub('x_urlaub01')] }), 4)
|
||||
assert.equal(planKarte(fx, root, P1), karte, 'dieselbe Karte')
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0], feld)
|
||||
assert.equal(feld.value, '07:45', 'die ungespeicherte Eingabe steht noch da')
|
||||
assert.equal(abschnitt(fx, root, 'feiertage'), feiertage, 'Feiertage unberührt')
|
||||
assert.notEqual(abschnitt(fx, root, 'ausnahmen'), ausnahmenVorher, 'Ausnahmen neu gezeichnet')
|
||||
assert.ok(fx.finde(root, 'li[data-id="x_urlaub01"]'))
|
||||
assert.match(meldung(fx, root), /eingetragen/)
|
||||
assert.equal(puts(fx)[0].body.daten.plaene[0].tage['1'].fenster[0][0], '09:00', 'der Entwurf ging nicht mit hinaus')
|
||||
// dasselbe beim Entfernen
|
||||
const zeile = fx.finde(root, 'li[data-id="x_urlaub01"]')
|
||||
await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]'))
|
||||
await fx.klick(fx.finde(zeile, '[data-aktion="entfernen"]'))
|
||||
fx.antworten.push(antwort(daten(), 5))
|
||||
await fx.klick(fx.finde(fx.finde(zeile, '.tm-frage-box'), 'button[type="submit"]'))
|
||||
assert.equal(feld.value, '07:45')
|
||||
assert.equal(fx.finde(root, 'li[data-id="x_urlaub01"]'), null)
|
||||
}))
|
||||
|
||||
test('„Nein, danke“ entfernt nur die Frage: andere ungespeicherte Eingaben und offene Formulare bleiben', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||||
const form = urlaubsformular(fx, root)
|
||||
const von = tagPlus(HEUTE, 20)
|
||||
const bis = tagPlus(HEUTE, 25)
|
||||
await fx.setze(feldIn(fx, form, 'von'), von)
|
||||
await fx.setze(feldIn(fx, form, 'bis'), bis)
|
||||
fx.antworten.push(antwort(daten({ ausnahmen: [{ ...kurzerUrlaub('x_urlaub01'), von, bis }] }), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.ok(fx.finde(root, '[data-frage="hinweis"]'))
|
||||
// jetzt nebenher etwas eingeben: ein Wochentag, ein offenes Sondertag-Formular mit Text
|
||||
const feld = zeitFelder(fx, tagZeile(fx, root, P1, '3'))[1]
|
||||
await fx.setze(feld, '17:20')
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="sondertag-neu"]'))
|
||||
const sonder = fx.finde(root, 'form[aria-label="Sondertag eintragen"]')
|
||||
await fx.setze(feldIn(fx, sonder, 'text'), 'Tag der offenen Tür')
|
||||
await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-nein"]'))
|
||||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null, 'die Frage ist weg')
|
||||
assert.equal(feld.value, '17:20')
|
||||
assert.equal(fx.finde(root, 'form[aria-label="Sondertag eintragen"]'), sonder, 'das offene Formular ist dasselbe')
|
||||
assert.equal(feldIn(fx, sonder, 'text').value, 'Tag der offenen Tür')
|
||||
assert.equal(puts(fx).length, 1)
|
||||
}))
|
||||
|
||||
test('Feiertage speichern lässt ein noch ungespeichertes Bundesland stehen; Bundesland speichern nimmt Eingaben für Tage mit ins neue Land', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const zeile = (tag) => fx.finde(root, `[data-feiertag="${tag}"]`)
|
||||
await fx.setze(fx.finde(root, 'select[data-feld="land"]'), 'BY')
|
||||
await fx.setze(fx.finde(zeile('2026-12-25'), 'select'), 'vereinbarung')
|
||||
fx.antworten.push(antwort(daten({ feiertage: { '2026-12-25': { modus: 'vereinbarung', fenster: [] } } }), 4))
|
||||
await fx.klick(fx.finde(root, 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||||
assert.equal(puts(fx)[0].body.daten.land, 'RP', 'das Bundesland ging nicht mit')
|
||||
assert.equal(fx.finde(root, 'select[data-feld="land"]').value, 'BY', 'und steht noch da')
|
||||
assert.match(meldung(fx, root), /Feiertage gespeichert/)
|
||||
|
||||
// jetzt das Bundesland speichern: ein Wochenplan-Entwurf bleibt, die Eingabe zu Neujahr (gibt es auch in Bayern) wandert mit
|
||||
await fx.setze(fx.finde(zeile('2027-01-01'), 'select'), 'rund')
|
||||
const feld = zeitFelder(fx, tagZeile(fx, root, P1, '4'))[0]
|
||||
await fx.setze(feld, '10:10')
|
||||
const bayern = [{ tag: '2026-12-25', name: 'Erster Weihnachtstag', art: 'feiertag' }, { tag: '2027-01-01', name: 'Neujahr', art: 'feiertag' }, { tag: '2027-01-06', name: 'Heilige Drei Könige', art: 'feiertag' }]
|
||||
fx.antworten.push(antwort(daten({ land: 'BY', feiertage: { '2026-12-25': { modus: 'vereinbarung', fenster: [] } } }), 5, { feiertage: bayern }))
|
||||
await fx.klick(fx.finde(root, 'form[aria-label="Bundesland"] button[type="submit"]'))
|
||||
assert.deepEqual(fx.alle(root, '[data-feiertag]').map((z) => z.getAttribute('data-feiertag')), ['2026-12-25', '2027-01-01', '2027-01-06'])
|
||||
assert.equal(fx.finde(zeile('2027-01-01'), 'select').value, 'rund', 'die Eingabe zu Neujahr ist erhalten')
|
||||
assert.equal(fx.finde(zeile('2026-12-25'), 'select').value, 'vereinbarung', 'der gespeicherte Stand')
|
||||
assert.equal(fx.finde(zeile('2027-01-06'), 'select').value, '')
|
||||
assert.equal(feld.value, '10:10', 'der Wochenplan-Entwurf bleibt')
|
||||
}))
|
||||
|
||||
test('Plan hinzufügen, Hauptplan wechseln und Plan löschen zeichnen die Planliste neu, behalten aber die ungespeicherten Eingaben der übrigen Karten und das offene „Plan hinzufügen“', mitDom({}, async (fx) => {
|
||||
const P3 = 'p_dritter3'
|
||||
const d3 = daten({
|
||||
plaene: [plan(P1), plan(P2), plan(P3, { kurz: 'bar', name: 'Bar', haupt: false })],
|
||||
ausnahmen: [{ ...kurzerUrlaub('x_kueche01', 'Nur Küche'), plan: P2 }, kurzerUrlaub('x_alle0001', 'Alle')],
|
||||
})
|
||||
const { root } = await oeffne(fx, { start: stand(d3) })
|
||||
const name3 = fx.finde(planKarte(fx, root, P3), `[data-feld="plaene.${P3}.name"]`)
|
||||
await fx.setze(name3, 'Bar Entwurf')
|
||||
const montag = zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0]
|
||||
await fx.setze(montag, '06:30')
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="plan-neu"]'))
|
||||
const neuForm = fx.finde(root, 'form[aria-label="Plan hinzufügen"]')
|
||||
await fx.setze(fx.finde(neuForm, 'input[type="text"]'), 'Halbfertig')
|
||||
const ausnahmenVorher = abschnitt(fx, root, 'ausnahmen')
|
||||
|
||||
// Hauptplan wechseln (P2): Pillen und Knöpfe aller Karten ändern sich, die Eingaben bleiben
|
||||
fx.antworten.push(antwort(daten({ ...d3, plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true }), plan(P3, { kurz: 'bar', name: 'Bar', haupt: false })] }), 4))
|
||||
await fx.klick(fx.finde(planKarte(fx, root, P2), '[data-aktion="haupt"]'))
|
||||
assert.match(fx.text(fx.finde(planKarte(fx, root, P2), 'summary')), /Hauptplan/)
|
||||
assert.doesNotMatch(fx.text(fx.finde(planKarte(fx, root, P1), 'summary')), /Hauptplan/)
|
||||
assert.ok(fx.finde(planKarte(fx, root, P1), '[data-aktion="haupt"]'), 'der frühere Hauptplan hat jetzt den Knopf')
|
||||
assert.equal(fx.finde(planKarte(fx, root, P3), `[data-feld="plaene.${P3}.name"]`).value, 'Bar Entwurf')
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0].value, '06:30')
|
||||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), neuForm, 'das offene Formular bleibt')
|
||||
assert.equal(fx.finde(neuForm, 'input[type="text"]').value, 'Halbfertig')
|
||||
assert.equal(abschnitt(fx, root, 'ausnahmen'), ausnahmenVorher, 'die Ausnahmen kennen die Pläne nur beim Namen: nichts zu tun')
|
||||
|
||||
// Plan löschen (P3, jetzt weder Haupt- noch letzter Plan): P1 und P2 behalten ihre Eingaben, Ausnahmen werden neu gezeichnet
|
||||
const montag2 = zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0]
|
||||
await fx.klick(fx.finde(planKarte(fx, root, P3), '[data-aktion="entfernen"]'))
|
||||
fx.antworten.push(antwort(daten({ ...d3, plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true })] }), 5))
|
||||
await fx.klick(fx.finde(fx.finde(planKarte(fx, root, P3), '.tm-frage-box'), 'button[type="submit"]'))
|
||||
assert.equal(planKarte(fx, root, P3), null)
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0].value, '06:30', 'Eingabe in P1 bleibt')
|
||||
assert.notEqual(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0], montag2, '(die Karte wurde mit ihren Werten neu gebaut)')
|
||||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), neuForm)
|
||||
assert.notEqual(abschnitt(fx, root, 'ausnahmen'), ausnahmenVorher, 'Ausnahmen neu gezeichnet')
|
||||
|
||||
// Plan hinzufügen: neue Karte, Formular zu, P1 behält die Eingabe, „Gilt für“ in neuen Formularen kennt den neuen Plan
|
||||
await fx.setze(fx.finde(neuForm, 'input[type="text"]'), 'Lieferdienst')
|
||||
fx.antworten.push((aufruf) => antwort(daten({ ...d3, plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true }), plan(aufruf.body.daten.plaene[2].id, { kurz: 'lieferdienst', name: 'Lieferdienst', haupt: false, tage: wochenplan() })] }), 6))
|
||||
await fx.klick(fx.finde(neuForm, 'button[type="submit"]'))
|
||||
const neueKarte = fx.alle(root, 'details[data-plan]').find((k) => /Lieferdienst/.test(fx.text(fx.finde(k, 'summary'))))
|
||||
assert.ok(neueKarte, 'die neue Karte steht in der Liste')
|
||||
assert.equal(fx.alle(root, 'details[data-plan]').length, 3)
|
||||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), null, 'das Formular ist zu')
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0].value, '06:30', 'auch das Hinzufügen lässt die Eingaben der anderen Karten stehen')
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||||
assert.match(fx.text(feldIn(fx, urlaubsformular(fx, root), 'plan')), /Lieferdienst/)
|
||||
}))
|
||||
|
||||
test('Plan umbenennen: die Liste der Ausnahmen nennt den neuen Namen, sonst bleibt sie stehen', mitDom({}, async (fx) => {
|
||||
const ausnahme = { ...kurzerUrlaub('x_kueche01', 'Nur Küche'), plan: P2 }
|
||||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [ausnahme] })) })
|
||||
const vorher = abschnitt(fx, root, 'ausnahmen')
|
||||
assert.match(fx.text(fx.finde(root, 'li[data-id="x_kueche01"]')), /nur Küche/)
|
||||
// erst gespeichert ohne neuen Namen: Ausnahmen bleiben
|
||||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [ausnahme] }), 4))
|
||||
await fx.klick(speichernKnopf(fx, root, P2))
|
||||
assert.equal(abschnitt(fx, root, 'ausnahmen'), vorher)
|
||||
await fx.setze(fx.finde(planKarte(fx, root, P2), `[data-feld="plaene.${P2}.name"]`), 'Restaurant')
|
||||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2, { name: 'Restaurant' })], ausnahmen: [ausnahme] }), 5))
|
||||
await fx.klick(speichernKnopf(fx, root, P2))
|
||||
assert.notEqual(abschnitt(fx, root, 'ausnahmen'), vorher)
|
||||
assert.match(fx.text(fx.finde(root, 'li[data-id="x_kueche01"]')), /nur Restaurant/)
|
||||
}))
|
||||
|
||||
test('Während ein Speichern läuft, sind Knöpfe und Eingabefelder gesperrt (nichts geht beim Tippen verloren); danach sind sie wieder frei', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)] })) })
|
||||
const echtes = globalThis.fetch
|
||||
let freigeben
|
||||
const wartet = new Promise((resolve) => { freigeben = resolve })
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (optionen?.method === 'PUT') await wartet
|
||||
return echtes(url, optionen)
|
||||
}
|
||||
try {
|
||||
const ausserhalb = zeitFelder(fx, tagZeile(fx, root, P2, '1'))[0]
|
||||
const selbst = zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0]
|
||||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)] }), 4))
|
||||
const lauf = fx.klick(speichernKnopf(fx, root, P1))
|
||||
await fx.ruhe()
|
||||
assert.equal(root.getAttribute('aria-busy'), 'true')
|
||||
for (const steuerung of fx.alle(root, 'input, select, textarea, button')) assert.equal(steuerung.disabled, true, `${steuerung.tagName} ist gesperrt`)
|
||||
await fx.setze(ausserhalb, '05:55')
|
||||
assert.notEqual(ausserhalb.value, '05:55', 'in ein gesperrtes Feld lässt sich nichts tippen')
|
||||
freigeben()
|
||||
await lauf
|
||||
assert.equal(root.hasAttribute('aria-busy'), false)
|
||||
assert.equal(ausserhalb.disabled, false, 'die Karte, die stehen blieb, ist wieder frei')
|
||||
assert.equal(fx.alle(root, 'input:disabled, select:disabled, button:disabled').length, 0)
|
||||
void selbst
|
||||
} finally {
|
||||
globalThis.fetch = echtes
|
||||
}
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Fehler des Servers
|
||||
test('409 beim Speichern: Meldung „Inzwischen geändert – bitte neu laden.“ und Neu-laden-Knopf; der Knopf lädt und zeichnet neu', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
|
||||
Reference in new issue
Block a user