/* Kundenansicht neu: Konto. Das Profilbild (Avatar) öffnet am Handy ein Sheet von unten, am Desktop ein Popover rechts oben. Reihenfolge der Zeilen (V3): Passwort ändern, Datenschutz, Impressum, App installieren (nur wenn sinnvoll), Verträge kündigen, Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden. 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 in der Reihenfolge von V3. */ export function kontoEintraege({ fristen, install } = {}) { const frist = fristen?.anzeigen === true return [ { id: 'passwort', text: 'Passwort ändern', href: '#passwort' }, { id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true }, { id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true }, ...(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' }, ] } /** 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' : ''}` 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 } }