Zusatzspeicher: Plus und Minus mit Bestellübersicht
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90a3c57aa3
commit
c95695c175
2 files changed
+154
-2
No files matched your search
+145
-2
@@ -1027,9 +1027,152 @@ function renderSideCard(card, data) {
|
||||
else renderAboSideOffer(card, data, { ended: Boolean(data.subscription) })
|
||||
}
|
||||
|
||||
/* Zusatzspeicher: Hülle, gefüllt in Aufgabe 17 */
|
||||
/* Zusatzspeicher: Plus/Minus mit Vorschau vom Server, Bestellübersicht (§ 312j BGB) und Speichern */
|
||||
const storageUi = { notice: '' }
|
||||
|
||||
function renderStoragePanel(container, data) {
|
||||
container.dataset.units = String(data?.subscription?.storage?.units ?? 0)
|
||||
const sub = data?.subscription
|
||||
const conf = data?.mailStorage || {}
|
||||
container.dataset.units = String(sub?.storage?.units ?? 0)
|
||||
if (!sub?.active) {
|
||||
container.innerHTML = ''
|
||||
return
|
||||
}
|
||||
const includedGb = Number(conf.includedGb) || 0
|
||||
const gbPer = Number(conf.gbPerUnit) || 0
|
||||
const unitCents = Number(conf.unitPriceCents) || 0
|
||||
const maxUnits = Number(conf.maxUnits) || 10
|
||||
const gbOf = (u) => includedGb + u * gbPer
|
||||
const storage = sub.storage || {}
|
||||
const pendingReduction = storage.pendingReduction || null
|
||||
const canChange = sub.canChangeStorage === true
|
||||
const current = Number(storage.units || 0)
|
||||
const start = pendingReduction && Number.isInteger(pendingReduction.units) ? pendingReduction.units : current
|
||||
const state = { target: start, preview: null, previewError: '', loading: false, busy: false, error: '', token: 0 }
|
||||
const notice = storageUi.notice
|
||||
storageUi.notice = ''
|
||||
|
||||
const paint = () => {
|
||||
const delta = state.target - start
|
||||
const p = state.preview
|
||||
let summary = ''
|
||||
if (canChange && delta !== 0) {
|
||||
const plus = delta > 0
|
||||
const totalCents = state.target * unitCents
|
||||
const facts = []
|
||||
facts.push(`<div><dt>Neue Gesamtgröße</dt><dd>${gbOf(state.target)} GB</dd></div>`)
|
||||
facts.push(`<div><dt>Preis</dt><dd>${fmtEuro(totalCents / 100)} pro Monat für ${state.target} Paket${state.target === 1 ? '' : 'e'} (${plus ? 'Mehrpreis' : 'Ersparnis'} ${fmtEuro(Math.abs(delta) * unitCents / 100)} pro Monat; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)</dd></div>`)
|
||||
if (state.previewError) {
|
||||
facts.push(`<div><dt>Vorschau</dt><dd class="kb-storage-error">${escapeHtml(state.previewError)}</dd></div>`)
|
||||
} else if (!p || state.loading) {
|
||||
facts.push('<div><dt>Vorschau</dt><dd>wird berechnet …</dd></div>')
|
||||
} else if (plus) {
|
||||
const pro = Number(p.prorationCents) || 0
|
||||
facts.push(`<div><dt>Zusätzlich jetzt</dt><dd>${pro > 0
|
||||
? `${fmtEuro(pro / 100)} anteilig für den Rest des laufenden Abrechnungsmonats (gemäß § 19 UStG ohne Umsatzsteuer)`
|
||||
: 'keine zusätzlichen Kosten, die Pakete sind für den laufenden Abrechnungsmonat schon bezahlt'}</dd></div>`)
|
||||
facts.push(`<div><dt>Nächste Abbuchung</dt><dd>${fmtDateShort(p.nextChargeAt)}, danach monatlich ${fmtEuro(totalCents / 100)}</dd></div>`)
|
||||
facts.push('<div><dt>Einrichtung</dt><dd>in der Regel innerhalb eines Werktags; bis dahin steht „wird eingerichtet“ da</dd></div>')
|
||||
} else {
|
||||
facts.push(`<div><dt>Wirkt zum</dt><dd>${fmtDateShort(p.effectiveAt)}, zum Ende des Abrechnungsmonats</dd></div>`)
|
||||
}
|
||||
facts.push('<div><dt>Laufzeit</dt><dd>monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo</dd></div>')
|
||||
const hint = plus
|
||||
? `<label class="abo-check"><input type="checkbox" id="storage-terms"${state.busy ? ' disabled' : ''}>
|
||||
<span>Ich habe die <a href="/abo-bedingungen.html#zusatzspeicher" target="_blank" rel="noopener">Regeln zum Zusatzspeicher</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>`
|
||||
: `<p class="abo-alert" style="margin:10px 0">Wichtig: Passen die gespeicherten E-Mails nach der Verkleinerung nicht mehr in den verbleibenden Speicher, nehmen die Postfächer keine neuen E-Mails mehr an. Bitte räumen Sie vorher auf. Mehr dazu in den <a href="/abo-bedingungen.html#zusatzspeicher" target="_blank" rel="noopener">Regeln zum Zusatzspeicher</a> und in der <a href="/abo-bedingungen.html#widerruf" target="_blank" rel="noopener">Widerrufsbelehrung</a>.</p>`
|
||||
summary = `
|
||||
<div class="kb-storage-summary">
|
||||
<dl class="kb-contract-facts">${facts.join('')}</dl>
|
||||
${hint}
|
||||
<p class="error hidden" id="storage-error"></p>
|
||||
<button type="button" class="kb-btn-primary" id="storage-submit"${state.busy || state.loading || !p || state.previewError ? ' disabled' : ''}>${state.busy ? 'Wird gespeichert …' : plus ? 'Zahlungspflichtig bestellen' : 'Speichern'}</button>
|
||||
</div>`
|
||||
}
|
||||
const status = []
|
||||
if (storage.pendingSetup) status.push('<p class="abo-alert" style="margin:12px 0 0">Ihr zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.</p>')
|
||||
if (pendingReduction) status.push(`<p class="abo-note" style="margin:12px 0 0">Geplant: ab ${fmtDayMonth(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.</p>`)
|
||||
container.innerHTML = `
|
||||
<h3>E-Mail-Speicher</h3>
|
||||
<p class="abo-note" style="margin-top:0">${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.</p>
|
||||
${notice ? `<p class="abo-alert kb-storage-notice" role="status">${escapeHtml(notice)}</p>` : ''}
|
||||
${canChange ? `
|
||||
<div class="kb-storage-stepper">
|
||||
<button type="button" class="kb-side-btn" id="storage-minus" aria-label="Ein Paket weniger"${state.busy || state.target <= 0 ? ' disabled' : ''}>−</button>
|
||||
<span class="kb-storage-count" aria-live="polite"><strong>${state.target}</strong> Paket${state.target === 1 ? '' : 'e'}</span>
|
||||
<button type="button" class="kb-side-btn" id="storage-plus" aria-label="Ein Paket mehr"${state.busy || state.target >= maxUnits ? ' disabled' : ''}>+</button>
|
||||
</div>` : `<p class="kb-storage-count"><strong>${state.target}</strong> Paket${state.target === 1 ? '' : 'e'}</p>`}
|
||||
<p class="kb-storage-total">= ${gbOf(state.target)} GB gesamt · + ${fmtEuro(state.target * unitCents / 100)} im Monat</p>
|
||||
${status.join('')}
|
||||
${summary}`
|
||||
container.querySelector('#storage-minus')?.addEventListener('click', () => step(-1))
|
||||
container.querySelector('#storage-plus')?.addEventListener('click', () => step(1))
|
||||
container.querySelector('#storage-submit')?.addEventListener('click', submit)
|
||||
}
|
||||
|
||||
const step = async (d) => {
|
||||
const next = state.target + d
|
||||
if (state.busy || next < 0 || next > maxUnits) return
|
||||
state.target = next
|
||||
state.preview = null
|
||||
state.previewError = ''
|
||||
state.loading = next !== start
|
||||
const token = ++state.token
|
||||
paint()
|
||||
if (next === start) return
|
||||
try {
|
||||
const p = await api('/api/abo/storage/preview', { method: 'POST', body: JSON.stringify({ units: next }) })
|
||||
if (token !== state.token) return
|
||||
state.preview = p
|
||||
} catch (err) {
|
||||
if (token !== state.token) return
|
||||
state.previewError = err.message
|
||||
}
|
||||
state.loading = false
|
||||
paint()
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
const errEl = container.querySelector('#storage-error')
|
||||
hideError(errEl)
|
||||
const plus = state.target > start
|
||||
const terms = container.querySelector('#storage-terms')
|
||||
if (plus && !terms?.checked) {
|
||||
showError(errEl, 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.')
|
||||
return
|
||||
}
|
||||
const body = { units: state.target }
|
||||
if (plus) Object.assign(body, { acceptTerms: true, termsVersion: data.termsVersion })
|
||||
const btn = container.querySelector('#storage-submit')
|
||||
state.busy = true
|
||||
btn.disabled = true
|
||||
container.querySelectorAll('#storage-minus, #storage-plus, #storage-terms').forEach((el) => { el.disabled = true })
|
||||
btn.textContent = 'Wird gespeichert …'
|
||||
try {
|
||||
const res = await api('/api/abo/storage', { method: 'POST', body: JSON.stringify(body) })
|
||||
const ch = res.change || {}
|
||||
storageUi.notice = plus
|
||||
? (ch.task === false ? 'Gespeichert. Es entstehen keine zusätzlichen Kosten, die Pakete waren schon bezahlt.' : 'Danke, Ihre Bestellung ist eingegangen. Der Speicher wird in der Regel innerhalb eines Werktags eingerichtet.')
|
||||
: `Gespeichert. Die Verkleinerung wirkt zum ${fmtDateShort(ch.effectiveAt)}.`
|
||||
if (res.subscription && aboState.data) {
|
||||
aboState.data.subscription = res.subscription
|
||||
const card = document.getElementById('abo-card')
|
||||
const contracts = document.getElementById('kb-contracts')
|
||||
if (card) renderSideCard(card, aboState.data)
|
||||
if (contracts) renderContractsView(contracts, aboState.data)
|
||||
return
|
||||
}
|
||||
loadContracts()
|
||||
} catch (err) {
|
||||
state.busy = false
|
||||
paint()
|
||||
const again = container.querySelector('#storage-error')
|
||||
showError(again, err.status === 409 ? `${err.message} Bitte laden Sie die Seite neu, falls sich der Stand geändert hat.` : err.message)
|
||||
if (plus) { const c = container.querySelector('#storage-terms'); if (c) c.checked = true }
|
||||
}
|
||||
}
|
||||
|
||||
paint()
|
||||
}
|
||||
|
||||
/* "Postfach anfragen": öffnet den Chat mit vorbereiteter Nachricht (Eingabefeld wird nicht automatisch gesendet) */
|
||||
|
||||
@@ -837,3 +837,12 @@ button.kb-avatar:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
||||
#kb-contracts { margin-top: 16px; }
|
||||
}
|
||||
#storage-panel:empty { display: none; }
|
||||
.kb-storage-stepper { display: flex; align-items: center; gap: 14px; margin: 14px 0 6px; }
|
||||
.kb-storage-stepper .kb-side-btn { width: 44px; height: 44px; padding: 0; font-size: 22px; line-height: 1; justify-content: center; }
|
||||
.kb-storage-stepper .kb-side-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.kb-storage-count { font-size: 15px; min-width: 6em; text-align: center; }
|
||||
.kb-storage-stepper + .kb-storage-total, .kb-storage-total { margin: 0; font-size: 14px; color: var(--kb-muted); }
|
||||
.kb-storage-summary { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--kb-border, rgba(127, 127, 127, 0.25)); }
|
||||
.kb-storage-summary .kb-btn-primary { width: 100%; }
|
||||
.kb-storage-error { color: #b3261e; }
|
||||
@media (min-width: 881px) { .kb-storage-summary .kb-btn-primary { width: auto; } }
|
||||
Reference in new issue
Block a user