/* Kundenansicht neu: Konto. Das Profilbild (Avatar) öffnet am Handy ein Sheet von unten, am Desktop ein Popover rechts oben. Reihenfolge der Zeilen (V3, geändert mit „Navigation leichter“ V8): Passwort ändern, App installieren (nur wenn sinnvoll), Verträge kündigen, Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden; darunter klein Datenschutz und Impressum. Namen und Mails kommen nur als Text in die Seite. Das Sheet ist ein Menü: Esc, ein Tipp aufs Dunkle und „Schließen“ schließen es; Zurück schließt es über das Overlay des Routers. */ import { h, leeren } from './dom.js' import { oeffneOverlay, beendeOverlay, geheZu } from './router.js' /** Initialen aus den ersten zwei Wörtern (nur Wörter, die mit Buchstabe oder Ziffer beginnen), sonst die ersten zwei Buchstaben. */ export function initialenVon(name) { const woerter = String(name || '').split(/\s+/).filter((w) => /^[\p{L}\p{N}]/u.test(w)) const buchstaben = woerter.length > 1 ? woerter[0][0] + woerter[1][0] : (woerter[0] || '').slice(0, 2) return buchstaben.toUpperCase() } /** Installationsangebot: nur am Handy und nicht in der installierten App; Chrome per Dialog ('prompt'), iPhone über Teilen ('ios'). */ export function installAngebot({ handy, standalone, prompt, ios }) { if (!handy || standalone) return null if (prompt) return 'prompt' return ios ? 'ios' : null } const HINWEIS_IOS = 'Tippe auf Teilen und dann auf Zum Home-Bildschirm.' /** Zeilen des Kontos: vier volle Zeilen, Datenschutz und Impressum klein darunter (klein: true). */ export function kontoEintraege({ fristen, install } = {}) { const frist = fristen?.anzeigen === true return [ { id: 'passwort', text: 'Passwort ändern', href: '#passwort' }, ...(install ? [{ id: 'installieren', text: 'App installieren', ...(install === 'ios' ? { hinweis: HINWEIS_IOS } : {}) }] : []), { id: 'kuendigen', text: 'Verträge kündigen', href: '/kuendigen.html', betont: true }, { id: 'widerrufen', text: 'Vertrag widerrufen', href: '/widerrufen.html', betont: true, ...(frist ? { pille: 'Frist läuft' } : {}) }, { id: 'abmelden', text: 'Abmelden' }, { id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true, klein: true }, { id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true, klein: true }, ] } /** Profilbild-Knopf: Initialen sofort, das Bild von /api/avatar.svg erscheint erst nach dem Laden (sonst bleiben die Initialen). */ export function baueAvatar({ name, kundenId }) { const knopf = h('button', { class: 'kn-avatar', type: 'button', 'aria-label': 'Mein Konto', 'aria-haspopup': 'dialog', 'aria-expanded': 'false' }, h('span', { class: 'kn-avatar-kreis' }, h('span', { class: 'kn-initialen' }, initialenVon(name)))) if (kundenId) { const bild = h('img', { alt: '', decoding: 'async', src: `/api/avatar.svg?c=${encodeURIComponent(kundenId)}`, on: { load: () => knopf.classList.add('has-img'), error: () => bild.remove() } }) knopf.firstElementChild.append(bild) } return knopf } const mitModifikator = (ev) => ev.defaultPrevented || ev.button > 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey export function baueKonto({ me, fristen, install, onAbmelden, onInstall, overlay = { oeffne: oeffneOverlay, beende: beendeOverlay }, navigiere = geheZu }) { const kunde = me?.customer ?? {} const stand = { fristen, install, offen: false, ausloeser: null } const liste = h('div', { class: 'kn-konto-liste' }) const zu = h('button', { class: 'kn-konto-zu', type: 'button', on: { click: () => schliesse() } }, 'Schließen') const panel = h('div', { class: 'kn-konto' }, h('div', { class: 'kn-konto-kopf' }, h('div', null, h('b', { class: 'kn-konto-name' }, kunde.companyName || kunde.name || kunde.email || ''), h('span', { class: 'kn-konto-mail' }, kunde.email || '')), zu), liste) const wurzel = h('div', { class: 'kn-konto-wurzel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Konto', tabindex: '-1', hidden: true, on: { keydown: beiTaste } }, h('div', { class: 'kn-konto-scrim', on: { click: () => schliesse() } }), panel) function zeile(e) { const inhalt = [h('span', { class: 'kn-konto-text' }, e.text, e.hinweis ? h('span', { class: 'kn-konto-hinweis' }, e.hinweis) : null), e.pille ? h('span', { class: 'kn-pille kn-pille-orange' }, e.pille) : null] const klasse = `kn-konto-zeile${e.betont ? ' kn-konto-zeile--betont' : ''}${e.klein ? ' kn-konto-zeile--klein' : ''}` if (e.href === undefined) return h('button', { class: klasse, type: 'button', 'data-konto': e.id, on: { click: () => (e.id === 'abmelden' ? onAbmelden() : onInstall?.()) } }, inhalt) const intern = e.href.startsWith('#') return h('a', { class: klasse, href: e.href, 'data-konto': e.id, target: e.neuerTab ? '_blank' : null, rel: e.neuerTab ? 'noopener' : null, on: { click: (ev) => { if (!intern || mitModifikator(ev)) return; ev.preventDefault(); navigiere(e.href) } } }, inhalt) // der Router schließt das Overlay selbst } function zeichne() { const aktiv = globalThis.document?.activeElement?.closest?.('[data-konto]')?.getAttribute('data-konto') leeren(liste) for (const e of kontoEintraege({ fristen: stand.fristen, install: stand.install })) liste.append(zeile(e)) if (stand.offen && aktiv) liste.querySelector(`[data-konto="${aktiv}"]`)?.focus() } function fokussierbare() { return wurzel.querySelectorAll('a[href], button:not([disabled])') } // Tab bleibt im Sheet (aria-modal) function beiTaste(ev) { if (ev.key === 'Escape') { ev.preventDefault(); schliesse(); return } if (ev.key !== 'Tab') return const alle = fokussierbare() const erstes = alle[0] const letztes = alle.at(-1) const aktiv = globalThis.document?.activeElement if (!ev.shiftKey && aktiv === letztes) { ev.preventDefault(); erstes.focus() } else if (ev.shiftKey && aktiv === erstes) { ev.preventDefault(); letztes.focus() } } // Zu und Fokus zurück; vom Router (Zurück-Taste, Navigation) und von schliesse() genutzt. Mehrfachaufruf schadet nicht. function schliesseAnsicht() { if (!stand.offen) return stand.offen = false wurzel.hidden = true stand.ausloeser?.setAttribute('aria-expanded', 'false') stand.ausloeser?.focus?.({ preventScroll: true }) } /** Vom Besitzer (Esc, Scrim, „Schließen“): zumachen und den Overlay-Eintrag des Routers zurücknehmen. */ function schliesse() { if (!stand.offen) return schliesseAnsicht() overlay.beende() } function oeffne(ausloeser) { if (stand.offen) return stand.offen = true stand.ausloeser = ausloeser ?? null ausloeser?.setAttribute('aria-expanded', 'true') wurzel.hidden = false overlay.oeffne({ schliesse: schliesseAnsicht }) liste.firstElementChild?.focus() } function aktualisiere({ fristen: f, install: i } = {}) { stand.fristen = f stand.install = i zeichne() } zeichne() return { wurzel, oeffne, schliesse, istOffen: () => stand.offen, aktualisiere } }