213 lines
10 KiB
JavaScript
213 lines
10 KiB
JavaScript
/* Buchungskalender im Kundenbereich (ES-Modul, von app.js beim Oeffnen der Ansicht #buchungen geladen). Aufbau wie shop.js:
|
|
initBuchungen(rootEl, { projectId }): Projekt waehlen (GET /api/funktionen, features.unterkunft), Stand holen (GET /api/unterkunft/status)
|
|
und je nach state die Reiter (REITER, lazy geladen, render(panel, ctx)) oder (state off) die Anfrage-/Bestellkarte (Task 14).
|
|
ctx = { projectId, status, nurLesen, aktuell(), neuLaden(), zeigeMeldung(text, art), request }.
|
|
Helfer: shop-dom.js (h, request, Karten), Styles: buchungen.css (bk-) und shop.css (sh-). Fremdtext nur ueber textContent. */
|
|
|
|
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
|
|
import { statusKarte } from './funktion-bestellen.js'
|
|
import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js'
|
|
|
|
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
|
|
export const REITER = [
|
|
{ key: 'anfragen', label: 'Anfragen', laden: () => import('./buchungen-tab-anfragen.js') },
|
|
{ key: 'kalender', label: 'Kalender', laden: () => import('./buchungen-tab-kalender.js') },
|
|
{ key: 'unterkuenfte', label: 'Unterkünfte & Preise', laden: () => import('./buchungen-tab-unterkuenfte.js') },
|
|
{ key: 'rechnungen', label: 'Rechnungen', laden: () => import('./buchungen-tab-rechnungen.js') },
|
|
{ key: 'einstellungen', label: 'Einstellungen', laden: () => import('./buchungen-tab-einstellungen.js') },
|
|
]
|
|
|
|
const NICHT_VERFUEGBAR = 'Der Buchungskalender ist noch nicht verfügbar.'
|
|
const auswahl = { projectId: '', reiter: 'anfragen' }
|
|
/** Gemerkte Auswahl vergessen (fuer Tests). */
|
|
export function vergissAuswahl() {
|
|
auswahl.projectId = ''
|
|
auswahl.reiter = 'anfragen'
|
|
}
|
|
let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueberschreiben
|
|
|
|
const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '')
|
|
const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off'
|
|
|
|
export function initBuchungen(root, { projectId } = {}) {
|
|
if (!root) return Promise.resolve()
|
|
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
|
if (root.dataset) delete root.dataset.wunschProjekt
|
|
return starte(root, wunsch)
|
|
}
|
|
|
|
async function starte(root, projectId) {
|
|
if (projectId) auswahl.projectId = projectId
|
|
const s = { root, projects: [], projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null }
|
|
S = s
|
|
let funktionen
|
|
try {
|
|
funktionen = await request('/api/funktionen')
|
|
} catch (err) {
|
|
if (S === s) root.replaceChildren(fehlerKarte(err.message, () => starte(root, auswahl.projectId)))
|
|
return
|
|
}
|
|
if (S !== s) return
|
|
if (!funktionen?.unterkunft?.verfuegbar) {
|
|
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
|
return
|
|
}
|
|
s.projects = Array.isArray(funktionen.projects) ? funktionen.projects : []
|
|
if (!s.projects.length) {
|
|
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
|
return
|
|
}
|
|
const hat = (zustaende) => s.projects.find((p) => zustaende.includes(zustandVon(p)))
|
|
s.projekt = s.projects.find((p) => p.projectId === auswahl.projectId) || hat(['active']) || hat(['setup']) || hat(['disabled']) || s.projects[0]
|
|
auswahl.projectId = s.projekt.projectId
|
|
s.meldungEl = h('div', { class: 'sh-meldung', role: 'status', 'aria-live': 'polite', tabindex: '-1' })
|
|
s.buehne = h('div', { id: 'buchungen-buehne' })
|
|
root.replaceChildren(...(s.projects.length > 1 ? [projektwahl(s)] : []), s.meldungEl, s.buehne)
|
|
await zeigeProjekt(s)
|
|
}
|
|
|
|
function projektwahl(s) {
|
|
const select = h('select', { id: 'buchungen-project' }, s.projects.map((p) => {
|
|
const option = h('option', { value: p.projectId }, p.projectName)
|
|
option.selected = p.projectId === s.projekt.projectId
|
|
return option
|
|
}))
|
|
select.addEventListener('change', () => {
|
|
auswahl.projectId = select.value
|
|
s.projekt = s.projects.find((p) => p.projectId === select.value)
|
|
s.neueAnfrage = false
|
|
zeigeMeldung(s, '')
|
|
zeigeProjekt(s)
|
|
})
|
|
return h('div', { class: 'fn-project kb-panel' }, h('label', { for: 'buchungen-project' }, 'Website'), select)
|
|
}
|
|
|
|
function zeigeMeldung(s, meldung, art = 'ok') {
|
|
const el = s.meldungEl
|
|
if (!el) return
|
|
el.textContent = meldung || ''
|
|
el.className = `sh-meldung sh-meldung-${art}`
|
|
el.setAttribute('role', art === 'fehler' ? 'alert' : 'status')
|
|
el.setAttribute('aria-live', art === 'fehler' ? 'assertive' : 'polite')
|
|
if (!meldung) return
|
|
if (art === 'fehler') el.focus({ preventScroll: true })
|
|
el.scrollIntoView?.({ block: 'nearest' })
|
|
}
|
|
|
|
const ladeStatus = (projectId) => request(`/api/unterkunft/status?projectId=${encodeURIComponent(projectId)}`)
|
|
|
|
async function zeigeProjekt(s) {
|
|
const lauf = ++s.lauf
|
|
const aktuell = () => S === s && s.lauf === lauf
|
|
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
|
let status
|
|
try {
|
|
status = await ladeStatus(s.projekt.projectId)
|
|
} catch (err) {
|
|
if (aktuell()) s.buehne.replaceChildren(fehlerKarte(err.message, () => zeigeProjekt(s)))
|
|
return
|
|
}
|
|
if (!aktuell()) return
|
|
zeichneAnsicht(s, { status, aktuell })
|
|
}
|
|
|
|
function zeichneAnsicht(s, { status, aktuell }) {
|
|
s.status = status
|
|
const zustand = status?.state || zustandVon(s.projekt)
|
|
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
|
|
else if (zustand === 'ordered') zeigeBestellung(s)
|
|
else if (zustand === 'off') zeigeAnfrage(s, { status, aktuell })
|
|
else s.buehne.replaceChildren(infoKarte('Buchungskalender', 'Der Stand deines Buchungskalenders ist gerade nicht lesbar. Bitte lade die Seite neu.'))
|
|
}
|
|
|
|
/* Zustand off: Anfrageformular, Karte „wird geprueft“ / „abgelehnt“ oder (angenommen) der Hinweis auf die Bestellung. */
|
|
function zeigeAnfrage(s, { status, aktuell }) {
|
|
const projectId = s.projekt.projectId
|
|
const ansicht = status?.anfrage
|
|
// nach einem 409 bleibt die Meldung des Servers sichtbar, waehrend die Ansicht neu laedt
|
|
const neuLaden = async (o) => { await zeigeProjekt(s); if (o?.meldung && S === s) zeigeMeldung(s, o.meldung, 'fehler') }
|
|
if (ansicht?.status === 'angenommen') {
|
|
s.buehne.replaceChildren(infoKarte('Deine Anfrage wurde angenommen', 'Wir melden uns bei dir mit den nächsten Schritten zur Bestellung.', { chat: true }))
|
|
} else if (ansicht?.status === 'offen') {
|
|
s.buehne.replaceChildren(anfrageKarte(ansicht, { projectId, neuLaden }))
|
|
} else if (ansicht?.status === 'abgelehnt' && !s.neueAnfrage) {
|
|
s.buehne.replaceChildren(anfrageKarte(ansicht, { projectId, neu: () => { s.neueAnfrage = true; zeigeAnfrage(s, { status, aktuell }) } }))
|
|
} else {
|
|
const wurzel = h('div', { class: 'sh-wurzel' })
|
|
s.buehne.replaceChildren(wurzel)
|
|
renderAnfrageFormular(wurzel, {
|
|
projectId,
|
|
vorgabe: ansicht?.anfrage || null,
|
|
preis: textOr(s.projekt?.features?.unterkunft?.priceLabel) || '199 €',
|
|
nachSenden: async (_a, o) => { s.neueAnfrage = false; if (aktuell()) await neuLaden(o) },
|
|
})
|
|
}
|
|
}
|
|
|
|
/* Zustand ordered: Statuskarte „Zahlung offen“ mit „Zahlung fortsetzen“ (generische Bestellseite, Checkout /api/funktionen/unterkunft/checkout). */
|
|
function zeigeBestellung(s) {
|
|
const eintrag = { ...s.projekt.features?.unterkunft, state: 'ordered' }
|
|
s.buehne.replaceChildren(statusKarte({ projekt: s.projekt, bestellung: eintrag, kundenansicht: false, key: 'unterkunft', name: 'Buchungskalender' }, {
|
|
weiter: async ({ meldung }) => { await zeigeProjekt(s); if (S === s) zeigeMeldung(s, meldung, 'fehler') },
|
|
}))
|
|
}
|
|
|
|
function zeigeReiter(s, { status, aktuell, zustand }) {
|
|
const neuLaden = async () => {
|
|
const neu = await ladeStatus(s.projekt.projectId)
|
|
if (!aktuell()) return neu
|
|
s.status = neu
|
|
ctx.status = neu
|
|
if ((neu.state || 'off') !== zustand) zeichneAnsicht(s, { status: neu, aktuell })
|
|
return neu
|
|
}
|
|
const ctx = {
|
|
projectId: s.projekt.projectId,
|
|
status,
|
|
nurLesen: zustand === 'disabled',
|
|
abwickeln: true, // Anfragen und Stornos bleiben auch bei abgeschalteter Funktion bearbeitbar (Pflichten gegenueber Gaesten)
|
|
aktuell,
|
|
request,
|
|
zeigeMeldung: (meldung, art) => { if (aktuell()) zeigeMeldung(s, meldung, art) },
|
|
neuLaden,
|
|
}
|
|
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'bk-panel', tabindex: '0' })
|
|
let aktiv = null
|
|
let zaehler = 0
|
|
const knoepfe = REITER.map((r) => h('button', {
|
|
type: 'button', role: 'tab', class: 'sh-reiter', id: `bk-tab-${r.key}`, 'aria-controls': 'bk-panel', 'data-reiter': r.key,
|
|
onclick: () => waehleReiter(r.key),
|
|
}, r.label))
|
|
const leiste = h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
|
|
leiste.addEventListener('keydown', (ev) => {
|
|
const i = REITER.findIndex((r) => r.key === aktiv)
|
|
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
|
|
if (ziel === undefined) return
|
|
ev.preventDefault()
|
|
waehleReiter(REITER[ziel].key)
|
|
knoepfe[ziel].focus()
|
|
})
|
|
function waehleReiter(key) {
|
|
const reiter = REITER.find((r) => r.key === key) || REITER[0]
|
|
auswahl.reiter = reiter.key
|
|
aktiv = reiter.key
|
|
const nr = ++zaehler
|
|
for (const k of knoepfe) {
|
|
const an = k.dataset.reiter === reiter.key
|
|
k.setAttribute('aria-selected', String(an))
|
|
k.tabIndex = an ? 0 : -1
|
|
}
|
|
panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
|
|
zeigeMeldung(s, '')
|
|
panel.replaceChildren()
|
|
reiter.laden()
|
|
.then((modul) => (nr === zaehler && aktuell() ? modul.render(panel, ctx) : undefined))
|
|
.catch((err) => { if (nr === zaehler && aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
|
|
}
|
|
const hinweis = zustand === 'setup' ? 'Wir bauen deinen Buchungskalender gerade ein. Du kannst schon Unterkünfte, Preise und Einstellungen vorbereiten.'
|
|
: zustand === 'disabled' ? 'Dein Buchungskalender ist abgeschaltet. Du kannst bestehende Buchungen noch ansehen und bearbeiten.' : ''
|
|
const hinweisBox = h('div', { class: 'sh-hinweise' }, hinweis ? h('div', { class: `sh-box ${zustand === 'disabled' ? 'sh-box-warn' : 'sh-box-info'}`, role: 'note', 'data-hinweis': zustand }, hinweis) : null)
|
|
s.buehne.replaceChildren(hinweisBox, leiste, panel)
|
|
waehleReiter(auswahl.reiter)
|
|
}
|