Kundenansicht neu P7: E-Mail-Speicher schlank, Plus mit Linkzeile, Warnung als eine Zeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e7ac625f26
commit
9b00771d11
4 files changed
+188
-86
No files matched your search
@@ -1,21 +1,24 @@
|
||||
/* 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). */
|
||||
/* Kundenansicht neu (P7): Reiter E-Mail in Verträge, der Zusatzspeicher, im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
Plus: Block „Was du bestellst“, eine Linkzeile zu den Regeln, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die
|
||||
Kundenart-Zeile; kein Häkchen (der Klick auf den Knopf ist die Zustimmung). Minus: genau eine Warnzeile und der Knopf „Speichern“.
|
||||
Die Sätze kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst, der Client sendet nie kundenart. */
|
||||
|
||||
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'
|
||||
import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.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 TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
|
||||
const KNOPF = 'Zahlungspflichtig bestellen'
|
||||
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 }) {
|
||||
function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) {
|
||||
const sub = daten.subscription
|
||||
const T = daten.bestelltexte
|
||||
const wortlaut = hatWortlaut(daten, 'speicher')
|
||||
const conf = daten.mailStorage || {}
|
||||
const includedGb = Number(conf.includedGb) || 0
|
||||
const gbPer = Number(conf.gbPerUnit) || 0
|
||||
@@ -31,10 +34,9 @@ function baueSpeicherPanel(daten, { viewAs, notice, nachErfolg }) {
|
||||
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'
|
||||
: sub.paymentProblem ? 'Zahlung offen' : 'bei Fragen schreib 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
|
||||
@@ -43,43 +45,44 @@ function baueSpeicherPanel(daten, { viewAs, notice, nachErfolg }) {
|
||||
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'))
|
||||
if (!wortlaut) {
|
||||
// Ohne Wortlaut vom Server (Linkzeile beim Plus, Warnzeile beim Minus) gibt es keine Änderung: nur der Satz, kein Knopf
|
||||
summary = h('div', { class: 'kn-speicher-zusammenfassung' }, h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR))
|
||||
} else {
|
||||
facts.push(fakt('Wirkt zum', `${fmtDatum(p.effectiveAt)}, zum Ende des Abrechnungsmonats`))
|
||||
const totalCents = state.target * unitCents
|
||||
const zeilen = []
|
||||
const proBad = plus && p && !state.loading && !(p.prorationCents != null && p.prorationCents !== '' && Number.isFinite(Number(p.prorationCents)))
|
||||
zeilen.push({ label: 'Neue Gesamtgröße', text: `${gbOf(state.target)} GB` })
|
||||
zeilen.push({ label: 'Preis', text: `${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) {
|
||||
zeilen.push({ label: 'Vorschau', text: state.previewError || 'Der Betrag konnte nicht berechnet werden. Bitte versuche es später erneut.', klasse: 'kn-fehler' })
|
||||
} else if (!p || state.loading) {
|
||||
zeilen.push({ label: 'Vorschau', text: 'wird berechnet …' })
|
||||
} else if (plus) {
|
||||
const pro = Number(p.prorationCents) || 0
|
||||
zeilen.push({ label: 'Zusätzlich jetzt', text: 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' })
|
||||
zeilen.push({ label: 'Nächste Abbuchung', text: `${fmtDatum(p.nextChargeAt)}, danach monatlich ${fmtEuro(totalCents / 100)}` })
|
||||
zeilen.push(pro === 0 && pendingReduction
|
||||
? { label: 'Geplante Reduzierung', text: 'wird aufgehoben oder angepasst; es ist nichts neu einzurichten' }
|
||||
: { label: 'Einrichtung', text: 'in der Regel innerhalb eines Werktags; bis dahin steht „wird eingerichtet“ da' })
|
||||
} else {
|
||||
zeilen.push({ label: 'Wirkt zum', text: `${fmtDatum(p.effectiveAt)}, zum Ende des Abrechnungsmonats` })
|
||||
}
|
||||
zeilen.push({ label: 'Laufzeit', text: 'monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo' })
|
||||
const gesperrt = sperre || state.loading || !p || state.previewError || proBad
|
||||
summary = h('div', { class: 'kn-speicher-zusammenfassung kn-bestellen' },
|
||||
baueBlock(zeilen, { id: 'storage-block' }),
|
||||
plus ? baueLinkzeile(T.speicher.linkzeile, { id: 'storage-linkzeile' }) : baueLinkzeile(T.speicher.warnung, { id: 'storage-warnung', klasse: 'kn-speicher-warnung' }),
|
||||
h('p', { class: 'kn-fehler', id: 'storage-error', hidden: !fehlerText }, fehlerText),
|
||||
h('div', { class: 'kn-aktionen' },
|
||||
h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? (T.knopf || KNOPF) : 'Speichern')),
|
||||
plus ? baueKundenartZeile(kundenart, T.kundenart, { id: 'storage-kundenart' }) : null)
|
||||
}
|
||||
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 (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Dein 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'),
|
||||
@@ -126,14 +129,8 @@ function baueSpeicherPanel(daten, { viewAs, notice, nachErfolg }) {
|
||||
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
|
||||
}
|
||||
// Plus: der Klick auf den Knopf ist die Zustimmung. Nie kundenart: die Kundenart bestimmt der Server aus dem Kundendokument.
|
||||
const body = { units: state.target }
|
||||
if (plus) Object.assign(body, { acceptTerms: true, termsVersion: daten.termsVersion })
|
||||
state.busy = true
|
||||
@@ -142,12 +139,12 @@ function baueSpeicherPanel(daten, { viewAs, notice, nachErfolg }) {
|
||||
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.')
|
||||
? (ch.task === false ? 'Gespeichert. Es entstehen keine zusätzlichen Kosten, die Pakete waren schon bezahlt.' : 'Danke, deine 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
|
||||
fehlerText = f.status === 409 ? `${f.message} Bitte lade die Seite neu, falls sich der Stand geändert hat.` : f.message
|
||||
paint()
|
||||
}
|
||||
}
|
||||
@@ -173,11 +170,13 @@ export async function zeigeSpeicher(wurzel, { route, me, nav, daten } = {}) {
|
||||
return
|
||||
}
|
||||
|
||||
const kundenart = kundenartVon(me, stand)
|
||||
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,
|
||||
kundenart,
|
||||
notice,
|
||||
// Nach einem Erfolg: Abo neu laden (Rückfall: die Antwort der Bestellung) und den Hinweis oben im Panel zeigen
|
||||
nachErfolg: async (hinweis, antwort) => {
|
||||
|
||||
@@ -334,6 +334,8 @@
|
||||
.kn-bestellen .kn-aktionen { margin-top: 0; }
|
||||
.kn-sperrzeile { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-danger); }
|
||||
.kn-speicher-warnung { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-orange-fg); }
|
||||
.kn-speicher-warnung a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.kn-block dd.kn-fehler { color: var(--kn-danger); }
|
||||
.kn-gutschrift-bedingungen { max-width: 640px; }
|
||||
|
||||
/* ------------------------------------------------------------------ Handy --- */
|
||||
|
||||
@@ -52,11 +52,11 @@ export function linkTeile(text, urls) {
|
||||
|
||||
const ausserhalb = (href) => !href.startsWith('#')
|
||||
|
||||
/** <p id> mit den Links der Vorlage; '/…' und http(s) öffnen in neuem Tab (target _blank, rel noopener), '#…' bleibt im Fenster. */
|
||||
export function baueLinkzeile({ text, urls } = {}, { id } = {}) {
|
||||
/** <p id> mit den Links der Vorlage; '/…' und http(s) öffnen in neuem Tab (target _blank, rel noopener), '#…' bleibt im Fenster. klasse: Vorgabe 'kn-linkzeile'. */
|
||||
export function baueLinkzeile({ text, urls } = {}, { id, klasse = 'kn-linkzeile' } = {}) {
|
||||
const kinder = linkTeile(text, urls).map((t) =>
|
||||
t.href ? h('a', { href: t.href, ...(ausserhalb(t.href) ? { target: '_blank', rel: 'noopener' } : {}) }, t.text) : t.text)
|
||||
return h('p', { id, class: 'kn-linkzeile' }, kinder)
|
||||
return h('p', { id, class: klasse }, kinder)
|
||||
}
|
||||
|
||||
/** <p id>: „Du bestellst als Betrieb“ | „Du bestellst privat“, ' · ', dann der Hinweis zur Korrektur mit seinem Link. */
|
||||
@@ -67,9 +67,9 @@ export function baueKundenartZeile(kundenart, texte, { id } = {}) {
|
||||
return h('p', { id, class: 'kn-kundenart-zeile' }, lage, ' · ', korrektur)
|
||||
}
|
||||
|
||||
/** Block „Was du bestellst“: zeilen [{ id?, label, text }] als Beschriftungsliste. Der Text ist immer Text. */
|
||||
/** Block „Was du bestellst“: zeilen [{ id?, label, text, klasse? }] als Beschriftungsliste. Der Text ist immer Text. */
|
||||
export function baueBlock(zeilen, { id } = {}) {
|
||||
return h('dl', { id, class: 'kn-block' }, (zeilen || []).map((z) => h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id }, z.text))))
|
||||
return h('dl', { id, class: 'kn-block' }, (zeilen || []).map((z) => h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id, class: z.klasse }, z.text))))
|
||||
}
|
||||
|
||||
const nichtLeer = (x) => typeof x === 'string' && x !== ''
|
||||
|
||||
Reference in new issue
Block a user