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

194 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): Reiter E-Mail in Verträge, der Zusatzspeicher. Text und Logik 1:1 aus app.js (renderStoragePanel,
Fassung 5), die Sie-Form bleibt (C14, N36). Ein geänderter Satz wäre eine unbemerkte Rechtsänderung: Das Wortlaut-Orakel
(test/support/knWortlautOrakel.js) vergleicht den sichtbaren Text Zeichen für Zeichen. Nur "Postfach anfragen" und die Sperrhinweise sind neu (du). */
import { api } from './api.js'
import { h, leeren } from './dom.js'
import { K, ersetze, fmtEuro, fmtDatum, fmtTagMonat, ladeAbo, navigiere, setzeChatVorgabe, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus nicht möglich.'
const BEDINGUNGEN = '/abo-bedingungen.html'
const link = (href, text) => h('a', { href, target: '_blank', rel: 'noopener' }, text)
const paket = (n) => `Paket${n === 1 ? '' : 'e'}`
const fakt = (titel, wert, klasse) => h('div', null, h('dt', null, titel), h('dd', { class: klasse }, wert))
// Der Zusatzspeicher: Plus/Minus mit Vorschau vom Server, Bestellübersicht (§ 312j BGB) und Speichern
function baueSpeicherPanel(daten, { viewAs, notice, nachErfolg }) {
const sub = daten.subscription
const conf = daten.mailStorage || {}
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: start !== current, busy: false, error: '', token: 0 }
const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt)
const lockedReason = sub.switchPending ? 'Wechsel wartet auf die Zahlung'
: gekuendigt ? 'Abo gekündigt'
: sub.paymentProblem ? 'Zahlung offen' : 'bei Fragen schreiben Sie uns gern im Chat'
const panel = h('div', { class: 'kn-panel', 'data-panel': 'speicher' })
let fehlerText = ''
let haken = false
const paint = () => {
const delta = state.target - current
const p = state.preview
const sperre = viewAs || state.busy
let summary = null
if (canChange && delta !== 0) {
const plus = delta > 0
const totalCents = state.target * unitCents
const facts = []
const proBad = plus && p && !state.loading && !(p.prorationCents != null && p.prorationCents !== '' && Number.isFinite(Number(p.prorationCents)))
facts.push(fakt('Neue Gesamtgröße', `${gbOf(state.target)} GB`))
facts.push(fakt('Preis', `${fmtEuro(totalCents / 100)} pro Monat für ${state.target} ${paket(state.target)} (${plus ? 'Mehrpreis' : 'Ersparnis'} ${fmtEuro(Math.abs(delta) * unitCents / 100)} pro Monat; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)`))
if (state.previewError || proBad) {
facts.push(fakt('Vorschau', state.previewError || 'Der Betrag konnte nicht berechnet werden. Bitte versuchen Sie es später erneut.', 'kn-fehler'))
} else if (!p || state.loading) {
facts.push(fakt('Vorschau', 'wird berechnet …'))
} else if (plus) {
const pro = Number(p.prorationCents) || 0
facts.push(fakt('Zusätzlich jetzt', 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'))
facts.push(fakt('Nächste Abbuchung', `${fmtDatum(p.nextChargeAt)}, danach monatlich ${fmtEuro(totalCents / 100)}`))
facts.push(pro === 0 && pendingReduction
? fakt('Geplante Reduzierung', 'wird aufgehoben oder angepasst; es ist nichts neu einzurichten')
: fakt('Einrichtung', 'in der Regel innerhalb eines Werktags; bis dahin steht „wird eingerichtet“ da'))
} else {
facts.push(fakt('Wirkt zum', `${fmtDatum(p.effectiveAt)}, zum Ende des Abrechnungsmonats`))
}
facts.push(fakt('Laufzeit', 'monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo'))
const hinweis = plus
? h('label', { class: 'kn-check' },
h('input', { type: 'checkbox', id: 'storage-terms', checked: haken, disabled: sperre }),
h('span', null, 'Ich habe die ', link(`${BEDINGUNGEN}#zusatzspeicher`, 'Regeln zum Zusatzspeicher'), ' gelesen und stimme ihnen zu. Die ', link(`${BEDINGUNGEN}#widerruf`, 'Widerrufsbelehrung'), ' habe ich zur Kenntnis genommen.'))
: h('p', { class: 'kn-meldung kn-meldung-warn' }, '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 ',
link(`${BEDINGUNGEN}#zusatzspeicher`, 'Regeln zum Zusatzspeicher'), ' und in der ', link(`${BEDINGUNGEN}#widerruf`, 'Widerrufsbelehrung'), '.')
const gesperrt = sperre || state.loading || !p || state.previewError || proBad
summary = h('div', { class: 'kn-speicher-zusammenfassung' },
h('dl', { class: 'kn-fakten' }, facts),
hinweis,
h('p', { class: 'kn-fehler', id: 'storage-error', hidden: !fehlerText }, fehlerText),
h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? 'Zahlungspflichtig bestellen' : 'Speichern'))
}
const status = []
if (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Ihr zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.'))
if (pendingReduction) status.push(h('p', { class: 'kn-hinweis' }, `Geplant: ab ${fmtTagMonat(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.`))
ersetze(panel,
h('h3', null, 'E-Mail-Speicher'),
h('p', { class: 'kn-hinweis' }, `${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.`),
notice ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
!canChange ? h('p', { class: 'kn-hinweis', id: 'storage-locked' }, `Änderungen sind derzeit nicht möglich – ${lockedReason}.`) : null,
canChange
? h('div', { class: 'kn-stepper' },
h('button', { type: 'button', class: K.knopf, id: 'storage-minus', 'aria-label': 'Ein Paket weniger', disabled: sperre || state.target <= 0, on: { click: () => schritt(-1) } }, '−'),
h('span', { class: 'kn-stepper-zahl', 'aria-live': 'polite' }, h('strong', null, state.target), ` ${paket(state.target)}`),
h('button', { type: 'button', class: K.knopf, id: 'storage-plus', 'aria-label': 'Ein Paket mehr', disabled: sperre || state.target >= maxUnits, on: { click: () => schritt(1) } }, '+'))
: h('p', { class: 'kn-stepper-zahl' }, h('strong', null, state.target), ` ${paket(state.target)}`),
h('p', { class: 'kn-speicher-summe' }, `= ${gbOf(state.target)} GB gesamt · + ${fmtEuro(state.target * unitCents / 100)} im Monat`),
canChange && viewAs ? h('p', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null,
...status,
summary)
}
const ladeVorschau = async (token) => {
try {
state.preview = await api('/api/abo/storage/preview', { method: 'POST', body: { units: state.target } })
} catch (f) {
if (token !== state.token) return
state.previewError = f.message
}
if (token !== state.token) return
state.loading = false
paint()
}
const schritt = (d) => {
const next = state.target + d
if (viewAs || state.busy || next < 0 || next > maxUnits) return
state.target = next
state.preview = null
state.previewError = ''
fehlerText = ''
state.loading = next !== current
const token = ++state.token
paint()
if (next !== current) ladeVorschau(token)
}
const absenden = async () => {
if (viewAs) return
const plus = state.target > current
const terms = panel.querySelector('#storage-terms')
haken = Boolean(terms?.checked)
fehlerText = ''
if (plus && !haken) {
fehlerText = 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.'
paint()
return
}
const body = { units: state.target }
if (plus) Object.assign(body, { acceptTerms: true, termsVersion: daten.termsVersion })
state.busy = true
paint()
try {
const res = await api('/api/abo/storage', { method: 'POST', body })
const ch = res.change || {}
const hinweis = 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 ${fmtDatum(ch.effectiveAt)}.`
await nachErfolg(hinweis, res)
} catch (f) {
state.busy = false
fehlerText = f.status === 409 ? `${f.message} Bitte laden Sie die Seite neu, falls sich der Stand geändert hat.` : f.message
paint()
}
}
paint()
if (start !== current && canChange) ladeVorschau(++state.token)
return panel
}
/** Reiter-Inhalt E-Mail: Knopf "Postfach anfragen" und der Zusatzspeicher. daten (GET /api/abo) ist optional. */
export async function zeigeSpeicher(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
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
let stand = daten ?? await ladeAbo()
if (!aktuell()) return
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
if (!stand.subscription?.active) {
leeren(wurzel).append(h('p', { class: K.leer }, 'Den E-Mail-Speicher gibt es mit einem laufenden Abo.'))
return
}
const anfrage = h('button', { type: 'button', class: K.knopf, on: { click: () => { setzeChatVorgabe('Bitte lege ein neues Postfach an: '); navigiere('#chat') } } }, 'Postfach anfragen')
const platz = h('div', { class: 'kn-speicher-platz' })
const zeichne = (notice = '') => {
leeren(platz).append(baueSpeicherPanel(stand, {
viewAs,
notice,
// Nach einem Erfolg: Abo neu laden (Rückfall: die Antwort der Bestellung) und den Hinweis oben im Panel zeigen
nachErfolg: async (hinweis, antwort) => {
const neu = await ladeAbo()
if (!aktuell()) return
stand = neu ?? (antwort?.subscription ? { ...stand, subscription: antwort.subscription } : stand)
zeichne(hinweis)
},
}))
}
zeichne()
leeren(wurzel).append(anfrage, platz)
}