Files
Webklar-Kundenbereich/public/unterkunft-buchung.js
T

313 lines
18 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.
/* Buchungskalender, Standalone-Seiten fuer Gaeste (Task 17).
* Ansicht "widget": setzt data-webklar-unterkunft auf den Container, public/embed/unterkunft.js (danach geladen) baut das Widget.
* Ansicht "buchung": /unterkunft/<siteKey>/buchung/<id>?t=<token> - Buchung pruefen und "Zahlungspflichtig buchen".
* Der Token (?t=) wird nur an die Schnittstelle gesendet, nie geloggt oder anderswohin geschickt. Alle Texte per textContent. */
(function () {
'use strict'
var API = '/api/public/unterkunft/v1/'
var app = document.getElementById('uk-app')
if (!app) return
var siteKey = app.getAttribute('data-sitekey') || ''
if (app.getAttribute('data-ansicht') !== 'buchung') {
app.textContent = ''
app.setAttribute('data-webklar-unterkunft', siteKey)
return
}
var teile = String(window.location.pathname || '').split('/')
var id = decodeURIComponent(teile[teile.length - 1] || '')
var params = new URLSearchParams(window.location.search || '')
var token = params.get('t') || ''
var zahlungParam = params.get('wk_zahlung') || ''
var basis = API + encodeURIComponent(siteKey) + '/buchung/' + encodeURIComponent(id)
var tokenQuery = '?t=' + encodeURIComponent(token)
var TXT = {
ungueltig: 'Dieser Link ist ungültig.',
abgelaufen: 'Die Reservierung ist abgelaufen. Bitte fragen Sie erneut an.',
abbruch: 'Zahlung abgebrochen – Sie können jetzt oder vor Ort bezahlen.',
widerruf: 'Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).',
kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
laden: 'Die Buchung konnte nicht geladen werden. Bitte versuchen Sie es später erneut.',
senden: 'Die Buchung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.',
belegt: 'Der Zeitraum ist leider inzwischen belegt. Bitte fragen Sie einen anderen Zeitraum an.',
knopf: 'Zahlungspflichtig buchen',
onlineAus: 'Online-Zahlung ist gerade nicht möglich – bitte wählen Sie „Vor Ort bezahlen“.',
nichtMoeglich: 'Die Buchung ist gerade nicht möglich. Bitte wenden Sie sich direkt an den Anbieter.',
standGeaendert: 'Der Stand Ihrer Buchung hat sich geändert.',
zahlungStart: 'Die Zahlung konnte nicht gestartet werden. Bitte versuchen Sie es erneut.',
}
function h(tag, attrs, kinder) {
var el = document.createElement(tag)
var a = attrs || {}
Object.keys(a).forEach(function (k) {
if (a[k] === null || a[k] === undefined || a[k] === false) return
if (k === 'text') el.textContent = a[k]
else if (k === 'class') el.className = a[k]
else el.setAttribute(k, a[k] === true ? '' : String(a[k]))
})
;(kinder || []).forEach(function (c) { if (c) el.appendChild(c) })
return el
}
var p = function (text, klasse) { return h('p', { text: text, class: klasse }) }
function euro(cent) {
return (cent / 100).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })
}
function datum(iso) {
var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''))
return m ? m[3] + '.' + m[2] + '.' + m[1] : String(iso || '')
}
function zeitpunkt(iso) {
var d = new Date(iso)
if (isNaN(d.getTime())) return String(iso || '')
return d.toLocaleString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).replace(',', '') + ' Uhr'
}
function api(pfad, methode, body) {
var opt = { method: methode || 'GET' }
if (body) { opt.headers = { 'Content-Type': 'application/json' }; opt.body = JSON.stringify(body) }
return fetch(basis + pfad + tokenQuery, opt).then(function (r) {
return r.json().then(function (b) { return { status: r.status, ok: r.ok, body: b || {} } }, function () { return { status: r.status, ok: r.ok, body: {} } })
})
}
function httpsUrl(u) { return typeof u === 'string' && /^https:\/\//i.test(u) ? u : '' }
var meldungNachLaden = ''
function zeige(karte, hinweis) {
app.textContent = ''
var text = [hinweis, meldungNachLaden].filter(Boolean).join(' ')
meldungNachLaden = ''
if (text) app.appendChild(h('p', { text: text, class: 'uk-hinweis', role: 'status' }))
app.appendChild(karte)
}
function nurText(titel, text) {
zeige(h('section', { class: 'uk-karte' }, [h('h1', { text: titel }), p(text)]))
}
function akzent(daten) {
var f = daten && daten.akzentfarbe
if (typeof f !== 'string' || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(f)) return
var s = f.slice(1)
if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
var hell = (0.299 * parseInt(s.slice(0, 2), 16) + 0.587 * parseInt(s.slice(2, 4), 16) + 0.114 * parseInt(s.slice(4, 6), 16)) > 160
app.setAttribute('style', '--uk-akzent:' + f + ';--uk-akzent-text:' + (hell ? '#111827' : '#ffffff'))
}
var hinweisAbbruch = function () { return zahlungParam === 'abbruch' ? TXT.abbruch : '' }
function anschrift(a) {
var zeile = [a.strasse, [a.plz, a.ort].filter(Boolean).join(' ')].filter(Boolean).join(', ')
return [a.firma, a.inhaber].filter(Boolean).join(', ') + (zeile ? ', ' + zeile : '')
}
function zahlartenText(d, wahl) {
var z = (d.zahlarten || []).map(function (x) { return x === 'online' ? 'online (Karte, über Stripe)' : 'vor Ort' })
return 'Zahlarten: ' + z.join(' oder ') + (wahl ? ' (gewählt: ' + (wahl === 'online' ? 'online' : 'vor Ort') + ')' : '')
}
/* ------------------------------------------------------------- Pflichtangaben --- */
function pflichtangaben(d, wahl) {
var s = d.summen || {}
var kopf = h('h2', { text: 'Ihre Buchung im Überblick' })
var a = d.anbieter || {}
var kontakt = [a.email, a.telefon].filter(Boolean).join(', ')
var u = d.unterkunft || {}
var posten = h('ul', { 'data-tag': 'posten' }, (d.posten || []).map(function (x) {
var bez = x.menge !== 1 ? x.text + ' (' + x.menge + ' × ' + euro(x.einzelCent) + ')' : x.text
return h('li', {}, [h('span', { text: bez }), h('span', { text: euro(x.menge * x.einzelCent) })])
}))
var gesamt = 'Gesamtpreis: ' + euro(s.bruttoCent || 0)
var ust = []
if (d.steuerart === 'kleinunternehmer') ust.push(TXT.kleinunternehmer)
else (s.ust || []).filter(function (z) { return z.satz > 0 }).forEach(function (z) { ust.push(z.satz + ' % USt ' + euro(z.steuerCent)) })
var preisText = d.steuerart === 'kleinunternehmer' ? gesamt + '. ' + TXT.kleinunternehmer : gesamt + (ust.length ? ' inkl. ' + ust.join(' und ') : ' inkl. USt')
var ds = a.datenschutzUrl && /^https:\/\//.test(a.datenschutzUrl) ? a.datenschutzUrl : ''
var datenschutz = [p('Datenschutz: ' + (a.firma || 'Der Anbieter') + ' verarbeitet Ihre Angaben zur Durchführung der Buchung und für die Rechnung (Art. 6 Abs. 1 lit. b und c DSGVO). Näheres steht in der Datenschutzerklärung des Anbieters.')]
if (ds) datenschutz.push(h('a', { class: 'uk-link', href: ds, target: '_blank', rel: 'noopener noreferrer', text: 'Datenschutzerklärung des Anbieters' }))
return h('section', { class: 'uk-pflicht', 'data-tag': 'pflichtangaben' }, [
kopf,
p('Anbieter: ' + anschrift(a) + (kontakt ? ' · ' + kontakt : ''), 'uk-anbieter'),
p('Unterkunft: ' + (u.name || '') + ', ' + datum(d.von) + ' bis ' + datum(d.bis) + ' (' + d.naechte + ' ' + (d.naechte === 1 ? 'Nacht' : 'Nächte') + '), Check-in ab ' + (u.checkIn || '') + ' Uhr, Check-out bis ' + (u.checkOut || '') + ' Uhr', 'uk-unterkunft'),
p('Personen: ' + d.personen, 'uk-personen'),
posten,
p(preisText, 'uk-gesamt'),
p(zahlartenText(d, wahl), 'uk-zahlarten'),
p('Stornobedingungen: ' + (d.stornobedingungen || ''), 'uk-storno'),
p(TXT.widerruf, 'uk-widerruf'),
h('div', { class: 'uk-datenschutz' }, datenschutz),
p('Reserviert bis ' + zeitpunkt(d.fristAt), 'uk-frist'),
])
}
/* ------------------------------------------------------------------- Formular --- */
function formular(d) {
var ra = d.rechnungsadresse || {}
var g = d.gast || {}
var felder = {}
function feld(name, bez, wert, extra) {
var input = h('input', { type: 'text', name: name, autocomplete: (extra && extra.ac) || 'off', maxlength: 120 })
input.value = wert || ''
felder[name] = input
var fehler = h('span', { class: 'uk-feldfehler', 'data-fehler': name, id: 'uk-fehler-' + name, hidden: true })
return { wurzel: h('label', { class: 'uk-feld' }, [h('span', { class: 'uk-bez', text: bez }), input, fehler]), fehler: fehler }
}
var fName = feld('name', 'Name', ra.name || g.name, { ac: 'name' })
var fFirma = feld('firma', 'Firma (optional)', ra.firma || g.firma, { ac: 'organization' })
var fStrasse = feld('strasse', 'Straße und Hausnummer', ra.strasse, { ac: 'street-address' })
var fPlz = feld('plz', 'PLZ', ra.plz, { ac: 'postal-code' })
var fOrt = feld('ort', 'Ort', ra.ort, { ac: 'address-level2' })
var fLand = feld('land', 'Land', ra.land || 'Deutschland', { ac: 'country-name' })
var alle = { name: fName, firma: fFirma, strasse: fStrasse, plz: fPlz, ort: fOrt, land: fLand }
var arten = (d.zahlarten || []).filter(function (z) { return z === 'vorort' || z === 'online' })
var gewaehlt = arten.indexOf(d.zahlart) >= 0 ? d.zahlart : arten[0]
var radios = arten.map(function (z) {
var r = h('input', { type: 'radio', name: 'zahlart', value: z, id: 'uk-zahlart-' + z })
if (z === gewaehlt) r.checked = true
return h('label', { class: 'uk-zahlart' }, [r, h('span', { text: z === 'online' ? 'Online bezahlen (Karte)' : 'Vor Ort bezahlen' })])
})
var meldung = h('div', { class: 'uk-fehler', role: 'alert', hidden: true })
var knopf = h('button', { type: 'submit', class: 'uk-knopf', text: TXT.knopf })
var form = h('form', { novalidate: true }, [
h('h2', { text: 'Rechnungsadresse' }),
fName.wurzel, fFirma.wurzel, fStrasse.wurzel,
h('div', { class: 'uk-zeile2' }, [fPlz.wurzel, fOrt.wurzel]),
fLand.wurzel,
h('fieldset', { class: 'uk-zahlart-gruppe', 'data-tag': 'zahlart' }, [h('legend', { text: 'Zahlart' })].concat(radios)),
pflichtangaben(d, gewaehlt),
knopf,
meldung,
])
radios.forEach(function (l) {
l.querySelector('input').addEventListener('change', function () {
var z = form.querySelector('.uk-zahlarten')
if (z) z.textContent = zahlartenText(d, waehl())
})
})
function waehl() { var r = radios.map(function (l) { return l.querySelector('input') }).find(function (i) { return i.checked }); return r ? r.value : gewaehlt }
var sendet = false
function fehlerAnzeigen(felderFehler, text) {
Object.keys(alle).forEach(function (k) { markiere(k, '') })
var erstes = null
Object.keys(alle).forEach(function (k) {
if (felderFehler && Object.prototype.hasOwnProperty.call(felderFehler, k)) { markiere(k, String(felderFehler[k])); if (!erstes) erstes = felder[k] }
})
meldung.textContent = text || ''
meldung.hidden = !text
if (erstes && erstes.focus) erstes.focus()
}
function markiere(k, text) {
alle[k].fehler.textContent = text
alle[k].fehler.hidden = !text
if (text) { felder[k].setAttribute('aria-invalid', 'true'); felder[k].setAttribute('aria-describedby', 'uk-fehler-' + k) }
else { felder[k].removeAttribute('aria-invalid'); felder[k].removeAttribute('aria-describedby') }
}
form.addEventListener('submit', function (ev) {
ev.preventDefault()
if (sendet) return
var val = {}
Object.keys(alle).forEach(function (k) { val[k] = String(felder[k].value || '').trim() })
var f = {}
if (!val.name && !val.firma) f.name = 'Bitte geben Sie einen Namen oder eine Firma an.'
if (!val.strasse) f.strasse = 'Bitte geben Sie die Straße an.'
if (!val.plz) f.plz = 'Bitte geben Sie die PLZ an.'
if (!val.ort) f.ort = 'Bitte geben Sie den Ort an.'
if (!val.land) f.land = 'Bitte geben Sie das Land an.'
if (!gewaehlt) return fehlerAnzeigen({}, 'Es ist keine Zahlart verfügbar. Bitte wenden Sie sich an den Anbieter.')
if (Object.keys(f).length) return fehlerAnzeigen(f, 'Bitte prüfen Sie Ihre Angaben.')
fehlerAnzeigen({}, '')
sendet = true
knopf.setAttribute('disabled', '')
var zahlart = waehl()
api('', 'POST', { rechnungsadresse: val, zahlart: zahlart }).then(function (r) {
if (r.ok && r.body.status === 'gebucht') {
if (zahlart === 'online' && httpsUrl(r.body.zahlungUrl)) { window.location.assign(r.body.zahlungUrl); return }
bestaetigt(Object.assign({}, d, { status: 'gebucht', zahlart: zahlart, bezahlt: false, rechnungNummer: r.body.rechnungNummer }))
return
}
if (r.status === 404) return nurText('Buchung', TXT.ungueltig)
if (r.status === 410 || r.body.fehler === 'frist_abgelaufen' || r.body.fehler === 'abgelaufen') return nurText('Buchung', TXT.abgelaufen)
if (r.body.fehler === 'zeitraum_belegt') return nurText('Buchung', TXT.belegt)
var code = r.body.fehler
if (r.status === 409 && (code === 'online_aus' || code === 'einstellungen_unvollstaendig' || code === 'zustimmung_fehlt')) {
sendet = false
knopf.removeAttribute('disabled')
if (code === 'online_aus') {
var vor = radios.map(function (l) { return l.querySelector('input') }).find(function (i) { return i.value === 'vorort' })
if (vor) vor.checked = true
}
return fehlerAnzeigen({}, code === 'online_aus' ? TXT.onlineAus : TXT.nichtMoeglich)
}
if (r.status === 409 || r.ok) { meldungNachLaden = TXT.standGeaendert; laden(); return }
sendet = false
knopf.removeAttribute('disabled')
fehlerAnzeigen(r.body.felder, r.status === 400 && r.body.felder ? 'Bitte prüfen Sie Ihre Angaben.' : TXT.senden)
}, function () {
sendet = false
knopf.removeAttribute('disabled')
fehlerAnzeigen({}, TXT.senden)
})
})
// Fehler verschwinden, sobald der Gast das Feld aendert; die allgemeine Meldung, wenn kein Feldfehler mehr steht
Object.keys(alle).forEach(function (k) {
felder[k].addEventListener('input', function () {
markiere(k, '')
if (!Object.keys(alle).some(function (n) { return !alle[n].fehler.hidden })) { meldung.textContent = ''; meldung.hidden = true }
})
})
return h('section', { class: 'uk-karte' }, [h('h1', { text: 'Ihre Buchung bestätigen' }), p('Hallo ' + (g.name || '') + ', Ihre Anfrage wurde freigegeben. Bitte prüfen Sie die Angaben und buchen Sie verbindlich.'), form])
}
/* --------------------------------------------------------------- Bestaetigung --- */
function bestaetigt(d) {
var kinder = [h('h1', { text: 'Ihre Buchung ist bestätigt' })]
kinder.push(p((d.unterkunft && d.unterkunft.name ? d.unterkunft.name + ', ' : '') + datum(d.von) + ' bis ' + datum(d.bis) + '. Buchungsnummer: ' + (d.nummer || '') + '.'))
if (d.rechnungNummer) kinder.push(p('Rechnung ' + d.rechnungNummer))
kinder.push(h('a', { class: 'uk-link', 'data-tag': 'rechnung', href: basis + '/rechnung.pdf' + tokenQuery + '&art=rechnung', text: 'Rechnung herunterladen (PDF)' }))
var online = (d.zahlarten || []).indexOf('online') >= 0
if (d.bezahlt || d.status === 'bezahlt') kinder.push(p('Bezahlt – vielen Dank.'))
else if (d.zahlart === 'online' && online) {
var meldung = h('div', { class: 'uk-fehler', role: 'alert', hidden: true })
var knopf = h('button', { type: 'button', class: 'uk-knopf', 'data-tag': 'online-zahlen', text: 'Jetzt online bezahlen' })
knopf.addEventListener('click', function () {
knopf.setAttribute('disabled', '')
api('/zahlung', 'POST', {}).then(function (r) {
if (r.ok && httpsUrl(r.body.url)) { window.location.assign(r.body.url); return }
if (r.status === 409 && r.body.fehler === 'status_ungueltig') { meldungNachLaden = TXT.standGeaendert; laden(); return }
knopf.removeAttribute('disabled')
meldung.textContent = r.status === 409 && r.body.fehler === 'online_aus' ? TXT.onlineAus : r.status === 409 && (r.body.fehler === 'einstellungen_unvollstaendig' || r.body.fehler === 'zustimmung_fehlt') ? TXT.nichtMoeglich : TXT.zahlungStart
meldung.hidden = false
}, function () {
knopf.removeAttribute('disabled')
meldung.textContent = 'Die Zahlung konnte nicht gestartet werden. Bitte versuchen Sie es erneut.'; meldung.hidden = false
})
})
kinder.push(knopf, meldung)
} else kinder.push(p('Die Zahlung erfolgt vor Ort.'))
kinder.push(p('Sie erhalten die Bestätigung auch per E-Mail.'))
zeige(h('section', { class: 'uk-karte' }, kinder), hinweisAbbruch())
}
/* ----------------------------------------------------------------------- Start --- */
function darstellen(d) {
akzent(d)
if (d.unterkunft && d.unterkunft.name) document.title = 'Buchung – ' + d.unterkunft.name
if (d.status === 'freigegeben') zeige(formular(d), hinweisAbbruch())
else if (d.status === 'gebucht' || d.status === 'bezahlt') bestaetigt(d)
else if (d.status === 'abgelaufen') nurText('Buchung', TXT.abgelaufen)
else if (d.status === 'abgelehnt') nurText('Buchung', 'Diese Anfrage wurde leider abgelehnt.')
else if (d.status === 'storniert') nurText('Buchung', 'Diese Buchung wurde storniert.')
else nurText('Buchung', 'Ihre Anfrage wird noch geprüft. Sie erhalten eine E-Mail, sobald sie freigegeben ist.')
}
if (!token || !id) { nurText('Buchung', TXT.ungueltig); return }
function laden() {
return api('', 'GET').then(function (r) {
if (r.status === 404 || r.status === 401 || r.status === 403) return nurText('Buchung', TXT.ungueltig)
if (!r.ok) return nurText('Buchung', TXT.laden)
darstellen(r.body)
}, function () { nurText('Buchung', TXT.laden) })
}
laden()
})()