313 lines
18 KiB
JavaScript
313 lines
18 KiB
JavaScript
/* 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()
|
||
})()
|