Kundenansicht neu P2: Reiter Abo und Abo-Auswahl
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf391eb117
commit
989e7be640
2 files changed
+228
No files matched your search
@@ -0,0 +1,174 @@
|
||||
/* 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, safeUrl } 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.'
|
||||
|
||||
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, 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 = safeUrl(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
|
||||
}
|
||||
})
|
||||
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 = safeUrl(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 = []
|
||||
if (sub?.active && sub.paymentProblem) teile.push(h('p', { class: `${K.meldung} kn-meldung-warn` }, TEXT_ZAHLUNG))
|
||||
if (sub?.active && sub.serviceStartsAt) teile.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.`))
|
||||
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 */ }
|
||||
}
|
||||
Reference in new issue
Block a user