Neuer Haken "Preis hängt von der Personenzahl ab" im Preis-Editor (nur bei Höchstzahl ab 2 Personen). Angeschaltet zeigt jede Staffel k Euro-Felder (1 Person bis k Personen, k = min(Höchstzahl, 10)) statt eines Preises; "Personen im Preis inklusive" und "Aufpreis je weiterer Person" sind dann ausgeblendet, ihre Werte gehen unverändert mit. Speichern sendet personenCent und preisProNachtCent = personenCent[0]. Beim Laden ist der Haken an, wenn personenCent gespeichert sind. Kundenansicht (nur lesen) sperrt Haken und Felder. Felder brechen auf dem Handy um. Tests im Fake-DOM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
242 lines
19 KiB
JavaScript
242 lines
19 KiB
JavaScript
/* Buchungskalender: Preis-Editor einer Unterkunft (Staffeln, Preise je Personenzahl, Reinigung, Zusatzpersonen, Rabatt, Steuer, Frist, Stornobedingungen) mit Beispielrechnung.
|
||
GET|PUT /api/unterkunft/unterkuenfte/:id/preise => { preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent,
|
||
direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }
|
||
POST /api/unterkunft/preisvorschau { unterkunftId, von, bis, personen } => { naechte, posten, summen:{ bruttoCent } } (rechnet mit den gespeicherten Preisen).
|
||
Zwei Preismodelle, umgeschaltet mit dem Haken "Preis hängt von der Personenzahl ab" (#bk-p-personenmodus, nur bei unterkunft.maxPersonen >= 2):
|
||
- aus: ein Preis pro Nacht je Staffel; "Personen im Preis inklusive" und "Aufpreis je weiterer Person" gelten wie bisher.
|
||
- an: je Staffel k Euro-Felder (k = min(maxPersonen, 10), "1 Person" ... "k Personen") = Preis pro Nacht fuer die ganze Unterkunft; gesendet wird
|
||
staffeln:[{abNaechten, preisProNachtCent: personenCent[0], personenCent}]. Die beiden Personenfelder sind dann ausgeblendet, ihre Werte gehen unveraendert mit.
|
||
Beim Laden ist der Haken an, wenn die gespeicherten Staffeln personenCent haben (vorhandene Werte eingesetzt, fehlende leer, ueberzaehlige verworfen).
|
||
ctx.nurLesen sperrt alle Felder samt Haken. */
|
||
|
||
import { abschnitt, allgemeinerFehler, euroEingabe, euroText, feld, h, hakenFeld, setzeFehler, zeigeFehler } from './shop-dom.js'
|
||
import { enc, heuteBerlin, plusTage, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
|
||
|
||
// Wortgleich zu docs/funktionen/unterkunft-storno-muster.md, Abschnitt "Stornobedingungen (Muster)" (ohne Markdown-Fettdruck); test/buchungenUi.test.js vergleicht beides.
|
||
export const STORNO_MUSTER = [
|
||
"1. Stornierung durch den Gast. Eine Stornierung der Buchung ist nur in Textform möglich (E-Mail an [E-Mail-Adresse des Vermieters]) und wird erst mit unserer Bestätigung wirksam; wir stornieren die Buchung in unserem System und senden Ihnen eine Stornorechnung. Maßgeblich für die Frist ist der Eingang Ihrer Nachricht bei uns.",
|
||
"",
|
||
"2. Kosten der Stornierung. Bei einer Stornierung berechnen wir folgenden Anteil des Gesamtpreises (ohne Endreinigung, soweit der Aufenthalt nicht angetreten wird):",
|
||
"- bis [30] Tage vor der Anreise: kostenfrei,",
|
||
"- [29] bis [14] Tage vor der Anreise: [30] Prozent,",
|
||
"- [13] bis [7] Tage vor der Anreise: [60] Prozent,",
|
||
"- ab [6] Tage vor der Anreise und bei Nichtanreise: [90] Prozent.",
|
||
"",
|
||
"Ihnen bleibt der Nachweis gestattet, dass uns kein oder ein geringerer Schaden entstanden ist. Uns bleibt unser Anspruch unberührt, wenn wir die Unterkunft anderweitig vermieten können: In diesem Fall berechnen wir nur den tatsächlichen Ausfall.",
|
||
"",
|
||
"3. Erstattung. Haben Sie bereits online bezahlt, erstatten wir den Teil des Preises, der nach Nr. 2 nicht anfällt, auf das Zahlungsmittel, mit dem Sie bezahlt haben. Die Erstattung geht über unser Stripe-Konto; die Gutschrift auf Ihrem Konto kann einige Tage dauern. Haben Sie noch nicht bezahlt, zahlen Sie den nach Nr. 2 anfallenden Betrag vor Ort oder nach Rechnung.",
|
||
"",
|
||
"4. Verspätete Anreise und vorzeitige Abreise. Reisen Sie später an oder früher ab, besteht kein Anspruch auf Erstattung des Preises für die nicht genutzten Nächte, es sei denn, wir können die Unterkunft anderweitig vermieten.",
|
||
"",
|
||
"5. Stornierung durch uns. Können wir die Unterkunft aus einem Grund, den wir zu vertreten haben, nicht bereitstellen, stornieren wir die Buchung, bieten nach Möglichkeit eine gleichwertige Unterkunft an und erstatten bereits gezahlte Beträge vollständig.",
|
||
"",
|
||
"6. Kein Widerrufsrecht. Für die Buchung zu einem bestimmten Zeitraum besteht kein gesetzliches Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Diese Stornobedingungen gelten stattdessen.",
|
||
"",
|
||
"7. Reiserücktrittsversicherung. Wir empfehlen Ihnen eine Reiserücktrittsversicherung.",
|
||
].join('\n')
|
||
export const STORNO_HINWEIS = 'Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung.'
|
||
const STANDARD = { staffeln: [{ abNaechten: 1, preisProNachtCent: null }], reinigungCent: 0, personenInklusive: 2, aufpreisPersonNachtCent: 0, direktrabattProzent: 0, steuerart: 'kleinunternehmer', ustUebernachtung: 7, ustReinigung: 7, fristTage: 3, stornobedingungen: '' }
|
||
const PERSONEN_MAX = 10
|
||
const ganzzahl = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,9}$/.test(t) ? Number(t) : null }
|
||
|
||
export async function zeichnePreise(ctx, unterkunft) {
|
||
const basis = `/api/unterkunft/unterkuenfte/${enc(unterkunft.id)}`
|
||
const q = `projectId=${enc(ctx.projectId)}`
|
||
const wurzel = h('div', { class: 'bk-preise' }, h('p', { class: 'kb-empty' }, 'Lade…'))
|
||
let preise = STANDARD
|
||
try {
|
||
const daten = await ctx.request(`${basis}/preise?${q}`)
|
||
if (daten?.preise) preise = { ...STANDARD, ...daten.preise }
|
||
} catch (err) {
|
||
wurzel.replaceChildren(h('p', { class: 'sh-fehler', role: 'alert' }, err.message))
|
||
return wurzel
|
||
}
|
||
// Preise je Personenzahl: nur sinnvoll, wenn mehr als eine Person in die Unterkunft passt; k Spalten (1 bis k Personen)
|
||
const gaeste = Number.isInteger(unterkunft?.maxPersonen) ? Math.min(unterkunft.maxPersonen, PERSONEN_MAX) : 0
|
||
const modusMoeglich = gaeste >= 2
|
||
const modusBox = h('input', { type: 'checkbox', id: 'bk-p-personenmodus' })
|
||
modusBox.checked = modusMoeglich && Array.isArray(preise.staffeln) && preise.staffeln.some((s) => Array.isArray(s?.personenCent) && s.personenCent.length > 0)
|
||
const personenText = (n) => `${n} ${n === 1 ? 'Person' : 'Personen'}`
|
||
const zeilenEl = h('div', { class: 'bk-staffeln' })
|
||
const zeile = (s) => {
|
||
const ab = h('input', { type: 'text', inputmode: 'numeric', 'aria-label': 'Ab Nächten', 'data-staffel': 'ab', autocomplete: 'off' })
|
||
ab.value = String(s.abNaechten ?? '')
|
||
const preis = euroEingabe({ cent: s.preisProNachtCent, label: 'Preis pro Nacht in Euro' })
|
||
preis.dataset.staffel = 'preis'
|
||
const spalten = Array.from({ length: modusMoeglich ? gaeste : 0 }, (_, i) => {
|
||
const eingabe = euroEingabe({ cent: Array.isArray(s.personenCent) ? s.personenCent[i] : null, label: `Preis pro Nacht in Euro für ${personenText(i + 1)}` })
|
||
eingabe.dataset.staffel = 'person'
|
||
eingabe.dataset.personen = String(i + 1)
|
||
return h('label', { class: 'bk-personpreis' }, h('span', null, personenText(i + 1)), eingabe)
|
||
})
|
||
const el = h('div', { class: 'bk-staffel', 'data-staffel-zeile': true }, 'ab ', ab, ' Nächten: ',
|
||
h('span', { class: 'bk-preis-einzeln' }, preis, ' € pro Nacht '),
|
||
h('span', { class: 'bk-personenpreise', 'data-personenpreise': true }, spalten),
|
||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-weg', 'aria-label': 'Staffel entfernen', onclick: () => { if (zeilenEl.children.length > 1) { el.remove(); halteErste() } } }, '−'))
|
||
return el
|
||
}
|
||
const halteErste = () => { const ab = zeilenEl.querySelector('[data-staffel="ab"]'); if (ab) { ab.value = '1'; ab.readOnly = true; ab.setAttribute('readonly', '') } }
|
||
zeilenEl.append(...(preise.staffeln?.length ? preise.staffeln : STANDARD.staffeln).map(zeile))
|
||
halteErste()
|
||
const reinigung = euroEingabe({ cent: preise.reinigungCent, id: 'bk-p-reinigung' })
|
||
const inklusive = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-inklusive' }); inklusive.value = String(preise.personenInklusive ?? 2)
|
||
const aufpreis = euroEingabe({ cent: preise.aufpreisPersonNachtCent, id: 'bk-p-aufpreis' })
|
||
const rabatt = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-rabatt' }); rabatt.value = String(preise.direktrabattProzent ?? 0)
|
||
const steuerart = h('select', { id: 'bk-p-steuerart' }, [['kleinunternehmer', 'Kleinunternehmer (keine Umsatzsteuer)'], ['regel', 'Regelbesteuerung (mit Umsatzsteuer)']].map(([w, t]) => h('option', { value: w }, t)))
|
||
steuerart.value = preise.steuerart === 'regel' ? 'regel' : 'kleinunternehmer'
|
||
const ustUe = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-ust-ue' }); ustUe.value = String(preise.ustUebernachtung ?? 7)
|
||
const ustRe = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-ust-re' }); ustRe.value = String(preise.ustReinigung ?? 7)
|
||
const frist = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-frist' }); frist.value = String(preise.fristTage ?? 3)
|
||
const storno = h('textarea', { id: 'bk-p-storno', rows: '6', maxlength: '5000' }); storno.value = zeichen(preise.stornobedingungen)
|
||
const ustFelder = h('div', { class: 'bk-ust' }, feld('ustUebernachtung', 'USt Übernachtung in %', ustUe), feld('ustReinigung', 'USt Endreinigung in %', ustRe))
|
||
const zeigeUst = () => { ustFelder.hidden = steuerart.value !== 'regel' }
|
||
steuerart.addEventListener('change', zeigeUst)
|
||
zeigeUst()
|
||
|
||
const vorschauText = h('p', { class: 'bk-vorschau', 'data-vorschau': true, role: 'status' })
|
||
const naechteEl = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-vn' }); naechteEl.value = '7'
|
||
const personenEl = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-vp' }); personenEl.value = '2'
|
||
async function vorschau() {
|
||
const naechte = ganzzahl(naechteEl)
|
||
const personen = ganzzahl(personenEl)
|
||
if (!naechte || !personen) { vorschauText.textContent = 'Gib Nächte und Personen als ganze Zahlen an.'; return }
|
||
const von = plusTage(heuteBerlin(), 60)
|
||
try {
|
||
const antwort = await ctx.request(`/api/unterkunft/preisvorschau?${q}`, { method: 'POST', body: { unterkunftId: unterkunft.id, von, bis: plusTage(von, naechte), personen } })
|
||
if (!ctx.aktuell()) return
|
||
vorschauText.textContent = Number.isInteger(antwort?.summen?.bruttoCent)
|
||
? `Beispiel: ${antwort.naechte ?? naechte} ${(antwort.naechte ?? naechte) === 1 ? 'Nacht' : 'Nächte'}, ${personen} ${personen === 1 ? 'Person' : 'Personen'} = ${euroText(antwort.summen.bruttoCent)}`
|
||
: 'Für diese Angaben gibt es noch keine Beispielrechnung.'
|
||
} catch (err) {
|
||
vorschauText.textContent = Object.values(err.body?.felder || {})[0] || 'Die Beispielrechnung klappt erst, wenn die Preise gespeichert sind.'
|
||
}
|
||
}
|
||
for (const el of [naechteEl, personenEl]) el.addEventListener('change', vorschau)
|
||
|
||
const modusFeld = hakenFeld('personenmodus', modusBox, 'Preis hängt von der Personenzahl ab', 'Zum Beispiel 55 € für 1 Person, 70 € für 2 Personen und 82 € für 3 Personen pro Nacht.')
|
||
modusFeld.hidden = !modusMoeglich
|
||
modusBox.hidden = !modusMoeglich
|
||
const modusHinweis = h('p', { class: 'sh-hinweis', 'data-hinweis': 'personenpreise' }, 'Preis pro Nacht für die ganze Unterkunft, je nach Anzahl der Gäste.')
|
||
const inklusiveFeld = feld('personenInklusive', 'Personen im Preis inklusive', inklusive)
|
||
const aufpreisFeld = feld('aufpreisPersonNachtCent', 'Aufpreis je weiterer Person und Nacht in Euro', aufpreis)
|
||
/** Haken an: je Staffel die Spalten je Personenzahl statt des einen Preises; die zwei Personenfelder des alten Modells verschwinden (ihre Werte gehen unveraendert mit). */
|
||
const zeigeModus = () => {
|
||
const an = modusMoeglich && modusBox.checked
|
||
for (const el of zeilenEl.querySelectorAll('.bk-preis-einzeln')) el.hidden = an
|
||
for (const el of zeilenEl.querySelectorAll('.bk-personenpreise')) el.hidden = !an
|
||
modusHinweis.hidden = !an
|
||
inklusiveFeld.hidden = an
|
||
aufpreisFeld.hidden = an
|
||
}
|
||
modusBox.addEventListener('change', zeigeModus)
|
||
const staffelDazu = () => { zeilenEl.append(zeile({ abNaechten: null, preisProNachtCent: null })); zeigeModus() }
|
||
|
||
const form = h('form', { novalidate: true, class: 'bk-preisform', onsubmit: (ev) => ev.preventDefault() },
|
||
h('h4', null, 'Preis pro Nacht (Staffeln)'),
|
||
h('p', { class: 'sh-hinweis' }, 'Je länger der Aufenthalt, desto günstiger die Nacht: Die passende Staffel gilt für alle Nächte der Buchung.'),
|
||
modusFeld, modusHinweis,
|
||
zeilenEl, h('div', { class: 'sh-fehler', 'data-fehler': 'staffeln', role: 'alert' }),
|
||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-dazu', onclick: staffelDazu }, '+ Staffel'),
|
||
feld('reinigungCent', 'Endreinigung in Euro', reinigung),
|
||
inklusiveFeld,
|
||
aufpreisFeld,
|
||
feld('direktrabattProzent', 'Direktbuchungsrabatt in %', rabatt, 'Ganze Zahl von 0 bis 50. Gilt für Übernachtung und Zusatzpersonen, nicht für die Reinigung.'),
|
||
feld('steuerart', 'Steuer', steuerart), ustFelder,
|
||
feld('fristTage', 'Frist für die Buchung nach Freigabe in Tagen', frist, '1 bis 14 Tage.'),
|
||
feld('stornobedingungen', 'Stornobedingungen', storno, 'Pflicht, bevor du den Buchungskalender freischaltest. Sie stehen auf der Buchungsseite.'),
|
||
ctx.nurLesen ? null : h('div', { class: 'bk-aktionen' },
|
||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'storno-muster', onclick: () => { storno.value = STORNO_MUSTER } }, 'Mustertext übernehmen'),
|
||
h('p', { class: 'sh-hinweis', 'data-hinweis': 'storno-muster' }, STORNO_HINWEIS)),
|
||
allgemeinerFehler(),
|
||
h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'preise-speichern', onclick: speichern }, 'Preise speichern')))
|
||
zeigeModus()
|
||
if (ctx.nurLesen) for (const el of form.querySelectorAll('input, select, textarea, button')) el.disabled = true
|
||
// Ein Feldfehler (z. B. Platzhalter in den Stornobedingungen) verschwindet, sobald im Feld getippt wird
|
||
for (const el of form.querySelectorAll('[data-feld]')) {
|
||
el.addEventListener('input', () => {
|
||
const name = el.getAttribute('data-feld')
|
||
const ziel = form.querySelector(`[data-fehler="${CSS.escape(name)}"]`)
|
||
if (ziel) ziel.textContent = ''
|
||
el.removeAttribute('aria-invalid')
|
||
})
|
||
}
|
||
|
||
async function speichern(ev) {
|
||
const knopf = ev.currentTarget
|
||
const fehler = {}
|
||
const staffeln = []
|
||
let staffelFehler = ''
|
||
const personenModus = modusMoeglich && modusBox.checked
|
||
for (const z of zeilenEl.children) {
|
||
const ab = ganzzahl(z.querySelector('[data-staffel="ab"]'))
|
||
if (personenModus) {
|
||
const spalten = [...z.querySelectorAll('[data-staffel="person"]')].map((el) => el.inCent())
|
||
if (ab === null || ab < 1 || spalten.some((p) => p === null)) staffelFehler ||= 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und für jede Personenzahl einen Preis an.'
|
||
else if (spalten.some((p) => p < 100 || p > 100000)) staffelFehler ||= 'Der Preis pro Nacht muss zwischen 1,00 € und 1.000,00 € liegen.'
|
||
else staffeln.push({ abNaechten: ab, preisProNachtCent: spalten[0], personenCent: spalten })
|
||
continue
|
||
}
|
||
const p = z.querySelector('[data-staffel="preis"]').inCent()
|
||
if (ab === null || ab < 1 || p === null) staffelFehler ||= 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und einen Preis an.'
|
||
else if (p < 100 || p > 100000) staffelFehler ||= 'Der Preis pro Nacht muss zwischen 1,00 € und 1.000,00 € liegen.'
|
||
else staffeln.push({ abNaechten: ab, preisProNachtCent: p })
|
||
}
|
||
if (!staffelFehler) {
|
||
if (staffeln.length < 1 || staffeln.length > 6) staffelFehler = 'Du kannst 1 bis 6 Staffeln anlegen.'
|
||
else if (staffeln[0].abNaechten !== 1) staffelFehler = 'Die erste Staffel beginnt bei 1 Nacht.'
|
||
else if (staffeln.some((s, i) => i > 0 && s.abNaechten <= staffeln[i - 1].abNaechten)) staffelFehler = 'Die Nächte der Staffeln müssen von oben nach unten größer werden, keine Zahl doppelt.'
|
||
}
|
||
if (staffelFehler) fehler.staffeln = staffelFehler
|
||
const zahl = (name, steuer, text, min, max) => { const n = ganzzahl(steuer); if (n === null || n < min || n > max) { fehler[name] = text; return null } return n }
|
||
const reinigungCent = reinigung.istLeer() ? 0 : reinigung.inCent()
|
||
if (reinigungCent === null) fehler.reinigungCent = 'Bitte gib einen Betrag wie 60,00 an.'
|
||
// Im Personen-Modus sind "inklusive" und "Aufpreis" ausgeblendet: ihr Wert geht unveraendert mit; Unbrauchbares im unsichtbaren Feld bleibt ohne Fehlermeldung beim gespeicherten Wert
|
||
let aufpreisCent = aufpreis.istLeer() ? 0 : aufpreis.inCent()
|
||
if (personenModus && (aufpreisCent === null || aufpreisCent > 100000)) aufpreisCent = Number.isInteger(preise.aufpreisPersonNachtCent) ? preise.aufpreisPersonNachtCent : 0
|
||
if (aufpreisCent === null) fehler.aufpreisPersonNachtCent = 'Bitte gib einen Betrag wie 15,00 an.'
|
||
const inklusiveAlt = Number.isInteger(preise.personenInklusive) && preise.personenInklusive >= 1 && preise.personenInklusive <= 50 ? preise.personenInklusive : 2
|
||
const inklusiveNeu = ganzzahl(inklusive)
|
||
const inklusiveWert = personenModus
|
||
? (inklusiveNeu !== null && inklusiveNeu >= 1 && inklusiveNeu <= 50 ? inklusiveNeu : inklusiveAlt)
|
||
: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl ab 1 an.', 1, 1000)
|
||
const regel = steuerart.value === 'regel'
|
||
const ustWert = (name, steuer, alt) => {
|
||
if (!regel) return ganzzahl(steuer) ?? alt // andere Steuerart: gespeicherten Wert unveraendert zurueckschicken
|
||
const n = ganzzahl(steuer)
|
||
if (![0, 7, 19].includes(n)) { fehler[name] = 'Erlaubt sind 0, 7 oder 19 Prozent.'; return null }
|
||
return n
|
||
}
|
||
const storn = String(storno.value).trim()
|
||
if (storn.length < 20 || storn.length > 5000) fehler.stornobedingungen = 'Die Stornobedingungen brauchen 20 bis 5000 Zeichen.'
|
||
if (!fehler.stornobedingungen && /\[[^\]]*\]/.test(storn)) fehler.stornobedingungen = 'Bitte ersetze die Platzhalter in eckigen Klammern durch deine Werte.'
|
||
const body = {
|
||
staffeln, reinigungCent, aufpreisPersonNachtCent: aufpreisCent,
|
||
personenInklusive: inklusiveWert,
|
||
direktrabattProzent: zahl('direktrabattProzent', rabatt, 'Bitte gib eine ganze Zahl von 0 bis 50 an.', 0, 50),
|
||
steuerart: steuerart.value,
|
||
ustUebernachtung: ustWert('ustUebernachtung', ustUe, preise.ustUebernachtung ?? 7),
|
||
ustReinigung: ustWert('ustReinigung', ustRe, preise.ustReinigung ?? 7),
|
||
fristTage: zahl('fristTage', frist, 'Bitte gib eine ganze Zahl von 1 bis 14 an.', 1, 14),
|
||
stornobedingungen: storn,
|
||
}
|
||
if (Object.keys(fehler).length) return zeigeFehler(form, { message: '', fehler })
|
||
knopf.disabled = true
|
||
try {
|
||
await ctx.request(`${basis}/preise?${q}`, { method: 'PUT', body })
|
||
if (!ctx.aktuell()) return
|
||
setzeFehler(form, {})
|
||
ctx.zeigeMeldung('Die Preise sind gespeichert.', 'ok')
|
||
await vorschau()
|
||
} catch (err) {
|
||
zeigeApiFehler(form, err)
|
||
}
|
||
knopf.disabled = false
|
||
}
|
||
|
||
wurzel.replaceChildren(form,
|
||
abschnitt('Beispielrechnung', 'So rechnet dein Gast. Sie nutzt die gespeicherten Preise.',
|
||
h('div', { class: 'bk-vorschaufelder' }, feld('naechte', 'Nächte', naechteEl), feld('personen', 'Personen', personenEl)), vorschauText))
|
||
vorschau()
|
||
return wurzel
|
||
}
|