/* 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 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, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } 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 (Leistung, Wartungs-Abo, Zusatzspeicher, Gutschrift, fällig, 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: '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.' }, { label: 'Wartungs-Abo', text: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST}); enthalten sind Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat` }, ...(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}), insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat` }] : []), ...((Number(p.creditCents) || 0) > 0 ? [{ label: 'Gutschrift für nicht genutzte Tage', text: `−${fmtEuro(p.creditCents / 100)} aus dem bereits bezahlten Hosting-Abo (${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' }))) } 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() }