„Ja, anlegen“ zeichnet nur noch die Hinweis-Liste neu (offene, ungespeicherte Formulare und Zeilen bleiben), statt den ganzen Abschnitt zu ersetzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
302 lines
17 KiB
JavaScript
302 lines
17 KiB
JavaScript
/* 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 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.
|
||
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 (danach werden die Ausnahmen neu gezeichnet und die Hinweis-Liste aufgefrischt, ohne offene Hinweis-Formulare anzufassen). */
|
||
|
||
import { BUNDESLAENDER, datumKurz, datumLang, heuteBerlin, istTag } from './termine-view.js'
|
||
import { abschnitt, allgemeinerFehler, editorZeile, feld, frage, gesperrt, h, neuerEintrag, zeigeFehler } from './termine-dom.js'
|
||
import { MODI, ZEITEN_LIMITS, ZEITEN_MAX, hinweisVorschlag, neueZeitenId, tagFehler } from './zeiten-view.js'
|
||
import { entfernenKnopf, nachSpeichern, stelleVon } from './zeiten-speichern.js'
|
||
import { MODUS_WAHL, tagFelder, tagText } from './zeiten-plaene.js'
|
||
import { hinweisListeErneuern } from './zeiten-leisten.js'
|
||
|
||
const nachBeginn = (x, y) => (x.von < y.von ? -1 : x.von > y.von ? 1 : x.bis < y.bis ? -1 : x.bis > y.bis ? 1 : 0)
|
||
const zeitraum = (a) => (a.von === a.bis ? datumKurz(a.von) : `${datumKurz(a.von)} – ${datumKurz(a.bis)}`)
|
||
|
||
/* -------------------------------------------------------------- Feiertage --- */
|
||
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
|
||
const nurLesen = ctx.nurLesen
|
||
const gespeichert = daten.feiertage !== null && typeof daten.feiertage === 'object' ? daten.feiertage : {}
|
||
|
||
// Bundesland: eigenes Formular, denn die Liste darunter kommt nach dem Speichern vom Server neu
|
||
const land = h('select', null, BUNDESLAENDER.map(([key, name]) => h('option', { value: key, selected: (daten.land || 'RP') === key }, name)))
|
||
const landForm = h('form', { novalidate: true, 'aria-label': 'Bundesland' },
|
||
gesperrt(nurLesen, feld('land', 'Bundesland', land, 'Danach richten sich die Feiertage unten.')),
|
||
allgemeinerFehler(),
|
||
nurLesen ? null : h('div', { class: 'tm-frage-knoepfe' }, h('button', { type: 'submit', class: 'kb-btn-outline', 'data-aktion': 'land-speichern' }, 'Bundesland speichern')))
|
||
landForm.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
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
|
||
// 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) => {
|
||
const pruefen = f.art === 'pruefen'
|
||
const abweichung = Object.hasOwn(gespeichert, f.tag) ? gespeichert[f.tag] : null
|
||
// ein gespeichertes „geschlossen“ an einem gesetzlichen Feiertag ist die Vorgabe
|
||
const modus = abweichung && MODI.includes(abweichung.modus) && !(!pruefen && abweichung.modus === 'zu') ? abweichung.modus : ''
|
||
const tag = tagFelder({ pfad: `feiertage.${f.tag}`, label: `${f.name}, ${datumKurz(f.tag)}`, wahl: pruefen ? PRUEFEN_WAHL : FEIERTAG_WAHL, modus, fenster: abweichung?.fenster })
|
||
const el = h('li', { class: 'tm-eintrag zt-feiertag', 'data-feiertag': f.tag },
|
||
h('div', { class: 'tm-kopf' },
|
||
h('div', { class: 'tm-haupt' }, h('strong', { class: 'tm-name' }, datumLang(f.tag)), h('span', { class: 'tm-meta' }, f.name)),
|
||
pruefen ? h('span', { class: 'tm-pille tm-pille-wait' }, 'bitte prüfen') : null),
|
||
tag.el)
|
||
return { f, tag, el }
|
||
})
|
||
|
||
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'feiertage-speichern' }, 'Feiertage speichern')
|
||
const form = h('form', { novalidate: true, 'aria-label': 'Feiertage' },
|
||
zeilen.length
|
||
? gesperrt(nurLesen, h('ul', { class: 'tm-eintraege', 'aria-label': 'Feiertage der nächsten 12 Monate' }, zeilen.map(({ el }) => el)))
|
||
: h('p', { class: 'tm-leer' }, 'Für dieses Bundesland gibt es in den nächsten 12 Monaten keine Feiertage.'),
|
||
allgemeinerFehler(),
|
||
nurLesen || !zeilen.length ? null : h('div', { class: 'tm-frage-knoepfe' }, speichern))
|
||
form.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
const ok = await sp.speichere((entwurf) => {
|
||
// nur Abweichungen von der Vorgabe: alles andere (auch Tage ausserhalb der Liste) bleibt, wie es war
|
||
const neu = { ...(entwurf.feiertage || {}) }
|
||
const fehler = {}
|
||
for (const { f, tag } of zeilen) {
|
||
const r = tag.lies()
|
||
if (r.fehler) {
|
||
Object.assign(fehler, r.fehler)
|
||
continue
|
||
}
|
||
if (r.modus === '' || (f.art !== 'pruefen' && r.modus === 'zu')) {
|
||
delete neu[f.tag]
|
||
continue
|
||
}
|
||
const eintrag = { modus: r.modus, fenster: r.fenster }
|
||
const meldung = tagFehler(eintrag)
|
||
if (meldung) fehler[`feiertage.${f.tag}`] = meldung
|
||
neu[f.tag] = eintrag
|
||
}
|
||
if (Object.keys(fehler).length) {
|
||
zeigeFehler(form, { fehler })
|
||
return false
|
||
}
|
||
entwurf.feiertage = neu
|
||
return true
|
||
}, { container: form })
|
||
if (!ok || !sp.nochHier()) return
|
||
// 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"]', () => {})
|
||
})
|
||
|
||
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 --- */
|
||
const gibtHinweis = (hinweise, v) => (Array.isArray(hinweise) ? hinweise : []).some((x) => x?.text === v.text && x.von === v.von && x.bis === v.bis)
|
||
const planName = (daten, id) => (daten.plaene || []).find((p) => p?.id === id)?.name
|
||
|
||
export function ausnahmen(t) {
|
||
const { ctx } = t
|
||
const daten = ctx.stand.daten
|
||
const alle = Array.isArray(daten.ausnahmen) ? daten.ausnahmen : []
|
||
const heute = heuteBerlin()
|
||
const kommend = alle.filter((a) => a.bis >= heute).sort(nachBeginn)
|
||
const vorbei = alle.filter((a) => a.bis < heute).sort(nachBeginn).reverse()
|
||
const voll = alle.length >= ZEITEN_LIMITS.ausnahmen
|
||
const grenzText = `Mehr als ${ZEITEN_LIMITS.ausnahmen} Ausnahmen sind nicht möglich.`
|
||
const urlaubNeu = neuerEintrag({
|
||
nurLesen: ctx.nurLesen, text: '+ Betriebsurlaub eintragen', werkzeug: 'urlaub-neu', voll, grenzText,
|
||
baue: (schliessen) => formular(t, { a: null, art: 'urlaub', schliessen }),
|
||
})
|
||
const sonderNeu = neuerEintrag({
|
||
nurLesen: ctx.nurLesen, text: '+ Sondertag eintragen', werkzeug: 'sondertag-neu', voll, grenzText: '',
|
||
baue: (schliessen) => formular(t, { a: null, art: 'sondertag', schliessen }),
|
||
})
|
||
|
||
const reihe = (a) => {
|
||
const art = a.art === 'sondertag' ? 'Sondertag' : 'Betriebsurlaub'
|
||
const plan = a.plan ? `nur ${planName(daten, a.plan) || 'ein gelöschter Plan'}` : ''
|
||
return editorZeile({
|
||
attrs: { 'data-id': a.id },
|
||
nurLesen: ctx.nurLesen,
|
||
label: `${art} ${datumKurz(a.von)}`,
|
||
haupt: [
|
||
h('strong', { class: 'tm-name' }, zeitraum(a)),
|
||
h('span', { class: 'tm-meta' }, [art, a.art === 'sondertag' ? tagText(a) : '', a.text, plan].filter(Boolean).join(' · ')),
|
||
],
|
||
baue: (schliessen) => formular(t, { a, art: a.art === 'sondertag' ? 'sondertag' : 'urlaub', schliessen }),
|
||
})
|
||
}
|
||
|
||
return abschnitt('Ausnahmen',
|
||
'Betriebsurlaub und einzelne Sondertage gehen dem Wochenplan und den Feiertagen vor. Jede Ausnahme gilt für alle Pläne oder nur für einen.',
|
||
hinweisFrage(t),
|
||
h('div', { class: 'zt-werkzeuge' }, urlaubNeu.leiste, sonderNeu.leiste),
|
||
urlaubNeu.box,
|
||
sonderNeu.box,
|
||
kommend.length
|
||
? h('ul', { class: 'tm-eintraege', 'aria-label': 'Kommende Ausnahmen' }, kommend.map(reihe))
|
||
: h('p', { class: 'tm-leer' }, 'Keine kommenden Ausnahmen eingetragen.'),
|
||
vorbei.length
|
||
? h('details', { class: 'tm-details' }, h('summary', null, `Vergangene Ausnahmen (${vorbei.length})`), h('ul', { class: 'tm-eintraege' }, vorbei.map(reihe)))
|
||
: null)
|
||
}
|
||
|
||
/**
|
||
* „Hinweis-Leiste dazu anlegen?“ nach einem Betriebsurlaub ab 2 Tagen (ctx.hinweisFrage = { vorschlag }): Text und Zeitraum sind vorbefuellt
|
||
* (hinweisVorschlag: sichtbar ab 7 Tage vor Beginn bis zum Urlaubsende). „Ja“ legt den Hinweis in daten.hinweise an und speichert; „Nein“ laesst es.
|
||
*/
|
||
function hinweisFrage(t) {
|
||
const { ctx, sp } = t
|
||
const f = ctx.hinweisFrage
|
||
if (!f || ctx.nurLesen) return null
|
||
const v = f.vorschlag
|
||
const form = frage('Hinweis-Leiste dazu anlegen?',
|
||
h('p', null, `„${v.text}“ – sichtbar vom ${datumKurz(v.von)} bis ${datumKurz(v.bis)}, oben auf deiner Website.`),
|
||
allgemeinerFehler(),
|
||
h('div', { class: 'tm-frage-knoepfe' },
|
||
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
|
||
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'
|
||
form.addEventListener('submit', async () => {
|
||
const ok = await sp.speichere((entwurf) => {
|
||
entwurf.hinweise = [...(Array.isArray(entwurf.hinweise) ? entwurf.hinweise : []), { id: neueZeitenId('h'), art: v.art, text: v.text, knopf: null, von: v.von, bis: v.bis }]
|
||
}, { 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'])
|
||
hinweisListeErneuern(t) // der neue Hinweis steht in der Liste; ein offenes, ungespeichertes Hinweis-Formular bleibt, wie es ist
|
||
})
|
||
})
|
||
return form
|
||
}
|
||
|
||
/**
|
||
* Formular einer neuen (a null) oder bestehenden Ausnahme der Art 'urlaub' (von, bis) oder 'sondertag' (ein Datum, Betriebsart, Zeiten).
|
||
* Die Felder heissen nach dem Pfad der Fehler des Servers (ausnahmen.<ID>.von …); Absenden sucht die Ausnahme per ID.
|
||
*/
|
||
function formular(t, { a, art, schliessen }) {
|
||
const { ctx, sp } = t
|
||
const daten = ctx.stand.daten
|
||
const neu = a === null
|
||
const alt = a || { id: neueZeitenId('x'), art, von: heuteBerlin(), bis: heuteBerlin(), text: '', modus: 'zu', fenster: [], plan: null }
|
||
const p = `ausnahmen.${alt.id}`
|
||
const urlaub = art === 'urlaub'
|
||
const wort = urlaub ? 'Betriebsurlaub' : 'Sondertag'
|
||
|
||
const von = h('input', { type: 'date', value: alt.von })
|
||
const bis = h('input', { type: 'date', value: alt.bis })
|
||
const text = h('input', { type: 'text', maxlength: ZEITEN_MAX.ausnahmeText, autocomplete: 'off', placeholder: urlaub ? 'z. B. Sommerpause' : 'z. B. Tag der offenen Tür', value: alt.text })
|
||
const tag = urlaub ? null : tagFelder({ pfad: `${p}.modus`, fensterPfad: `${p}.fenster`, label: 'Sondertag', wahl: MODUS_WAHL, sichtbar: true, modus: alt.modus, fenster: alt.fenster })
|
||
const plaene = Array.isArray(daten.plaene) ? daten.plaene : []
|
||
const geltung = plaene.length > 1 || alt.plan
|
||
? h('select', null,
|
||
h('option', { value: '', selected: !alt.plan }, 'Alle Pläne'),
|
||
plaene.map((x) => h('option', { value: x.id, selected: alt.plan === x.id }, x.name)))
|
||
: null
|
||
|
||
const entfernen = neu || ctx.nurLesen ? null : entfernenKnopf(sp, {
|
||
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"]', () => 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` },
|
||
h('h3', null, neu ? (urlaub ? 'Neuer Betriebsurlaub' : 'Neuer Sondertag') : wort),
|
||
gesperrt(ctx.nurLesen, [
|
||
urlaub
|
||
? h('div', { class: 'tm-raster' }, feld(`${p}.von`, 'Von (erster Tag)', von), feld(`${p}.bis`, 'Bis (letzter Tag)', bis))
|
||
: feld(`${p}.von`, 'Datum', von),
|
||
tag?.el,
|
||
feld(`${p}.text`, 'Text (optional)', text, 'Höchstens 60 Zeichen.'),
|
||
geltung ? feld(`${p}.plan`, 'Gilt für', geltung) : null,
|
||
]),
|
||
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()
|
||
let eintrag = null
|
||
const ok = await sp.speichere((entwurf) => {
|
||
const fehler = {}
|
||
if (!istTag(von.value)) fehler[`${p}.von`] = 'Bitte gib ein gültiges Datum an.'
|
||
let ende = von.value
|
||
if (urlaub) {
|
||
ende = bis.value
|
||
if (!istTag(bis.value)) fehler[`${p}.bis`] = 'Bitte gib ein gültiges Datum an.'
|
||
else if (istTag(von.value) && bis.value < von.value) fehler[`${p}.bis`] = 'Das Ende darf nicht vor dem Beginn liegen.'
|
||
}
|
||
let betrieb = { modus: 'zu', fenster: [] }
|
||
if (tag) {
|
||
const r = tag.lies()
|
||
if (r.fehler) Object.assign(fehler, r.fehler)
|
||
else {
|
||
const meldung = tagFehler(r)
|
||
if (meldung) fehler[`${p}.fenster`] = meldung
|
||
betrieb = r
|
||
}
|
||
}
|
||
if (Object.keys(fehler).length) {
|
||
zeigeFehler(form, { fehler })
|
||
return false
|
||
}
|
||
eintrag = { ...alt, art, von: von.value, bis: ende, text: text.value.trim(), modus: betrieb.modus, fenster: betrieb.fenster, plan: geltung ? (geltung.value || null) : alt.plan ?? null }
|
||
if (neu) {
|
||
entwurf.ausnahmen = [...(Array.isArray(entwurf.ausnahmen) ? entwurf.ausnahmen : []), eintrag]
|
||
return true
|
||
}
|
||
const stelle = stelleVon(entwurf.ausnahmen, alt.id, form, `Dieser ${wort}`)
|
||
if (stelle < 0) return false
|
||
entwurf.ausnahmen[stelle] = eintrag
|
||
return true
|
||
}, { container: form })
|
||
if (!ok || !sp.nochHier()) return
|
||
// Urlaub ab 2 Tagen: die Hinweis-Leiste dazu anbieten (nicht noch einmal, wenn es sie schon gibt oder sich der Zeitraum nicht geaendert hat)
|
||
if (urlaub) {
|
||
const vorschlag = hinweisVorschlag(eintrag)
|
||
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"]`, () => t.zeichne(['ausnahmen']))
|
||
})
|
||
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
|
||
}
|