184 lines
11 KiB
JavaScript
184 lines
11 KiB
JavaScript
/* 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, safeUrl, 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 = safeUrl(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()
|
||
}
|