Buchungskalender: Preise je Personenzahl (Portal-Editor)

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>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-11 15:06:04 +00:00
1 parent a4ade5f649
commit 1e0eb9f569
3 files changed
+274 -10

No files matched your search

+66 -10
View File
@@ -1,9 +1,15 @@
/* Buchungskalender: Preis-Editor einer Unterkunft (Staffeln, Reinigung, Zusatzpersonen, Rabatt, Steuer, Frist, Stornobedingungen) mit Beispielrechnung.
GET|PUT /api/unterkunft/unterkuenfte/:id/preise => { preise:{ staffeln:[{abNaechten,preisProNachtCent}], reinigungCent, personenInklusive, aufpreisPersonNachtCent,
/* 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). */
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, setzeFehler, zeigeFehler } from './shop-dom.js'
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.
@@ -30,6 +36,7 @@ export const STORNO_MUSTER = [
].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) {
@@ -44,13 +51,27 @@ export async function zeichnePreise(ctx, unterkunft) {
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 el = h('div', { class: 'bk-staffel', 'data-staffel-zeile': true }, 'ab ', ab, ' Nächten: ', preis, ' € pro Nacht ',
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
}
@@ -92,14 +113,33 @@ export async function zeichnePreise(ctx, unterkunft) {
}
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: () => { zeilenEl.append(zeile({ abNaechten: null, preisProNachtCent: null })) } }, '+ Staffel'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-dazu', onclick: staffelDazu }, '+ Staffel'),
feld('reinigungCent', 'Endreinigung in Euro', reinigung),
feld('personenInklusive', 'Personen im Preis inklusive', inklusive),
feld('aufpreisPersonNachtCent', 'Aufpreis je weiterer Person und Nacht in Euro', aufpreis),
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.'),
@@ -109,6 +149,7 @@ export async function zeichnePreise(ctx, unterkunft) {
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]')) {
@@ -125,8 +166,16 @@ export async function zeichnePreise(ctx, unterkunft) {
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.'
@@ -141,8 +190,15 @@ export async function zeichnePreise(ctx, unterkunft) {
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.'
const aufpreisCent = aufpreis.istLeer() ? 0 : aufpreis.inCent()
// 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
@@ -155,7 +211,7 @@ export async function zeichnePreise(ctx, unterkunft) {
if (!fehler.stornobedingungen && /\[[^\]]*\]/.test(storn)) fehler.stornobedingungen = 'Bitte ersetze die Platzhalter in eckigen Klammern durch deine Werte.'
const body = {
staffeln, reinigungCent, aufpreisPersonNachtCent: aufpreisCent,
personenInklusive: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl ab 1 an.', 1, 1000),
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),
+5
View File
@@ -52,6 +52,11 @@
.bk-farbe { width: 16px; height: 16px; border-radius: 4px; border: 1px solid var(--kb-border); display: inline-block; }
.bk-staffel { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.bk-staffel input { width: 90px; }
/* Preise je Personenzahl: ein Euro-Feld je Gaestezahl unter der Beschriftung; bricht auf dem Handy um, kein Seitwaertsscrollen */
.bk-personenpreise { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: flex-end; min-width: 0; max-width: 100%; }
.bk-personenpreise[hidden], .bk-preis-einzeln[hidden] { display: none; }
.bk-personpreis { display: grid; gap: 2px; font-size: 13px; font-weight: 600; }
.bk-personpreis > span { white-space: nowrap; }
.bk-vorschau { font-weight: 700; }
.bk-import { display: flex; align-items: center; gap: 4px 12px; flex-wrap: wrap; }
.bk-anleitung details { margin: 8px 0; }
+203
View File
@@ -868,3 +868,206 @@ test('Preis-Editor: Platzhalter-Feldfehler verschwindet beim Tippen', mit(ukServ
await fx.setze(storno, 'Bis zu 14 Tage vor Anreise kostenfrei stornierbar, danach 100 %.')
assert.equal(fehlerText(fx, panel, 'stornobedingungen'), '')
}))
// ---- Preis-Editor: Preise je Personenzahl (staffeln[].personenCent), Haken #bk-p-personenmodus
const PERSONEN_PREISE = {
...PREISE,
staffeln: [
{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] },
{ abNaechten: 7, preisProNachtCent: 5000, personenCent: [5000, 6500, 7700] },
],
}
const PUT_PREISE = { 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }
/** Server mit anderen Preisen und/oder anderer Gaestezahl der Unterkunft. */
const personenServer = (preise, maxPersonen = 3, o = {}) => ukServer({
'GET /api/unterkunft/unterkuenfte': () => ({ body: { unterkuenfte: [{ ...UK, maxPersonen }] } }),
'GET /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: { preise } }),
...PUT_PREISE,
...o,
})
const zeilen = (fx, panel) => fx.alle(panel, '[data-staffel-zeile]')
const spalten = (fx, zeile) => fx.alle(zeile, '[data-staffel="person"]')
const putPreise = (fx) => posts(fx).filter((a) => a.url.includes('/preise')).at(-1)
const modusBox = (fx, panel) => fx.finde(panel, '#bk-p-personenmodus')
const sichtbar = (el) => { for (let e = el; e && e.hidden !== undefined; e = e.parentElement) if (e.hidden) return false; return true }
test('Preis-Editor Personen-Modus: gespeicherte personenCent laden (Haken an, k Felder je Staffel), Speichern sendet sie unverändert', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
assert.equal(modusBox(fx, panel).checked, true)
assert.equal(zeilen(fx, panel).length, 2)
for (const [i, erwartet] of [['5500', '7000', '8200'], ['5000', '6500', '7700']].entries()) {
const felder = spalten(fx, zeilen(fx, panel)[i])
assert.equal(felder.length, 3)
assert.deepEqual(felder.map((f) => f.value), erwartet.map((c) => `${c.slice(0, -2)},${c.slice(-2)}`))
assert.deepEqual(felder.map((f) => f.dataset.personen), ['1', '2', '3'])
assert.ok(felder.every(sichtbar))
assert.equal(sichtbar(fx.finde(zeilen(fx, panel)[i], '[data-staffel="preis"]')), false, 'der eine Preis ist ausgeblendet')
}
const beschriftung = fx.text(zeilen(fx, panel)[0])
assert.match(beschriftung, /1 Person(?!en)/)
assert.match(beschriftung, /2 Personen/)
assert.match(beschriftung, /3 Personen/)
assert.equal(sichtbar(fx.finde(panel, '[data-hinweis="personenpreise"]')), true)
assert.equal(fx.text(fx.finde(panel, '[data-hinweis="personenpreise"]')), 'Preis pro Nacht für die ganze Unterkunft, je nach Anzahl der Gäste.')
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), false)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-aufpreis')), false)
assert.equal(await versuche(fx, panel), 1)
const put = putPreise(fx)
assert.deepEqual(put.body.staffeln, PERSONEN_PREISE.staffeln)
assert.equal(put.body.personenInklusive, 2, 'ausgeblendete Felder gehen unverändert mit')
assert.equal(put.body.aufpreisPersonNachtCent, 1000)
assert.equal(put.body.reinigungCent, 6000)
}))
test('Preis-Editor Personen-Modus: preisProNachtCent im Body ist personenCent[0]; Felder lassen sich ändern', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
await fx.setze(spalten(fx, zeilen(fx, panel)[1])[0], '48,50')
await fx.setze(spalten(fx, zeilen(fx, panel)[1])[2], '75')
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln[1], { abNaechten: 7, preisProNachtCent: 4850, personenCent: [4850, 6500, 7500] })
}))
test('Preis-Editor Personen-Modus: Haken schaltet vom alten Modell um (k Felder aus maxPersonen, Hilfsfelder weg) und zurück', mit(personenServer(PREISE, 4), async (fx) => {
const panel = await preisEditor(fx)
const box = modusBox(fx, panel)
assert.equal(box.checked, false)
assert.equal(sichtbar(box), true)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), true)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-aufpreis')), true)
assert.equal(sichtbar(fx.finde(panel, '[data-hinweis="personenpreise"]')), false)
assert.ok(zeilen(fx, panel).every((z) => sichtbar(fx.finde(z, '[data-staffel="preis"]')) && spalten(fx, z).every((f) => !sichtbar(f))))
await fx.klick(box)
assert.equal(box.checked, true)
for (const z of zeilen(fx, panel)) {
assert.equal(spalten(fx, z).length, 4, 'k = maxPersonen')
assert.ok(spalten(fx, z).every((f) => sichtbar(f) && f.value === ''), 'fehlende Werte bleiben leer')
assert.equal(sichtbar(fx.finde(z, '[data-staffel="preis"]')), false)
}
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), false)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-aufpreis')), false)
assert.equal(sichtbar(fx.finde(panel, '[data-hinweis="personenpreise"]')), true)
// neue Staffel hat die Felder ebenfalls
await fx.klick(aktion(fx, panel, 'staffel-dazu'))
assert.equal(spalten(fx, zeilen(fx, panel)[2]).length, 4)
assert.ok(spalten(fx, zeilen(fx, panel)[2]).every(sichtbar))
await fx.klick(fx.finde(zeilen(fx, panel)[2], '[data-aktion="staffel-weg"]'))
// zurück: alter Modus wie bisher, der eine Preis blieb erhalten
await fx.klick(box)
assert.equal(box.checked, false)
assert.equal(fx.finde(zeilen(fx, panel)[0], '[data-staffel="preis"]').value, '55,00')
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), true)
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, PREISE.staffeln)
assert.ok(putPreise(fx).body.staffeln.every((s) => !('personenCent' in s)))
}))
test('Preis-Editor Personen-Modus: Umschalten, alle Felder füllen, Speichern', mit(personenServer(PREISE, 3), async (fx) => {
const panel = await preisEditor(fx)
await fx.klick(modusBox(fx, panel))
assert.equal(await versuche(fx, panel), 0, 'leere Felder werden nicht gesendet')
assert.match(fehlerText(fx, panel, 'staffeln'), /für jede Personenzahl einen Preis/)
const werte = [['55', '70', '82'], ['50', '65', '77']]
for (const [i, z] of zeilen(fx, panel).entries()) for (const [j, f] of spalten(fx, z).entries()) await fx.setze(f, werte[i][j])
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, [
{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] },
{ abNaechten: 7, preisProNachtCent: 5000, personenCent: [5000, 6500, 7700] },
])
assert.equal(putPreise(fx).body.personenInklusive, 2)
assert.equal(putPreise(fx).body.aufpreisPersonNachtCent, 1000)
}))
test('Preis-Editor Personen-Modus: Client prüft Preisgrenzen und Staffeln wie der Server', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
const zeile = (i) => zeilen(fx, panel)[i]
await fx.setze(spalten(fx, zeile(0))[1], '0,50')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /1,00 € und 1\.000,00 €/)
await fx.setze(spalten(fx, zeile(0))[1], '1.000,01')
assert.equal(await versuche(fx, panel), 0)
await fx.setze(spalten(fx, zeile(0))[1], 'abc')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /für jede Personenzahl einen Preis/)
await fx.setze(spalten(fx, zeile(0))[1], '70')
await fx.setze(fx.finde(zeile(1), '[data-staffel="ab"]'), '1')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /größer werden/)
await fx.setze(fx.finde(zeile(1), '[data-staffel="ab"]'), '7')
assert.equal(await versuche(fx, panel), 1)
}))
test('Preis-Editor Personen-Modus: weniger gespeicherte Werte als Felder bleiben leer, überzählige werden verworfen', async () => {
const gespeichert = { ...PERSONEN_PREISE, staffeln: [{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] }] }
// Unterkunft jetzt für 4 Gäste: vierte Spalte leer
await mit(personenServer(gespeichert, 4), async (fx) => {
const panel = await preisEditor(fx)
assert.deepEqual(spalten(fx, zeilen(fx, panel)[0]).map((f) => f.value), ['55,00', '70,00', '82,00', ''])
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /für jede Personenzahl einen Preis/)
})()
// Unterkunft jetzt für 2 Gäste: dritter Wert fällt weg
await mit(personenServer(gespeichert, 2), async (fx) => {
const panel = await preisEditor(fx)
assert.deepEqual(spalten(fx, zeilen(fx, panel)[0]).map((f) => f.value), ['55,00', '70,00'])
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000] }])
})()
// mehr als 10 Gäste: höchstens 10 Felder
await mit(personenServer(gespeichert, 15), async (fx) => {
const panel = await preisEditor(fx)
assert.equal(spalten(fx, zeilen(fx, panel)[0]).length, 10)
assert.match(fx.text(zeilen(fx, panel)[0]), /10 Personen/)
})()
})
test('Preis-Editor Personen-Modus: bei einer Person Höchstzahl kein Haken, altes Modell, personenCent fällt weg', mit(personenServer(PERSONEN_PREISE, 1), async (fx) => {
const panel = await preisEditor(fx)
const box = modusBox(fx, panel)
assert.equal(box.checked, false)
assert.equal(sichtbar(box), false)
assert.ok(zeilen(fx, panel).every((z) => spalten(fx, z).length === 0 && sichtbar(fx.finde(z, '[data-staffel="preis"]'))))
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), true)
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500 }, { abNaechten: 7, preisProNachtCent: 5000 }])
}))
test('Preis-Editor Personen-Modus: nur lesen sperrt Haken und alle Felder', async () => {
await mit(personenServer(PERSONEN_PREISE), async (fx) => {
const { panel } = await zeigeMit(fx, ukTab.render, { nurLesen: true })
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
assert.ok(modusBox(fx, panel).disabled)
assert.equal(spalten(fx, zeilen(fx, panel)[0]).length, 3)
assert.ok(fx.alle(panel, '.bk-preisform input, .bk-preisform select, .bk-preisform textarea, .bk-preisform button').every((el) => el.disabled), 'alles gesperrt')
assert.equal(aktion(fx, panel, 'preise-speichern'), null)
// Klick auf den gesperrten Haken ändert nichts
await fx.klick(modusBox(fx, panel))
assert.equal(modusBox(fx, panel).checked, true)
})()
// auch im alten Modell sperrt nur lesen den Haken
await mit(personenServer(PREISE, 3), async (fx) => {
const { panel } = await zeigeMit(fx, ukTab.render, { nurLesen: true })
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
assert.ok(modusBox(fx, panel).disabled)
await fx.klick(modusBox(fx, panel))
assert.equal(modusBox(fx, panel).checked, false)
assert.ok(zeilen(fx, panel).every((z) => spalten(fx, z).every((f) => f.disabled)))
})()
})
test('Preis-Editor Personen-Modus: unlesbare Reste in den ausgeblendeten Feldern blockieren das Speichern nicht', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
await fx.setze(fx.finde(panel, '#bk-p-inklusive'), 'xyz')
await fx.setze(fx.finde(panel, '#bk-p-aufpreis'), 'abc')
assert.equal(await versuche(fx, panel), 1)
assert.equal(putPreise(fx).body.personenInklusive, 2)
assert.equal(putPreise(fx).body.aufpreisPersonNachtCent, 1000)
}))
test('CSS: Felder je Personenzahl brechen um, ausgeblendete Teile bleiben unsichtbar', () => {
const css = readFileSync(new URL('../public/buchungen.css', import.meta.url), 'utf8')
assert.match(css, /\.bk-personenpreise\s*\{[^}]*flex-wrap:\s*wrap/)
assert.match(css, /\.bk-personenpreise\[hidden\][^{]*\{\s*display:\s*none/)
assert.match(css, /\.bk-preis-einzeln\[hidden\]/)
})