/* 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'], ['monteurzimmer', '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 : '') /** Der Server liest 'monteur' als Alias, gibt es aber in der Anfrage so zurueck; die Auswahl kennt nur 'monteurzimmer'. */ const artWert = (a) => (a === 'monteur' ? 'monteurzimmer' : a) 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, artWert(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 === artWert(v.art)) ? artWert(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, { meldung: fehlerFuerFormular(err).message }) } 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({ meldung: err.message }) } 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'))) }