Files
Webklar-Kundenbereich/public/termine-formulare.js
T

171 lines
9.2 KiB
JavaScript
Raw 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: 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
}