145 lines
10 KiB
JavaScript
145 lines
10 KiB
JavaScript
/* Kundenansicht neu (P2): Bestellseite je Abo (#bestellen-hosting, #bestellen-wartung), Text und Logik 1:1 aus app.js renderOrderView
|
||
(Fassung 5, Sie-Form, C14). Die Bestellübersicht steht unmittelbar über dem Bestellknopf (§ 312j BGB), ohne Klappe dazwischen.
|
||
Ein geänderter Satz wäre eine unbemerkte Rechtsänderung: das Wortlaut-Orakel vergleicht den sichtbaren Text Zeichen für Zeichen.
|
||
Neu sind nur der entfallene Seitenkopf (Titel zeichnet die Hülle), der Titel-Hinweis und die Sperre im Ansichtsmodus. */
|
||
|
||
import { api } from './api.js'
|
||
import { h, leeren } from './dom.js'
|
||
import { K, ladeAbo, sichereAdresse, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||
|
||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||
const BEDINGUNGEN = '/abo-bedingungen.html'
|
||
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
|
||
const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreiben Sie uns gern im Chat.'
|
||
const link = (href, text) => h('a', { href, target: '_blank', rel: 'noopener' }, text)
|
||
|
||
/** 'hosting' | 'wartung' aus der Route, sonst aus dem Hash (#bestellen-hosting), sonst ''. */
|
||
export function produktAusRoute(route, hash = (typeof window !== 'undefined' ? window.location?.hash : '') || '') {
|
||
if (route?.produkt === 'hosting' || route?.produkt === 'wartung') return route.produkt
|
||
const treffer = /bestellen-(hosting|wartung)/.exec(String(hash || ''))
|
||
return treffer ? treffer[1] : ''
|
||
}
|
||
|
||
const produktVon = (daten, key) => (daten?.products || []).find((p) => p.key === key) || null
|
||
const labelVon = (daten, key) => (produktVon(daten, key)?.name || (key === 'hosting' ? 'Hosting-Abo' : 'Wartungs-Abo')).replace(/^WEBklar\s+/, '')
|
||
const reihe = (titel, ...inhalt) => h('tr', null, h('td', null, titel), ...inhalt)
|
||
|
||
function bestellung(daten, productKey, viewAs) {
|
||
const product = produktVon(daten, productKey)
|
||
const offer = daten.offers?.[productKey] || null
|
||
const price = fmtEuro(product.amountCents / 100)
|
||
const lateStart = daten.lateStartAt ? fmtDatum(daten.lateStartAt) : ''
|
||
// Betrag der ersten Abbuchung: Angebotspreis während des Rabatts, sonst Katalogpreis
|
||
const offerMonthly = Number(offer?.monthlyPriceCents)
|
||
const offerRegular = Number(offer?.regularPriceCents) || product.amountCents
|
||
const offerMonths = Number(offer?.months)
|
||
const hasOfferNumbers = Boolean(offer) && Number.isFinite(offerMonthly) && offer.monthlyPriceCents != null && offerMonths > 0
|
||
const firstCents = hasOfferNumbers ? offerMonthly : product.amountCents
|
||
const firstAmount = fmtEuro(firstCents / 100)
|
||
|
||
const start = (wert, fett, rest) => h('label', { class: 'kn-check' },
|
||
h('input', { type: 'radio', name: 'abo-start-choice', value: wert, disabled: viewAs }),
|
||
h('span', null, h('strong', null, fett), rest))
|
||
const dueZelle = h('td', { id: 'abo-due-summary' }, 'Bitte oben wählen: sofort oder nach Ablauf der 14-tägigen Widerrufsfrist')
|
||
const startZelle = h('td', { id: 'abo-start-summary' }, `Bitte oben wählen: sofort oder nach Ablauf der 14-tägigen Widerrufsfrist${lateStart ? ` (am ${lateStart})` : ''}`)
|
||
const terms = h('input', { type: 'checkbox', id: 'abo-terms', disabled: viewAs })
|
||
const fehler = h('p', { class: K.fehler, id: 'abo-error', hidden: true })
|
||
const kaufen = h('button', { type: 'button', class: K.primaer, id: 'abo-buy', disabled: viewAs }, 'Zahlungspflichtig bestellen')
|
||
const zeigeFehler = (text) => { fehler.textContent = text; fehler.hidden = false }
|
||
|
||
const rootInfo = h('div', { class: 'kn-panel', id: 'abo-order' },
|
||
h('h3', null, 'Ihre Bestellung'),
|
||
daten.testAccount ? h('div', { class: 'kn-meldung kn-meldung-warn' }, `Testkonto: Bei Stripe wird ein Test-Rabatt verrechnet, abgebucht werden nur 0,50 € pro Monat.${offer ? ' Das Angebot wird nach dem Abschluss zusätzlich ans Abo gehängt.' : ''}`) : null,
|
||
h('label', { class: 'kn-check' }, terms,
|
||
h('span', null, 'Ich habe die ', link(BEDINGUNGEN, 'Vertragsbedingungen mit Leistungsbeschreibung und Auftragsverarbeitungsvertrag'), ' gelesen und stimme ihnen zu. Die ', link(`${BEDINGUNGEN}#widerruf`, 'Widerrufsbelehrung'), ' habe ich zur Kenntnis genommen.')),
|
||
h('fieldset', { class: 'kn-beginn' },
|
||
h('legend', null, 'Wann sollen wir mit der Leistung beginnen?'),
|
||
start('sofort', 'Sofort.', ' Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Leistung beginnt. Mir ist bekannt, dass ich bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachten Leistungen zahle.'),
|
||
start('nach_frist', 'Nach Ablauf der 14-tägigen Widerrufsfrist', `${lateStart ? ` (am ${lateStart})` : ''}. Bis dahin erbringt WEBklar keine Leistungen und bucht nichts ab.`),
|
||
h('p', { class: 'kn-hinweis' }, 'Firmenkunden haben kein Widerrufsrecht und wählen am besten „Sofort“.')),
|
||
h('div', { class: 'kn-uebersicht' },
|
||
h('table', { class: 'kn-zusammenfassung' }, h('tbody', null,
|
||
reihe('Leistung', h('td', null, `${product.name}: ${(product.features || []).map(([t]) => t).join(', ')}`)),
|
||
reihe('Preis', h('td', null, ...(offer
|
||
? [hasOfferNumbers ? h('strong', null, `${fmtEuro(offerMonthly / 100)} pro Monat für die ersten ${offerMonths} Monate`) : null,
|
||
hasOfferNumbers ? `, danach ${fmtEuro(offerRegular / 100)} pro Monat` : null,
|
||
' ', h('span', null, `(individuelles Angebot: ${offer.text}; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)`), ', keine weiteren Kosten']
|
||
: [h('strong', null, `${price} pro Monat`), ' (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), keine weiteren Kosten']))),
|
||
reihe('Abrechnung', h('td', null, 'monatlich im Voraus, erstmals zum Leistungsbeginn')),
|
||
reihe('Fällig', dueZelle),
|
||
reihe('Leistungsbeginn', startZelle),
|
||
reihe('Laufzeit', h('td', null, h('strong', null, `Mindestlaufzeit ${product.minTermMonths} Monate`), ' ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar')),
|
||
reihe('Zahlung', h('td', null, 'Kreditkarte oder SEPA-Lastschrift über Stripe')),
|
||
reihe('Anbieter', h('td', null, 'WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler · Telefon 0176 23726355 · support@webklar.com')))),
|
||
h('p', { class: 'kn-hinweis' }, 'Es gilt das gesetzliche Mängelhaftungsrecht. Einzelheiten: ', link(BEDINGUNGEN, 'Vertragsbedingungen'), ' · ', link('/datenschutz.html', 'Datenschutz'))),
|
||
fehler,
|
||
h('div', { class: 'kn-aktionen' }, kaufen,
|
||
h('span', { class: 'kn-hinweis' }, 'Danach hinterlegen Sie bei unserem Zahlungsdienstleister Stripe Ihre Zahlungsart. Bis zum Abschluss dort können Sie jederzeit abbrechen.'),
|
||
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null))
|
||
|
||
// Die zwei Zeilen "Fällig" und "Leistungsbeginn" folgen der Wahl
|
||
rootInfo.querySelectorAll('input[name="abo-start-choice"]').forEach((radio) => radio.addEventListener('change', () => {
|
||
dueZelle.textContent = radio.value === 'sofort'
|
||
? `Heute fällig: ${firstAmount} (gemäß § 19 UStG ohne Umsatzsteuer)`
|
||
: `Erste Abbuchung am ${lateStart || 'Ende der 14-tägigen Widerrufsfrist'}: ${firstAmount} (gemäß § 19 UStG ohne Umsatzsteuer); heute wird nichts abgebucht`
|
||
startZelle.textContent = radio.value === 'sofort'
|
||
? 'sofort, auf Ihren ausdrücklichen Wunsch vor Ablauf der Widerrufsfrist'
|
||
: `nach Ablauf der 14-tägigen Widerrufsfrist${lateStart ? ` (am ${lateStart})` : ''}; bis dahin keine Leistung und keine Abbuchung`
|
||
}))
|
||
|
||
kaufen.addEventListener('click', async () => {
|
||
if (viewAs) return
|
||
fehler.hidden = true
|
||
const acceptTerms = terms.checked
|
||
const startChoice = rootInfo.querySelector('input[name="abo-start-choice"]:checked')?.value || ''
|
||
if (!acceptTerms) { zeigeFehler('Bitte stimmen Sie den Vertragsbedingungen zu.'); return }
|
||
if (!startChoice) { zeigeFehler('Bitte wählen Sie, wann wir mit der Leistung beginnen sollen.'); return }
|
||
kaufen.disabled = true
|
||
kaufen.textContent = 'Weiter zur Zahlung…'
|
||
try {
|
||
const { url } = await api('/api/abo/checkout', {
|
||
method: 'POST',
|
||
body: { productKey, acceptTerms, startChoice, termsVersion: daten.termsVersion, offerId: offer?.id || '', lateStartAt: daten.lateStartAt || '' },
|
||
})
|
||
const ziel = sichereAdresse(url)
|
||
if (!ziel) throw new Error('Die Zahlungsseite konnte nicht geöffnet werden. Bitte später noch einmal versuchen.')
|
||
window.location.href = ziel
|
||
} catch (f) {
|
||
zeigeFehler(f.message)
|
||
kaufen.disabled = false
|
||
kaufen.textContent = 'Zahlungspflichtig bestellen'
|
||
}
|
||
})
|
||
|
||
const angebotKasten = offer
|
||
? h('div', { class: 'kn-angebot-kasten' },
|
||
h('strong', null, 'Ihr persönliches Angebot'),
|
||
h('p', null, `${offer.text}. Sie sparen ${fmtEuro(offer.savingsCents / 100)}.`),
|
||
offer.note ? h('p', { class: 'kn-hinweis' }, offer.note) : null,
|
||
h('p', { class: 'kn-hinweis' }, `Gültig bis ${fmtDatum(offer.validUntil)} · wird beim Abschluss automatisch berücksichtigt`))
|
||
: null
|
||
const leistungen = h('div', { class: 'kn-panel' },
|
||
h('ul', { class: 'kn-leistungen' }, (product.features || []).map(([titel, detail]) => h('li', null, titel, h('small', null, detail)))),
|
||
h('p', { class: 'kn-hinweis' }, 'Schon bei einem anderen Anbieter für Domain, Hosting oder E-Mail bezahlt? Nach dem Abschluss können Sie eine Übernahmegutschrift beantragen – wir rechnen die doppelt bezahlte Zeit anteilig an.'))
|
||
return [angebotKasten, leistungen, rootInfo]
|
||
}
|
||
|
||
/** Bestellseite. Das Produkt kommt aus route.produkt, hilfsweise aus dem Hash; daten (GET /api/abo) ist optional. */
|
||
export async function zeigeBestellen(wurzel, { route, me, nav, daten } = {}) {
|
||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||
laeufe.set(wurzel, nr)
|
||
const aktuell = () => laeufe.get(wurzel) === nr
|
||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||
const stand = daten ?? await ladeAbo()
|
||
if (!aktuell()) return
|
||
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
|
||
const key = produktAusRoute(route)
|
||
const product = key ? produktVon(stand, key) : null
|
||
if (key) meldeTitel(wurzel, labelVon(stand, key))
|
||
if (!product || !stand.available) {
|
||
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR)))
|
||
return
|
||
}
|
||
leeren(wurzel).append(...bestellung(stand, key, me?.viewAs === true).filter(Boolean))
|
||
}
|