/* 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 }