128 lines
8.6 KiB
JavaScript
128 lines
8.6 KiB
JavaScript
/* Buchungskalender, Reiter „Unterkünfte & Preise“: Liste (anlegen, sortieren, aktivieren), Bearbeiten (Stammdaten, Preise, iCal).
|
|
API (alle mit ?projectId=): GET /api/unterkunft/unterkuenfte => { unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv,
|
|
icalImporte, icalExportUrl }] }; POST /unterkuenfte und PUT /unterkuenfte/:id mit { name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }
|
|
(Antwort { unterkunft:{…} }). Preise: buchungen-preise.js, iCal: buchungen-ical.js. Fremdtext nur ueber textContent. */
|
|
|
|
import { abschnitt, allgemeinerFehler, fehlerKarte, feld, h, hakenFeld, setzeFehler, zeigeFehler } from './shop-dom.js'
|
|
import { enc, ladeUnterkuenfte, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
|
|
import { zeichnePreise } from './buchungen-preise.js'
|
|
import { zeichneIcal } from './buchungen-ical.js'
|
|
|
|
const ganz = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,3}$/.test(t) ? Number(t) : null }
|
|
const zeit = (t) => /^([01]\d|2[0-3]):[0-5]\d$/.test(t)
|
|
|
|
export async function render(panel, ctx) {
|
|
const q = `projectId=${enc(ctx.projectId)}`
|
|
const inhalt = h('div', { class: 'bk-unterkuenfte' })
|
|
panel.replaceChildren(inhalt)
|
|
await zeigeListe()
|
|
|
|
async function zeigeListe() {
|
|
inhalt.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
|
let liste
|
|
try {
|
|
liste = await ladeUnterkuenfte(ctx)
|
|
} catch (err) {
|
|
if (ctx.aktuell()) inhalt.replaceChildren(fehlerKarte(err.message, zeigeListe))
|
|
return
|
|
}
|
|
if (!ctx.aktuell()) return
|
|
const kopf = h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'neu', onclick: () => zeigeForm(null, liste) }, 'Unterkunft hinzufügen'))
|
|
if (!liste.length) {
|
|
inhalt.replaceChildren(kopf, h('div', { class: 'kb-panel kb-empty', 'data-leer': true }, 'Noch keine Unterkunft. Lege deine erste Unterkunft an.'))
|
|
return
|
|
}
|
|
inhalt.replaceChildren(kopf, h('ul', { class: 'bk-liste' }, liste.map((u, i) => h('li', { class: 'kb-panel', 'data-unterkunft': u.id },
|
|
h('div', { class: 'bk-detail-kopf' },
|
|
h('strong', null, zeichen(u.name)),
|
|
h('span', { class: `sh-pille sh-pille-${u.aktiv === false ? 'off' : 'ok'}` }, u.aktiv === false ? 'Deaktiviert' : 'Aktiv')),
|
|
h('p', { class: 'sh-hinweis' }, `bis ${u.maxPersonen ?? '?'} Personen, ab ${u.mindestNaechte ?? 1} ${u.mindestNaechte === 1 ? 'Nacht' : 'Nächten'}, Check-in ${zeichen(u.checkIn)}, Check-out ${zeichen(u.checkOut)}`),
|
|
h('div', { class: 'bk-aktionen' },
|
|
h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'bearbeiten', onclick: () => zeigeForm(u, liste) }, 'Bearbeiten'),
|
|
ctx.nurLesen ? null : [
|
|
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'hoch', 'aria-label': `${zeichen(u.name)} nach oben`, disabled: i === 0, onclick: () => schiebe(liste, i, -1) }, '↑'),
|
|
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'runter', 'aria-label': `${zeichen(u.name)} nach unten`, disabled: i === liste.length - 1, onclick: () => schiebe(liste, i, 1) }, '↓'),
|
|
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'umschalten', onclick: () => speichere(u, { aktiv: u.aktiv === false }, 'Gespeichert.') }, u.aktiv === false ? 'Aktivieren' : 'Deaktivieren'),
|
|
])))))
|
|
}
|
|
|
|
const koerper = (u, extra = {}) => ({
|
|
name: u.name, beschreibung: zeichen(u.beschreibung), maxPersonen: u.maxPersonen, mindestNaechte: u.mindestNaechte, checkIn: u.checkIn, checkOut: u.checkOut, sort: u.sort ?? 0, aktiv: u.aktiv !== false, ...extra,
|
|
})
|
|
async function speichere(u, extra, meldung) {
|
|
try {
|
|
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(u.id)}?${q}`, { method: 'PUT', body: koerper(u, extra) })
|
|
if (!ctx.aktuell()) return
|
|
ctx.zeigeMeldung(meldung, 'ok')
|
|
await zeigeListe()
|
|
} catch (err) { ctx.zeigeMeldung(err.message, 'fehler') }
|
|
}
|
|
async function schiebe(liste, i, d) {
|
|
const a = liste[i]
|
|
const b = liste[i + d]
|
|
try {
|
|
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(a.id)}?${q}`, { method: 'PUT', body: koerper(a, { sort: i + d }) })
|
|
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(b.id)}?${q}`, { method: 'PUT', body: koerper(b, { sort: i }) })
|
|
if (ctx.aktuell()) await zeigeListe()
|
|
} catch (err) { ctx.zeigeMeldung(err.message, 'fehler') }
|
|
}
|
|
|
|
/** Formular (u = null: neu). Bei einer bestehenden Unterkunft folgen Preise und iCal. */
|
|
async function zeigeForm(u, liste) {
|
|
const name = h('input', { type: 'text', id: 'bk-u-name', maxlength: '120', autocomplete: 'off' }); name.value = zeichen(u?.name)
|
|
const beschr = h('textarea', { id: 'bk-u-beschr', rows: '3', maxlength: '500' }); beschr.value = zeichen(u?.beschreibung)
|
|
const maxP = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-u-max' }); maxP.value = String(u?.maxPersonen ?? 2)
|
|
const minN = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-u-min' }); minN.value = String(u?.mindestNaechte ?? 1)
|
|
const ein = h('input', { type: 'text', id: 'bk-u-ein', placeholder: '15:00', maxlength: '5' }); ein.value = zeichen(u?.checkIn) || '15:00'
|
|
const aus = h('input', { type: 'text', id: 'bk-u-aus', placeholder: '10:00', maxlength: '5' }); aus.value = zeichen(u?.checkOut) || '10:00'
|
|
const aktiv = h('input', { type: 'checkbox', id: 'bk-u-aktiv' }); aktiv.checked = u ? u.aktiv !== false : true
|
|
const form = h('form', { novalidate: true, onsubmit: (ev) => ev.preventDefault() },
|
|
feld('name', 'Name der Unterkunft', name), feld('beschreibung', 'Kurzbeschreibung (optional)', beschr),
|
|
feld('maxPersonen', 'Höchstens Personen (1 bis 20)', maxP), feld('mindestNaechte', 'Mindestaufenthalt in Nächten (1 bis 90)', minN),
|
|
feld('checkIn', 'Check-in ab (HH:MM)', ein), feld('checkOut', 'Check-out bis (HH:MM)', aus),
|
|
hakenFeld('aktiv', aktiv, 'Aktiv (für Gäste buchbar)'), allgemeinerFehler(),
|
|
h('div', { class: 'bk-aktionen' },
|
|
ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'speichern', onclick: senden }, u ? 'Speichern' : 'Unterkunft anlegen'),
|
|
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zur-liste', onclick: zeigeListe }, 'Zur Liste')))
|
|
if (ctx.nurLesen) for (const el of form.querySelectorAll('input, textarea')) el.disabled = true
|
|
const bereiche = h('div', { class: 'bk-bearbeiten' })
|
|
inhalt.replaceChildren(abschnitt(u ? `Unterkunft „${zeichen(u.name)}“` : 'Neue Unterkunft', null, form), bereiche)
|
|
name.focus()
|
|
if (u) {
|
|
bereiche.replaceChildren(abschnitt('Preise', 'Preise sind Bruttopreise in Euro, so sieht sie dein Gast.', await zeichnePreise(ctx, u)), zeichneIcal(ctx, u, async (meldung) => {
|
|
ctx.zeigeMeldung(meldung, 'ok')
|
|
const neu = (await ladeUnterkuenfte(ctx)).find((x) => x.id === u.id)
|
|
if (ctx.aktuell()) await zeigeForm(neu || u, liste)
|
|
}))
|
|
}
|
|
|
|
async function senden(ev) {
|
|
const fehler = {}
|
|
const n = String(name.value).trim()
|
|
const mp = ganz(maxP)
|
|
const mn = ganz(minN)
|
|
if (!n) fehler.name = 'Bitte gib einen Namen an.'
|
|
if (mp === null || mp < 1 || mp > 20) fehler.maxPersonen = 'Bitte gib eine Zahl von 1 bis 20 an.'
|
|
if (mn === null || mn < 1 || mn > 90) fehler.mindestNaechte = 'Bitte gib eine Zahl von 1 bis 90 an.'
|
|
if (!zeit(String(ein.value).trim())) fehler.checkIn = 'Bitte gib eine Uhrzeit wie 15:00 an.'
|
|
if (!zeit(String(aus.value).trim())) fehler.checkOut = 'Bitte gib eine Uhrzeit wie 10:00 an.'
|
|
if (Object.keys(fehler).length) return zeigeFehler(form, { message: '', fehler })
|
|
setzeFehler(form, {})
|
|
const body = { name: n, beschreibung: String(beschr.value).trim(), maxPersonen: mp, mindestNaechte: mn, checkIn: String(ein.value).trim(), checkOut: String(aus.value).trim(), sort: u?.sort ?? liste.length, aktiv: aktiv.checked }
|
|
const knopf = ev.currentTarget
|
|
knopf.disabled = true
|
|
try {
|
|
const antwort = await ctx.request(u ? `/api/unterkunft/unterkuenfte/${enc(u.id)}?${q}` : `/api/unterkunft/unterkuenfte?${q}`, { method: u ? 'PUT' : 'POST', body })
|
|
if (!ctx.aktuell()) return
|
|
ctx.zeigeMeldung(u ? 'Gespeichert.' : 'Die Unterkunft ist angelegt. Jetzt kannst du die Preise eintragen.', 'ok')
|
|
const neu = antwort?.unterkunft?.id ? antwort.unterkunft : null
|
|
if (u || !neu) await zeigeListe()
|
|
else await zeigeForm({ ...neu }, [...liste, neu])
|
|
} catch (err) {
|
|
knopf.disabled = false
|
|
zeigeApiFehler(form, err)
|
|
}
|
|
}
|
|
}
|
|
}
|