Terminplaner: Portal-Ansicht Einstellungen – Nachbesserungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b75b31009
commit
3ad056f261
10 files changed
+568
-342
No files matched your search
@@ -4,9 +4,11 @@
|
||||
(termine-speichern.js); Pruefen und Begrenzen bleibt Sache des Servers, hier steht nur, was ein Feld ohne Antwort des Servers wissen kann.
|
||||
Abgeschaltete Funktion (ctx.nurLesen): dieselben Felder, aber gesperrt und ohne Knoepfe zum Aendern. */
|
||||
|
||||
import { GESUNDHEIT_HINWEIS, LIMITS, MAX, centZuEuro, eindeutigeId, euroZuCent, ganzzahl, preisAnzeige, verschiebeEintrag } from './termine-view.js'
|
||||
import { allgemeinerFehler, feld, gesperrt, h, hakenFeld, zeigeFehler } from './termine-dom.js'
|
||||
import { entfernenFrage, neuerSpeicher } from './termine-speichern.js'
|
||||
import {
|
||||
GESUNDHEIT_HINWEIS, LIMITS, MAX, centZuEuro, eindeutigeId, euroZuCent, ganzzahl, ordneNachIds, preisAnzeige, verschiebeEintrag,
|
||||
} from './termine-view.js'
|
||||
import { allgemeinerFehler, editorZeile, feld, gesperrt, h, hakenFeld, neuerEintrag, zeigeFehler } from './termine-dom.js'
|
||||
import { entfernenKnopf, nachSpeichern, neuerSpeicher, stelleVon } from './termine-speichern.js'
|
||||
|
||||
const BUCHUNGSART = [['sofort', 'Sofort buchbar'], ['anfrage', 'Nur als Anfrage (du nimmst jeden Termin an)']]
|
||||
const PREISART = [['keiner', 'Ohne Preis'], ['fest', 'Festpreis'], ['ab', 'Preis ab']]
|
||||
@@ -18,88 +20,124 @@ export function render(panel, ctx) {
|
||||
panel.replaceChildren(wurzel)
|
||||
const sp = neuerSpeicher(ctx, wurzel, zeichne)
|
||||
const t = { ctx, sp, zeichne, wurzel }
|
||||
// Reihenfolge (Ruling R51): die Pfeile aendern nur diese gemerkte Reihenfolge (IDs), gespeichert wird sie mit „Reihenfolge speichern“ in einem
|
||||
// Speichern; null = wie gespeichert. Sie bleibt beim Neuzeichnen (z. B. nach dem Speichern einer Leistung) erhalten.
|
||||
let ordnung = null
|
||||
|
||||
/** Anzeigereihenfolge (IDs): die gemerkte, auf den aktuellen Stand der Liste angewandt; ist sie die gespeicherte, gilt keine gemerkte mehr. */
|
||||
function anzeigeIds() {
|
||||
const liste = ctx.daten.einstellung.leistungen || []
|
||||
const gespeichert = liste.map((l) => l.id)
|
||||
const ids = ordnung ? ordneNachIds(liste, ordnung).map((l) => l.id) : gespeichert
|
||||
ordnung = ids.join('|') === gespeichert.join('|') ? null : ids
|
||||
return ids
|
||||
}
|
||||
|
||||
function zeichne() {
|
||||
const e = ctx.daten.einstellung
|
||||
const leistungen = e.leistungen || []
|
||||
const kopfFehler = h('div', null, allgemeinerFehler())
|
||||
const neuBox = h('div', { class: 'tm-formular-box' })
|
||||
const voll = leistungen.length >= LIMITS.leistungen
|
||||
const neuKnopf = ctx.nurLesen ? null : h('button', {
|
||||
type: 'button', class: 'kb-btn-primary', 'data-werkzeug': 'leistung-neu', 'aria-expanded': 'false', disabled: voll,
|
||||
onclick: (ev) => {
|
||||
if (neuBox.firstChild) { neuBox.replaceChildren(); ev.currentTarget.setAttribute('aria-expanded', 'false'); return }
|
||||
ev.currentTarget.setAttribute('aria-expanded', 'true')
|
||||
const form = formular(t, { index: leistungen.length, leistung: null, schliessen: () => { neuBox.replaceChildren(); neuKnopf.setAttribute('aria-expanded', 'false'); neuKnopf.focus() } })
|
||||
neuBox.replaceChildren(form)
|
||||
form.querySelector('input')?.focus()
|
||||
},
|
||||
}, '+ Leistung anlegen')
|
||||
const neu = neuerEintrag({
|
||||
nurLesen: ctx.nurLesen, text: '+ Leistung anlegen', werkzeug: 'leistung-neu', primaer: true,
|
||||
voll: leistungen.length >= LIMITS.leistungen, grenzText: `Mehr als ${LIMITS.leistungen} Leistungen sind nicht möglich.`,
|
||||
baue: (schliessen) => formular(t, { leistung: null, schliessen }),
|
||||
})
|
||||
const zeilen = new Map(leistungen.map((l) => [l.id, zeile(t, l, verschiebe)]))
|
||||
const liste = h('ul', { class: 'tm-eintraege', 'aria-label': 'Leistungen in der Reihenfolge, in der Gäste sie sehen' })
|
||||
const status = h('span', { role: 'status' })
|
||||
const speichern = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'reihenfolge-speichern' }, 'Reihenfolge speichern')
|
||||
const verwerfen = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'reihenfolge-verwerfen' }, 'Verwerfen')
|
||||
const leiste = h('div', { class: 'tm-box tm-box-info tm-reihenfolge', hidden: true }, status, h('div', { class: 'tm-frage-knoepfe' }, speichern, verwerfen))
|
||||
|
||||
/** Zeilen in Anzeigereihenfolge bringen (vorhandene Zeilen werden nur verschoben: offene Editoren und ihre Eingaben bleiben), Pfeile und Leiste nachziehen. */
|
||||
function aktualisiere() {
|
||||
const ids = anzeigeIds()
|
||||
liste.replaceChildren(...ids.map((id) => zeilen.get(id)))
|
||||
ids.forEach((id, i) => {
|
||||
const li = zeilen.get(id)
|
||||
const hoch = li.querySelector('[data-aktion="hoch"]')
|
||||
if (hoch) hoch.disabled = i === 0
|
||||
const runter = li.querySelector('[data-aktion="runter"]')
|
||||
if (runter) runter.disabled = i === ids.length - 1
|
||||
})
|
||||
leiste.hidden = !ordnung
|
||||
}
|
||||
function verschiebe(id, richtung, aktion) {
|
||||
const ids = anzeigeIds()
|
||||
ordnung = verschiebeEintrag(ids, ids.indexOf(id), richtung)
|
||||
aktualisiere()
|
||||
const stelle = anzeigeIds().indexOf(id) + 1
|
||||
status.textContent = ordnung
|
||||
? `„${leistungen.find((l) => l.id === id).name}“ steht jetzt an Stelle ${stelle} von ${ids.length}. Die neue Reihenfolge ist noch nicht gespeichert.`
|
||||
: 'Die Reihenfolge ist wieder wie gespeichert.'
|
||||
const li = zeilen.get(id)
|
||||
;(li.querySelector(`[data-aktion="${aktion}"]:not(:disabled)`) || li.querySelector('[data-aktion="bearbeiten"]'))?.focus()
|
||||
}
|
||||
const fokusErsteZeile = () => liste.querySelector('li [data-aktion="bearbeiten"]')?.focus()
|
||||
verwerfen.addEventListener('click', () => {
|
||||
ordnung = null
|
||||
aktualisiere()
|
||||
fokusErsteZeile()
|
||||
ctx.meldung('Die gemerkte Reihenfolge ist verworfen.')
|
||||
})
|
||||
speichern.addEventListener('click', async () => {
|
||||
const gewuenscht = [...(ordnung || [])]
|
||||
// die gemerkte Reihenfolge wirkt auf eine frische Kopie des gespeicherten Stands, nach IDs (nicht nach Stellen)
|
||||
const ok = await sp.speichere((entwurf) => { entwurf.leistungen = ordneNachIds(entwurf.leistungen, gewuenscht) }, { container: kopfFehler })
|
||||
if (!ok || !sp.nochHier()) return
|
||||
ordnung = null
|
||||
zeichne()
|
||||
ctx.meldung('Reihenfolge gespeichert.')
|
||||
fokusErsteZeile()
|
||||
})
|
||||
|
||||
aktualisiere()
|
||||
wurzel.replaceChildren(
|
||||
sp.konflikt,
|
||||
h('div', { class: 'tm-box tm-box-info', role: 'note' },
|
||||
h('span', null, h('strong', null, GESUNDHEIT_HINWEIS), ' Namen und Beschreibungen deiner Leistungen liest jeder, der auf deiner Website bucht. Schreib dort zum Beispiel keine Krankheiten oder Beschwerden hinein.')),
|
||||
ctx.nurLesen ? null : h('div', { class: 'tm-werkzeug-zeile' }, neuKnopf, voll ? h('p', { class: 'tm-hinweis' }, `Mehr als ${LIMITS.leistungen} Leistungen sind nicht möglich.`) : null),
|
||||
neuBox,
|
||||
neu.leiste,
|
||||
neu.box,
|
||||
leiste,
|
||||
kopfFehler,
|
||||
leistungen.length
|
||||
? h('ul', { class: 'tm-eintraege', 'aria-label': 'Leistungen in der Reihenfolge, in der Gäste sie sehen' }, leistungen.map((l, i) => zeile(t, l, i, leistungen.length, kopfFehler)))
|
||||
: h('p', { class: 'tm-leer' }, 'Noch keine Leistungen. Ohne Leistung kann niemand buchen.'))
|
||||
leistungen.length ? liste : h('p', { class: 'tm-leer' }, 'Noch keine Leistungen. Ohne Leistung kann niemand buchen.'))
|
||||
}
|
||||
zeichne()
|
||||
}
|
||||
|
||||
/** Zeile einer Leistung: Kurzfassung, Aktiv-Pille, Reihenfolge (hoch/runter) und der Knopf, der das Formular unter der Zeile oeffnet. */
|
||||
function zeile(t, l, index, anzahl, kopfFehler) {
|
||||
const { ctx, sp } = t
|
||||
/** Zeile einer Leistung: Kurzfassung, Aktiv-Pille, Pfeile fuer die Reihenfolge und der Knopf, der das Formular unter der Zeile oeffnet. */
|
||||
function zeile(t, l, verschiebe) {
|
||||
const { ctx } = t
|
||||
const e = ctx.daten.einstellung
|
||||
const namen = (l.ressourcen || []).map((id) => (e.ressourcen || []).find((r) => r.id === id)?.name).filter(Boolean)
|
||||
const li = h('li', { class: 'tm-eintrag', 'data-id': l.id })
|
||||
const editor = h('div', { class: 'tm-editor-box' })
|
||||
const meta = [`${l.dauer} Min.`, l.pufferNach ? `${l.pufferNach} Min. Pause danach` : '', preisAnzeige(l), l.buchungsart === 'anfrage' ? 'Nur als Anfrage' : 'Sofort buchbar', namen.join(', ') || 'niemand zugeordnet']
|
||||
const oeffnen = h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'bearbeiten', 'aria-expanded': 'false', 'aria-label': `${ctx.nurLesen ? 'Ansehen' : 'Bearbeiten'}: ${l.name}`,
|
||||
onclick: () => {
|
||||
if (editor.firstChild) { editor.replaceChildren(); oeffnen.setAttribute('aria-expanded', 'false'); return }
|
||||
oeffnen.setAttribute('aria-expanded', 'true')
|
||||
const form = formular(t, { index, leistung: l, schliessen: () => { editor.replaceChildren(); oeffnen.setAttribute('aria-expanded', 'false'); oeffnen.focus() } })
|
||||
editor.replaceChildren(form)
|
||||
form.querySelector('input')?.focus()
|
||||
},
|
||||
}, ctx.nurLesen ? 'Ansehen' : 'Bearbeiten')
|
||||
|
||||
async function verschiebe(richtung, knopf) {
|
||||
const ok = await sp.speichere((entwurf) => { entwurf.leistungen = verschiebeEintrag(entwurf.leistungen, index, richtung) }, { container: kopfFehler, knopf })
|
||||
if (!ok || !sp.nochHier()) return
|
||||
t.zeichne()
|
||||
ctx.meldung(`„${l.name}“ steht jetzt ${richtung < 0 ? 'weiter oben' : 'weiter unten'}.`)
|
||||
const neu = t.wurzel.querySelector(`li[data-id="${l.id}"]`)
|
||||
;(neu?.querySelector(`[data-aktion="${richtung < 0 ? 'hoch' : 'runter'}"]:not([disabled])`) || neu?.querySelector('[data-aktion="bearbeiten"]'))?.focus()
|
||||
}
|
||||
const pfeil = (aktion, richtung, zeichen, text, aus) => h('button', {
|
||||
type: 'button', class: 'tm-pfeil', 'data-aktion': aktion, 'aria-label': `${text}: ${l.name}`, disabled: aus,
|
||||
onclick: (ev) => verschiebe(richtung, ev.currentTarget),
|
||||
const pfeil = (aktion, richtung, zeichen, text) => h('button', {
|
||||
type: 'button', class: 'tm-pfeil', 'data-aktion': aktion, 'aria-label': `${text}: ${l.name}`, onclick: () => verschiebe(l.id, richtung, aktion),
|
||||
}, h('span', { 'aria-hidden': 'true' }, zeichen))
|
||||
|
||||
li.append(
|
||||
h('div', { class: 'tm-kopf' },
|
||||
h('div', { class: 'tm-haupt' },
|
||||
h('strong', { class: 'tm-name' }, l.name),
|
||||
h('span', { class: 'tm-meta' }, meta.filter(Boolean).join(' · ')),
|
||||
l.beschreibung ? h('span', { class: 'tm-meta' }, l.beschreibung) : null),
|
||||
h('span', { class: `tm-pille tm-pille-${l.aktiv ? 'ok' : 'off'}` }, l.aktiv ? 'Aktiv' : 'Inaktiv')),
|
||||
h('div', { class: 'tm-aktionen' },
|
||||
ctx.nurLesen ? null : [pfeil('hoch', -1, '↑', 'Nach oben', index === 0), pfeil('runter', 1, '↓', 'Nach unten', index === anzahl - 1)],
|
||||
oeffnen),
|
||||
editor)
|
||||
return li
|
||||
return editorZeile({
|
||||
attrs: { 'data-id': l.id },
|
||||
nurLesen: ctx.nurLesen,
|
||||
label: l.name,
|
||||
haupt: [
|
||||
h('strong', { class: 'tm-name' }, l.name),
|
||||
h('span', { class: 'tm-meta' }, meta.filter(Boolean).join(' · ')),
|
||||
l.beschreibung ? h('span', { class: 'tm-meta' }, l.beschreibung) : null,
|
||||
],
|
||||
pille: h('span', { class: `tm-pille tm-pille-${l.aktiv ? 'ok' : 'off'}` }, l.aktiv ? 'Aktiv' : 'Inaktiv'),
|
||||
vorKnopf: ctx.nurLesen ? [] : [pfeil('hoch', -1, '↑', 'Nach oben'), pfeil('runter', 1, '↓', 'Nach unten')],
|
||||
baue: (schliessen) => formular(t, { leistung: l, schliessen }),
|
||||
})
|
||||
}
|
||||
|
||||
/** Formular einer neuen (leistung null) oder bestehenden Leistung; index = ihre Stelle in der Liste (so heissen die Pfade der Fehler des Servers). */
|
||||
function formular(t, { index, leistung, schliessen }) {
|
||||
const { ctx, sp, zeichne } = t
|
||||
/**
|
||||
* Formular einer neuen (leistung null) oder bestehenden Leistung. Die Pfade der Fehler des Servers nennen die Stelle in der gespeicherten
|
||||
* Liste (leistungen.2.dauer); die Felder heissen danach. Beim Absenden wird die Leistung per ID gesucht, nicht per Stelle.
|
||||
*/
|
||||
function formular(t, { leistung, schliessen }) {
|
||||
const { ctx, sp } = t
|
||||
const e = ctx.daten.einstellung
|
||||
const neu = leistung === null
|
||||
const index = neu ? (e.leistungen || []).length : (e.leistungen || []).findIndex((x) => x.id === leistung.id)
|
||||
const l = leistung || { id: eindeutigeId('s', (e.leistungen || []).map((x) => x.id)), name: '', beschreibung: '', dauer: 30, pufferNach: 0, preisArt: 'keiner', preisCent: 0, buchungsart: 'sofort', ressourcen: [], aktiv: true }
|
||||
const p = `leistungen.${index}`
|
||||
|
||||
@@ -138,8 +176,13 @@ function formular(t, { index, leistung, schliessen }) {
|
||||
]
|
||||
|
||||
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary' }, neu ? 'Leistung anlegen' : 'Speichern')
|
||||
const entfernenBox = h('div', { class: 'tm-frage' }) // die Rueckfrage steht neben dem Formular, nicht darin (ein submit steigt sonst ins aeussere Formular auf)
|
||||
const entfernen = neu || ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline tm-gefahr', onclick: () => frageEntfernen() }, 'Entfernen')
|
||||
const entfernen = neu || ctx.nurLesen ? null : entfernenKnopf(sp, {
|
||||
titel: `Leistung „${l.name}“ entfernen?`,
|
||||
text: 'Das kann nicht rückgängig gemacht werden. Gibt es für die Leistung noch künftige Termine, geht das Entfernen nicht. Dann deaktiviere sie stattdessen.',
|
||||
fehlerPfad: `leistungen.${l.id}`,
|
||||
aendere: (entwurf) => { entwurf.leistungen = entwurf.leistungen.filter((x) => x.id !== l.id) },
|
||||
nachher: () => nachSpeichern(t, `Leistung „${l.name}“ entfernt.`, '[data-werkzeug="leistung-neu"]'),
|
||||
})
|
||||
const form = h('form', { class: 'kb-panel tm-formular', novalidate: true, 'aria-label': neu ? 'Leistung anlegen' : `${ctx.nurLesen ? 'Leistung' : 'Leistung bearbeiten'}: ${l.name}` },
|
||||
h('h3', null, neu ? 'Neue Leistung' : l.name),
|
||||
gesperrt(ctx.nurLesen, felder),
|
||||
@@ -147,7 +190,7 @@ function formular(t, { index, leistung, 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))
|
||||
|
||||
/** Leistung aus den Feldern; Eingaben, die kein Feld verstehen kann, werden hier am Feld gemeldet. */
|
||||
function lese() {
|
||||
@@ -178,27 +221,18 @@ function formular(t, { index, leistung, schliessen }) {
|
||||
zeigeFehler(form, { fehler: gelesen.fehler })
|
||||
return false
|
||||
}
|
||||
if (neu) entwurf.leistungen.push(gelesen.leistung)
|
||||
else entwurf.leistungen[index] = gelesen.leistung
|
||||
if (neu) {
|
||||
entwurf.leistungen.push(gelesen.leistung)
|
||||
return true
|
||||
}
|
||||
const stelle = stelleVon(entwurf.leistungen, l.id, form, 'Diese Leistung')
|
||||
if (stelle < 0) return false
|
||||
entwurf.leistungen[stelle] = gelesen.leistung
|
||||
return true
|
||||
}, { container: form, knopf: speichern })
|
||||
}, { container: form })
|
||||
if (!ok || !sp.nochHier()) return
|
||||
zeichne()
|
||||
ctx.meldung(neu ? 'Leistung angelegt.' : 'Leistung gespeichert.')
|
||||
t.wurzel.querySelector(`li[data-id="${l.id}"] [data-aktion="bearbeiten"]`)?.focus()
|
||||
nachSpeichern(t, neu ? 'Leistung angelegt.' : 'Leistung gespeichert.', `li[data-id="${l.id}"] [data-aktion="bearbeiten"]`)
|
||||
})
|
||||
|
||||
function frageEntfernen() {
|
||||
const box = entfernenFrage(sp, {
|
||||
titel: `Leistung „${l.name}“ entfernen?`,
|
||||
text: 'Das kann nicht rückgängig gemacht werden. Gibt es für die Leistung noch künftige Termine, geht das Entfernen nicht. Dann deaktiviere sie stattdessen.',
|
||||
fehlerPfad: `leistungen.${l.id}`,
|
||||
aendere: (entwurf) => { entwurf.leistungen = entwurf.leistungen.filter((x) => x.id !== l.id) },
|
||||
nachher: () => { zeichne(); ctx.meldung(`Leistung „${l.name}“ entfernt.`) },
|
||||
zurueck: () => { entfernenBox.replaceChildren(); entfernen.focus() },
|
||||
})
|
||||
entfernenBox.replaceChildren(box)
|
||||
box.focus()
|
||||
}
|
||||
return h('div', { class: 'tm-editor' }, form, entfernenBox)
|
||||
return h('div', { class: 'tm-editor' }, form, entfernen?.box)
|
||||
}
|
||||
Reference in new issue
Block a user