From 306c53871d4f6ee114bd230368997a7013ffb67d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 21:40:29 +0000 Subject: [PATCH] Kundenansicht neu E-Fix M1: Widerrufsstreifen und Pille nach Kauf, Wechsel und Speicher-Plus neu laden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ladeFristen lief nur einmal beim Start; eine neue 14-Tage-Frist nach einem Wechsel oder Speicher-Plus erschien erst nach Neuladen, ein abgelaufener Streifen blieb in einem langen PWA-Tab stehen. Die Hülle lädt die Fristen jetzt bei kn:zaehler-neu (vertraege, alle) und beim Zurückkehren in den Tab neu (ladeFristenNeu: ein Fehler lässt den Stand stehen, die jüngste Antwort gilt); das Speicher-Plus meldet kn:zaehler-neu vertraege nach Erfolg, der Wechsel tat es schon. Co-Authored-By: Claude Sonnet 5.5 --- public/neu/ansicht-speicher.js | 4 +++- public/neu/fristen-streifen.js | 10 ++++++++++ public/neu/huelle.js | 21 ++++++++++++++++++--- test/knAnsichtSpeicher.test.js | 3 +++ test/knHuelleStart.test.js | 19 +++++++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/public/neu/ansicht-speicher.js b/public/neu/ansicht-speicher.js index 26d2d39..c5ff891 100644 --- a/public/neu/ansicht-speicher.js +++ b/public/neu/ansicht-speicher.js @@ -5,7 +5,7 @@ import { api } from './api.js' import { h, leeren } from './dom.js' -import { K, ersetze, fmtEuro, fmtDatum, fmtTagMonat, ladeAbo, navigiere, setzeChatVorgabe, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' +import { K, ersetze, fmtEuro, fmtDatum, fmtTagMonat, ladeAbo, meldeZaehlerNeu, navigiere, setzeChatVorgabe, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.js' const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs @@ -142,6 +142,8 @@ function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) { ? (ch.task === false ? 'Gespeichert. Es entstehen keine zusätzlichen Kosten, die Pakete waren schon bezahlt.' : 'Danke, deine Bestellung ist eingegangen. Der Speicher wird in der Regel innerhalb eines Werktags eingerichtet.') : `Gespeichert. Die Verkleinerung wirkt zum ${fmtDatum(ch.effectiveAt)}.` await nachErfolg(hinweis, res) + // Ein Plus eröffnet für Privatkunden eine neue Widerrufsfrist: die Hülle lädt Streifen und Pille „Frist läuft“ neu (E-Fix M1) + if (plus) meldeZaehlerNeu('vertraege') } catch (f) { state.busy = false fehlerText = f.status === 409 ? `${f.message} Bitte lade die Seite neu, falls sich der Stand geändert hat.` : f.message diff --git a/public/neu/fristen-streifen.js b/public/neu/fristen-streifen.js index f4bb026..fb62ad7 100644 --- a/public/neu/fristen-streifen.js +++ b/public/neu/fristen-streifen.js @@ -14,6 +14,16 @@ export async function ladeFristen(api) { } } +/** Wie ladeFristen, aber für das Neuladen in der Sitzung: ein Fehler ergibt undefined (der bisherige Stand bleibt), keine Frist null. */ +export async function ladeFristenNeu(api) { + try { + const daten = await api('/api/fristen') + return daten?.anzeigen === true ? daten : null + } catch { + return undefined + } +} + const datumBerlin = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit' }) /** 'Widerrufsfrist bis TT.MM.' in der Zeit von Berlin; ohne gültiges Datum 'Widerrufsfrist läuft'. */ diff --git a/public/neu/huelle.js b/public/neu/huelle.js index 7cab63a..d52bc11 100644 --- a/public/neu/huelle.js +++ b/public/neu/huelle.js @@ -10,7 +10,7 @@ import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, leisteSichtbar } from './leiste.js' import { baueEtage } from './etage.js' import { erzeugeZaehler } from './zaehler.js' -import { baueStreifen, ladeFristen } from './fristen-streifen.js' +import { baueStreifen, ladeFristen, ladeFristenNeu } from './fristen-streifen.js' import { baueAvatar, baueKonto, installAngebot } from './konto.js' export const GERUEST_IDS = ['kn-banner', 'kn-ansage', 'kn-fehler', 'kn-menue', 'kn-kopf', 'kn-kopf-titel', 'kn-kopf-konto', 'kn-inhalt', 'bereich-website', 'bereich-funktionen', 'bereich-chat', 'bereich-vertraege', 'website-body', 'funktionen-body', 'chat-body', 'vertraege-body', 'kn-blatt', 'kn-brot', 'kn-etage-titel', 'kn-reiter-slot', 'news-body', 'termine-body', 'webshop-body', 'buchungen-body', 'zeiten-body', 'analyse-body', 'etage-body', 'kn-streifen', 'kn-leiste', 'kn-zeile-haupt', 'kn-zeile-etage', 'kn-konto-wurzel'] @@ -246,8 +246,23 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan router.geheZu(ansicht) }) doc.querySelector?.('.kn-skip')?.addEventListener('click', (ev) => { ev.preventDefault(); el('kn-inhalt').focus() }) // kein Fragment-Link: der Router läse #kn-inhalt als Route - doc.addEventListener('kn:zaehler-neu', (ev) => { zaehler.aktualisiere(ev.detail?.was ?? 'alle') }) - doc.addEventListener('visibilitychange', () => { if (doc.visibilityState === 'visible') zaehler.aktualisiere('alle') }) + // Widerrufsstreifen und Pille „Frist läuft“ (E-Fix M1): nach einem Kauf, Wechsel oder Speicher-Plus in der Sitzung und beim Zurückkehren in den Tab neu laden, + // damit eine neue Frist erscheint und ein abgelaufener Streifen nicht stehen bleibt. Ein Fehler lässt den bisherigen Stand stehen; die jüngste Antwort gilt. + let fristenLauf = 0 + async function frischeFristen() { + const nr = ++fristenLauf + const daten = await ladeFristenNeu(api) + if (nr !== fristenLauf || daten === undefined) return + fristen = daten + streifen.setze(daten) + aktualisiereKonto() + } + doc.addEventListener('kn:zaehler-neu', (ev) => { + const was = ev.detail?.was ?? 'alle' + zaehler.aktualisiere(was) + if (was === 'vertraege' || was === 'alle') frischeFristen() + }) + doc.addEventListener('visibilitychange', () => { if (doc.visibilityState === 'visible') { zaehler.aktualisiere('alle'); frischeFristen() } }) win.addEventListener('scroll', beiScroll, { passive: true }) el('kn-blatt').addEventListener('scroll', beiScroll, { passive: true }) win.addEventListener('beforeinstallprompt', (ev) => { ev.preventDefault(); installPrompt = ev; aktualisiereKonto() }) diff --git a/test/knAnsichtSpeicher.test.js b/test/knAnsichtSpeicher.test.js index 973f0cf..aa9bf88 100644 --- a/test/knAnsichtSpeicher.test.js +++ b/test/knAnsichtSpeicher.test.js @@ -21,6 +21,7 @@ const zeigeNeu = async (fx, daten, m = meMit('unternehmen')) => { } test('Plus: Block, Linkzeile, Knopf, Kundenart-Zeile, kein Häkchen, Body ohne Kundenart', mit({ 'POST /api/abo/storage/preview': PLUS, 'POST /api/abo/storage': { change: { task: true }, subscription: abo() }, 'GET /api/abo': speicher() }, async (fx) => { + const events = []; fx.document.dispatchEvent = (e) => events.push(`${e.type}:${e.detail.was}`) const w = await zeigeNeu(fx, speicher(), meMit('unternehmen')) await fx.klick(fx.finde(w, '#storage-plus')) await fx.ruhe() @@ -35,6 +36,8 @@ test('Plus: Block, Linkzeile, Knopf, Kundenart-Zeile, kein Häkchen, Body ohne K await fx.ruhe() assert.deepEqual(fx.fetchLog.find((a) => a.url === '/api/abo/storage').body, { units: 3, acceptTerms: true, termsVersion: NEU }) assert.match(fx.text(w), /Danke, deine Bestellung ist eingegangen\. Der Speicher wird in der Regel innerhalb eines Werktags eingerichtet\./) + // M1: ein Plus eröffnet für Privatkunden eine neue Widerrufsfrist: die Hülle lädt Streifen und Pille neu (kn:zaehler-neu vertraege) + assert.ok(events.includes('kn:zaehler-neu:vertraege'), `Ereignisse: ${events.join(', ')}`) })) test('Minus: genau eine Warnzeile aus dem Wortlaut, Knopf „Speichern“, Body nur mit units', mit({ 'POST /api/abo/storage/preview': VORSCHAU_SPEICHER, 'POST /api/abo/storage': { change: { effectiveAt: '2026-11-01T00:00:00.000Z' }, subscription: abo() } }, async (fx) => { diff --git a/test/knHuelleStart.test.js b/test/knHuelleStart.test.js index 831d726..cd035d2 100644 --- a/test/knHuelleStart.test.js +++ b/test/knHuelleStart.test.js @@ -113,6 +113,25 @@ test('Fristen 404 (Server ohne P5): kein Streifen, "Vertrag widerrufen" bleibt o assert.deepEqual([p.el('kn-streifen').hidden, p.fx.text(p.el('kn-streifen'))], [false, 'Widerrufsfrist bis 21.10. · Widerrufen']); assert.match(p.fx.text(p.el('kn-konto-wurzel')), /Frist läuft/) }) +test('M1: Widerrufsstreifen und Pille werden nach Kauf, Wechsel oder Speicher-Plus in der Sitzung neu geladen (kn:zaehler-neu vertraege/alle, Sichtbarkeit); ein Fehler lässt den Stand stehen', async () => { + const frist = { fristen: [{ quelle: 'wechsel', bezeichnung: 'Wechsel', bis: '2026-10-21T21:59:59.999Z' }], fruehestesEnde: '2026-10-21T21:59:59.999Z', anzeigen: true } + const karte = { ...KARTE_KUNDE } + const b = await boot({ hash: '', karte }); const n = () => b.log.filter((l) => l[1] === '/api/fristen').length + assert.equal(b.el('kn-streifen').hidden, true); assert.doesNotMatch(b.fx.text(b.el('kn-konto-wurzel')), /Frist läuft/) + const n0 = n() + // der Kunde wechselt oder bestellt Speicher: die Bestellseite meldet kn:zaehler-neu, jetzt gibt es eine Frist + karte['/api/fristen'] = frist + b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'chat' } })); await b.fx.ruhe(); assert.equal(n(), n0, 'Chat-Zähler laden die Fristen nicht') + b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'vertraege' } })); await b.fx.ruhe() + assert.equal(n(), n0 + 1); assert.deepEqual([b.el('kn-streifen').hidden, b.fx.text(b.el('kn-streifen'))], [false, 'Widerrufsfrist bis 21.10. · Widerrufen']); assert.match(b.fx.text(b.el('kn-konto-wurzel')), /Frist läuft/) + // ein Netzfehler lässt Streifen und Pille stehen + karte['/api/fristen'] = new Error('Netz weg'); b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'alle' } })); await b.fx.ruhe() + assert.equal(n(), n0 + 2); assert.equal(b.el('kn-streifen').hidden, false) + // die Frist ist abgelaufen (Server: anzeigen false): der Streifen verschwindet, auch beim Zurückkehren in den Tab + karte['/api/fristen'] = { fristen: [], fruehestesEnde: null, anzeigen: false }; b.doc.visibilityState = 'visible'; b.doc.dispatchEvent(new Event('visibilitychange')); await b.fx.ruhe() + assert.equal(n(), n0 + 3); assert.equal(b.el('kn-streifen').hidden, true); assert.doesNotMatch(b.fx.text(b.el('kn-konto-wurzel')), /Frist läuft/) +}) + test('Zähler: Verträge laden beim Betreten und bei kn:zaehler-neu (document) neu; der Chat-Zähler fällt beim Öffnen des Chats weg', async () => { const b = await boot({ hash: '', karte: { ...KARTE_KUNDE, '/api/chat/unread-count': { unread: 3 } } }); const n = (pfad) => b.log.filter((l) => l[1] === pfad).length const i0 = n('/api/invoices'); const chatTab = () => b.fx.finde(b.el('kn-zeile-haupt'), '[role=tab][aria-label="Chat, 3 ungelesen"]')