171 lines
9.2 KiB
JavaScript
171 lines
9.2 KiB
JavaScript
/* Terminplaner im Kundenbereich: Formulare des Reiters "Termine" – freie Uhrzeiten waehlen, Termin von Hand eintragen, Zeit sperren.
|
||
Die Formulare kennen den Reiter nicht: sie bekommen ctx (siehe termine.js) und hooks = { startTag, erfolg(tag, text), abbrechen() }.
|
||
erfolg wird nach dem Speichern aufgerufen (der Reiter zeigt dann den Tag und laedt die Liste neu), abbrechen schliesst das Formular. */
|
||
|
||
import { GESUNDHEIT_HINWEIS, MAX, heuteBerlin, istTag, ressourcenWort, zeitNormal } from './termine-view.js'
|
||
import { allgemeinerFehler, feld, h, setzeFehler, zeigeFehler } from './termine-dom.js'
|
||
|
||
/** Freie Uhrzeiten als Auswahl-Knoepfe: laden() holt sie fuer query() (leistung/ressource/tag oder ohne/tag), wert() ist die gewaehlte. */
|
||
export function zeitWahl(ctx, query, { onWahl } = {}) {
|
||
let gewaehlt = ''
|
||
let nr = 0
|
||
const status = h('p', { class: 'tm-hinweis', 'aria-live': 'polite' })
|
||
const knoepfe = h('div', { class: 'tm-zeiten', role: 'group', 'aria-label': 'Freie Uhrzeiten' })
|
||
const fehler = h('p', { class: 'tm-fehler', 'data-fehler': 'zeit', role: 'alert' })
|
||
const el = h('div', { class: 'tm-feld', 'data-feld': 'zeit' }, h('span', { class: 'tm-label' }, 'Uhrzeit'), status, knoepfe, fehler)
|
||
async function laden() {
|
||
const mein = ++nr
|
||
gewaehlt = ''
|
||
knoepfe.replaceChildren()
|
||
fehler.textContent = ''
|
||
const q = query()
|
||
if (!q || !istTag(q.tag)) { status.textContent = 'Wähle zuerst einen Tag.'; return }
|
||
status.textContent = 'Freie Zeiten werden geladen …'
|
||
try {
|
||
const { zeiten } = await ctx.api.get('/zeiten', q)
|
||
if (mein !== nr) return
|
||
status.textContent = zeiten.length ? 'Wähle eine Uhrzeit:' : 'An diesem Tag ist nichts frei.'
|
||
knoepfe.replaceChildren(...zeiten.map((z) => h('button', {
|
||
type: 'button', class: 'tm-chip tm-zeit-knopf', 'aria-pressed': 'false', 'data-zeit': z,
|
||
onclick: (ev) => {
|
||
gewaehlt = z
|
||
for (const k of knoepfe.children) k.setAttribute('aria-pressed', String(k === ev.currentTarget))
|
||
fehler.textContent = ''
|
||
onWahl?.(z)
|
||
},
|
||
}, z)))
|
||
} catch (err) {
|
||
if (mein === nr) status.textContent = err.message
|
||
}
|
||
}
|
||
return { el, laden, wert: () => gewaehlt }
|
||
}
|
||
|
||
export function formularTermin(ctx, hooks) {
|
||
const e = ctx.daten.einstellung
|
||
const aktiveRessourcen = (e.ressourcen || []).filter((r) => r.aktiv)
|
||
const leistungen = (e.leistungen || []).filter((l) => l.aktiv && (l.ressourcen || []).some((id) => aktiveRessourcen.some((r) => r.id === id)))
|
||
if (!leistungen.length) {
|
||
return h('div', { class: 'kb-panel tm-formular' }, h('p', null, 'Lege zuerst im Reiter „Leistungen“ eine Leistung an, die du buchen kannst.'),
|
||
h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => ctx.zumReiter('leistungen') }, 'Zu den Leistungen'))
|
||
}
|
||
const wort = ressourcenWort(aktiveRessourcen)
|
||
const leistung = h('select', null, leistungen.map((l) => h('option', { value: l.id }, `${l.name} (${l.dauer} Min.)`)))
|
||
const person = h('select')
|
||
const tag = h('input', { type: 'date', value: hooks.startTag, min: heuteBerlin() })
|
||
const name = h('input', { type: 'text', maxlength: String(MAX.name), autocomplete: 'off' })
|
||
const email = h('input', { type: 'email', maxlength: '254', autocomplete: 'off' })
|
||
const telefon = h('input', { type: 'tel', maxlength: String(MAX.telefon), autocomplete: 'off' })
|
||
const nachricht = h('textarea', { rows: '3', maxlength: String(MAX.nachricht) })
|
||
const senden = h('input', { type: 'checkbox', id: 'tm-senden' })
|
||
const sendenHinweis = h('span', { class: 'tm-hinweis' })
|
||
const zeiten = zeitWahl(ctx, () => ({ leistung: leistung.value, ressource: person.value, tag: tag.value }))
|
||
|
||
function ressourcenOptionen() {
|
||
const l = leistungen.find((x) => x.id === leistung.value)
|
||
const infrage = aktiveRessourcen.filter((r) => (l.ressourcen || []).includes(r.id))
|
||
person.replaceChildren(...(infrage.length > 1 ? [h('option', { value: 'egal' }, 'Egal (erste freie)')] : []), ...infrage.map((r) => h('option', { value: r.id }, r.name)))
|
||
person.disabled = infrage.length === 1
|
||
}
|
||
ressourcenOptionen()
|
||
leistung.addEventListener('change', () => { ressourcenOptionen(); zeiten.laden() })
|
||
person.addEventListener('change', () => zeiten.laden())
|
||
tag.addEventListener('change', () => zeiten.laden())
|
||
const mailAn = () => { senden.disabled = email.value.trim() === ''; if (senden.disabled) senden.checked = false; sendenHinweis.textContent = senden.disabled ? ' (nur mit E-Mail-Adresse)' : '' }
|
||
email.addEventListener('input', mailAn)
|
||
mailAn()
|
||
|
||
const ok = h('button', { type: 'submit', class: 'kb-btn-primary' }, 'Termin eintragen')
|
||
const form = h('form', { class: 'kb-panel tm-formular', novalidate: true, 'aria-label': 'Termin eintragen' },
|
||
h('h3', null, 'Termin eintragen'),
|
||
h('p', { class: 'tm-hinweis' }, 'Für Anrufe und Laufkundschaft. Der Termin ist sofort bestätigt. Fristen und Vorlauf gelten dafür nicht.'),
|
||
h('div', { class: 'tm-raster' },
|
||
feld('leistung', 'Leistung', leistung),
|
||
feld('ressource', wort, person),
|
||
feld('tag', 'Tag', tag)),
|
||
zeiten.el,
|
||
h('div', { class: 'tm-raster' },
|
||
feld('name', 'Name des Gastes', name),
|
||
feld('email', 'E-Mail (optional)', email),
|
||
feld('telefon', 'Telefon (optional)', telefon)),
|
||
feld('nachricht', 'Notiz (optional)', nachricht, GESUNDHEIT_HINWEIS),
|
||
h('label', { class: 'tm-haken', for: 'tm-senden' }, senden, h('span', null, 'Bestätigung an den Gast senden', sendenHinweis)),
|
||
allgemeinerFehler(),
|
||
h('div', { class: 'tm-frage-knoepfe' }, ok, h('button', { type: 'button', class: 'kb-btn-outline', onclick: hooks.abbrechen }, 'Abbrechen')))
|
||
zeiten.laden()
|
||
form.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
setzeFehler(form, {})
|
||
if (!zeiten.wert()) {
|
||
setzeFehler(form, { zeit: 'Bitte wähle eine Uhrzeit aus.' })
|
||
return
|
||
}
|
||
ok.disabled = true
|
||
try {
|
||
const gewaehlt = tag.value
|
||
await ctx.api.send('POST', '/buchungen', {
|
||
art: 'manuell', leistung: leistung.value, ressource: person.value, tag: gewaehlt, zeit: zeiten.wert(), name: name.value,
|
||
email: email.value, telefon: telefon.value, nachricht: nachricht.value, bestaetigungSenden: senden.checked,
|
||
})
|
||
await hooks.erfolg(gewaehlt, 'Termin eingetragen.')
|
||
} catch (err) {
|
||
ok.disabled = false
|
||
if (err.status === 400 || err.code === 'SLOT_VERGEBEN') {
|
||
zeigeFehler(form, err)
|
||
if (err.code === 'SLOT_VERGEBEN') zeiten.laden()
|
||
} else {
|
||
ctx.meldung(err.message, 'fehler')
|
||
}
|
||
}
|
||
})
|
||
return form
|
||
}
|
||
|
||
export function formularSperre(ctx, hooks) {
|
||
const e = ctx.daten.einstellung
|
||
const ressourcen = (e.ressourcen || []).filter((r) => r.aktiv)
|
||
const wort = ressourcenWort(ressourcen)
|
||
const person = h('select', null, ressourcen.map((r) => h('option', { value: r.id }, r.name)))
|
||
const tag = h('input', { type: 'date', value: hooks.startTag, min: heuteBerlin() })
|
||
const von = h('input', { type: 'text', inputmode: 'numeric', maxlength: '5', placeholder: '12:00', autocomplete: 'off' })
|
||
const bis = h('input', { type: 'text', inputmode: 'numeric', maxlength: '5', placeholder: '13:00', autocomplete: 'off' })
|
||
const titel = h('input', { type: 'text', maxlength: String(MAX.titel), placeholder: 'z. B. Mittagspause', autocomplete: 'off' })
|
||
const ok = h('button', { type: 'submit', class: 'kb-btn-primary' }, 'Zeit sperren')
|
||
const form = h('form', { class: 'kb-panel tm-formular', novalidate: true, 'aria-label': 'Zeit sperren' },
|
||
h('h3', null, 'Zeit sperren'),
|
||
h('p', { class: 'tm-hinweis' }, 'In dieser Zeit kann niemand buchen. Eine Sperre kann höchstens 8 Stunden 20 Minuten (500 Minuten) dauern. Für ganze Tage oder Urlaub nutze die Abwesenheiten im Reiter „Team & Plätze“.'),
|
||
h('div', { class: 'tm-raster' },
|
||
feld('ressource', wort, person),
|
||
feld('tag', 'Tag', tag)),
|
||
h('div', { class: 'tm-raster' },
|
||
feld('von', 'Von (Uhrzeit)', von, 'In 5-Minuten-Schritten, z. B. 12:00.'),
|
||
feld('bis', 'Bis (Uhrzeit)', bis, 'Auch 24:00 ist möglich.')),
|
||
feld('titel', 'Titel (optional)', titel, 'Nur für dich sichtbar.'),
|
||
allgemeinerFehler(),
|
||
h('div', { class: 'tm-frage-knoepfe' }, ok, h('button', { type: 'button', class: 'kb-btn-outline', onclick: hooks.abbrechen }, 'Abbrechen')))
|
||
form.addEventListener('submit', async (ev) => {
|
||
ev.preventDefault()
|
||
setzeFehler(form, {})
|
||
const vonZeit = zeitNormal(von.value)
|
||
const bisZeit = zeitNormal(bis.value, { ende: true })
|
||
const fehler = {}
|
||
if (!vonZeit) fehler.von = 'Bitte gib die Uhrzeit so an: 12:00.'
|
||
if (!bisZeit) fehler.bis = 'Bitte gib die Uhrzeit so an: 13:00.'
|
||
if (Object.keys(fehler).length) {
|
||
zeigeFehler(form, { fehler })
|
||
return
|
||
}
|
||
ok.disabled = true
|
||
try {
|
||
const gewaehlt = tag.value
|
||
await ctx.api.send('POST', '/buchungen', { art: 'sperre', ressource: person.value, tag: gewaehlt, von: vonZeit, bis: bisZeit, titel: titel.value })
|
||
await hooks.erfolg(gewaehlt, 'Zeit gesperrt.')
|
||
} catch (err) {
|
||
ok.disabled = false
|
||
if (err.status === 400 || err.code === 'SLOT_VERGEBEN') zeigeFehler(form, err)
|
||
else ctx.meldung(err.message, 'fehler')
|
||
}
|
||
})
|
||
return form
|
||
}
|