Buchungskalender: Kalender und Unterkünfte im Portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ecea05b52
commit
db9fc61894
8 files changed
+814
-8
No files matched your search
@@ -0,0 +1,108 @@
|
||||
/* Buchungskalender: iCal-Bereich einer Unterkunft (Importe von Booking.com/Airbnb, Export-Link, Kurzanleitungen).
|
||||
unterkunft = Eintrag aus GET /api/unterkunft/unterkuenfte: { id, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }.
|
||||
POST /unterkuenfte/:id/ical-importe { name, url } · DELETE …/ical-importe/:importId · POST …/ical-export/erneuern => { icalExportUrl }.
|
||||
Nach jeder Aenderung ruft der Aufrufer aenderung() (laedt die Unterkunft neu). */
|
||||
|
||||
import { abschnitt, allgemeinerFehler, datumText, feld, frage, h, zeigeFehler } from './shop-dom.js'
|
||||
import { enc, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
|
||||
|
||||
export function zeichneIcal(ctx, unterkunft, aenderung) {
|
||||
const basis = `/api/unterkunft/unterkuenfte/${enc(unterkunft.id)}`
|
||||
const q = `projectId=${enc(ctx.projectId)}`
|
||||
const importe = Array.isArray(unterkunft.icalImporte) ? unterkunft.icalImporte : []
|
||||
const dialog = h('div', { class: 'bk-dialog' })
|
||||
|
||||
const status = (i) => {
|
||||
if (i.fehler) return `Der letzte Abgleich hat nicht geklappt: ${zeichen(i.fehler)}${i.letzterErfolgAt ? ` (zuletzt erfolgreich ${datumText(i.letzterErfolgAt)})` : ''}`
|
||||
if (i.letzterErfolgAt) return `Zuletzt abgeglichen ${datumText(i.letzterErfolgAt)}`
|
||||
return 'Noch nicht abgeglichen. Das passiert automatisch alle 15 Minuten.'
|
||||
}
|
||||
const importListe = importe.length
|
||||
? h('ul', { class: 'bk-liste' }, importe.map((i) => h('li', { class: 'bk-import', 'data-import': i.id },
|
||||
h('strong', null, zeichen(i.name)),
|
||||
h('span', { class: i.fehler ? 'sh-fehler' : 'sh-hinweis', 'data-status': true }, status(i)),
|
||||
ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'import-weg', onclick: () => entfernenDialog(i) }, 'Entfernen'))))
|
||||
: h('p', { class: 'sh-hinweis' }, 'Noch kein Kalender angebunden.')
|
||||
|
||||
function entfernenDialog(i) {
|
||||
const box = frage('Kalender entfernen', h('p', null, `Die Belegung aus „${zeichen(i.name)}“ verschwindet dann aus deinem Kalender.`), allgemeinerFehler(),
|
||||
h('div', { class: 'bk-aktionen' },
|
||||
h('button', {
|
||||
type: 'button', class: 'kb-btn', 'data-aktion': 'import-weg-ok',
|
||||
onclick: async (ev) => {
|
||||
ev.currentTarget.disabled = true
|
||||
try {
|
||||
await ctx.request(`${basis}/ical-importe/${enc(i.id)}?${q}`, { method: 'DELETE' })
|
||||
if (ctx.aktuell()) await aenderung('Der Kalender ist entfernt.')
|
||||
} catch (err) { ev.currentTarget.disabled = false; zeigeApiFehler(box, err) }
|
||||
},
|
||||
}, 'Entfernen'),
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => dialog.replaceChildren() }, 'Abbrechen')))
|
||||
dialog.replaceChildren(box)
|
||||
}
|
||||
|
||||
const name = h('input', { type: 'text', id: 'bk-i-name', maxlength: '60', autocomplete: 'off', placeholder: 'Booking.com' })
|
||||
const url = h('input', { type: 'text', id: 'bk-i-url', inputmode: 'url', autocomplete: 'off', placeholder: 'https://…' })
|
||||
const neu = ctx.nurLesen ? null : h('form', { novalidate: true, class: 'bk-importform', onsubmit: (ev) => ev.preventDefault() },
|
||||
feld('name', 'Name', name), feld('url', 'Link (iCal-Adresse)', url, 'Der Link beginnt mit https:// und endet oft auf .ics.'), allgemeinerFehler(),
|
||||
h('button', {
|
||||
type: 'button', class: 'kb-btn', 'data-aktion': 'import-dazu',
|
||||
onclick: async (ev) => {
|
||||
const form = ev.currentTarget.form || ev.currentTarget.closest('form')
|
||||
const body = { name: String(name.value).trim(), url: String(url.value).trim() }
|
||||
if (!body.name || !/^https:\/\//i.test(body.url)) return zeigeFehler(form, { message: '', fehler: { _: 'Bitte gib einen Namen und einen Link an, der mit https:// beginnt.' } })
|
||||
ev.currentTarget.disabled = true
|
||||
try {
|
||||
await ctx.request(`${basis}/ical-importe?${q}`, { method: 'POST', body })
|
||||
if (ctx.aktuell()) await aenderung('Der Kalender ist angebunden.')
|
||||
} catch (err) { ev.currentTarget.disabled = false; zeigeApiFehler(form, err) }
|
||||
},
|
||||
}, 'Kalender hinzufügen'))
|
||||
|
||||
const exportUrl = zeichen(unterkunft.icalExportUrl)
|
||||
const kopiert = h('p', { class: 'sh-hinweis', role: 'status', 'data-kopiert': true })
|
||||
const erneuern = () => {
|
||||
const box = frage('Link erneuern', h('p', null, 'Der alte Link funktioniert danach nicht mehr. Du musst den neuen Link bei Booking.com und Airbnb eintragen.'), allgemeinerFehler(),
|
||||
h('div', { class: 'bk-aktionen' },
|
||||
h('button', {
|
||||
type: 'button', class: 'kb-btn', 'data-aktion': 'erneuern-ok',
|
||||
onclick: async (ev) => {
|
||||
ev.currentTarget.disabled = true
|
||||
try {
|
||||
await ctx.request(`${basis}/ical-export/erneuern?${q}`, { method: 'POST' })
|
||||
if (ctx.aktuell()) await aenderung('Der Link ist erneuert. Trage den neuen Link bei den Portalen ein.')
|
||||
} catch (err) { ev.currentTarget.disabled = false; zeigeApiFehler(box, err) }
|
||||
},
|
||||
}, 'Link erneuern'),
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => dialog.replaceChildren() }, 'Abbrechen')))
|
||||
dialog.replaceChildren(box)
|
||||
}
|
||||
const export_ = h('div', { class: 'bk-link' },
|
||||
h('p', { class: 'sh-hinweis' }, 'Mit diesem Link sehen Booking.com und Airbnb, wann deine Unterkunft belegt ist. Es werden keine Gastdaten weitergegeben.'),
|
||||
exportUrl ? h('input', { type: 'text', readonly: true, value: exportUrl, 'aria-label': 'Export-Link', 'data-export': true }) : h('p', { class: 'sh-hinweis' }, 'Der Link ist noch nicht verfügbar.'),
|
||||
h('div', { class: 'bk-aktionen' },
|
||||
exportUrl ? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline', 'data-aktion': 'export-kopieren',
|
||||
onclick: async () => {
|
||||
try { await navigator.clipboard.writeText(exportUrl); kopiert.textContent = 'Link kopiert.' } catch { kopiert.textContent = 'Kopieren hat nicht geklappt. Markiere den Link und kopiere ihn von Hand.' }
|
||||
},
|
||||
}, 'Kopieren') : null,
|
||||
ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'export-erneuern', onclick: erneuern }, 'Link erneuern')),
|
||||
kopiert)
|
||||
|
||||
const anleitung = h('div', { class: 'bk-anleitung' },
|
||||
h('details', { 'data-anleitung': 'booking' }, h('summary', null, 'Anleitung Booking.com'),
|
||||
h('ol', null,
|
||||
h('li', null, 'Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen.'),
|
||||
h('li', null, 'Den Export-Link von oben dort einfügen und speichern.'),
|
||||
h('li', null, 'Für die andere Richtung: Booking.com zeigt dir dort auch einen Link zum Exportieren. Den trägst du oben bei „Kalender anbinden“ ein.'))),
|
||||
h('details', { 'data-anleitung': 'airbnb' }, h('summary', null, 'Anleitung Airbnb'),
|
||||
h('ol', null,
|
||||
h('li', null, 'Inserat → Verfügbarkeit → Kalender synchronisieren → Kalender verbinden.'),
|
||||
h('li', null, 'Den Export-Link von oben dort einfügen und speichern.'),
|
||||
h('li', null, 'Airbnb zeigt dir unter „Kalender exportieren“ einen Link. Den trägst du oben bei „Kalender anbinden“ ein.'))))
|
||||
|
||||
return abschnitt('Kalender-Abgleich (iCal)', 'So verhinderst du Doppelbuchungen: Belegungen von Booking.com oder Airbnb kommen in deinen Kalender, und deine Belegungen gehen dorthin.',
|
||||
h('h4', null, 'Kalender anbinden'), importListe, neu,
|
||||
h('h4', null, 'Dein Export-Link'), export_, anleitung, dialog)
|
||||
}
|
||||
Reference in new issue
Block a user