/* Kundenansicht neu (P2): Bereich Verträge mit den Reitern Abo und Rechnungen (Navigation leichter V7, vorher dazu Stunden und E-Mail). Das Stundenkonto des Wartungs-Abos steht als Abschnitt unter dem Abo; #stunden öffnet das Abo und springt dorthin. Der E-Mail-Speicher ist die Etage #speicher (Link „ändern“ im Abo). Der Rahmen lädt Abo und Rechnungen parallel (jede Quelle mit eigenem Fehlerweg), wählt den Reiter allein aus route.reiter und gibt die schon geladenen Antworten an die Reiter-Inhalte weiter. Bestellen, Wechsel, Gutschrift und Speicher sind Etagen, die die Hülle direkt ruft. */ import { api } from './api.js' import { h, leeren } from './dom.js' import { baueReiter } from './reiter.js' import { aktuelleRoute } from './router.js' import { K, ersetze, ladeAbo, wechselFunktionAus, navigiere, nimmHinweis, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { offeneRechnungen } from './rechnung-offen.js' import { zeigeRechnungen } from './vertraege-rechnungen.js' import { zeigeAbo } from './vertraege-abo.js' import { zeigeStunden } from './ansicht-stunden.js' /** Nach ?abo=success: so oft und in diesem Abstand wird auf ein laufendes Abo gewartet (Tests verkürzen es). */ export const AUF_ABO_WARTEN = { versuche: 10, pauseMs: 2000 } const LABEL = { abo: 'Abo', rechnungen: 'Rechnungen' } // Reiterklicks navigieren mit den ausdrücklichen Hashes (P1 schreibt #wartung und #abo kanonisch um) const HASH = { abo: '#abo', rechnungen: '#rechnungen' } const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs let reiterWechsel = null // Reiterklick mit laufendem Router: der nächste Aufruf nimmt die schon geladenen Antworten /** Sichtbare Reiter: immer Abo und Rechnungen (Navigation leichter V7). Stunden stehen im Abo, der E-Mail-Speicher ist die Etage #speicher. */ export function sichtbareReiter() { return ['abo', 'rechnungen'] } /** Gibt es ein Stundenkonto unter dem Abo? Nur mit laufendem Wartungs-Abo. */ export const mitStundenkonto = (daten) => Boolean(daten?.subscription?.active && daten.subscription.productKey === 'wartung') /** Reiterwahl allein aus route.reiter (routeReiter) und den Daten, nie aus der Adressleiste. Nur der Bereichs-Einstieg (ohne Reiter) springt auf Rechnungen. */ export function waehleReiter({ routeReiter, sichtbar, offeneRechnungen: offen }) { if (routeReiter) return sichtbar.includes(routeReiter) ? routeReiter : 'abo' return offen > 0 ? 'rechnungen' : 'abo' } const pause = (ms) => new Promise((r) => setTimeout(r, ms)) const laden = (api_) => api_('/api/invoices').catch(() => null) export async function zeigeVertraege(wurzel, { route, me, nav } = {}) { const nr = (laeufe.get(wurzel) ?? 0) + 1 laeufe.set(wurzel, nr) const aktuell = () => laeufe.get(wurzel) === nr const vorgabe = reiterWechsel && reiterWechsel.wurzel === wurzel && reiterWechsel.bis > Date.now() ? reiterWechsel : null reiterWechsel = null const suche = new URLSearchParams(globalThis.window?.location?.search ?? '') const aboParam = suche.get('abo') const nachStripe = aboParam === 'success' || aboParam === 'cancel' let daten let rechnungen if (vorgabe) { ({ daten, rechnungen } = vorgabe) } else { leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …')) ;[daten, rechnungen] = await Promise.all([ladeAbo(), laden(api)]) if (!aktuell()) return } // Rückkehr von Stripe: auf das laufende Abo warten (der Webhook kann noch unterwegs sein) if (aboParam === 'success') { for (let i = 0; i < AUF_ABO_WARTEN.versuche && !daten?.subscription?.active; i += 1) { ersetze(wurzel, h('p', { class: K.meldung }, 'Vielen Dank! Dein Abo wird aktiviert …'), h('p', { class: 'kn-hinweis' }, 'Das dauert nur einen Moment.')) await pause(AUF_ABO_WARTEN.pauseMs) if (!aktuell()) return daten = (await ladeAbo()) ?? daten if (!aktuell()) return } } if (nachStripe) window.history.replaceState(window.history.state, '', window.location.pathname + window.location.hash) if (aboParam === 'success' && wechselFunktionAus(daten?.subscription)) { navigiere('#funktionen', { ersetzen: true }) return } const sichtbar = sichtbareReiter(daten) const offen = offeneRechnungen(rechnungen).length const gewaehlt = waehleReiter({ routeReiter: route?.reiter, sichtbar, offeneRechnungen: offen }) const stundenGewuenscht = route?.reiter === 'stunden' // #stunden: Abo zeigen und zum Stundenkonto springen const stundenkonto = mitStundenkonto(daten) const hinweis = nimmHinweis() const inhalt = h('div', { class: 'kn-reiter-inhalt' }) const zeichneReiter = (key) => { leeren(inhalt) if (key === 'rechnungen') return zeigeRechnungen(inhalt, { me, antwort: rechnungen ?? undefined }) if (!daten) return Promise.resolve() if (stundenGewuenscht && !stundenkonto) inhalt.append(h('p', { class: 'kn-hinweis' }, 'Ein Stundenkonto gibt es im Wartungs-Abo.')) const platz = h('div', { class: 'kn-reiter-abo' }) inhalt.append(platz) const fertig = [zeigeAbo(platz, { daten, me })] if (stundenkonto) { const konto = h('div') const abschnitt = h('section', { class: 'kn-sektion kn-stundenkonto', id: 'kn-stundenkonto', 'aria-labelledby': 'kn-stundenkonto-titel' }, h('h2', { class: K.abschnitt, id: 'kn-stundenkonto-titel' }, 'Stundenkonto'), konto) inhalt.append(abschnitt) fertig.push(zeigeStunden(konto, { route, me, nav, daten, eingebettet: true }).then(() => { if (stundenGewuenscht && nav !== 'back' && typeof abschnitt.scrollIntoView === 'function') abschnitt.scrollIntoView({ block: 'start' }) })) } return Promise.all(fertig) } const bei = (key) => { if (aktuelleRoute() !== null) reiterWechsel = { wurzel, daten, rechnungen, bis: Date.now() + 5000 } // der Router ruft uns gleich neu else zeichneReiter(key) navigiere(HASH[key], { ersetzen: true }) } const reiterZeile = baueReiter({ eintraege: sichtbar.map((key) => ({ key, label: LABEL[key] })), aktiv: gewaehlt, art: 'unterstrich', ariaLabel: 'Verträge', bei }) ersetze(wurzel, hinweis ? h('p', { class: `${K.meldung} kn-meldung-ok`, role: 'status' }, hinweis) : null, daten ? null : h('p', { class: K.fehler }, ABO_NICHT_GELADEN), reiterZeile, inhalt, h('p', { class: 'kn-kuendigen' }, h('a', { href: '/kuendigen.html' }, 'Verträge hier kündigen'))) await zeichneReiter(gewaehlt) }