Buchungskalender: Stornobedingungen je Unterkunft, Zustimmung und Bestellstatus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:26:22 +00:00
1 parent efeb99e214
commit 05006f1480
5 files changed
+123 -29

No files matched your search

+1 -1
View File
@@ -77,7 +77,7 @@ export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis =
} catch (err) { } catch (err) {
zeigeFehler(form, fehlerFuerFormular(err)) zeigeFehler(form, fehlerFuerFormular(err))
if (err?.status === 409 && typeof nachSenden === 'function') { if (err?.status === 409 && typeof nachSenden === 'function') {
try { await nachSenden(null) } catch { /* der Fehler steht schon am Formular */ } try { await nachSenden(null, { meldung: fehlerFuerFormular(err).message }) } catch { /* der Fehler steht schon am Formular */ }
return return
} }
} }
+5
View File
@@ -6,6 +6,8 @@
import { abschnitt, allgemeinerFehler, euroEingabe, euroText, feld, h, setzeFehler, zeigeFehler } from './shop-dom.js' import { abschnitt, allgemeinerFehler, euroEingabe, euroText, feld, h, setzeFehler, zeigeFehler } from './shop-dom.js'
import { enc, heuteBerlin, plusTage, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js' import { enc, heuteBerlin, plusTage, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
export const STORNO_MUSTER = 'Stornierung bis 14 Tage vor Anreise: kostenlos. Stornierung 13 bis 3 Tage vor Anreise: 50 % des Gesamtpreises. Stornierung ab 2 Tage vor Anreise oder Nichtanreise: 90 % des Gesamtpreises. Die Stornierung bedarf der Textform (z. B. E-Mail). Maßgeblich ist der Eingang bei uns.'
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 STANDARD = { staffeln: [{ abNaechten: 1, preisProNachtCent: null }], reinigungCent: 0, personenInklusive: 2, aufpreisPersonNachtCent: 0, direktrabattProzent: 0, steuerart: 'kleinunternehmer', ustUebernachtung: 7, ustReinigung: 7, fristTage: 3, stornobedingungen: '' }
const ganzzahl = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,9}$/.test(t) ? Number(t) : null } const ganzzahl = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,9}$/.test(t) ? Number(t) : null }
@@ -81,6 +83,9 @@ export async function zeichnePreise(ctx, unterkunft) {
feld('steuerart', 'Steuer', steuerart), ustFelder, feld('steuerart', 'Steuer', steuerart), ustFelder,
feld('fristTage', 'Frist für die Buchung nach Freigabe in Tagen', frist, '1 bis 14 Tage.'), 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.'), 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(), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'preise-speichern', onclick: speichern }, 'Preise speichern'))) h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'preise-speichern', onclick: speichern }, 'Preise speichern')))
if (ctx.nurLesen) for (const el of form.querySelectorAll('input, select, textarea, button')) el.disabled = true if (ctx.nurLesen) for (const el of form.querySelectorAll('input, select, textarea, button')) el.disabled = true
+24 -19
View File
@@ -1,18 +1,17 @@
/* Buchungskalender, Reiter „Einstellungen“: Firmendaten, Steuernummer/USt-IdNr., Rechnungspraefix, Auftrag zur Rechnungsstellung, /* Buchungskalender, Reiter „Einstellungen“: Firmendaten, Steuernummer/USt-IdNr., Rechnungspraefix, Auftrag zur Rechnungsstellung,
Bankverbindung, Akzentfarbe, Stornobedingungen (Mustertext), Stripe-Konto und Online-Zahlung. Bankverbindung, Akzentfarbe, Stripe-Konto und Online-Zahlung.
API (docs/funktionen/unterkunft-api.md, ?projectId=): GET /einstellungen => { einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon, API (docs/funktionen/unterkunft-api.md, ?projectId=): GET /einstellungen => { einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon,
steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung, rechnungAuftrag, stornobedingungen }, steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung, rechnungAuftrag },
stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto:boolean }, rechnungenVorhanden:boolean }; stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto:boolean }, rechnungenVorhanden:boolean };
PUT /einstellungen mit denselben Feldern (ohne Wrapper; rechnungAuftrag:true setzt rechnungAuftragAt auf dem Server); PUT /einstellungen mit denselben Feldern (ohne Wrapper; rechnungAuftrag:true setzt rechnungAuftragAt, false widerruft);
POST /stripe/verbinden => { url } (Onboarding bei Stripe), POST /stripe/vorhandenes-konto und /stripe/aktualisieren => { stripe }. POST /stripe/verbinden => { url } (Onboarding bei Stripe), POST /stripe/vorhandenes-konto und /stripe/aktualisieren => { stripe }.
Geprueft wird auf dem Server; Feldfehler stehen am Feld. Text nur ueber textContent (h). render(panel, ctx) wie shop-tab-*.js. */ Geprueft wird auf dem Server; Feldfehler stehen am Feld. Text nur ueber textContent (h). render(panel, ctx) wie shop-tab-*.js. */
import { abschnitt, allgemeinerFehler, feld, h, hakenFeld, setzeFehler } from './shop-dom.js' import { abschnitt, allgemeinerFehler, feld, frage, h, hakenFeld, setzeFehler } from './shop-dom.js'
import { enc, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js' import { enc, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
export const STORNO_MUSTER = 'Stornierung bis 14 Tage vor Anreise: kostenlos. Stornierung 13 bis 3 Tage vor Anreise: 50 % des Gesamtpreises. Stornierung ab 2 Tage vor Anreise oder Nichtanreise: 90 % des Gesamtpreises. Die Stornierung bedarf der Textform (z. B. E-Mail). Maßgeblich ist der Eingang bei uns.'
export const STORNO_HINWEIS = 'Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung.'
export const AUFTRAG_TEXT = 'Ich beauftrage WEBklar, in meinem Namen und auf meine Rechnung Rechnungen an meine Gäste auszustellen (§ 14 Abs. 2 Satz 4 UStG).' export const AUFTRAG_TEXT = 'Ich beauftrage WEBklar, in meinem Namen und auf meine Rechnung Rechnungen an meine Gäste auszustellen (§ 14 Abs. 2 Satz 4 UStG).'
export const AUFTRAG_WIDERRUF = 'Ohne diese Zustimmung können keine Buchungen bestätigt werden. Zustimmung wirklich zurückziehen?'
export const AUFTRAG_FEHLT = 'Ohne diesen Auftrag kannst du Buchungen nicht bestätigen, weil wir keine Rechnungen für dich ausstellen dürfen.' export const AUFTRAG_FEHLT = 'Ohne diesen Auftrag kannst du Buchungen nicht bestätigen, weil wir keine Rechnungen für dich ausstellen dürfen.'
const STRIPE_FEHLER = 'Stripe hat keine gültige Adresse geliefert. Bitte versuch es gleich noch einmal.' const STRIPE_FEHLER = 'Stripe hat keine gültige Adresse geliefert. Bitte versuch es gleich noch einmal.'
const PFLICHT = { firma: 'Bitte gib den Namen deiner Firma oder Unterkunft an.', inhaber: 'Bitte gib den Namen des Inhabers an.', strasse: 'Bitte gib Straße und Hausnummer an.', plz: 'Bitte gib die Postleitzahl an.', ort: 'Bitte gib den Ort an.', email: 'Bitte gib eine E-Mail-Adresse an.' } const PFLICHT = { firma: 'Bitte gib den Namen deiner Firma oder Unterkunft an.', inhaber: 'Bitte gib den Namen des Inhabers an.', strasse: 'Bitte gib Straße und Hausnummer an.', plz: 'Bitte gib die Postleitzahl an.', ort: 'Bitte gib den Ort an.', email: 'Bitte gib eine E-Mail-Adresse an.' }
@@ -44,23 +43,31 @@ export async function render(panel, ctx = {}) {
const praefix = textfeld({ maxlength: 8, placeholder: 'RE' }); praefix.value = zeichen(e.rechnungsPraefix) || 'RE' const praefix = textfeld({ maxlength: 8, placeholder: 'RE' }); praefix.value = zeichen(e.rechnungsPraefix) || 'RE'
if (rechnungenVorhanden) { praefix.disabled = true; praefix.setAttribute('aria-disabled', 'true') } if (rechnungenVorhanden) { praefix.disabled = true; praefix.setAttribute('aria-disabled', 'true') }
const auftrag = h('input', { type: 'checkbox' }); auftrag.checked = e.rechnungAuftrag === true const auftrag = h('input', { type: 'checkbox' }); auftrag.checked = e.rechnungAuftrag === true
const iban = textfeld({ maxlength: 40 }); iban.value = zeichen(e.iban) const iban = textfeld({ maxlength: 34 }); iban.value = zeichen(e.iban)
const bic = textfeld({ maxlength: 11 }); bic.value = zeichen(e.bic) const bic = textfeld({ maxlength: 11 }); bic.value = zeichen(e.bic)
const bank = textfeld({ maxlength: 80 }); bank.value = zeichen(e.bank) const bank = textfeld({ maxlength: 80 }); bank.value = zeichen(e.bank)
const akzent = h('input', { type: 'color' }); akzent.value = /^#[0-9a-fA-F]{6}$/.test(zeichen(e.akzentfarbe)) ? e.akzentfarbe : '#111111' const akzent = h('input', { type: 'color' }); akzent.value = /^#[0-9a-fA-F]{6}$/.test(zeichen(e.akzentfarbe)) ? e.akzentfarbe : '#111111'
const storno = h('textarea', { rows: 6, maxlength: 5000 }); storno.value = zeichen(e.stornobedingungen)
const online = h('input', { type: 'checkbox' }); online.checked = e.onlineZahlung === true && stripe.status === 'aktiv' const online = h('input', { type: 'checkbox' }); online.checked = e.onlineZahlung === true && stripe.status === 'aktiv'
const auftragHinweis = h('div', { class: 'sh-box sh-box-warn', role: 'note', 'data-hinweis': 'auftrag' }, AUFTRAG_FEHLT) const auftragHinweis = h('div', { class: 'sh-box sh-box-warn', role: 'note', 'data-hinweis': 'auftrag' }, AUFTRAG_FEHLT)
const zeigeAuftrag = () => { auftragHinweis.hidden = auftrag.checked } const zeigeAuftrag = () => { auftragHinweis.hidden = auftrag.checked }
auftrag.addEventListener('change', zeigeAuftrag) // Widerruf einer gespeicherten Zustimmung nur nach Rueckfrage in der Seite
const widerrufPlatz = h('div', { 'data-widerruf-platz': '' })
auftrag.addEventListener('change', () => {
if (!auftrag.checked && e.rechnungAuftrag === true && !widerrufPlatz.firstChild) {
auftrag.checked = true
const zu = () => { widerrufPlatz.replaceChildren(); auftrag.focus() }
const box = frage('Zustimmung zurückziehen', h('p', null, AUFTRAG_WIDERRUF),
h('div', { class: 'sh-aktionen' },
h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'widerruf-ja', onclick: () => { auftrag.checked = false; zeigeAuftrag(); zu() } }, 'Ja, zurückziehen'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'widerruf-nein', onclick: zu }, 'Abbrechen')))
widerrufPlatz.replaceChildren(box)
box.focus()
}
zeigeAuftrag()
})
zeigeAuftrag() zeigeAuftrag()
const musterKnopf = h('button', {
type: 'button', class: 'kb-btn-outline', 'data-aktion': 'storno-muster',
onclick: () => { storno.value = STORNO_MUSTER },
}, 'Mustertext übernehmen')
/* ------------------------------------------------------------------ Stripe --- */ /* ------------------------------------------------------------------ Stripe --- */
let beschaeftigt = false let beschaeftigt = false
async function stripeAktion(pfad, { weiter = false } = {}) { async function stripeAktion(pfad, { weiter = false } = {}) {
@@ -116,15 +123,13 @@ export async function render(panel, ctx = {}) {
? 'Der Präfix ist gesperrt, weil es schon Rechnungen gibt. Die Nummern bleiben so lückenlos.' ? 'Der Präfix ist gesperrt, weil es schon Rechnungen gibt. Die Nummern bleiben so lückenlos.'
: '2 bis 8 Zeichen (A–Z, 0–9). Die Nummern lauten dann zum Beispiel RE-2026-0001. Du kannst den Präfix nur vor der ersten Rechnung ändern.'), : '2 bis 8 Zeichen (A–Z, 0–9). Die Nummern lauten dann zum Beispiel RE-2026-0001. Du kannst den Präfix nur vor der ersten Rechnung ändern.'),
hakenFeld('rechnungAuftrag', auftrag, AUFTRAG_TEXT), hakenFeld('rechnungAuftrag', auftrag, AUFTRAG_TEXT),
auftragHinweis), auftragHinweis,
widerrufPlatz),
abschnitt('Bankverbindung (freiwillig)', 'Erscheint als Hinweis, wenn Gäste vor Ort oder per Überweisung zahlen.', abschnitt('Bankverbindung (freiwillig)', 'Erscheint als Hinweis, wenn Gäste vor Ort oder per Überweisung zahlen.',
feld('iban', 'IBAN', iban), feld('iban', 'IBAN', iban),
feld('bic', 'BIC', bic), feld('bic', 'BIC', bic),
feld('bank', 'Name der Bank', bank)), feld('bank', 'Name der Bank', bank)),
abschnitt('Darstellung', null, feld('akzentfarbe', 'Akzentfarbe', akzent, 'Diese Farbe bekommen Knöpfe auf deiner Buchungsseite.')), abschnitt('Darstellung', null, feld('akzentfarbe', 'Akzentfarbe', akzent, 'Diese Farbe bekommen Knöpfe auf deiner Buchungsseite.')),
abschnitt('Stornobedingungen', 'Sie stehen auf der Buchungsseite und gelten für neue Freigaben.',
feld('stornobedingungen', 'Stornobedingungen', storno, STORNO_HINWEIS),
h('div', { class: 'sh-aktionen' }, musterKnopf)),
status === 'aktiv' status === 'aktiv'
? abschnitt('Online-Zahlung', null, hakenFeld('onlineZahlung', online, 'Online-Zahlung anbieten', 'Gäste können bei der Buchung per Stripe bezahlen.')) ? abschnitt('Online-Zahlung', null, hakenFeld('onlineZahlung', online, 'Online-Zahlung anbieten', 'Gäste können bei der Buchung per Stripe bezahlen.'))
: null, : null,
@@ -149,7 +154,7 @@ export async function render(panel, ctx = {}) {
email: email.value.trim(), telefon: telefon.value.trim(), steuernummer: steuernummer.value.trim(), ustId: ustId.value.trim(), email: email.value.trim(), telefon: telefon.value.trim(), steuernummer: steuernummer.value.trim(), ustId: ustId.value.trim(),
iban: iban.value.trim(), bic: bic.value.trim(), bank: bank.value.trim(), iban: iban.value.trim(), bic: bic.value.trim(), bank: bank.value.trim(),
rechnungsPraefix: rechnungenVorhanden ? zeichen(e.rechnungsPraefix) || 'RE' : p, rechnungsPraefix: rechnungenVorhanden ? zeichen(e.rechnungsPraefix) || 'RE' : p,
rechnungAuftrag: auftrag.checked, akzentfarbe: akzent.value, stornobedingungen: storno.value.trim(), rechnungAuftrag: auftrag.checked, akzentfarbe: akzent.value,
onlineZahlung: status === 'aktiv' && online.checked, onlineZahlung: status === 'aktiv' && online.checked,
}, },
} }
+14 -2
View File
@@ -5,6 +5,7 @@
Helfer: shop-dom.js (h, request, Karten), Styles: buchungen.css (bk-) und shop.css (sh-). Fremdtext nur ueber textContent. */ Helfer: shop-dom.js (h, request, Karten), Styles: buchungen.css (bk-) und shop.css (sh-). Fremdtext nur ueber textContent. */
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js' import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
import { statusKarte } from './funktion-bestellen.js'
import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js' import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js'
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */ /** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
@@ -74,6 +75,7 @@ function projektwahl(s) {
select.addEventListener('change', () => { select.addEventListener('change', () => {
auswahl.projectId = select.value auswahl.projectId = select.value
s.projekt = s.projects.find((p) => p.projectId === select.value) s.projekt = s.projects.find((p) => p.projectId === select.value)
s.neueAnfrage = false
zeigeMeldung(s, '') zeigeMeldung(s, '')
zeigeProjekt(s) zeigeProjekt(s)
}) })
@@ -113,6 +115,7 @@ function zeichneAnsicht(s, { status, aktuell }) {
s.status = status s.status = status
const zustand = status?.state || zustandVon(s.projekt) const zustand = status?.state || zustandVon(s.projekt)
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand }) if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
else if (zustand === 'ordered') zeigeBestellung(s)
else if (zustand === 'off') zeigeAnfrage(s, { status, aktuell }) else if (zustand === 'off') zeigeAnfrage(s, { status, aktuell })
else s.buehne.replaceChildren(infoKarte('Buchungskalender', 'Der Stand deines Buchungskalenders ist gerade nicht lesbar. Bitte lade die Seite neu.')) else s.buehne.replaceChildren(infoKarte('Buchungskalender', 'Der Stand deines Buchungskalenders ist gerade nicht lesbar. Bitte lade die Seite neu.'))
} }
@@ -121,7 +124,8 @@ function zeichneAnsicht(s, { status, aktuell }) {
function zeigeAnfrage(s, { status, aktuell }) { function zeigeAnfrage(s, { status, aktuell }) {
const projectId = s.projekt.projectId const projectId = s.projekt.projectId
const ansicht = status?.anfrage const ansicht = status?.anfrage
const neuLaden = () => zeigeProjekt(s) // nach einem 409 bleibt die Meldung des Servers sichtbar, waehrend die Ansicht neu laedt
const neuLaden = async (o) => { await zeigeProjekt(s); if (o?.meldung && S === s) zeigeMeldung(s, o.meldung, 'fehler') }
if (ansicht?.status === 'angenommen') { if (ansicht?.status === 'angenommen') {
s.buehne.replaceChildren(infoKarte('Deine Anfrage wurde angenommen', 'Wir melden uns bei dir mit den nächsten Schritten zur Bestellung.', { chat: true })) s.buehne.replaceChildren(infoKarte('Deine Anfrage wurde angenommen', 'Wir melden uns bei dir mit den nächsten Schritten zur Bestellung.', { chat: true }))
} else if (ansicht?.status === 'offen') { } else if (ansicht?.status === 'offen') {
@@ -135,11 +139,19 @@ function zeigeAnfrage(s, { status, aktuell }) {
projectId, projectId,
vorgabe: ansicht?.anfrage || null, vorgabe: ansicht?.anfrage || null,
preis: textOr(s.projekt?.features?.unterkunft?.priceLabel) || '199 €', preis: textOr(s.projekt?.features?.unterkunft?.priceLabel) || '199 €',
nachSenden: async () => { s.neueAnfrage = false; if (aktuell()) await neuLaden() }, nachSenden: async (_a, o) => { s.neueAnfrage = false; if (aktuell()) await neuLaden(o) },
}) })
} }
} }
/* Zustand ordered: Statuskarte „Zahlung offen“ mit „Zahlung fortsetzen“ (generische Bestellseite, Checkout /api/funktionen/unterkunft/checkout). */
function zeigeBestellung(s) {
const eintrag = { ...s.projekt.features?.unterkunft, state: 'ordered' }
s.buehne.replaceChildren(statusKarte({ projekt: s.projekt, bestellung: eintrag, kundenansicht: false, key: 'unterkunft', name: 'Buchungskalender' }, {
weiter: async ({ meldung }) => { await zeigeProjekt(s); if (S === s) zeigeMeldung(s, meldung, 'fehler') },
}))
}
function zeigeReiter(s, { status, aktuell, zustand }) { function zeigeReiter(s, { status, aktuell, zustand }) {
const neuLaden = async () => { const neuLaden = async () => {
const neu = await ladeStatus(s.projekt.projectId) const neu = await ladeStatus(s.projekt.projectId)
+79 -7
View File
@@ -470,7 +470,8 @@ test('Kalender: vergangene Tage lassen sich nicht sperren', mit(ukServer(), asyn
/* ===================================================== Task 14: Rechnungen, Einstellungen, Anfrage ===================================================== */ /* ===================================================== Task 14: Rechnungen, Einstellungen, Anfrage ===================================================== */
const { render: renderRechnungen } = await import('../public/buchungen-tab-rechnungen.js') const { render: renderRechnungen } = await import('../public/buchungen-tab-rechnungen.js')
const { render: renderEinstellungen, STORNO_MUSTER, STORNO_HINWEIS, AUFTRAG_TEXT } = await import('../public/buchungen-tab-einstellungen.js') const { STORNO_MUSTER, STORNO_HINWEIS } = await import('../public/buchungen-preise.js')
const { render: renderEinstellungen, AUFTRAG_TEXT, AUFTRAG_WIDERRUF } = await import('../public/buchungen-tab-einstellungen.js')
const { renderAnfrageFormular, anfrageKarte } = await import('../public/buchungen-anfrage.js') const { renderAnfrageFormular, anfrageKarte } = await import('../public/buchungen-anfrage.js')
const { initBuchungen, vergissAuswahl } = await import('../public/buchungen.js') const { initBuchungen, vergissAuswahl } = await import('../public/buchungen.js')
@@ -482,7 +483,7 @@ const zeigeMit = async (fx, modul, ctxExtra = {}) => {
await fx.ruhe() await fx.ruhe()
return { panel, ctx } return { panel, ctx }
} }
const ES = { firma: 'Haus am See', inhaber: 'Mimi M.', strasse: 'Seeweg 1', plz: '78532', ort: 'Tuttlingen', email: 'mimi@example.com', telefon: '', steuernummer: '12/345/67890', ustId: '', iban: '', bic: '', bank: '', rechnungsPraefix: 'RE', akzentfarbe: '#336699', onlineZahlung: false, rechnungAuftrag: true, stornobedingungen: '' } const ES = { firma: 'Haus am See', inhaber: 'Mimi M.', strasse: 'Seeweg 1', plz: '78532', ort: 'Tuttlingen', email: 'mimi@example.com', telefon: '', steuernummer: '12/345/67890', ustId: '', iban: '', bic: '', bank: '', rechnungsPraefix: 'RE', akzentfarbe: '#336699', onlineZahlung: false, rechnungAuftrag: true }
const esServer = (o = {}, antwort = {}) => server({ 'GET /api/unterkunft/einstellungen': () => ({ body: { einstellungen: ES, stripe: { status: 'keins', vorhandenesShopKonto: false }, rechnungenVorhanden: false, ...antwort } }), 'PUT /api/unterkunft/einstellungen': () => ({ body: {} }), ...o }) const esServer = (o = {}, antwort = {}) => server({ 'GET /api/unterkunft/einstellungen': () => ({ body: { einstellungen: ES, stripe: { status: 'keins', vorhandenesShopKonto: false }, rechnungenVorhanden: false, ...antwort } }), 'PUT /api/unterkunft/einstellungen': () => ({ body: {} }), ...o })
test('Rechnungen: Liste mit PDF-Link, Art und Betrag; Fremdtext nur als Text', mit(server({ test('Rechnungen: Liste mit PDF-Link, Art und Betrag; Fremdtext nur als Text', mit(server({
@@ -561,18 +562,43 @@ test('Einstellungen: Präfix prüfen; gesperrt mit Hinweis, sobald Rechnungen ex
})() })()
}) })
test('Einstellungen: Mustertext übernehmen füllt die Stornobedingungen, Hinweis „keine Rechtsberatung“', mit(esServer(), async (fx) => { test('Einstellungen: keine Stornobedingungen mehr; IBAN maxlength 34', mit(esServer(), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen) const { panel } = await zeigeMit(fx, renderEinstellungen)
const t = fx.finde(panel, '[data-feld="stornobedingungen"]') assert.equal(fx.finde(panel, '[data-feld="stornobedingungen"]'), null)
assert.equal(t.value, '') assert.equal(fx.finde(panel, '[data-feld="iban"]').getAttribute('maxlength'), '34')
assert.match(fx.text(t.parentNode), new RegExp(STORNO_HINWEIS.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal('stornobedingungen' in posts(fx)[0].body, false)
}))
test('Preis-Editor: Mustertext übernehmen füllt nur das Feld (kein Speichern), Hinweis „keine Rechtsberatung“', mit(ukServer(), async (fx) => {
const panel = await preisEditor(fx)
const t = fx.finde(panel, '#bk-p-storno')
assert.match(fx.text(fx.finde(panel, '[data-hinweis="storno-muster"]')), /Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung\./)
assert.equal(STORNO_HINWEIS, 'Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung.')
const vorher = posts(fx).length
await fx.klick(fx.finde(panel, '[data-aktion="storno-muster"]')) await fx.klick(fx.finde(panel, '[data-aktion="storno-muster"]'))
assert.equal(t.value, STORNO_MUSTER) assert.equal(t.value, STORNO_MUSTER)
assert.match(STORNO_MUSTER, /^Stornierung bis 14 Tage vor Anreise: kostenlos\./) assert.match(STORNO_MUSTER, /^Stornierung bis 14 Tage vor Anreise: kostenlos\./)
assert.match(STORNO_MUSTER, /Maßgeblich ist der Eingang bei uns\.$/) assert.match(STORNO_MUSTER, /Maßgeblich ist der Eingang bei uns\.$/)
assert.equal(posts(fx).length, vorher)
}))
test('Einstellungen: Zustimmung zurückziehen fragt nach; Abbrechen behält, Ja sendet rechnungAuftrag:false', mit(esServer(), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
const box = fx.finde(panel, '[data-feld="rechnungAuftrag"]')
await fx.klick(box)
assert.ok(box.checked)
assert.match(fx.text(panel), new RegExp(AUFTRAG_WIDERRUF.slice(0, 50)))
await fx.klick(fx.finde(panel, '[data-aktion="widerruf-nein"]'))
assert.ok(box.checked)
assert.equal(fx.finde(panel, '[data-aktion="widerruf-ja"]'), null)
await fx.klick(box)
await fx.klick(fx.finde(panel, '[data-aktion="widerruf-ja"]'))
assert.equal(box.checked, false)
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]')) await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe() await fx.ruhe()
assert.equal(posts(fx)[0].body.stornobedingungen, STORNO_MUSTER) assert.equal(posts(fx)[0].body.rechnungAuftrag, false)
})) }))
test('Einstellungen: Auftrag zur Rechnungsstellung – ohne Haken Hinweis, mit Haken weg und rechnungAuftrag im PUT', mit(esServer({}, { einstellungen: { ...ES, rechnungAuftrag: false } }), async (fx) => { test('Einstellungen: Auftrag zur Rechnungsstellung – ohne Haken Hinweis, mit Haken weg und rechnungAuftrag im PUT', mit(esServer({}, { einstellungen: { ...ES, rechnungAuftrag: false } }), async (fx) => {
@@ -708,3 +734,49 @@ test('Ansicht #buchungen: Zustand off zeigt das Anfrageformular, Projekt vom Wun
})() })()
vergissAuswahl() vergissAuswahl()
}) })
test('Ansicht #buchungen: Zustand ordered zeigt „Zahlung offen“ mit Zahlung fortsetzen (Checkout unterkunft)', async () => {
vergissAuswahl()
await mit((a) => {
if (a.url === '/api/funktionen') return { body: { unterkunft: { verfuegbar: true }, projects: [{ projectId: 'p1', projectName: 'A', features: { unterkunft: { state: 'ordered' } } }] } }
if (a.url.startsWith('/api/unterkunft/status')) return { body: { state: 'ordered' } }
if (a.url === '/api/funktionen/unterkunft/checkout') return { status: 409, body: { error: 'Bitte stimme zu.', code: 'andere' } }
return { body: {} }
}, async (fx) => {
const root = h('div')
fx.document.body.append(root)
await initBuchungen(root)
await fx.ruhe()
assert.match(fx.text(root), /Zahlung offen/)
await fx.klick(fx.finde(root, '[data-aktion="fortsetzen"]'))
await fx.ruhe()
assert.ok(posts(fx).some((a) => a.url === '/api/funktionen/unterkunft/checkout'))
})()
vergissAuswahl()
})
test('Anfrageformular: 409 lädt die Ansicht neu, die Meldung bleibt sichtbar; Projektwechsel setzt „neue Anfrage“ zurück', async () => {
vergissAuswahl()
let status = { state: 'off', anfrage: { status: 'abgelehnt', begruendung: 'Nein' } }
await mit((a) => {
if (a.url === '/api/funktionen') return { body: { unterkunft: { verfuegbar: true }, projects: [{ projectId: 'p1', projectName: 'A', features: { unterkunft: { state: 'off' } } }, { projectId: 'p2', projectName: 'B', features: { unterkunft: { state: 'off' } } }] } }
if (a.url.startsWith('/api/unterkunft/status')) return { body: status }
if (a.method === 'POST') { status = { state: 'off', anfrage: { status: 'offen', anfrage: {} } }; return { status: 409, body: { fehler: 'anfrage_laeuft' } } }
return { body: {} }
}, async (fx) => {
const root = h('div')
fx.document.body.append(root)
await initBuchungen(root)
await fx.ruhe()
await fx.klick(fx.finde(root, '[data-aktion="neue-anfrage"]'))
await fx.klick(fx.finde(root, '[data-aktion="anfrage-senden"]'))
await fx.ruhe()
assert.ok(fx.finde(root, '[data-anfrage="offen"]'))
assert.match(fx.text(fx.finde(root, '.sh-meldung')), /nicht geklappt/)
status = { state: 'off', anfrage: { status: 'abgelehnt', begruendung: 'Nein' } }
await fx.setze(fx.finde(root, '#buchungen-project'), 'p2')
await fx.ruhe()
assert.ok(fx.finde(root, '[data-anfrage="abgelehnt"]'))
})()
vergissAuswahl()
})