137 lines
7.7 KiB
JavaScript
137 lines
7.7 KiB
JavaScript
/* Buchungskalender im Kundenbereich: die Anfrage der Funktion (Zustand off), Muster shop-anfrage.js.
|
|
POST /api/unterkunft/anfrage-funktion?projectId= { art, einheiten, portale, nachricht } => { anfrage } (portale: Liste aus 'booking' | 'airbnb'),
|
|
POST /api/unterkunft/anfrage-funktion/zurueckziehen?projectId=. Status im Stand (GET /status): anfrage:{ status:'offen'|'abgelehnt'|'angenommen',
|
|
at, begruendung?, anfrage:{ art, einheiten, portale, nachricht } }. Geprueft wird auf dem Server (Feldfehler am Feld). Text nur ueber textContent (h). */
|
|
|
|
import { allgemeinerFehler, datumText, feld, h, hakenFeld, request, setzeFehler, zeigeFehler } from './shop-dom.js'
|
|
import { fehlerFuerFormular } from './buchungen-fehler.js'
|
|
|
|
export const ARTEN = [['ferienwohnung', 'Ferienwohnung oder Ferienhaus'], ['monteur', 'Monteurwohnung'], ['pension', 'Pension oder Gästezimmer'], ['sonstiges', 'Sonstiges']]
|
|
export const PORTALE = [['booking', 'Booking.com'], ['airbnb', 'Airbnb']]
|
|
export const KALENDER_PREIS = '199 €'
|
|
|
|
const text = (wert) => (typeof wert === 'string' ? wert : '')
|
|
const labelVon = (liste, key) => liste.find(([k]) => k === key)?.[1] || ''
|
|
const enc = encodeURIComponent
|
|
|
|
/** Angaben der Anfrage als Definitionsliste (nur Text). */
|
|
export function anfrageListe(a) {
|
|
const s = a && typeof a === 'object' ? a : {}
|
|
const portale = (Array.isArray(s.portale) ? s.portale : []).map((p) => labelVon(PORTALE, p)).filter(Boolean).join(', ')
|
|
const zeilen = [
|
|
['Art der Unterkunft', labelVon(ARTEN, s.art)],
|
|
['Anzahl Einheiten', Number.isInteger(s.einheiten) ? String(s.einheiten) : ''],
|
|
['Portale', portale || 'keine'],
|
|
['Nachricht', text(s.nachricht)],
|
|
].filter(([, wert]) => wert !== '')
|
|
return h('dl', { class: 'sh-fakten' }, zeilen.map(([name, wert]) => h('div', null, h('dt', null, name), h('dd', null, wert))))
|
|
}
|
|
|
|
/** Formular „Buchungskalender anfragen“ in root; nachSenden(anfrage) wird nach dem Speichern aufgerufen (die Ansicht zeichnet sich neu). */
|
|
export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis = KALENDER_PREIS, nachSenden } = {}) {
|
|
const v = vorgabe && typeof vorgabe === 'object' ? vorgabe : {}
|
|
const art = h('select', null, [h('option', { value: '' }, 'Bitte wählen …'), ...ARTEN.map(([w, n]) => h('option', { value: w }, n))])
|
|
art.value = ARTEN.some(([k]) => k === v.art) ? v.art : ''
|
|
const einheiten = h('input', { type: 'text', inputmode: 'numeric', maxlength: 3, autocomplete: 'off' })
|
|
einheiten.value = Number.isInteger(v.einheiten) ? String(v.einheiten) : ''
|
|
const portale = PORTALE.map(([wert]) => {
|
|
const box = h('input', { type: 'checkbox', value: wert })
|
|
box.checked = Array.isArray(v.portale) && v.portale.includes(wert)
|
|
return box
|
|
})
|
|
const nachricht = h('textarea', { rows: 3, maxlength: 1000 })
|
|
nachricht.value = text(v.nachricht)
|
|
const absenden = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'anfrage-senden' }, 'Anfrage senden')
|
|
|
|
const form = h('form', { class: 'sh-form', novalidate: true, 'aria-label': 'Buchungskalender anfragen' },
|
|
feld('art', 'Welche Art von Unterkunft vermietest du?', art),
|
|
feld('einheiten', 'Wie viele Einheiten (Wohnungen, Zimmer) vermietest du?', einheiten),
|
|
h('fieldset', { class: 'sh-gruppe' },
|
|
h('legend', null, 'Nutzt du Booking.com oder Airbnb?'),
|
|
...PORTALE.map(([wert, name], i) => hakenFeld(`portale.${wert}`, portale[i], name)),
|
|
h('p', { class: 'sh-fehler', 'data-fehler': 'portale', role: 'alert' })),
|
|
feld('nachricht', 'Noch etwas, das wir wissen sollten? (freiwillig)', nachricht),
|
|
allgemeinerFehler(),
|
|
h('div', { class: 'sh-aktionen' }, absenden))
|
|
|
|
let laeuft = false
|
|
form.addEventListener('submit', async (ev) => {
|
|
ev.preventDefault()
|
|
if (laeuft) return
|
|
laeuft = true
|
|
absenden.disabled = true
|
|
setzeFehler(form, {})
|
|
const zahl = einheiten.value.trim()
|
|
try {
|
|
const antwort = await request(`/api/unterkunft/anfrage-funktion?projectId=${enc(projectId)}`, {
|
|
method: 'POST',
|
|
body: {
|
|
art: art.value,
|
|
einheiten: /^\d{1,3}$/.test(zahl) ? Number(zahl) : zahl,
|
|
portale: portale.filter((b) => b.checked).map((b) => b.value),
|
|
nachricht: nachricht.value.trim(),
|
|
},
|
|
})
|
|
if (typeof nachSenden === 'function') await nachSenden(antwort?.anfrage ?? null)
|
|
return
|
|
} catch (err) {
|
|
zeigeFehler(form, fehlerFuerFormular(err))
|
|
if (err?.status === 409 && typeof nachSenden === 'function') {
|
|
try { await nachSenden(null) } catch { /* der Fehler steht schon am Formular */ }
|
|
return
|
|
}
|
|
}
|
|
laeuft = false
|
|
absenden.disabled = false
|
|
})
|
|
|
|
const erklaerung = h('div', { class: 'sh-erklaerung' },
|
|
h('h3', null, 'Buchungskalender anfragen'),
|
|
h('p', null, 'Mit dem Buchungskalender zeigst du auf deiner Website, wann deine Unterkunft frei ist. Gäste fragen direkt an, du gibst frei, und die Rechnung entsteht automatisch.'),
|
|
h('ul', null,
|
|
h('li', null, `${preis} einmalig für den Einbau in deine Website.`),
|
|
h('li', null, 'Belegungskalender mit Abgleich zu Booking.com und Airbnb (über iCal).'),
|
|
h('li', null, 'Rechnung in deinem Namen und optional Online-Zahlung auf dein eigenes Stripe-Konto.'),
|
|
h('li', null, 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du bestellen.')))
|
|
const karte = h('section', { class: 'kb-panel sh-anfrage' }, erklaerung, form)
|
|
root.replaceChildren(karte)
|
|
return karte
|
|
}
|
|
|
|
/** Karte einer Anfrage im Stand offen (mit „Anfrage zurückziehen“) oder abgelehnt (Begründung, „Neue Anfrage stellen“). */
|
|
export function anfrageKarte(ansicht, { projectId, neu, neuLaden } = {}) {
|
|
const offen = ansicht?.status === 'offen'
|
|
const meldung = h('p', { class: 'sh-fehler', role: 'alert', 'data-meldung': 'anfrage' })
|
|
const datum = datumText(ansicht?.at)
|
|
let aktion
|
|
if (offen) {
|
|
aktion = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueckziehen' }, 'Anfrage zurückziehen')
|
|
aktion.addEventListener('click', async () => {
|
|
if (aktion.disabled) return
|
|
aktion.disabled = true
|
|
meldung.textContent = ''
|
|
try {
|
|
await request(`/api/unterkunft/anfrage-funktion/zurueckziehen?projectId=${enc(projectId)}`, { method: 'POST' })
|
|
if (typeof neuLaden === 'function') await neuLaden()
|
|
return
|
|
} catch (err) {
|
|
meldung.textContent = err.message
|
|
if (err?.status === 409 && typeof neuLaden === 'function') { try { await neuLaden() } catch { /* Meldung bleibt */ } return }
|
|
}
|
|
aktion.disabled = false
|
|
})
|
|
} else {
|
|
aktion = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'neue-anfrage', onclick: () => { if (typeof neu === 'function') neu() } }, 'Neue Anfrage stellen')
|
|
}
|
|
return h('section', { class: 'kb-panel sh-info sh-anfrage-karte', 'data-anfrage': offen ? 'offen' : 'abgelehnt' },
|
|
h('span', { class: `sh-pille ${offen ? 'sh-pille-wait' : 'sh-pille-bad'}` }, offen ? 'In Prüfung' : 'Abgelehnt'),
|
|
h('h3', null, offen ? 'Deine Anfrage wird geprüft' : 'Deine Anfrage wurde leider abgelehnt'),
|
|
offen
|
|
? h('p', null, `${datum ? `Du hast deine Anfrage am ${datum} Uhr gesendet. ` : ''}Wir melden uns, sobald wir sie angesehen haben. Bis dahin kannst du sie zurückziehen und eine neue stellen.`)
|
|
: h('p', null, 'Unsere Begründung:'),
|
|
!offen ? h('blockquote', { class: 'sh-begruendung' }, text(ansicht?.begruendung) || 'Dazu liegt keine Begründung vor.') : null,
|
|
ansicht?.anfrage ? h('div', { class: 'sh-zusammenfassung' }, h('h4', null, 'Deine Angaben'), anfrageListe(ansicht.anfrage)) : null,
|
|
meldung,
|
|
h('div', { class: 'sh-aktionen' }, aktion, h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Frage dazu? Chat öffnen')))
|
|
}
|