/* Kundenansicht neu (P2): Reiter Abo in Verträge. Mit laufendem Abo der Vertrag samt Einstiegen (Wechsel, Gutschrift, Widerruf), ohne Abo die Auswahl der beiden Abos. Texte der Vertragsseite stammen aus app.js, in der Du-Form. */ import { api } from './api.js' import { h, leeren } from './dom.js' import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse, beiRueckkehr } from './ansicht-hilfen.js' const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.' const TEXT_ZAHLUNG = 'Die letzte Abbuchung ist fehlgeschlagen. Bitte aktualisiere deine Zahlungsart, damit deine Website online bleibt.' const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus nicht möglich.' const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.' /** Die zwei Warnungen über dem Vertrag (auch im Stundenkonto): fehlgeschlagene Abbuchung, späterer Beginn nach der Widerrufsfrist. */ export function aboWarnungen(sub) { const aus = [] if (sub?.active && sub.paymentProblem) aus.push(h('p', { class: `${K.meldung} kn-meldung-warn` }, TEXT_ZAHLUNG)) if (sub?.active && sub.serviceStartsAt) aus.push(h('p', { class: K.meldung }, `Wie gewählt beginnen wir nach Ablauf der Widerrufsfrist am ${fmtDatum(sub.serviceStartsAt)}. Dann wird auch zum ersten Mal abgebucht.`)) return aus } const gekuendigt = (sub) => Boolean(sub?.cancelAtPeriodEnd || sub?.cancelAt) const produktName = (daten, key) => { const p = (daten?.products || []).find((x) => x.key === key) return (p?.name || (key === 'hosting' ? 'Hosting-Abo' : 'Wartungs-Abo')).replace(/^WEBklar\s+/, '') } const produkt = (daten, key) => (daten?.products || []).find((x) => x.key === key) || null /** 'ohne' | 'beendet' | 'laeuft' | 'gekuendigt'. */ export function aboZustand(daten) { const sub = daten?.subscription if (!sub) return 'ohne' if (!sub.active) return 'beendet' return gekuendigt(sub) ? 'gekuendigt' : 'laeuft' } /** Zeile zur Abrechnung (app.js aboDateLine). */ export function aboZeile(sub) { if (gekuendigt(sub)) return `Leistungen bis ${fmtDatum(sub.cancelAt || sub.currentPeriodEnd)}` if (sub.serviceStartsAt) return `Beginn und erste Abbuchung am ${fmtDatum(sub.serviceStartsAt)}` return `nächste Abbuchung ${fmtDatum(sub.currentPeriodEnd)}` } /** Laufzeit-Hinweis (app.js aboTermText, § 8 der Bedingungen). */ export function laufzeitText(sub) { if (sub.cancelAtPeriodEnd) return `Gekündigt zum ${fmtDatum(sub.cancelAt || sub.currentPeriodEnd)}. Bis dahin bleiben alle Leistungen verfügbar.` if (sub.minTermEnd && new Date(sub.minTermEnd) > new Date()) return `Mindestlaufzeit bis ${fmtDatum(sub.minTermEnd)}. Kündigung frühestens zu diesem Termin, mit einem Monat Frist.` return 'Monatlich kündbar zum Ende des Abrechnungsmonats.' } const leistungen = (features) => h('ul', { class: 'kn-leistungen' }, (features || []).map(([titel, detail]) => h('li', null, titel, detail ? h('small', null, detail) : null))) const fakt = (titel, wert) => h('div', null, h('dt', null, titel), h('dd', null, wert)) // "Zahlungsart aktualisieren": Stripe-Kundenportal; im Ansichtsmodus gesperrt function zahlungsKnopf(viewAs) { const fehler = h('p', { class: K.fehler, role: 'alert', hidden: true }) const knopf = h('button', { type: 'button', class: K.primaer, disabled: viewAs }, 'Zahlungsart aktualisieren') knopf.addEventListener('click', async () => { if (viewAs) return knopf.disabled = true fehler.hidden = true try { const { url } = await api('/api/abo/portal', { method: 'POST', body: {} }) const ziel = sichereAdresse(url) if (!ziel) throw new Error('Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuche es später noch einmal.') window.location.href = ziel } catch (f) { fehler.textContent = f.message fehler.hidden = false knopf.disabled = false } }) beiRueckkehr(knopf, () => { knopf.disabled = viewAs }) // zurück von Stripe (Bfcache): wieder bedienbar (E-Fix M6) return [knopf, viewAs ? h('span', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null, fehler] // ohne Umhüllung: der Knopf ist das Element mit diesem Text } function wechselKarte(daten, sub) { if (sub.productKey !== 'hosting') return null const wartung = produkt(daten, 'wartung') if (sub.switchPending) { const url = sichereAdresse(sub.switchPending.paymentUrl) return h('section', { class: 'kn-sektion kn-wechsel' }, h('h3', null, 'Dein Wechsel wartet auf die Zahlung'), h('p', { class: 'kn-hinweis' }, `Dein Wechsel zum Wartungs-Abo ist erst abgeschlossen, wenn die Zahlung bestätigt ist.${url ? '' : ' Schreib uns gern im Chat, wenn du Hilfe brauchst.'}`), url ? h('a', { class: K.primaer, href: url }, 'Jetzt bezahlen') : null) } if (gekuendigt(sub)) { return h('section', { class: 'kn-sektion kn-wechsel' }, h('h3', null, 'Wartungs-Abo'), h('p', { class: 'kn-hinweis' }, 'Dein Abo ist gekündigt. Bitte schreib uns im Chat, wenn du zum Wartungs-Abo wechseln möchtest.')) } if (!sub.canSwitch || !wartung) return null return h('section', { class: 'kn-sektion kn-wechsel' }, h('h3', null, 'Mehr als Hosting? Das Wartungs-Abo'), h('p', { class: 'kn-preis' }, h('strong', null, fmtEuro(wartung.amountCents / 100)), ' pro Monat'), h('p', { class: 'kn-hinweis' }, `${TEXT_UST} Zusätzlich zum Hosting: Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat.`), h('a', { class: K.primaer, ...zuAnsicht('wechsel') }, 'Zum Wartungs-Abo wechseln')) } function vertragsBlock(daten, sub, viewAs) { const zustand = aboZustand(daten) const pille = zustand === 'gekuendigt' ? K.pille.grau : sub.paymentProblem ? K.pille.orange : K.pille.gruen const pilleText = zustand === 'gekuendigt' ? 'gekündigt' : sub.paymentProblem ? 'Zahlung offen' : 'aktiv' const mail = daten.mailStorage || {} const einheiten = Number(sub.storage?.units || 0) const gb = (Number(mail.includedGb) || 0) + einheiten * (Number(mail.gbPerUnit) || 0) const katalog = produkt(daten, sub.productKey) return h('section', { class: 'kn-sektion kn-vertrag', id: 'kn-vertrag' }, h('div', { class: 'kn-vertrag-kopf' }, h('h2', { class: K.abschnitt }, 'Mein Vertrag'), h('span', { class: pille }, pilleText)), h('h3', null, produktName(daten, sub.productKey)), h('p', { class: 'kn-preis' }, h('strong', null, fmtEuro(sub.priceAmount / 100)), ' pro Monat'), h('p', { class: 'kn-hinweis' }, TEXT_UST), h('dl', { class: 'kn-fakten' }, fakt(gekuendigt(sub) ? 'Leistungen' : 'Abrechnung', aboZeile(sub)), fakt('Laufzeit', laufzeitText(sub)), sub.offerText ? fakt('Angebot', sub.offerText) : null, fakt('E-Mail-Speicher', `${gb} GB${einheiten ? ` (${einheiten} Zusatzpaket${einheiten === 1 ? '' : 'e'})` : ''}`)), leistungen(katalog?.features), h('p', { class: 'kn-links' }, h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Vertragsbedingungen'), ' · ', h('a', { href: sub.termsPdfUrl || '/WEBklar-Abo-Vertragsunterlagen.pdf', target: '_blank', rel: 'noopener noreferrer' }, 'als PDF')), sub.withdrawalOpen ? h('p', { class: 'kn-widerruf' }, h('a', { href: '/widerrufen.html' }, `Vertrag widerrufen · bis ${fmtTagMonat(sub.withdrawalUntil)}`)) : null, sub.paymentProblem ? zahlungsKnopf(viewAs) : null) } function gutschriftLink() { const badge = h('span', { class: 'kn-badge', 'data-badge': true, hidden: true }) const link = h('a', { class: `${K.knopf} kn-gutschrift-link`, ...zuAnsicht('takeover') }, 'Übernahmegutschrift beantragen') return { link, setzeZahl(n) { if (n > 0) { badge.textContent = String(n); badge.hidden = false; link.append(' ', badge) } } } } function auswahlKarte(daten, key) { const p = produkt(daten, key) if (!p) return null const angebot = daten.offers?.[key] const link = daten.available ? h('a', { class: K.primaer, ...zuAnsicht(`bestellen-${key}`) }, 'Abo abschließen') : h('a', { class: `${K.primaer} kn-gesperrt`, 'aria-disabled': 'true', role: 'link' }, 'Abo abschließen') return h('article', { class: 'kn-abo-karte', 'data-produkt': key }, h('h3', null, produktName(daten, key)), angebot ? h('p', { class: 'kn-angebot' }, h('span', { class: K.pille.violett }, 'Dein persönliches Angebot'), h('span', { class: 'kn-angebot-text' }, `${angebot.text}${angebot.validUntil ? ` · gültig bis ${fmtDatum(angebot.validUntil)}` : ''}`)) : null, h('p', { class: 'kn-preis' }, h('strong', null, fmtEuro(p.amountCents / 100)), ' pro Monat'), h('p', { class: 'kn-hinweis' }, `${TEXT_UST} ${p.minTermMonths} Monate Mindestlaufzeit, danach monatlich kündbar.`), leistungen(p.features), link) } function auswahl(daten, beendet) { return h('div', { class: 'kn-abo-auswahl' }, beendet ? h('p', { class: K.meldung }, 'Dein bisheriges Abo ist beendet. Du kannst jederzeit ein neues abschließen.') : null, daten.available ? null : h('p', { class: K.meldung }, TEXT_NICHT_VERFUEGBAR), h('div', { class: 'kn-abo-karten' }, auswahlKarte(daten, 'hosting'), auswahlKarte(daten, 'wartung')), h('p', { class: 'kn-hinweis' }, 'Alle Preise gemäß § 19 UStG ohne Umsatzsteuer. Es gelten die ', h('a', { href: '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Vertragsbedingungen'), '.')) } /** Reiter-Inhalt Abo; daten ist die Antwort von GET /api/abo. */ export async function zeigeAbo(box, { daten, me } = {}) { const nr = (laeufe.get(box) ?? 0) + 1 laeufe.set(box, nr) const aktuell = () => laeufe.get(box) === nr const viewAs = me?.viewAs === true const sub = daten?.subscription ?? null const zustand = aboZustand(daten) const gutschrift = sub ? gutschriftLink() : null const teile = aboWarnungen(sub) if (sub?.active) { teile.push(vertragsBlock(daten, sub, viewAs), wechselKarte(daten, sub)) } else { teile.push(auswahl(daten ?? {}, zustand === 'beendet')) } if (gutschrift) teile.push(h('p', { class: 'kn-gutschrift' }, gutschrift.link)) leeren(box).append(...teile.filter(Boolean)) // Zahl der offenen Rückfragen zur Übernahmegutschrift: ein Ausfall lässt nur die Zahl weg if (!gutschrift) return try { const antwort = await api('/api/abo/takeover-credits') if (!aktuell()) return gutschrift.setzeZahl((Array.isArray(antwort?.requests) ? antwort.requests : []).filter((r) => r?.status === 'rueckfrage').length) } catch { /* ohne Zahl bleibt der Einstieg stehen */ } }