Wechsel ins Wartungs-Abo: Übersicht, Zustimmung, Bestellung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c95695c175
commit
f7eeda8c19
2 files changed
+158
-2
No files matched your search
+156
@@ -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 = '<header class="kb-head"><h1>Wechsel ins Wartungs-Abo</h1><p>Aus dem Hosting-Abo in das Wartungs-Abo wechseln.</p></header>'
|
||||
if (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch || !wartung) {
|
||||
switchUi.loadedAt = 0
|
||||
view.innerHTML = `${head}<div class="kb-panel"><p class="abo-note" style="margin:0">Ein Wechsel ist derzeit nicht möglich. Details finden Sie unter <a href="#vertraege" data-view="vertraege">Verträge</a>.</p></div>`
|
||||
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(`<tr><td>Wartungs-Abo</td><td><strong>${escapeHtml(fmtEuro(monthly / 100))} pro Monat</strong> (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer); enthalten sind Pflege Ihrer Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat</td></tr>`)
|
||||
if (units > 0) {
|
||||
rows.push(`<tr><td>Zusatzspeicher</td><td>${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</td></tr>`)
|
||||
}
|
||||
if ((Number(p.creditCents) || 0) > 0) {
|
||||
rows.push(`<tr><td>Gutschrift für nicht genutzte Tage</td><td>−${escapeHtml(fmtEuro(p.creditCents / 100))} aus dem bereits bezahlten Hosting-Abo (gemäß § 19 UStG ohne Umsatzsteuer)</td></tr>`)
|
||||
}
|
||||
rows.push(`<tr><td>Heute fällig</td><td><strong>${escapeHtml(fmtEuro(p.amountDueCents / 100))}</strong> (gemäß § 19 UStG ohne Umsatzsteuer)</td></tr>`)
|
||||
rows.push(`<tr><td>Neuer Abbuchungstag</td><td>Ab 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)}</td></tr>`)
|
||||
rows.push(`<tr><td>Laufzeit</td><td><strong>Mindestlaufzeit bis ${fmtDateShort(p.minTermEnd)}</strong> (12 Monate ab dem Wechsel), danach monatlich kündbar</td></tr>`)
|
||||
rows.push('<tr><td>Beginn</td><td>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.</td></tr>')
|
||||
}
|
||||
const summary = state.error
|
||||
? `<p class="kb-storage-error">${escapeHtml(state.error)}</p>`
|
||||
: state.loading || !p ? '<p class="abo-note">Ihre Übersicht wird berechnet …</p>'
|
||||
: `<table class="abo-summary">${rows.join('')}</table>`
|
||||
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}
|
||||
<div class="kb-panel" id="switch-order">
|
||||
<h3>Ihre Bestellung</h3>
|
||||
<p class="abo-note" style="margin-top:0">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.</p>
|
||||
<label class="abo-check"><input type="checkbox" id="switch-terms"${checked('#switch-terms')}${busy ? ' disabled' : ''}>
|
||||
<span>Ich habe die <a href="/abo-bedingungen.html" target="_blank" rel="noopener">Vertragsbedingungen mit Leistungsbeschreibung und Auftragsverarbeitungsvertrag</a> gelesen und stimme ihnen zu. Die <a href="/abo-bedingungen.html#widerruf" target="_blank" rel="noopener">Widerrufsbelehrung</a> habe ich zur Kenntnis genommen.</span></label>
|
||||
<label class="abo-check"><input type="checkbox" id="switch-early"${checked('#switch-early')}${busy ? ' disabled' : ''}>
|
||||
<span><strong>Sofortbeginn.</strong> 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.</span></label>
|
||||
<div class="abo-summary-box">
|
||||
${summary}
|
||||
<p class="abo-note">Es gilt das gesetzliche Mängelhaftungsrecht. Einzelheiten: <a href="/abo-bedingungen.html" target="_blank" rel="noopener">Vertragsbedingungen</a> · <a href="/datenschutz.html" target="_blank" rel="noopener">Datenschutz</a></p>
|
||||
</div>
|
||||
<p class="error${errText ? '' : ' hidden'}" id="switch-error">${escapeHtml(errText)}</p>
|
||||
<div class="kb-actions">
|
||||
<button type="button" class="kb-btn-primary" id="switch-buy"${busy || state.loading || !p ? ' disabled' : ''}>${busy ? 'Wird bestellt …' : 'Zahlungspflichtig bestellen'}</button>
|
||||
<span class="abo-note">Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart. Bis zum Klick können Sie abbrechen.</span>
|
||||
</div>
|
||||
</div>`
|
||||
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', `<div class="abo-alert" role="status">${escapeHtml(switchUi.notice)}</div>`)
|
||||
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 = '<header class="kb-head"><h1>Stundenkonto</h1></header><div class="kb-panel"><p class="abo-note" style="margin:0">Ein Stundenkonto gibt es im Wartungs-Abo. Ihre Verträge finden Sie unter <a href="#vertraege" data-view="vertraege">Verträge</a>.</p></div>'
|
||||
card.classList.remove('hidden')
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user