From 5cd49d6a4a04a1cd5fa812234a13e39190bd182e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 22:44:20 +0000 Subject: [PATCH] Kundenansicht neu E-Fix F1 (Nachkorrektur): Widerrufsstreifen auf den Bestellseiten ausgeblendet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf Abo bestellen, Wechsel, Zusatzspeicher und Übernahmegutschrift fehlt der feste Widerrufsstreifen, damit Kerninfo und Knopf "Zahlungspflichtig bestellen" auch bei Privatkunden in laufender Frist auf 375x667 auf einem Bildschirm stehen (Spec Abschnitt 4). Auf den Hauptseiten bleibt er (Spec 5.2), "Vertrag widerrufen" bleibt im Konto und unter Verträge erreichbar. Co-Authored-By: Claude Sonnet 5.5 --- public/neu/fristen-streifen.js | 33 ++++++++++++++++++++++----------- public/neu/huelle.js | 3 ++- public/neu/routen.js | 14 ++++++++++++++ test/knHuelleFristen.test.js | 15 +++++++++++++++ test/knHuelleRouten.test.js | 14 +++++++++++++- test/knHuelleStart.test.js | 18 ++++++++++++++++++ 6 files changed, 84 insertions(+), 13 deletions(-) diff --git a/public/neu/fristen-streifen.js b/public/neu/fristen-streifen.js index fb62ad7..49bd3c1 100644 --- a/public/neu/fristen-streifen.js +++ b/public/neu/fristen-streifen.js @@ -41,22 +41,33 @@ export function fruehesteFrist(daten) { return zeiten.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? '' } -/** Streifen in `wurzel` füllen: setze(daten) zeigt ihn, setze(null) versteckt ihn. Am Körper steht `kn-hat-streifen`, solange er sichtbar ist. */ +/** + * Streifen in `wurzel` füllen: setze(daten) nimmt die Fristen (null: keine), erlaube(ok) sagt, ob die gezeigte Seite den Streifen tragen darf + * (nicht die Bestellseiten, siehe zeigtWiderrufsstreifen). Sichtbar ist er nur mit Fristen auf einer erlaubten Seite; am Körper steht dann `kn-hat-streifen`. + */ export function baueStreifen(wurzel, { koerper = document.body } = {}) { wurzel.classList.add('kn-streifen') wurzel.setAttribute('role', 'region') wurzel.setAttribute('aria-label', 'Widerrufsfrist') + let stand = null + let erlaubt = true + const zeichne = () => { + leeren(wurzel) + if (!stand || !erlaubt) { + wurzel.hidden = true + koerper.classList.remove('kn-hat-streifen') + return + } + wurzel.append(streifenText(fruehesteFrist(stand)), ' · ', h('a', { class: 'kn-streifen-link', href: '/widerrufen.html' }, 'Widerrufen')) + wurzel.hidden = false + koerper.classList.add('kn-hat-streifen') + } return { - setze(daten) { - leeren(wurzel) - if (!daten) { - wurzel.hidden = true - koerper.classList.remove('kn-hat-streifen') - return - } - wurzel.append(streifenText(fruehesteFrist(daten)), ' · ', h('a', { class: 'kn-streifen-link', href: '/widerrufen.html' }, 'Widerrufen')) - wurzel.hidden = false - koerper.classList.add('kn-hat-streifen') + setze(daten) { stand = daten || null; zeichne() }, + erlaube(ok) { + if ((ok !== false) === erlaubt) return + erlaubt = ok !== false + zeichne() }, } } diff --git a/public/neu/huelle.js b/public/neu/huelle.js index 22deffb..81258c0 100644 --- a/public/neu/huelle.js +++ b/public/neu/huelle.js @@ -6,7 +6,7 @@ import { h, leeren, text } from './dom.js' import { api as apiStandard } from './api.js' import * as routerStandard from './router.js' -import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer } from './routen.js' +import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer, zeigtWiderrufsstreifen } from './routen.js' import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, leisteSichtbar } from './leiste.js' import { baueEtage } from './etage.js' import { erzeugeZaehler } from './zaehler.js' @@ -192,6 +192,7 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan } else etage.schliesse() navigation.setzeAktiv(route.bereich) navigation.setzeEtage(Boolean(route.etage)) + streifen.erlaube(zeigtWiderrufsstreifen(route)) // Bestellseiten: Kerninfo und Knopf auf einem Bildschirm, ohne den festen Streifen text(el('kn-kopf-titel'), titel) text(el('kn-ansage'), titel) if (route.bereich === 'chat') zaehler.setzeChat(0) diff --git a/public/neu/routen.js b/public/neu/routen.js index 889ed09..8a055dd 100644 --- a/public/neu/routen.js +++ b/public/neu/routen.js @@ -106,3 +106,17 @@ export function elternVon(route, herkunft = '') { export function titelFuer(route) { return TITEL_ETAGE[route.etage] ?? TITEL_BEREICH[route.bereich] ?? '' } + +/** Etagen, auf denen etwas bestellt wird: Abo bestellen, Wechsel und die Übernahmegutschrift. Dazu der Reiter E-Mail der Verträge (Zusatzspeicher). */ +const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift'] + +/** + * Steht auf dieser Seite der Widerrufsstreifen? Nicht auf den Bestellseiten: Dort müssen Kerninfo und Knopf „Zahlungspflichtig bestellen“ auf einem + * Bildschirm stehen (Spec Abschnitt 4), und der feste Streifen nähme am Handy 44 px davon weg. Er gehört auf die Hauptseiten (Spec 5.2); + * „Vertrag widerrufen“ bleibt im Konto und unter Verträge erreichbar. Ohne Route: ja. + */ +export function zeigtWiderrufsstreifen(route) { + if (!route) return true + if (BESTELL_ETAGEN.includes(route.etage)) return false + return !(route.bereich === 'vertraege' && route.reiter === 'email') +} diff --git a/test/knHuelleFristen.test.js b/test/knHuelleFristen.test.js index f15fd9e..c962ee2 100644 --- a/test/knHuelleFristen.test.js +++ b/test/knHuelleFristen.test.js @@ -47,3 +47,18 @@ test('Streifen: ohne gültiges Datum steht ein allgemeiner Text, mehrere Fristen assert.deepEqual([w.getAttribute('role'), w.getAttribute('aria-label'), w.classList.contains('kn-streifen')], ['region', 'Widerrufsfrist', true]) } finally { fx.restore() } }) + +test('baueStreifen.erlaube: auf Bestellseiten versteckt, Körperklasse weg; ein neuer Stand erscheint dort nicht, danach zeigt er den letzten Stand', () => { + const fx = installFakeDom() + try { + const w = h('div', { hidden: true }); const s = baueStreifen(w) + const sichtbar = () => [w.hidden, fx.document.body.classList.contains('kn-hat-streifen')] + s.setze(FRIST); assert.deepEqual(sichtbar(), [false, true]) + s.erlaube(false); assert.deepEqual([...sichtbar(), fx.text(w)], [true, false, '']) // Bestellseite: weg, auch der Platz unten + s.setze({ ...FRIST, fruehestesEnde: '2026-10-30T10:00:00Z' }); assert.deepEqual(sichtbar(), [true, false]) // ein Neuladen der Fristen bringt ihn dort nicht zurück + s.erlaube(true); assert.deepEqual([...sichtbar(), fx.text(w)], [false, true, 'Widerrufsfrist bis 30.10. · Widerrufen']) // zurück auf einer Hauptseite + s.setze(null); s.erlaube(false); s.erlaube(true); assert.deepEqual(sichtbar(), [true, false]) // ohne Frist bleibt er in jedem Fall weg + const wt = h('div', { hidden: true }); const t = baueStreifen(wt); t.erlaube(false); t.setze(FRIST) // erst die Seite, dann die Daten + assert.deepEqual([wt.hidden, fx.document.body.classList.contains('kn-hat-streifen')], [true, false]) + } finally { fx.restore() } +}) diff --git a/test/knHuelleRouten.test.js b/test/knHuelleRouten.test.js index f162ccc..26b560c 100644 --- a/test/knHuelleRouten.test.js +++ b/test/knHuelleRouten.test.js @@ -1,6 +1,6 @@ import { test } from 'node:test' import assert from 'node:assert/strict' -import { BEREICHE, ROUTEN, routeAusHash, hashFuerRoute, hashFuerAnsicht, startHash, ankerKette, elternVon, titelFuer } from '../public/neu/routen.js' +import { BEREICHE, ROUTEN, routeAusHash, hashFuerRoute, hashFuerAnsicht, startHash, ankerKette, elternVon, titelFuer, zeigtWiderrufsstreifen } from '../public/neu/routen.js' const FAELLE = [ // Alte Links (Review Focus 1) ['', { bereich: 'website', query: '' }], @@ -59,3 +59,15 @@ test('ankerKette, elternVon, titelFuer', () => { assert.deepEqual(elternVon(r('#passwort'), '#chat'), { bereich: 'chat', query: '' }); assert.equal(elternVon(r('#passwort')).bereich, 'website'); assert.equal(elternVon(r('#chat')), null) assert.deepEqual(['#termine', '#zeiten', '#bestellen-wartung', '#passwort', '#chat'].map((h) => titelFuer(r(h))), ['Terminplaner', 'Öffnungszeiten', 'Abo bestellen', 'Passwort ändern', 'Chat']) }) + +// E-Fix F1 (Nachkorrektur): Auf den Bestellseiten stehen Kerninfo und Knopf auf einem Bildschirm; der Widerrufsstreifen gehört auf die Hauptseiten (Spec 5.2) +test('zeigtWiderrufsstreifen: nicht auf Abo bestellen, Wechsel, Zusatzspeicher und Gutschrift; auf allen anderen Seiten schon', () => { + const ohne = ['#bestellen-wartung', '#bestellen-hosting', '#wechsel', '#speicher', '#gutschrift', '#wechsel?x=1', '#bestellen-hosting?abo=cancel'] + for (const hash of ohne) assert.equal(zeigtWiderrufsstreifen(routeAusHash(hash)), false, hash) + const mit = ['', '#projekt', '#fortschritt', '#passwort', '#funktionen', '#entdecken', '#news', '#termine', '#webshop', '#buchungen', '#zeiten', '#analyse', '#chat', '#vertraege', '#abo', '#wartung', '#rechnungen', '#stunden'] + for (const hash of mit) assert.equal(zeigtWiderrufsstreifen(routeAusHash(hash)), true, hash) + assert.equal(zeigtWiderrufsstreifen(undefined), true) // ohne Route nichts verstecken + // jede Etage der Tabelle ist entschieden: versteckt sind genau die Bestellseiten + const versteckt = [...new Set(Object.values(ROUTEN).filter((r) => !zeigtWiderrufsstreifen(r)).map((r) => r.etage ?? `reiter:${r.reiter}`))].sort() + assert.deepEqual(versteckt, ['bestellen', 'gutschrift', 'reiter:email', 'wechsel']) +}) diff --git a/test/knHuelleStart.test.js b/test/knHuelleStart.test.js index cd035d2..aa5a7e0 100644 --- a/test/knHuelleStart.test.js +++ b/test/knHuelleStart.test.js @@ -132,6 +132,24 @@ test('M1: Widerrufsstreifen und Pille werden nach Kauf, Wechsel oder Speicher-Pl 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('E-Fix F1 (Nachkorrektur): auf den Bestellseiten (Abo bestellen, Wechsel, Zusatzspeicher, Gutschrift) ist der Widerrufsstreifen weg, auf den Hauptseiten da; ein Neuladen der Fristen ändert daran nichts', async () => { + const frist = { fristen: [{ quelle: 'abschluss', bezeichnung: 'Abo', bis: '2026-10-21T21:59:59.999Z' }], fruehestesEnde: '2026-10-21T21:59:59.999Z', anzeigen: true } + const karte = { ...KARTE_KUNDE, '/api/fristen': frist } + const b = await boot({ hash: '#wechsel', karte }) // Einstieg direkt auf der Bestellseite + const zustand = () => [b.el('kn-streifen').hidden, b.fx.document.body.classList.contains('kn-hat-streifen')] + assert.deepEqual(zustand(), [true, false]) + await b.zeige('#vertraege', 'push'); assert.deepEqual(zustand(), [false, true]); assert.equal(b.fx.text(b.el('kn-streifen')), 'Widerrufsfrist bis 21.10. · Widerrufen') + for (const hash of ['#bestellen-wartung', '#bestellen-hosting', '#wechsel', '#speicher', '#gutschrift']) { + await b.zeige('#abo', 'push'); assert.deepEqual(zustand(), [false, true], `Hauptseite vor ${hash}`) + await b.zeige(hash, 'push'); assert.deepEqual(zustand(), [true, false], hash) + b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'vertraege' } })); await b.fx.ruhe() // Frist neu geladen (z. B. nach Zusatzspeicher) + assert.deepEqual(zustand(), [true, false], `${hash} nach dem Neuladen der Fristen`) + } + await b.zeige('#chat', 'push'); assert.deepEqual(zustand(), [false, true]) + // "Vertrag widerrufen" bleibt im Konto erreichbar (mit der Pille "Frist läuft") + assert.match(b.fx.text(b.el('kn-konto-wurzel')), /Vertrag widerrufen/) +}) + 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"]')