250 lines
15 KiB
JavaScript
250 lines
15 KiB
JavaScript
/* Terminplaner im Kundenbereich: Reiter "Team & Plaetze", die Abschnitte fuer Ausnahmen vom Wochenplan – Abwesenheiten und Urlaub,
|
||
Feiertage (Bundesland) und Sonderoeffnungen. Jeder Abschnitt ist eine Funktion (t) => Element; t = { ctx, sp, zeichne, wurzel } kommt
|
||
aus termine-tab-team.js (sp = Speicher des Reiters, zeichne() zeichnet den Reiter neu). Jede Aenderung speichert die ganze Einstellung
|
||
(termine-speichern.js). Abgeschaltete Funktion (ctx.nurLesen): dieselben Felder gesperrt, ohne Knoepfe zum Aendern. */
|
||
|
||
import { BUNDESLAENDER, FENSTER_FEHLER, LIMITS, MAX, datumKurz, datumLang, eindeutigeId, fensterParsen, fensterText, heuteBerlin, istTag } from './termine-view.js'
|
||
import { abschnitt, allgemeinerFehler, feld, gesperrt, h, hakenFeld, zeigeFehler } from './termine-dom.js'
|
||
import { entfernenFrage } from './termine-speichern.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)
|
||
|
||
/* ------------------------------------------------- Abwesenheiten, Urlaub --- */
|
||
export function abwesenheiten(t) {
|
||
const { ctx } = t
|
||
const e = ctx.daten.einstellung
|
||
const heute = heuteBerlin()
|
||
const alle = (e.abwesenheiten || []).map((a, index) => ({ ...a, index }))
|
||
const kommend = alle.filter((a) => a.bis >= heute).sort(nachBeginn)
|
||
const vorbei = alle.filter((a) => a.bis < heute).sort(nachBeginn).reverse()
|
||
const neuBox = h('div', { class: 'tm-formular-box' })
|
||
const voll = alle.length >= LIMITS.abwesenheiten
|
||
const neuKnopf = ctx.nurLesen ? null : h('button', {
|
||
type: 'button', class: 'kb-btn-outline', 'data-werkzeug': 'abwesenheit-neu', 'aria-expanded': 'false', disabled: voll,
|
||
onclick: () => {
|
||
if (neuBox.firstChild) { neuBox.replaceChildren(); neuKnopf.setAttribute('aria-expanded', 'false'); return }
|
||
neuKnopf.setAttribute('aria-expanded', 'true')
|
||
const form = formular(t, { index: alle.length, a: null, schliessen: () => { neuBox.replaceChildren(); neuKnopf.setAttribute('aria-expanded', 'false'); neuKnopf.focus() } })
|
||
neuBox.replaceChildren(form)
|
||
form.querySelector('select, input')?.focus()
|
||
},
|
||
}, '+ Abwesenheit eintragen')
|
||
|
||
const wer = (a) => (a.ressourceId ? (e.ressourcen || []).find((r) => r.id === a.ressourceId)?.name || 'Unbekannt' : 'Ganzer Betrieb')
|
||
const reihe = (a) => {
|
||
const li = h('li', { class: 'tm-eintrag', 'data-id': a.id })
|
||
const editor = h('div', { class: 'tm-editor-box' })
|
||
const oeffnen = h('button', {
|
||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'bearbeiten', 'aria-expanded': 'false',
|
||
'aria-label': `${ctx.nurLesen ? 'Ansehen' : 'Bearbeiten'}: ${a.titel || 'Abwesenheit'}, ${datumKurz(a.von)}`,
|
||
onclick: () => {
|
||
if (editor.firstChild) { editor.replaceChildren(); oeffnen.setAttribute('aria-expanded', 'false'); return }
|
||
oeffnen.setAttribute('aria-expanded', 'true')
|
||
const form = formular(t, { index: a.index, a, schliessen: () => { editor.replaceChildren(); oeffnen.setAttribute('aria-expanded', 'false'); oeffnen.focus() } })
|
||
editor.replaceChildren(form)
|
||
form.querySelector('select, input')?.focus()
|
||
},
|
||
}, ctx.nurLesen ? 'Ansehen' : 'Bearbeiten')
|
||
li.append(
|
||
h('div', { class: 'tm-kopf' },
|
||
h('div', { class: 'tm-haupt' },
|
||
h('strong', { class: 'tm-name' }, a.von === a.bis ? datumKurz(a.von) : `${datumKurz(a.von)} – ${datumKurz(a.bis)}`),
|
||
h('span', { class: 'tm-meta' }, [wer(a), a.titel].filter(Boolean).join(' · ')))),
|
||
h('div', { class: 'tm-aktionen' }, oeffnen),
|
||
editor)
|
||
return li
|
||
}
|
||
|
||
return abschnitt('Abwesenheiten und Urlaub',
|
||
'In dieser Zeit kann niemand neu buchen, für eine Person, einen Platz oder den ganzen Betrieb. Termine, die schon gebucht sind, bleiben bestehen. Sag sie bei Bedarf selbst ab.',
|
||
ctx.nurLesen ? null : h('div', { class: 'tm-werkzeug-zeile' }, neuKnopf, voll ? h('p', { class: 'tm-hinweis' }, `Mehr als ${LIMITS.abwesenheiten} Abwesenheiten sind nicht möglich.`) : null),
|
||
neuBox,
|
||
kommend.length ? h('ul', { class: 'tm-eintraege', 'aria-label': 'Abwesenheiten' }, kommend.map(reihe)) : h('p', { class: 'tm-leer' }, 'Keine Abwesenheiten eingetragen.'),
|
||
vorbei.length
|
||
? h('details', { class: 'tm-details' }, h('summary', null, `Vergangene Abwesenheiten (${vorbei.length})`), h('ul', { class: 'tm-eintraege' }, vorbei.map(reihe)))
|
||
: null)
|
||
}
|
||
|
||
function formular(t, { index, a, schliessen }) {
|
||
const { ctx, sp, zeichne } = t
|
||
const e = ctx.daten.einstellung
|
||
const neu = a === null
|
||
const alt = a || { id: eindeutigeId('a', (e.abwesenheiten || []).map((x) => x.id)), ressourceId: null, von: heuteBerlin(), bis: heuteBerlin(), titel: '' }
|
||
const p = `abwesenheiten.${index}`
|
||
const wer = h('select', null,
|
||
h('option', { value: '', selected: !alt.ressourceId }, 'Ganzer Betrieb'),
|
||
(e.ressourcen || []).map((r) => h('option', { value: r.id, selected: alt.ressourceId === r.id }, r.name)))
|
||
const von = h('input', { type: 'date', value: alt.von })
|
||
const bis = h('input', { type: 'date', value: alt.bis })
|
||
const titel = h('input', { type: 'text', maxlength: MAX.abwesenheitTitel, autocomplete: 'off', placeholder: 'z. B. Betriebsurlaub', value: alt.titel })
|
||
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary' }, neu ? 'Abwesenheit eintragen' : 'Speichern')
|
||
const frageBox = h('div', { class: 'tm-frage' })
|
||
const entfernen = neu || ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline tm-gefahr', onclick: () => fragen() }, 'Entfernen')
|
||
const form = h('form', { class: 'kb-panel tm-formular', novalidate: true, 'aria-label': neu ? 'Abwesenheit eintragen' : 'Abwesenheit bearbeiten' },
|
||
h('h3', null, neu ? 'Neue Abwesenheit' : 'Abwesenheit'),
|
||
gesperrt(ctx.nurLesen, [
|
||
feld(`${p}.ressourceId`, 'Wer ist weg?', wer),
|
||
h('div', { class: 'tm-raster' }, feld(`${p}.von`, 'Von (erster Tag)', von), feld(`${p}.bis`, 'Bis (letzter Tag)', bis)),
|
||
feld(`${p}.titel`, 'Titel (optional)', titel, 'Nur für dich sichtbar.'),
|
||
]),
|
||
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))
|
||
form.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
const ok = await sp.speichere((entwurf) => {
|
||
const eintrag = { ...alt, ressourceId: wer.value || null, von: von.value, bis: bis.value, titel: titel.value }
|
||
if (neu) entwurf.abwesenheiten = [...(entwurf.abwesenheiten || []), eintrag]
|
||
else entwurf.abwesenheiten[index] = eintrag
|
||
}, { container: form, knopf: speichern })
|
||
if (!ok || !sp.nochHier()) return
|
||
zeichne()
|
||
ctx.meldung(neu ? 'Abwesenheit eingetragen.' : 'Abwesenheit gespeichert.')
|
||
})
|
||
function fragen() {
|
||
const box = entfernenFrage(sp, {
|
||
titel: 'Abwesenheit entfernen?',
|
||
text: 'In dieser Zeit kann danach wieder gebucht werden.',
|
||
aendere: (entwurf) => { entwurf.abwesenheiten = entwurf.abwesenheiten.filter((x) => x.id !== alt.id) },
|
||
nachher: () => { zeichne(); ctx.meldung('Abwesenheit entfernt.') },
|
||
zurueck: () => { frageBox.replaceChildren(); entfernen.focus() },
|
||
})
|
||
frageBox.replaceChildren(box)
|
||
box.focus()
|
||
}
|
||
return h('div', { class: 'tm-editor' }, form, frageBox)
|
||
}
|
||
|
||
/* -------------------------------------------------------------- Feiertage --- */
|
||
export function feiertage(t) {
|
||
const { ctx, sp, zeichne } = t
|
||
const e = ctx.daten.einstellung
|
||
const land = h('select', null, BUNDESLAENDER.map(([key, name]) => h('option', { value: key, selected: e.bundesland === key }, name)))
|
||
const zu = h('input', { type: 'checkbox', checked: e.feiertageGeschlossen !== false })
|
||
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary' }, 'Speichern')
|
||
const form = h('form', { novalidate: true, 'aria-label': 'Feiertage' },
|
||
gesperrt(ctx.nurLesen, [
|
||
feld('bundesland', 'Bundesland', land, 'Danach richten sich die gesetzlichen Feiertage.'),
|
||
hakenFeld('feiertageGeschlossen', zu, 'An gesetzlichen Feiertagen geschlossen', 'Dann kann an Feiertagen niemand buchen. Eine Sonderöffnung (unten) geht vor.'),
|
||
]),
|
||
allgemeinerFehler(),
|
||
ctx.nurLesen ? null : h('div', { class: 'tm-frage-knoepfe' }, speichern))
|
||
form.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
const ok = await sp.speichere((entwurf) => { entwurf.bundesland = land.value; entwurf.feiertageGeschlossen = zu.checked }, { container: form, knopf: speichern })
|
||
if (!ok || !sp.nochHier()) return
|
||
zeichne()
|
||
ctx.meldung('Feiertage gespeichert.')
|
||
})
|
||
return abschnitt('Feiertage', null, form)
|
||
}
|
||
|
||
/* ------------------------------------------------------- Sonderöffnungen --- */
|
||
export function sonderoeffnungen(t) {
|
||
const { ctx, sp, zeichne } = t
|
||
const e = ctx.daten.einstellung
|
||
const tage = Object.keys(e.sonderoeffnungen || {}).sort()
|
||
const neuBox = h('div', { class: 'tm-formular-box' })
|
||
const voll = tage.length >= LIMITS.sonderoeffnungen
|
||
const neuKnopf = ctx.nurLesen ? null : h('button', {
|
||
type: 'button', class: 'kb-btn-outline', 'data-werkzeug': 'sonder-neu', 'aria-expanded': 'false', disabled: voll,
|
||
onclick: () => {
|
||
if (neuBox.firstChild) { neuBox.replaceChildren(); neuKnopf.setAttribute('aria-expanded', 'false'); return }
|
||
neuKnopf.setAttribute('aria-expanded', 'true')
|
||
const form = sonderFormular(t, { tag: null, schliessen: () => { neuBox.replaceChildren(); neuKnopf.setAttribute('aria-expanded', 'false'); neuKnopf.focus() } })
|
||
neuBox.replaceChildren(form)
|
||
form.querySelector('input')?.focus()
|
||
},
|
||
}, '+ Sonderöffnung eintragen')
|
||
|
||
const reihe = (tag) => {
|
||
const li = h('li', { class: 'tm-eintrag', 'data-tag': tag })
|
||
const editor = h('div', { class: 'tm-editor-box' })
|
||
const zeiten = fensterText(e.sonderoeffnungen[tag])
|
||
const oeffnen = h('button', {
|
||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'bearbeiten', 'aria-expanded': 'false',
|
||
'aria-label': `${ctx.nurLesen ? 'Ansehen' : 'Bearbeiten'}: Sonderöffnung ${datumKurz(tag)}`,
|
||
onclick: () => {
|
||
if (editor.firstChild) { editor.replaceChildren(); oeffnen.setAttribute('aria-expanded', 'false'); return }
|
||
oeffnen.setAttribute('aria-expanded', 'true')
|
||
const form = sonderFormular(t, { tag, schliessen: () => { editor.replaceChildren(); oeffnen.setAttribute('aria-expanded', 'false'); oeffnen.focus() } })
|
||
editor.replaceChildren(form)
|
||
form.querySelector('input')?.focus()
|
||
},
|
||
}, ctx.nurLesen ? 'Ansehen' : 'Bearbeiten')
|
||
li.append(
|
||
h('div', { class: 'tm-kopf' }, h('div', { class: 'tm-haupt' },
|
||
h('strong', { class: 'tm-name' }, datumLang(tag)),
|
||
h('span', { class: 'tm-meta' }, zeiten || 'Geschlossen'))),
|
||
h('div', { class: 'tm-aktionen' }, oeffnen),
|
||
editor)
|
||
return li
|
||
}
|
||
return abschnitt('Sonderöffnungen',
|
||
'Für einzelne Tage, an denen es anders ist als im Wochenplan: Heiligabend nur bis 13 Uhr, ein Sonntag mit Öffnung oder ein Brückentag mit geschlossener Tür. Das gilt für alle Personen und Plätze und gehen dem Wochenplan und den Feiertagen vor.',
|
||
ctx.nurLesen ? null : h('div', { class: 'tm-werkzeug-zeile' }, neuKnopf, voll ? h('p', { class: 'tm-hinweis' }, `Mehr als ${LIMITS.sonderoeffnungen} Sonderöffnungen sind nicht möglich.`) : null),
|
||
neuBox,
|
||
tage.length ? h('ul', { class: 'tm-eintraege', 'aria-label': 'Sonderöffnungen' }, tage.map(reihe)) : h('p', { class: 'tm-leer' }, 'Keine Sonderöffnungen eingetragen.'))
|
||
}
|
||
|
||
/** Formular fuer einen neuen (tag null: mit Datum) oder bestehenden Tag (Datum steht fest; wer den Tag aendern will, entfernt ihn und legt einen neuen an). */
|
||
function sonderFormular(t, { tag, schliessen }) {
|
||
const { ctx, sp, zeichne } = t
|
||
const e = ctx.daten.einstellung
|
||
const neu = tag === null
|
||
const datum = h('input', { type: 'date', value: heuteBerlin() })
|
||
const zeiten = h('input', { type: 'text', autocomplete: 'off', placeholder: '09:00–13:00', value: neu ? '' : fensterText(e.sonderoeffnungen[tag]) })
|
||
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary' }, neu ? 'Sonderöffnung eintragen' : 'Speichern')
|
||
const frageBox = h('div', { class: 'tm-frage' })
|
||
const entfernen = neu || ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline tm-gefahr', onclick: () => fragen() }, 'Entfernen')
|
||
const datumFeld = feld('datum', 'Tag', datum)
|
||
const zeitenFeld = feld('zeiten', 'Geöffnet von – bis', zeiten, 'Mehrere Zeitfenster mit Komma trennen: 09:00–12:00, 14:00–18:00. Leer lassen: an diesem Tag geschlossen.')
|
||
const zeitenFehler = zeitenFeld.querySelector('[data-fehler]')
|
||
// Fehler des Servers heissen 'sonderoeffnungen.<Tag>': beim Absenden nimmt das Feld der Zeiten diesen Namen an
|
||
const zeitenName = (name) => { zeitenFehler.dataset.fehler = name; zeiten.dataset.feld = name }
|
||
const form = h('form', { class: 'kb-panel tm-formular', novalidate: true, 'aria-label': neu ? 'Sonderöffnung eintragen' : `Sonderöffnung ${datumKurz(tag)}` },
|
||
h('h3', null, neu ? 'Neue Sonderöffnung' : datumLang(tag)),
|
||
gesperrt(ctx.nurLesen, [neu ? datumFeld : null, zeitenFeld]),
|
||
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))
|
||
form.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
const ok = await sp.speichere((entwurf) => {
|
||
const tagWert = neu ? datum.value : tag
|
||
zeitenName('zeiten')
|
||
const fehler = {}
|
||
if (!istTag(tagWert)) fehler.datum = 'Bitte wähle einen Tag.'
|
||
else if (neu && Object.hasOwn(entwurf.sonderoeffnungen || {}, tagWert)) fehler.datum = 'Für diesen Tag gibt es schon eine Sonderöffnung. Bearbeite sie in der Liste.'
|
||
const fenster = fensterParsen(zeiten.value)
|
||
if (fenster === null) fehler.zeiten = FENSTER_FEHLER
|
||
if (Object.keys(fehler).length) {
|
||
zeigeFehler(form, { fehler })
|
||
return false
|
||
}
|
||
zeitenName(`sonderoeffnungen.${tagWert}`)
|
||
entwurf.sonderoeffnungen = { ...(entwurf.sonderoeffnungen || {}), [tagWert]: fenster }
|
||
return true
|
||
}, { container: form, knopf: speichern })
|
||
if (!ok || !sp.nochHier()) return
|
||
zeichne()
|
||
ctx.meldung(neu ? 'Sonderöffnung eingetragen.' : 'Sonderöffnung gespeichert.')
|
||
})
|
||
function fragen() {
|
||
const box = entfernenFrage(sp, {
|
||
titel: `Sonderöffnung am ${datumKurz(tag)} entfernen?`,
|
||
text: 'Danach gilt an diesem Tag wieder der Wochenplan (und der Feiertag, falls einer ist).',
|
||
aendere: (entwurf) => { const rest = { ...entwurf.sonderoeffnungen }; delete rest[tag]; entwurf.sonderoeffnungen = rest },
|
||
nachher: () => { zeichne(); ctx.meldung('Sonderöffnung entfernt.') },
|
||
zurueck: () => { frageBox.replaceChildren(); entfernen.focus() },
|
||
})
|
||
frageBox.replaceChildren(box)
|
||
box.focus()
|
||
}
|
||
return h('div', { class: 'tm-editor' }, form, frageBox)
|
||
}
|