Files
Webklar-Kundenbereich/public/neu/ansicht-wechsel.js
T
ClaudeandClaude Sonnet 5.5 d4050f870a Kundenansicht neu E-Fix F1: Kerninfo und Knopf der Bestellseiten passen in den nutzbaren Bereich (390x844, 375x667)
Die Messung im Werkzeug nahm innerHeight; nutzbar ist am Handy nur der Bereich zwischen Kopf und fester Leiste (735 px bei 844, 558 px bei 667).
Gemessen vor der Korrektur (Blockanfang bis Knopfunterkante, 375x667): Abo bestellen privat 799 px, Wechsel privat 888 px, Zusatzspeicher 567 px.

- Block knapp: Leistung als eine Zeile mit Link "Leistungen ansehen" auf die Leistungsbeschreibung, Preis, Fällig, Beginn, Laufzeit und Zahlung in je ein bis zwei Zeilen.
- Die ausführlichen Sätze stehen unter dem Knopf und der Kundenart-Zeile (baueEinzelheiten), ohne Klappe.
- Handy: Beschriftung läuft in der Zeile mit, kleinere Überschrift, knappere Abstände.
- Die Wortlaute des Blocks sind gekürzt und gehören in die Rechtsrunde.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:25:48 +00:00

220 lines
12 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 (P7): Wechsel vom Hosting-Abo ins Wartungs-Abo (#wechsel) im schlanken Muster der Spec 5.6, Fassung 6.
Reihenfolge ohne Klappe: Block „Was du bestellst“ (Vorschau vom Server), eine Linkzeile, bei Privatkunden das Sofortbeginn-Häkchen mit
Sperrzeile, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die Kundenart-Zeile. Der Wechsel wirkt erst mit der Zahlung
der Wechselrechnung (Bedingungen § 9 Abs. 3). Die Sätze kommen vom Server (GET /api/abo → bestelltexte); der Server entscheidet die
Kundenart selbst, der Client sendet nie kundenart. Ein Betrieb braucht kein Häkchen.
Die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen.
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
import { api } from './api.js'
import { h, leeren } from './dom.js'
import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
const SCHLUESSEL = 'kn-wechsel-funktion'
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
const KNOPF = 'Zahlungspflichtig bestellen'
const UST = 'gemäß § 19 UStG ohne Umsatzsteuer'
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
/** Die von P3 gemerkte Funktion { key, projectId } oder null (kaputter oder fremder Wert, kein sessionStorage). */
export function gemerkteFunktion() {
try {
const wert = JSON.parse(globalThis.sessionStorage?.getItem(SCHLUESSEL) ?? 'null')
return wert && typeof wert.key === 'string' && wert.key && typeof wert.projectId === 'string' && wert.projectId ? { key: wert.key, projectId: wert.projectId } : null
} catch {
return null
}
}
function vergissFunktion() {
try { globalThis.sessionStorage?.removeItem(SCHLUESSEL) } catch { /* ohne Speicher bleibt nichts liegen */ }
}
// Zurück aus dem Bfcache (z. B. von der Zahlungsseite): eine hängende Bestellung nicht gesperrt lassen
let aktiv = null
const mitPageshow = new WeakSet()
function registrierePageshow() {
const win = window
if (mitPageshow.has(win)) return
mitPageshow.add(win)
win.addEventListener('pageshow', () => { if (aktiv?.busy) { aktiv.busy = false; aktiv.zeichne() } })
}
export async function zeigeWechsel(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
meldeTitel(wurzel, 'Wechsel ins Wartungs-Abo')
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
registrierePageshow()
const stand = daten ?? await ladeAbo()
if (!aktuell()) return
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
const sub = stand.subscription
const wartung = (stand.products || []).find((p) => p.key === 'wartung') || null
if (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch || !wartung) {
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, 'Ein Wechsel ist derzeit nicht möglich. Details findest du unter ', h('a', zuAnsicht('vertraege'), 'Verträge'), '.')))
navigiere('#vertraege', { ersetzen: true })
return
}
// Ohne Wortlaut vom Server gibt es keinen Bestellknopf und keine Vorschau
if (!hatWortlaut(stand, 'wechsel')) {
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR)))
return
}
const T = stand.bestelltexte
const kundenart = kundenartVon(me, stand)
const privat = kundenart !== 'unternehmen'
const units = Number(sub.storage?.units) || 0
const mail = stand.mailStorage || {}
const unitCents = Number(mail.unitPriceCents) || 0
const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, early: false, fehler: '', fehlerKlasse: null, zeichne: null }
aktiv = lauf
/** Zeilen des Blocks aus der Vorschau des Servers, knapp: Leistung, Preis, Zusatzspeicher, Gutschrift, Fällig (mit Abbuchungstag), Laufzeit, Beginn. */
const zeilenVon = (p) => {
const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents
const tag = berlinTag(p.nextChargeAt)
return [
{ label: 'Leistung', text: 'Wartungs-Abo statt Hosting-Abo', link: { text: 'Leistungen ansehen', href: leistungLink(T.wechsel.linkzeile, 'wartung') } },
{ label: 'Preis', text: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` },
...(units > 0
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht, insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat` }]
: []),
...((Number(p.creditCents) || 0) > 0
? [{ label: 'Gutschrift', text: `−${fmtEuro(p.creditCents / 100)} für nicht genutzte Tage des Hosting-Abos` }]
: []),
{ label: 'Fällig', text: `heute ${fmtEuro(p.amountDueCents / 100)}; danach ab ${fmtDatum(p.nextChargeAt)}${tag ? ` jeweils am ${tag.t}.` : ''}` },
{ label: 'Laufzeit', text: `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)} (12 Monate), danach monatlich kündbar` },
{ label: 'Beginn', text: 'sofort mit der Zahlung; einen späteren Beginn gibt es nicht' },
]
}
/** Die ausführlichen Sätze unter dem Knopf (Wortlaut wie bisher im Block). */
const einzelheitenVon = (p) => {
const tag = berlinTag(p.nextChargeAt)
return [
{ label: 'Leistung', text: 'Zusätzlich zum Hosting: Pflege deiner 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.' },
...(units > 0
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST})` }]
: []),
{ label: 'Heute fällig', text: `${fmtEuro(p.amountDueCents / 100)} (${UST})` },
{ label: 'Neuer Abbuchungstag', text: `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}` },
{ label: 'Laufzeit', text: `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)} (12 Monate ab dem Wechsel), danach monatlich kündbar` },
{ label: 'Beginn', text: 'Der Wechsel wird erst mit der Zahlung wirksam. Bei Kreditkarte, sobald die Zahlung erfolgreich ist (ggf. nach Bestätigung bei deiner 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.' },
]
}
const zeichne = () => {
const p = lauf.preview
const uebersicht = lauf.error
? h('p', { class: K.fehler }, lauf.error)
: lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Deine Übersicht wird berechnet …')
: baueBlock(zeilenVon(p), { id: 'switch-block' })
const sperre = lauf.busy || viewAs
const haken = privat
? h('input', {
type: 'checkbox', id: 'switch-beginn', checked: lauf.early, disabled: sperre,
on: { change: (e) => { lauf.early = e.currentTarget.checked; zeichne(); wurzel.querySelector('#switch-beginn')?.focus() } },
})
: null
const gesperrt = privat && !lauf.early
leeren(wurzel).append(h('section', { class: 'kn-panel kn-bestellen', id: 'switch-order' },
h('h2', null, 'Was du bestellst'),
uebersicht,
baueLinkzeile(T.wechsel.linkzeile, { id: 'switch-linkzeile' }),
haken ? h('label', { class: K.check }, haken, h('span', { id: 'switch-beginn-text' }, T.wechsel.haken)) : null,
gesperrt ? h('p', { class: 'kn-sperrzeile', id: 'switch-sperre' }, T.wechsel.gesperrt) : null,
h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', hidden: !lauf.fehler }, lauf.fehler),
h('div', { class: 'kn-aktionen' },
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)),
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
baueKundenartZeile(kundenart, T.kundenart, { id: 'switch-kundenart' }),
p ? baueEinzelheiten(einzelheitenVon(p), { id: 'switch-einzelheiten' }) : null))
}
lauf.zeichne = zeichne
const ladeVorschau = async () => {
const token = ++lauf.token
lauf.loading = true
lauf.error = ''
zeichne()
try {
const p = await api('/api/abo/switch/preview', { method: 'POST', body: {} })
if (token !== lauf.token || !aktuell()) return
lauf.preview = p
} catch (f) {
if (token !== lauf.token || !aktuell()) return
lauf.preview = null
lauf.error = f.message
}
lauf.loading = false
zeichne()
}
const zeigeFehler = (text, klasse) => { lauf.fehler = text; lauf.fehlerKlasse = klasse; zeichne() }
async function absenden() {
if (viewAs || lauf.busy || !lauf.preview) return
if (privat && !lauf.early) return // ohne Häkchen kein Wechsel; die Sperrzeile steht da
lauf.fehler = ''
const p = lauf.preview
const funktion = gemerkteFunktion()
const gesendet = funktion !== null
lauf.busy = true
zeichne()
try {
// Der Klick auf den Knopf ist die Zustimmung (acceptTerms). Nie kundenart: ob ein Häkchen nötig ist, entscheidet der Server.
const body = { acceptTerms: true, termsVersion: stand.termsVersion, prorationDate: p.prorationDate, amountDueCents: p.amountDueCents }
if (privat) body.earlyStart = true
if (gesendet) body.funktion = funktion
const res = await api('/api/abo/switch', { method: 'POST', body })
if (gesendet) vergissFunktion()
if (res.paymentUrl) {
const ziel = sichereAdresse(res.paymentUrl)
if (!ziel) {
lauf.busy = false
zeigeFehler('Die Zahlungsseite konnte nicht geöffnet werden. Der Wechsel wartet auf die Zahlung; Näheres steht unter Verträge.')
return
}
const knopf = wurzel.querySelector('#switch-buy')
if (knopf) knopf.textContent = 'Weiter zur Zahlung …'
zeigeFehler('Dein Wechsel wartet auf die Zahlung. Wir leiten dich zur Zahlung weiter.', 'kn-meldung')
window.location.href = ziel
return
}
lauf.busy = false
const sepa = Boolean(res.subscription?.switchPending)
const hinweis = sepa
? 'Dein Wechsel ins Wartungs-Abo gilt ab sofort. Die Lastschrift wird noch eingezogen; wir bestätigen dir den Wechsel per E-Mail.'
: 'Dein Wechsel ins Wartungs-Abo ist abgeschlossen. Wir bestätigen ihn per E-Mail.'
meldeZaehlerNeu('vertraege')
// Mit gemerkter Funktion zurück zu den Funktionen (dort öffnet P3 den Einschalt-Ablauf), sonst wie bisher zu den Verträgen
if (gesendet) {
let zurueck = wechselFunktionAus(res.subscription)
if (!zurueck) zurueck = wechselFunktionAus((await ladeAbo())?.subscription)
if (zurueck) { navigiere('#funktionen', { ersetzen: true }); return }
}
merkeHinweis(hinweis)
navigiere('#vertraege')
} catch (f) {
lauf.busy = false
zeigeFehler(f.message)
// Betrag oder Stichtag haben sich geändert: neue Vorschau laden, Fehlerhinweis bleibt stehen
if (f.status === 409) {
const msg = f.message
await ladeVorschau()
zeigeFehler(msg)
}
}
}
await ladeVorschau()
}