Files
Webklar-Kundenbereich/public/termine-team-ausnahmen.js

200 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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, editorZeile, feld, gesperrt, h, hakenFeld, neuerEintrag, zeigeFehler } from './termine-dom.js'
import { entfernenKnopf, nachSpeichern, stelleVon } 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 || []
const kommend = alle.filter((a) => a.bis >= heute).sort(nachBeginn)
const vorbei = alle.filter((a) => a.bis < heute).sort(nachBeginn).reverse()
const neu = neuerEintrag({
nurLesen: ctx.nurLesen, text: '+ Abwesenheit eintragen', werkzeug: 'abwesenheit-neu',
voll: alle.length >= LIMITS.abwesenheiten, grenzText: `Mehr als ${LIMITS.abwesenheiten} Abwesenheiten sind nicht möglich.`,
baue: (schliessen) => formular(t, { a: null, schliessen }),
})
const wer = (a) => (a.ressourceId ? (e.ressourcen || []).find((r) => r.id === a.ressourceId)?.name || 'Unbekannt' : 'Ganzer Betrieb')
const reihe = (a) => editorZeile({
attrs: { 'data-id': a.id },
nurLesen: ctx.nurLesen,
label: `${a.titel || 'Abwesenheit'}, ${datumKurz(a.von)}`,
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(' · ')),
],
baue: (schliessen) => formular(t, { a, schliessen }),
})
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.',
neu.leiste,
neu.box,
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)
}
/** Formular einer neuen (a null) oder bestehenden Abwesenheit; Felder nach der Stelle in der gespeicherten Liste, Absenden sucht per ID. */
function formular(t, { a, schliessen }) {
const { ctx, sp } = t
const e = ctx.daten.einstellung
const neu = a === null
const index = neu ? (e.abwesenheiten || []).length : (e.abwesenheiten || []).findIndex((x) => x.id === a.id)
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 entfernen = neu || ctx.nurLesen ? null : entfernenKnopf(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: () => nachSpeichern(t, 'Abwesenheit entfernt.', '[data-werkzeug="abwesenheit-neu"]'),
})
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?.knopf))
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]
return true
}
const stelle = stelleVon(entwurf.abwesenheiten, alt.id, form, 'Diese Abwesenheit')
if (stelle < 0) return false
entwurf.abwesenheiten[stelle] = eintrag
return true
}, { container: form })
if (!ok || !sp.nochHier()) return
nachSpeichern(t, neu ? 'Abwesenheit eingetragen.' : 'Abwesenheit gespeichert.', `li[data-id="${alt.id}"] [data-aktion="bearbeiten"]`)
})
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
}
/* -------------------------------------------------------------- Feiertage --- */
export function feiertage(t) {
const { ctx, sp } = 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 })
if (!ok || !sp.nochHier()) return
nachSpeichern(t, 'Feiertage gespeichert.', 'form[aria-label="Feiertage"] button[type="submit"]')
})
return abschnitt('Feiertage', null, form)
}
/* ------------------------------------------------------- Sonderöffnungen --- */
export function sonderoeffnungen(t) {
const { ctx } = t
const e = ctx.daten.einstellung
const tage = Object.keys(e.sonderoeffnungen || {}).sort()
const neu = neuerEintrag({
nurLesen: ctx.nurLesen, text: '+ Sonderöffnung eintragen', werkzeug: 'sonder-neu',
voll: tage.length >= LIMITS.sonderoeffnungen, grenzText: `Mehr als ${LIMITS.sonderoeffnungen} Sonderöffnungen sind nicht möglich.`,
baue: (schliessen) => sonderFormular(t, { tag: null, schliessen }),
})
const reihe = (tag) => editorZeile({
attrs: { 'data-tag': tag },
nurLesen: ctx.nurLesen,
label: `Sonderöffnung ${datumKurz(tag)}`,
haupt: [
h('strong', { class: 'tm-name' }, datumLang(tag)),
h('span', { class: 'tm-meta' }, fensterText(e.sonderoeffnungen[tag]) || 'Geschlossen'),
],
baue: (schliessen) => sonderFormular(t, { tag, schliessen }),
})
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 geht dem Wochenplan und den Feiertagen vor.',
neu.leiste,
neu.box,
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 } = 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 entfernen = neu || ctx.nurLesen ? null : entfernenKnopf(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: () => nachSpeichern(t, 'Sonderöffnung entfernt.', '[data-werkzeug="sonder-neu"]'),
})
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?.knopf))
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 })
if (!ok || !sp.nochHier()) return
nachSpeichern(t, neu ? 'Sonderöffnung eingetragen.' : 'Sonderöffnung gespeichert.', `li[data-tag="${neu ? datum.value : tag}"] [data-aktion="bearbeiten"]`)
})
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
}