Buchungskalender: Preis-Editor prüft Grenzen wie der Server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37cd312549
commit
8c8fa05591
5 files changed
+116
-12
No files matched your search
@@ -9,7 +9,9 @@ const CODES = {
|
||||
/** => { message, fehler: { feld: text } } (Feldfehler aus body.felder, sonst ein Text unter „_“). */
|
||||
export function fehlerFuerFormular(err) {
|
||||
const felder = err?.body?.felder
|
||||
const message = (typeof err?.fehler === 'string' && CODES[err.fehler]) || err?.message || 'Das hat nicht geklappt. Bitte versuch es noch einmal.'
|
||||
const STANDARD = 'Das hat nicht geklappt. Bitte versuch es noch einmal.'
|
||||
const code = typeof err?.fehler === 'string' ? err.fehler : ''
|
||||
const message = (code && CODES[code]) || (code ? STANDARD : err?.message) || STANDARD
|
||||
if (felder && typeof felder === 'object' && Object.keys(felder).length) return { message, fehler: felder }
|
||||
return { message, fehler: { _: message } }
|
||||
}
|
||||
@@ -89,28 +89,44 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
const knopf = ev.currentTarget
|
||||
const fehler = {}
|
||||
const staffeln = []
|
||||
let staffelFehler = ''
|
||||
for (const z of zeilenEl.children) {
|
||||
const ab = ganzzahl(z.querySelector('[data-staffel="ab"]'))
|
||||
const p = z.querySelector('[data-staffel="preis"]').inCent()
|
||||
if (ab === null || ab < 1 || p === null) fehler.staffeln = 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und einen Preis an.'
|
||||
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 })
|
||||
}
|
||||
const zahl = (name, steuer, text) => { const n = ganzzahl(steuer); if (n === null) fehler[name] = text; return n }
|
||||
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.'
|
||||
const aufpreisCent = aufpreis.istLeer() ? 0 : aufpreis.inCent()
|
||||
if (aufpreisCent === null) fehler.aufpreisPersonNachtCent = 'Bitte gib einen Betrag wie 15,00 an.'
|
||||
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.'
|
||||
const body = {
|
||||
staffeln, reinigungCent, aufpreisPersonNachtCent: aufpreisCent,
|
||||
personenInklusive: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl an.'),
|
||||
direktrabattProzent: zahl('direktrabattProzent', rabatt, 'Bitte gib eine ganze Zahl von 0 bis 50 an.'),
|
||||
personenInklusive: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl ab 1 an.', 1, 1000),
|
||||
direktrabattProzent: zahl('direktrabattProzent', rabatt, 'Bitte gib eine ganze Zahl von 0 bis 50 an.', 0, 50),
|
||||
steuerart: steuerart.value,
|
||||
ustUebernachtung: zahl('ustUebernachtung', ustUe, 'Bitte gib eine ganze Zahl an.'),
|
||||
ustReinigung: zahl('ustReinigung', ustRe, 'Bitte gib eine ganze Zahl an.'),
|
||||
fristTage: zahl('fristTage', frist, 'Bitte gib eine ganze Zahl von 1 bis 14 an.'),
|
||||
stornobedingungen: String(storno.value).trim(),
|
||||
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 (steuerart.value !== 'regel') { delete fehler.ustUebernachtung; delete fehler.ustReinigung; body.ustUebernachtung = 7; body.ustReinigung = 7 }
|
||||
if (Object.keys(fehler).length) return zeigeFehler(form, { message: '', fehler })
|
||||
knopf.disabled = true
|
||||
try {
|
||||
|
||||
@@ -174,7 +174,7 @@ export async function render(panel, ctx) {
|
||||
function zeigeLink(link, a) {
|
||||
const box = h('section', { class: 'kb-panel sh-abschnitt bk-link', 'data-freigegeben': a.id },
|
||||
h('h3', null, `Anfrage ${zeichen(a.nummer)} ist freigegeben`),
|
||||
h('p', null, 'Dein Gast hat den Buchungslink per Mail bekommen.'))
|
||||
h('p', null, 'Der Link wurde per Mail verschickt. Falls er nicht ankommt, kopiere ihn hier und schick ihn selbst.'))
|
||||
if (typeof link === 'string' && link) {
|
||||
const meldung = h('p', { class: 'sh-hinweis', role: 'status' })
|
||||
box.append(
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function render(panel, ctx) {
|
||||
if (st.lauf !== lauf || !ctx.aktuell()) return
|
||||
st.bereiche = Array.isArray(daten?.bereiche) ? daten.bereiche : []
|
||||
} catch (err) {
|
||||
if (st.lauf === lauf && ctx.aktuell()) { st.bereiche = []; hinweis.textContent = err.message }
|
||||
if (st.lauf === lauf && ctx.aktuell()) { st.bereiche = []; zeichne(); hinweis.textContent = err.message }
|
||||
return
|
||||
}
|
||||
zeichne()
|
||||
@@ -115,6 +115,10 @@ export async function render(panel, ctx) {
|
||||
return
|
||||
}
|
||||
if (ctx.nurLesen) return
|
||||
if (tag < heuteBerlin()) {
|
||||
hinweis.textContent = 'Vergangene Tage kannst du nicht sperren.'
|
||||
return
|
||||
}
|
||||
if (!st.start) {
|
||||
st.start = tag
|
||||
zeichne()
|
||||
|
||||
@@ -380,3 +380,85 @@ test('Freigeben: Serverfehler 400 mit felder zeigt genau den Feldtext', mit(serv
|
||||
assert.equal(fx.text(fx.finde(panel, '[data-fehler="posten"]')), 'Die Summe muss größer als 0 sein.')
|
||||
assert.equal(fx.text(fx.finde(panel, '[data-fehler="_"]')), '')
|
||||
}))
|
||||
|
||||
// Fix round 1: Preis-Editor prüft Grenzen wie der Server
|
||||
const preisEditor = async (fx, preise = PREISE) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
await fx.klick(aktion(fx, panel, 'bearbeiten'))
|
||||
await fx.ruhe()
|
||||
return panel
|
||||
}
|
||||
const versuche = async (fx, panel) => {
|
||||
const vorher = posts(fx).filter((a) => a.url.includes('/preise')).length
|
||||
await fx.klick(aktion(fx, panel, 'preise-speichern'))
|
||||
await fx.ruhe()
|
||||
return posts(fx).filter((a) => a.url.includes('/preise')).length - vorher
|
||||
}
|
||||
const fehlerText = (fx, panel, n) => fx.text(fx.finde(panel, `[data-fehler="${n}"]`))
|
||||
|
||||
test('Preis-Editor: Staffeln (Anzahl, erste ab 1, aufsteigend, Preisgrenzen)', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
|
||||
const panel = await preisEditor(fx)
|
||||
const zeilen = () => fx.alle(panel, '[data-staffel-zeile]')
|
||||
const ab = (i) => fx.finde(zeilen()[i], '[data-staffel="ab"]')
|
||||
const preis = (i) => fx.finde(zeilen()[i], '[data-staffel="preis"]')
|
||||
await fx.setze(ab(1), '1')
|
||||
assert.equal(await versuche(fx, panel), 0)
|
||||
assert.match(fehlerText(fx, panel, 'staffeln'), /größer werden/)
|
||||
await fx.setze(ab(1), '7')
|
||||
await fx.setze(preis(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(preis(1), '1.000,01')
|
||||
assert.equal(await versuche(fx, panel), 0)
|
||||
await fx.setze(preis(1), '50')
|
||||
for (let i = 0; i < 5; i += 1) { await fx.klick(aktion(fx, panel, 'staffel-dazu')); await fx.setze(ab(zeilen().length - 1), String(10 + i)); await fx.setze(preis(zeilen().length - 1), '40') }
|
||||
assert.equal(zeilen().length, 7)
|
||||
assert.equal(await versuche(fx, panel), 0)
|
||||
assert.match(fehlerText(fx, panel, 'staffeln'), /1 bis 6/)
|
||||
await fx.klick(fx.finde(zeilen()[6], '[data-aktion="staffel-weg"]'))
|
||||
assert.equal(await versuche(fx, panel), 1)
|
||||
}))
|
||||
|
||||
test('Preis-Editor: Rabatt, Frist, Personen, Stornobedingungen, USt', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
|
||||
const panel = await preisEditor(fx)
|
||||
const fall = async (sel, wert, ok, feldName, re) => {
|
||||
const alt = fx.finde(panel, sel).value
|
||||
await fx.setze(fx.finde(panel, sel), wert)
|
||||
assert.equal(await versuche(fx, panel), 0, `${sel}=${wert}`)
|
||||
assert.match(fehlerText(fx, panel, feldName), re)
|
||||
await fx.setze(fx.finde(panel, sel), alt)
|
||||
}
|
||||
await fall('#bk-p-rabatt', '51', 0, 'direktrabattProzent', /0 bis 50/)
|
||||
await fall('#bk-p-frist', '0', 0, 'fristTage', /1 bis 14/)
|
||||
await fall('#bk-p-frist', '15', 0, 'fristTage', /1 bis 14/)
|
||||
await fall('#bk-p-inklusive', '0', 0, 'personenInklusive', /ab 1/)
|
||||
await fall('#bk-p-storno', 'zu kurz', 0, 'stornobedingungen', /20 bis 5000/)
|
||||
await fall('#bk-p-ust-ue', '8', 0, 'ustUebernachtung', /0, 7 oder 19/)
|
||||
await fall('#bk-p-ust-re', '20', 0, 'ustReinigung', /0, 7 oder 19/)
|
||||
assert.equal(await versuche(fx, panel), 1)
|
||||
}))
|
||||
|
||||
test('Preis-Editor: gespeicherte USt 19 bleibt bei Kleinunternehmer unverändert', mit(ukServer({
|
||||
'GET /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: { preise: { ...PREISE, steuerart: 'kleinunternehmer', ustUebernachtung: 19, ustReinigung: 0 } } }),
|
||||
'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }),
|
||||
}), async (fx) => {
|
||||
const panel = await preisEditor(fx)
|
||||
assert.equal(await versuche(fx, panel), 1)
|
||||
const put = posts(fx).find((a) => a.url.includes('/preise'))
|
||||
assert.equal(put.body.steuerart, 'kleinunternehmer')
|
||||
assert.equal(put.body.ustUebernachtung, 19)
|
||||
assert.equal(put.body.ustReinigung, 0)
|
||||
}))
|
||||
|
||||
test('fehlerFuerFormular: unbekannter Code → deutscher Standardtext', async () => {
|
||||
const { fehlerFuerFormular } = await import('../public/buchungen-fehler.js')
|
||||
assert.equal(fehlerFuerFormular({ fehler: 'komischer_code', message: 'Fehler 400' }).fehler._, 'Das hat nicht geklappt. Bitte versuch es noch einmal.')
|
||||
assert.equal(fehlerFuerFormular({ fehler: 'zeitraum_belegt', message: 'x' }).fehler._, 'Der Zeitraum ist bereits belegt.')
|
||||
})
|
||||
|
||||
test('Kalender: vergangene Tage lassen sich nicht sperren', mit(ukServer(), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, kal)
|
||||
await fx.klick(tag(fx, panel, '2026-10-01'))
|
||||
assert.match(fx.text(fx.finde(panel, '[data-hinweis]')), /Vergangene Tage/)
|
||||
assert.equal(fx.finde(panel, '[data-aktion="sperren-senden"]'), null)
|
||||
}))
|
||||
Reference in new issue
Block a user