Buchungskalender: Stornobedingungen je Unterkunft, Zustimmung und Bestellstatus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
efeb99e214
commit
05006f1480
5 files changed
+123
-29
No files matched your search
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
Reference in new issue
Block a user