Terminplaner: Portal-Ansicht – Nachbesserungen (1/2: Module getrennt)
public/termine.js in Huelle und Module zerlegt, Verhalten unveraendert (R48): termine-dom.js (Helfer ohne Zustand), termine-tab-termine.js (Liste, Kopf, Zeilen), termine-aktionen.js (Aktionen mit Rueckfrage), termine-formulare.js (Uhrzeiten, Termin eintragen, Zeit sperren). Reiter bekommen Zustand und aktuell() nur ueber ctx; die Huelle haengt sie ueber die Registry REITER ein. Statiktest prueft alle public/termine*.js. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43e4826ae3
commit
9c07f6a8c5
7 files changed
+697
-645
No files matched your search
@@ -0,0 +1,170 @@
|
||||
/* 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 { MAX, heuteBerlin, 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 || !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, 'Bitte keine Gesundheitsangaben.'),
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user