Files
Webklar-Kundenbereich/public/neu/ansicht-wechsel.js
T

184 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Kundenansicht neu (P2): Wechsel vom Hosting-Abo ins Wartungs-Abo (#wechsel), Text und Logik 1:1 aus app.js renderSwitchView
(Fassung 5, Sie-Form, C14). Vorschau vom Server, Bestellübersicht unmittelbar über dem Knopf (§ 312j BGB), Zustimmung, Bestellung.
Der Wechsel wirkt erst mit der Zahlung der Wechselrechnung (Bedingungen § 9 Abs. 3). Das Wortlaut-Orakel prüft den sichtbaren Text.
Neu: die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen. */
import { api } from './api.js'
import { h, leeren } from './dom.js'
import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
const SCHLUESSEL = 'kn-wechsel-funktion'
const BEDINGUNGEN = '/abo-bedingungen.html'
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
const link = (href, text) => h('a', { href, target: '_blank', rel: 'noopener' }, text)
const reihe = (titel, ...inhalt) => h('tr', null, h('td', null, titel), h('td', null, ...inhalt))
/** Die von P3 gemerkte Funktion { key, projectId } oder null (kaputter oder fremder Wert, kein sessionStorage). */
export function gemerkteFunktion() {
try {
const wert = JSON.parse(globalThis.sessionStorage?.getItem(SCHLUESSEL) ?? 'null')
return wert && typeof wert.key === 'string' && wert.key && typeof wert.projectId === 'string' && wert.projectId ? { key: wert.key, projectId: wert.projectId } : null
} catch {
return null
}
}
function vergissFunktion() {
try { globalThis.sessionStorage?.removeItem(SCHLUESSEL) } catch { /* ohne Speicher bleibt nichts liegen */ }
}
// Zurück aus dem Bfcache (z. B. von der Zahlungsseite): eine hängende Bestellung nicht gesperrt lassen
let aktiv = null
const mitPageshow = new WeakSet()
function registrierePageshow() {
const win = window
if (mitPageshow.has(win)) return
mitPageshow.add(win)
win.addEventListener('pageshow', () => { if (aktiv?.busy) { aktiv.busy = false; aktiv.zeichne() } })
}
export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
const nr = (laeufe.get(wurzel) ?? 0) + 1
laeufe.set(wurzel, nr)
const aktuell = () => laeufe.get(wurzel) === nr
const viewAs = me?.viewAs === true
meldeTitel(wurzel, 'Wechsel ins Wartungs-Abo')
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
registrierePageshow()
const stand = daten ?? await ladeAbo()
if (!aktuell()) return
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
const sub = stand.subscription
const wartung = (stand.products || []).find((p) => p.key === 'wartung') || null
if (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch || !wartung) {
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, 'Ein Wechsel ist derzeit nicht möglich. Details finden Sie unter ', h('a', zuAnsicht('vertraege'), 'Verträge'), '.')))
navigiere('#vertraege', { ersetzen: true })
return
}
const units = Number(sub.storage?.units) || 0
const mail = stand.mailStorage || {}
const unitCents = Number(mail.unitPriceCents) || 0
const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, terms: false, early: false, fehler: '', zeichne: null }
aktiv = lauf
const zeichne = () => {
const p = lauf.preview
const rows = []
if (p) {
const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents
rows.push(reihe('Wartungs-Abo', h('strong', null, `${fmtEuro(monthly / 100)} pro Monat`), ' (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer); enthalten sind Pflege Ihrer Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat'))
if (units > 0) {
rows.push(reihe('Zusatzspeicher', `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (gemäß § 19 UStG ohne Umsatzsteuer), insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat`))
}
if ((Number(p.creditCents) || 0) > 0) {
rows.push(reihe('Gutschrift für nicht genutzte Tage', `−${fmtEuro(p.creditCents / 100)} aus dem bereits bezahlten Hosting-Abo (gemäß § 19 UStG ohne Umsatzsteuer)`))
}
rows.push(reihe('Heute fällig', h('strong', null, fmtEuro(p.amountDueCents / 100)), ' (gemäß § 19 UStG ohne Umsatzsteuer)'))
const tag = berlinTag(p.nextChargeAt)
rows.push(reihe('Neuer Abbuchungstag', `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}`))
rows.push(reihe('Laufzeit', h('strong', null, `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)}`), ' (12 Monate ab dem Wechsel), danach monatlich kündbar'))
rows.push(reihe('Beginn', 'Der Wechsel wird erst mit der Zahlung wirksam. Bei Kreditkarte, sobald die Zahlung erfolgreich ist (ggf. nach Bestätigung bei Ihrer Bank); bei SEPA-Lastschrift sofort mit der Bestellung, wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. Einen späteren Beginn gibt es nicht.'))
}
const uebersicht = lauf.error
? h('p', { class: K.fehler }, lauf.error)
: lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Ihre Übersicht wird berechnet …')
: h('table', { class: 'kn-zusammenfassung' }, h('tbody', null, rows))
const sperre = lauf.busy || viewAs
const terms = h('input', { type: 'checkbox', id: 'switch-terms', checked: lauf.terms, disabled: sperre, on: { change: (e) => { lauf.terms = e.currentTarget.checked } } })
const early = h('input', { type: 'checkbox', id: 'switch-early', checked: lauf.early, disabled: sperre, on: { change: (e) => { lauf.early = e.currentTarget.checked } } })
leeren(wurzel).append(h('div', { class: 'kn-panel', id: 'switch-order' },
h('h3', null, 'Ihre Bestellung'),
h('p', { class: 'kn-hinweis' }, 'Zusätzlich zum Hosting: Pflege Ihrer Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.'),
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('label', { class: 'kn-check' }, early,
h('span', null, h('strong', null, 'Sofortbeginn.'), ' Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Wartungs-Abo beginnt. Mir ist bekannt, dass ich bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachten Leistungen zahle (§ 357a Abs. 2 BGB; siehe Hinweise zum Widerruf). Firmenkunden haben kein Widerrufsrecht.')),
h('div', { class: 'kn-uebersicht' }, uebersicht,
h('p', { class: 'kn-hinweis' }, 'Es gilt das gesetzliche Mängelhaftungsrecht. Einzelheiten: ', link(BEDINGUNGEN, 'Vertragsbedingungen'), ' · ', link('/datenschutz.html', 'Datenschutz'))),
h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', hidden: !lauf.fehler }, lauf.fehler),
h('div', { class: 'kn-aktionen' },
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : 'Zahlungspflichtig bestellen'),
h('span', { class: 'kn-hinweis' }, 'Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart. Bis zum Klick können Sie abbrechen.'),
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null)))
}
lauf.zeichne = zeichne
const ladeVorschau = async () => {
const token = ++lauf.token
lauf.loading = true
lauf.error = ''
zeichne()
try {
const p = await api('/api/abo/switch/preview', { method: 'POST', body: {} })
if (token !== lauf.token || !aktuell()) return
lauf.preview = p
} catch (f) {
if (token !== lauf.token || !aktuell()) return
lauf.preview = null
lauf.error = f.message
}
lauf.loading = false
zeichne()
}
const zeigeFehler = (text, klasse) => { lauf.fehler = text; lauf.fehlerKlasse = klasse; zeichne() }
async function absenden() {
if (viewAs || lauf.busy || !lauf.preview) return
lauf.fehler = ''
if (!lauf.terms) { zeigeFehler('Bitte stimmen Sie den Vertragsbedingungen zu.'); return }
if (!lauf.early) { zeigeFehler('Bitte bestätigen Sie den Sofortbeginn, sonst können wir den Wechsel nicht ausführen.'); return }
const p = lauf.preview
const funktion = gemerkteFunktion()
const gesendet = funktion !== null
lauf.busy = true
zeichne()
try {
const body = { acceptTerms: true, earlyStart: true, termsVersion: stand.termsVersion, prorationDate: p.prorationDate, amountDueCents: p.amountDueCents }
if (gesendet) body.funktion = funktion
const res = await api('/api/abo/switch', { method: 'POST', body })
if (gesendet) vergissFunktion()
if (res.paymentUrl) {
const ziel = sichereAdresse(res.paymentUrl)
if (!ziel) {
lauf.busy = false
zeigeFehler('Die Zahlungsseite konnte nicht geöffnet werden. Der Wechsel wartet auf die Zahlung; Näheres steht unter Verträge.')
return
}
const knopf = wurzel.querySelector('#switch-buy')
if (knopf) knopf.textContent = 'Weiter zur Zahlung …'
zeigeFehler('Ihr Wechsel wartet auf die Zahlung. Wir leiten Sie zur Zahlung weiter.', 'kn-meldung')
window.location.href = ziel
return
}
lauf.busy = false
const sepa = Boolean(res.subscription?.switchPending)
const hinweis = sepa
? 'Ihr Wechsel ins Wartungs-Abo gilt ab sofort. Die Lastschrift wird noch eingezogen; wir bestätigen Ihnen den Wechsel per E-Mail.'
: 'Ihr Wechsel ins Wartungs-Abo ist abgeschlossen. Wir bestätigen ihn per E-Mail.'
meldeZaehlerNeu('vertraege')
// Mit gemerkter Funktion zurück zu den Funktionen (dort öffnet P3 den Einschalt-Ablauf), sonst wie bisher zu den Verträgen
if (gesendet) {
let zurueck = wechselFunktionAus(res.subscription)
if (!zurueck) zurueck = wechselFunktionAus((await ladeAbo())?.subscription)
if (zurueck) { navigiere('#funktionen', { ersetzen: true }); return }
}
merkeHinweis(hinweis)
navigiere('#vertraege')
} catch (f) {
lauf.busy = false
zeigeFehler(f.message)
// Betrag oder Stichtag haben sich geändert: neue Vorschau laden, Fehlerhinweis bleibt stehen
if (f.status === 409) {
const msg = f.message
await ladeVorschau()
zeigeFehler(msg)
}
}
}
await ladeVorschau()
}