diff --git a/public/app.js b/public/app.js index 76c883d..64f8095 100644 --- a/public/app.js +++ b/public/app.js @@ -979,6 +979,160 @@ function renderOrderView(view, data, productKey) { }) } +/* Wechsel ins Wartungs-Abo (#wechsel): Vorschau vom Server, Bestellübersicht direkt über dem Knopf (§ 312j BGB), Zustimmung, Bestellung. + Der Wechsel wirkt erst mit der Zahlung der Wechselrechnung (Bedingungen § 9 Abs. 3). */ +const switchUi = { notice: '', loadedAt: 0, busy: false } + +function renderSwitchView(view, data) { + const sub = data.subscription + const wartung = productOf(data, 'wartung') + const head = '

Wechsel ins Wartungs-Abo

Aus dem Hosting-Abo in das Wartungs-Abo wechseln.

' + if (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch || !wartung) { + switchUi.loadedAt = 0 + view.innerHTML = `${head}

Ein Wechsel ist derzeit nicht möglich. Details finden Sie unter Verträge.

` + if (view.classList.contains('active')) showView('vertraege') + return + } + const units = Number(sub.storage?.units) || 0 + const mail = data.mailStorage || {} + const unitCents = Number(mail.unitPriceCents) || 0 + const state = { preview: null, error: '', loading: true, token: 0 } + + const paint = () => { + const p = state.preview + const rows = [] + if (p) { + const monthly = wartung.amountCents + rows.push(`Wartungs-Abo${escapeHtml(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(`Zusatzspeicher${units} Paket${units === 1 ? '' : 'e'} (+${escapeHtml(String(units * (Number(mail.gbPerUnit) || 0)))} GB) bleiben gebucht: ${escapeHtml(fmtEuro(units * unitCents / 100))} pro Monat zusätzlich (gemäß § 19 UStG ohne Umsatzsteuer), insgesamt ${escapeHtml(fmtEuro((monthly + units * unitCents) / 100))} pro Monat`) + } + if ((Number(p.creditCents) || 0) > 0) { + rows.push(`Gutschrift für nicht genutzte Tage−${escapeHtml(fmtEuro(p.creditCents / 100))} aus dem bereits bezahlten Hosting-Abo (gemäß § 19 UStG ohne Umsatzsteuer)`) + } + rows.push(`Heute fällig${escapeHtml(fmtEuro(p.amountDueCents / 100))} (gemäß § 19 UStG ohne Umsatzsteuer)`) + rows.push(`Neuer AbbuchungstagAb dem Wechsel jeweils am ${escapeHtml(String(new Date(p.nextChargeAt).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: 'numeric' })))}. eines Monats; nächste Abbuchung am ${fmtDateShort(p.nextChargeAt)}`) + rows.push(`LaufzeitMindestlaufzeit bis ${fmtDateShort(p.minTermEnd)} (12 Monate ab dem Wechsel), danach monatlich kündbar`) + rows.push('BeginnDer 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 summary = state.error + ? `

${escapeHtml(state.error)}

` + : state.loading || !p ? '

Ihre Übersicht wird berechnet …

' + : `${rows.join('')}
` + const checked = (id) => view.querySelector(id)?.checked ? ' checked' : '' + const prevErr = view.querySelector('#switch-error') + const errText = prevErr && !prevErr.classList.contains('hidden') ? prevErr.textContent : '' + const busy = switchUi.busy + view.innerHTML = `${head} +
+

Ihre Bestellung

+

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.

+ + +
+ ${summary} +

Es gilt das gesetzliche Mängelhaftungsrecht. Einzelheiten: Vertragsbedingungen · Datenschutz

+
+

${escapeHtml(errText)}

+
+ + Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart. Bis zum Klick können Sie abbrechen. +
+
` + view.querySelector('#switch-buy')?.addEventListener('click', submit) + } + + const loadPreview = async () => { + const token = ++state.token + state.loading = true + state.error = '' + paint() + try { + const p = await api('/api/abo/switch/preview', { method: 'POST', body: '{}' }) + if (token !== state.token) return + state.preview = p + switchUi.loadedAt = Date.now() + } catch (err) { + if (token !== state.token) return + state.preview = null + state.error = err.message + } + state.loading = false + paint() + } + + const submit = async () => { + if (switchUi.busy || !state.preview) return + const errEl = view.querySelector('#switch-error') + hideError(errEl) + if (!view.querySelector('#switch-terms').checked) { + showError(errEl, 'Bitte stimmen Sie den Vertragsbedingungen zu.') + return + } + if (!view.querySelector('#switch-early').checked) { + showError(errEl, 'Bitte bestätigen Sie den Sofortbeginn, sonst können wir den Wechsel nicht ausführen.') + return + } + const p = state.preview + switchUi.busy = true + paint() + try { + const res = await api('/api/abo/switch', { + method: 'POST', + body: JSON.stringify({ acceptTerms: true, earlyStart: true, termsVersion: data.termsVersion, prorationDate: p.prorationDate, amountDueCents: p.amountDueCents }), + }) + if (res.paymentUrl) { + view.querySelector('#switch-buy').textContent = 'Weiter zur Zahlung …' + view.querySelector('#switch-error').classList.remove('hidden') + view.querySelector('#switch-error').classList.remove('error') + view.querySelector('#switch-error').textContent = 'Ihr Wechsel wartet auf die Zahlung. Wir leiten Sie zur Zahlung weiter.' + window.location.href = res.paymentUrl + return + } + switchUi.busy = false + const sepa = Boolean(res.subscription?.switchPending) + switchUi.notice = 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.' + if (res.subscription && aboState.data) aboState.data.subscription = res.subscription + const card = document.getElementById('abo-card') + const contracts = document.getElementById('kb-contracts') + if (aboState.data) { + if (card) renderSideCard(card, aboState.data) + if (contracts) renderContractsView(contracts, aboState.data) + renderSwitchView(view, aboState.data) + } + showView('vertraege') + contracts?.insertAdjacentHTML('afterbegin', `
${escapeHtml(switchUi.notice)}
`) + switchUi.notice = '' + } catch (err) { + switchUi.busy = false + paint() + showError(view.querySelector('#switch-error'), err.message) + // Betrag oder Stichtag haben sich geändert: neue Vorschau laden, Fehlerhinweis bleibt stehen + if (err.status === 409) { + const msg = err.message + await loadPreview() + showError(view.querySelector('#switch-error'), msg) + } + } + } + + view._switchReload = () => { if (!switchUi.busy) loadPreview() } + loadPreview() +} + +/* Wer #wechsel öffnet, ohne wechseln zu können, landet bei "Verträge"; eine alte Vorschau (> 5 Minuten) wird neu berechnet */ +document.addEventListener('kb:view', (ev) => { + if (ev.detail.name !== 'wechsel') return + const view = document.getElementById('view-wechsel') + const sub = aboState.data?.subscription + if (aboState.data && (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch)) { showView('vertraege'); return } + if (view?._switchReload && Date.now() - switchUi.loadedAt > 5 * 60 * 1000) view._switchReload() +}) + /* Ansicht "Wartungs-Abo": Stundenkonto, Verlauf, Vertrag (Bedingungen § 4: jederzeit im Portal einsehbar) */ function renderHoursView(view, data) { const sub = data.subscription @@ -1312,6 +1466,8 @@ async function loadContracts({ afterCheckout = false } = {}) { const orderView = document.getElementById(`view-bestellen-${key}`) if (orderView) renderOrderView(orderView, data, key) } + const switchView = document.getElementById('view-wechsel') + if (switchView) renderSwitchView(switchView, data) if (data.subscription?.active && data.subscription.productKey === 'wartung') renderHoursView(view, data) else view.innerHTML = '

Stundenkonto

Ein Stundenkonto gibt es im Wartungs-Abo. Ihre Verträge finden Sie unter Verträge.

' card.classList.remove('hidden') diff --git a/public/kundenbereich.css b/public/kundenbereich.css index b4c75d4..ef3a821 100644 --- a/public/kundenbereich.css +++ b/public/kundenbereich.css @@ -745,8 +745,8 @@ button.kb-avatar:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } .abo-summary td:first-child { margin-bottom: 3px; font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--kb-faint); } - #abo-order .kb-actions { flex-direction: column; align-items: stretch; } - #abo-order .kb-btn-primary { padding: 15px 22px; font-size: 15px; } + #abo-order .kb-actions, #switch-order .kb-actions { flex-direction: column; align-items: stretch; } + #abo-order .kb-btn-primary, #switch-order .kb-btn-primary { padding: 15px 22px; font-size: 15px; } .abo-actions .abo-btn { flex: 1 1 auto; } .kb-legal { justify-content: center; gap: 10px 16px; margin-top: 32px; text-align: center; }