Öffnungszeiten: Editor für Pläne, Feiertage und Ausnahmen

Speichern über PUT /api/zeiten (Version, 409, Feldfehler per ID-Pfad), Zeitpläne mit
Wochentagen, Hauptplan wechseln und Löschen (R10, R33), Feiertage je Bundesland,
Betriebsurlaub und Sondertage mit Hinweis-Leisten-Frage. Die Hülle behält den Editor
bei einem neuen Stand und zeichnet nur den Kopf neu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 06:02:31 +00:00
1 parent cee44d6852
commit 04a3525f79
8 files changed
+1974 -21

No files matched your search

+33 -13
View File
@@ -19,10 +19,21 @@ const STRIPE = 'https://checkout.stripe.test/c/pay/cs_test_1'
const KNOPF_BEZAHLT = REGISTER.consent.knopf
const HAKEN_BEZAHLT = REGISTER.consent.items.filter((item) => item.haken).map((item) => item.id)
// Der Editor im Editorplatz (public/zeiten-editor.js, eigene Tests in test/zeitenEditor.test.js) lädt GET /api/zeiten/<id>: hier mit fester Antwort
// (noch nichts gespeichert), ohne die Warteschlange der Antworten und fx.fetchLog zu berühren; fx.editorAufrufe zählt diese Abrufe.
const EDITOR_STAND = { state: 'active', daten: null, version: 0, kundenansicht: false, vorschau: null, feiertage: [], googleText: '' }
const mitDom = (optionen, fn) => async () => {
const fx = installFakeDom(optionen)
const echtes = globalThis.fetch
fx.editorAufrufe = []
globalThis.fetch = async (url, anfrage) => {
if (!String(url).startsWith('/api/zeiten/')) return echtes(url, anfrage)
fx.editorAufrufe.push(String(url))
return { ok: true, status: 200, json: async () => structuredClone(EDITOR_STAND) }
}
try {
await fn(fx)
await fx.ruhe() // ein Editor, der noch lädt, soll fertig sein, bevor das Fake-DOM geht
} finally {
fx.restore()
}
@@ -703,11 +714,14 @@ test('Ansicht: der Editorplatz steht nur in setup, active und (mit Daten) disabl
assert.equal(await platz(projekt('p1', 'disabled', { disabledReason: 'team' }, { zeitenLeer: true })), null)
}))
test('app.js ruft initZeiten bei jedem Wechsel in die Ansicht: gleiches Projekt und gleicher Stand bauen den Editorplatz nicht neu auf, ein neuer Stand oder die Rückkehr von Stripe schon', mitDom({}, async (fx) => {
test('app.js ruft initZeiten bei jedem Wechsel in die Ansicht: gleiches Projekt und gleicher Stand bauen den Editorplatz nicht neu auf, ein neuer Stand zeichnet nur den Kopf neu, die Rückkehr von Stripe alles', mitDom({}, async (fx) => {
const aktiv = () => liste([projekt('p1', 'active', {}, { zeitenLeer: false })], angebot(true))
fx.antworten.push(aktiv())
const root = await oeffne(fx)
let editor = fx.finde(root, '.zt-editor')
await fx.ruhe() // der Editor lädt (fester Stand, siehe mitDom)
const editor = fx.finde(root, '.zt-editor')
assert.deepEqual(fx.editorAufrufe, ['/api/zeiten/p1'])
assert.ok(fx.finde(editor, 'details[data-plan]'), 'der Editor steht im Platz')
const eingabe = h('input', { type: 'text' })
eingabe.value = 'ungespeichert'
editor.append(eingabe)
@@ -716,7 +730,7 @@ test('app.js ruft initZeiten bei jedem Wechsel in die Ansicht: gleiches Projekt
fx.antworten.push(aktiv())
await initZeiten(root, { projectId: 'p1' })
assert.equal(fx.finde(root, '.zt-editor'), editor, 'derselbe Editorplatz')
assert.equal(editor.firstChild, eingabe)
assert.equal(eingabe.parentNode, editor)
assert.equal(eingabe.value, 'ungespeichert')
assert.equal(fx.fetchLog.length, 2)
assert.equal(fx.fetchLog.every((a) => a.method === 'GET' && a.url === '/api/funktionen'), true, 'keine Zusatzabfrage')
@@ -727,35 +741,41 @@ test('app.js ruft initZeiten bei jedem Wechsel in die Ansicht: gleiches Projekt
await initZeiten(root)
assert.equal(fx.finde(root, '.zt-editor'), editor)
// die Abo-Lage hat sich geändert (Behalten gibt es nur ohne Abo): die Karte muss stimmen, also neu aufgebaut
// die Abo-Lage hat sich geändert (Behalten gibt es nur ohne Abo): die Karte muss stimmen, also wird der Kopf neu gezeichnet – der Editor bleibt mit seinen Eingaben
const kopf = fx.finde(root, '.zt-kopf')
fx.antworten.push(liste([projekt('p1', 'active', {}, { zeitenLeer: false })], angebot(false, 'kein_abo')))
await initZeiten(root)
assert.notEqual(fx.finde(root, '.zt-editor'), editor)
editor = fx.finde(root, '.zt-editor')
assert.equal(fx.finde(root, '.zt-editor'), editor, 'der Editorplatz bleibt stehen')
assert.equal(eingabe.value, 'ungespeichert', 'ungespeicherte Eingaben gehen nicht verloren')
assert.notEqual(fx.finde(root, '.zt-kopf'), kopf, 'der Kopf ist neu gezeichnet')
assert.equal(root.dataset.zeitenKey.includes('kein_abo'), true, 'der neue Stand gilt')
// „Für 30 € behalten“ aus der Kachel: Behalten-Seite; der Stand hat sich geändert (aboEndeAt), also neu aufgebaut
// „Für 30 € behalten“ aus der Kachel: Behalten-Seite; der Stand hat sich geändert (aboEndeAt), der Editor bleibt auch dann
fx.antworten.push(liste([projekt('p1', 'active', ABO_ENDE, { zeitenLeer: false })], angebot(false, 'kein_abo')))
root.dataset.wunschWeg = 'behalten'
await initZeiten(root)
assert.notEqual(fx.finde(root, '.zt-editor'), editor)
assert.equal(fx.finde(root, '.zt-editor'), editor)
assert.equal(editor.hidden, true, 'die Behalten-Seite versteckt den Editor')
assert.ok(fx.finde(root, '[data-variante="behalten"]'))
// gleicher neuer Stand noch einmal mit Weg: der Editor bleibt, die Seite öffnet
const editor2 = fx.finde(root, '.zt-editor')
await fx.klick(knopf(fx, root, 'zurueck'))
assert.equal(editor.hidden, false)
fx.antworten.push(liste([projekt('p1', 'active', ABO_ENDE, { zeitenLeer: false })], angebot(false, 'kein_abo')))
root.dataset.wunschWeg = 'behalten'
await initZeiten(root)
assert.equal(fx.finde(root, '.zt-editor'), editor2, 'der Editor bleibt')
assert.equal(editor2.hidden, true)
assert.equal(fx.finde(root, '.zt-editor'), editor, 'der Editor bleibt')
assert.equal(editor.hidden, true)
assert.ok(fx.finde(root, '[data-variante="behalten"]'))
assert.deepEqual(fx.editorAufrufe, ['/api/zeiten/p1'], 'der Editor hat in der ganzen Zeit nur einmal geladen')
// Rückkehr von Stripe: immer neu
fx.antworten.push(liste([projekt('p1', 'active', ABO_ENDE, { zeitenLeer: false })], angebot(false, 'kein_abo')))
fx.window.location.hash = '#zeiten?behalten=1&projekt=p1'
await initZeiten(root)
assert.notEqual(fx.finde(root, '.zt-editor'), editor2)
assert.notEqual(fx.finde(root, '.zt-editor'), editor)
assert.ok(fx.finde(root, '[data-danke="behalten"]'))
assert.equal(fx.editorAufrufe.length, 2, 'ein frischer Editor lädt')
}))
test('Fremdtext (Projektname, Meldung des Servers) kommt nur als Text in die Seite', mitDom({}, async (fx) => {
@@ -819,7 +839,7 @@ test('Portal: Ansicht #zeiten ist in app.js und dashboard.html verdrahtet', () =
test('Quelltext: zeiten.js und zeiten-bestellen.js importieren nur relativ und nur das Nötige; kein HTML aus Daten, kein Speicher, Du-Form', () => {
const importe = (datei) => [...ohneKommentare(lesen(`public/${datei}`)).matchAll(/\bfrom\s+'([^']+)'/g)].map((m) => m[1])
assert.deepEqual(importe('zeiten.js'), ['./termine-dom.js', './zeiten-bestellen.js'])
assert.deepEqual(importe('zeiten.js'), ['./termine-dom.js', './zeiten-bestellen.js', './zeiten-editor.js'])
assert.deepEqual(importe('zeiten-bestellen.js'), ['./funktion-bestellen.js', './termine-dom.js'])
const dateien = readdirSync(new URL('../public/', import.meta.url)).filter((f) => /^zeiten.*\.js$/.test(f))
for (const datei of ['zeiten.js', 'zeiten-bestellen.js']) {