/* 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 const paketText = `${units} Paket${units === 1 ? '' : 'e'}` const speicherGb = units * (Number(mail.gbPerUnit) || 0) /** * Zeilen des Blocks aus der Vorschau des Servers, knapp: Leistung, Preis (mit gebuchtem Zusatzspeicher), Fällig (mit der Gutschrift und dem Abbuchungstag), * Laufzeit, Beginn. Die Einzelposten stehen ausführlich unter dem Knopf (einzelheitenVon); so passt der Block auch mit Zusatzspeicher in 375 x 667. */ const zeilenVon = (p) => { const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents const tag = berlinTag(p.nextChargeAt) const gutschrift = (Number(p.creditCents) || 0) > 0 ? ` (nach Gutschrift von ${fmtEuro(p.creditCents / 100)} für nicht genutzte Tage des Hosting-Abos)` : '' return [ { label: 'Leistung', text: 'Wartungs-Abo statt Hosting-Abo', link: { text: 'Leistungen ansehen', href: leistungLink(T.wechsel.linkzeile, 'wartung') } }, { label: 'Preis', text: units > 0 ? `${fmtEuro((monthly + units * unitCents) / 100)} pro Monat mit ${paketText} Zusatzspeicher (+${speicherGb} GB) (Gesamtpreis, ${UST})` : `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` }, { label: 'Fällig', text: `heute ${fmtEuro(p.amountDueCents / 100)}${gutschrift}; 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: `${paketText} (+${speicherGb} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST})` }] : []), ...((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, role: 'alert' }, 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', role: 'alert', 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() }