/* 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) }