Terminplaner: Portal-Ansicht Leistungen, Team, Einstellungen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:44 +00:00
1 parent 650919b022
commit 0b75b31009
15 files changed
+2479 -79

No files matched your search

+249
View File
@@ -0,0 +1,249 @@
/* 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)
}