Terminplaner: Portal-Ansicht Einstellungen – Nachbesserungen

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 0b75b31009
commit 3ad056f261
10 files changed
+568 -342

No files matched your search

+75 -125
View File
@@ -4,8 +4,8 @@
(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'
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)
@@ -14,61 +14,43 @@ 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 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 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 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) => {
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
}
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.',
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,
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)
}
function formular(t, { index, a, schliessen }) {
const { ctx, sp, zeichne } = t
/** 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,
@@ -78,8 +60,12 @@ function formular(t, { index, a, schliessen }) {
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 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, [
@@ -91,35 +77,29 @@ function formular(t, { index, a, schliessen }) {
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))
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]
else entwurf.abwesenheiten[index] = eintrag
}, { container: form, knopf: speichern })
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
zeichne()
ctx.meldung(neu ? 'Abwesenheit eingetragen.' : 'Abwesenheit gespeichert.')
nachSpeichern(t, neu ? 'Abwesenheit eingetragen.' : 'Abwesenheit gespeichert.', `li[data-id="${alt.id}"] [data-aktion="bearbeiten"]`)
})
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)
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
}
/* -------------------------------------------------------------- Feiertage --- */
export function feiertage(t) {
const { ctx, sp, zeichne } = 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 })
@@ -133,72 +113,54 @@ export function feiertage(t) {
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 })
const ok = await sp.speichere((entwurf) => { entwurf.bundesland = land.value; entwurf.feiertageGeschlossen = zu.checked }, { container: form })
if (!ok || !sp.nochHier()) return
zeichne()
ctx.meldung('Feiertage gespeichert.')
nachSpeichern(t, 'Feiertage gespeichert.', 'form[aria-label="Feiertage"] button[type="submit"]')
})
return abschnitt('Feiertage', null, form)
}
/* ------------------------------------------------------- Sonderöffnungen --- */
export function sonderoeffnungen(t) {
const { ctx, sp, zeichne } = t
const { ctx } = 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
}
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 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,
'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, zeichne } = t
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 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 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]')
@@ -211,7 +173,7 @@ function sonderFormular(t, { tag, schliessen }) {
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))
entfernen?.knopf))
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
const ok = await sp.speichere((entwurf) => {
@@ -229,21 +191,9 @@ function sonderFormular(t, { tag, schliessen }) {
zeitenName(`sonderoeffnungen.${tagWert}`)
entwurf.sonderoeffnungen = { ...(entwurf.sonderoeffnungen || {}), [tagWert]: fenster }
return true
}, { container: form, knopf: speichern })
}, { container: form })
if (!ok || !sp.nochHier()) return
zeichne()
ctx.meldung(neu ? 'Sonderöffnung eingetragen.' : 'Sonderöffnung gespeichert.')
nachSpeichern(t, neu ? 'Sonderöffnung eingetragen.' : 'Sonderöffnung gespeichert.', `li[data-tag="${neu ? datum.value : tag}"] [data-aktion="bearbeiten"]`)
})
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)
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
}