From 8c8fa055911f05c4d9470a94d8401cd720d61f35 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 14:14:38 +0000 Subject: [PATCH] =?UTF-8?q?Buchungskalender:=20Preis-Editor=20pr=C3=BCft?= =?UTF-8?q?=20Grenzen=20wie=20der=20Server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- public/buchungen-fehler.js | 4 +- public/buchungen-preise.js | 34 +++++++++---- public/buchungen-tab-anfragen.js | 2 +- public/buchungen-tab-kalender.js | 6 ++- test/buchungenUi.test.js | 82 ++++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 12 deletions(-) diff --git a/public/buchungen-fehler.js b/public/buchungen-fehler.js index 9eab0c3..8f45297 100644 --- a/public/buchungen-fehler.js +++ b/public/buchungen-fehler.js @@ -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 } } } diff --git a/public/buchungen-preise.js b/public/buchungen-preise.js index 1254415..8085f63 100644 --- a/public/buchungen-preise.js +++ b/public/buchungen-preise.js @@ -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 { diff --git a/public/buchungen-tab-anfragen.js b/public/buchungen-tab-anfragen.js index 92705cf..d94e08e 100644 --- a/public/buchungen-tab-anfragen.js +++ b/public/buchungen-tab-anfragen.js @@ -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( diff --git a/public/buchungen-tab-kalender.js b/public/buchungen-tab-kalender.js index 9c6334d..61a997b 100644 --- a/public/buchungen-tab-kalender.js +++ b/public/buchungen-tab-kalender.js @@ -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() diff --git a/test/buchungenUi.test.js b/test/buchungenUi.test.js index 7264d23..549182e 100644 --- a/test/buchungenUi.test.js +++ b/test/buchungenUi.test.js @@ -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) +}))