Kundenansicht neu E-Fix F1 (Nachkorrektur): Widerrufsstreifen auf den Bestellseiten ausgeblendet

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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 22:44:20 +00:00
1 parent c5f1d9545c
commit 5cd49d6a4a
6 files changed
+78 -7

No files matched your search

+16 -5
View File
@@ -41,22 +41,33 @@ export function fruehesteFrist(daten) {
return zeiten.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? '' 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 } = {}) { export function baueStreifen(wurzel, { koerper = document.body } = {}) {
wurzel.classList.add('kn-streifen') wurzel.classList.add('kn-streifen')
wurzel.setAttribute('role', 'region') wurzel.setAttribute('role', 'region')
wurzel.setAttribute('aria-label', 'Widerrufsfrist') wurzel.setAttribute('aria-label', 'Widerrufsfrist')
return { let stand = null
setze(daten) { let erlaubt = true
const zeichne = () => {
leeren(wurzel) leeren(wurzel)
if (!daten) { if (!stand || !erlaubt) {
wurzel.hidden = true wurzel.hidden = true
koerper.classList.remove('kn-hat-streifen') koerper.classList.remove('kn-hat-streifen')
return return
} }
wurzel.append(streifenText(fruehesteFrist(daten)), ' · ', h('a', { class: 'kn-streifen-link', href: '/widerrufen.html' }, 'Widerrufen')) wurzel.append(streifenText(fruehesteFrist(stand)), ' · ', h('a', { class: 'kn-streifen-link', href: '/widerrufen.html' }, 'Widerrufen'))
wurzel.hidden = false wurzel.hidden = false
koerper.classList.add('kn-hat-streifen') koerper.classList.add('kn-hat-streifen')
}
return {
setze(daten) { stand = daten || null; zeichne() },
erlaube(ok) {
if ((ok !== false) === erlaubt) return
erlaubt = ok !== false
zeichne()
}, },
} }
} }
+2 -1
View File
@@ -6,7 +6,7 @@
import { h, leeren, text } from './dom.js' import { h, leeren, text } from './dom.js'
import { api as apiStandard } from './api.js' import { api as apiStandard } from './api.js'
import * as routerStandard from './router.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 { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, leisteSichtbar } from './leiste.js'
import { baueEtage } from './etage.js' import { baueEtage } from './etage.js'
import { erzeugeZaehler } from './zaehler.js' import { erzeugeZaehler } from './zaehler.js'
@@ -192,6 +192,7 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan
} else etage.schliesse() } else etage.schliesse()
navigation.setzeAktiv(route.bereich) navigation.setzeAktiv(route.bereich)
navigation.setzeEtage(Boolean(route.etage)) 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-kopf-titel'), titel)
text(el('kn-ansage'), titel) text(el('kn-ansage'), titel)
if (route.bereich === 'chat') zaehler.setzeChat(0) if (route.bereich === 'chat') zaehler.setzeChat(0)
+14
View File
@@ -106,3 +106,17 @@ export function elternVon(route, herkunft = '') {
export function titelFuer(route) { export function titelFuer(route) {
return TITEL_ETAGE[route.etage] ?? TITEL_BEREICH[route.bereich] ?? '' 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')
}
+15
View File
@@ -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]) assert.deepEqual([w.getAttribute('role'), w.getAttribute('aria-label'), w.classList.contains('kn-streifen')], ['region', 'Widerrufsfrist', true])
} finally { fx.restore() } } 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() }
})
+13 -1
View File
@@ -1,6 +1,6 @@
import { test } from 'node:test' import { test } from 'node:test'
import assert from 'node:assert/strict' 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) const FAELLE = [ // Alte Links (Review Focus 1)
['', { bereich: 'website', query: '' }], ['', { 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(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']) 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'])
})
+18
View File
@@ -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/) 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 () => { 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 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"]') const i0 = n('/api/invoices'); const chatTab = () => b.fx.finde(b.el('kn-zeile-haupt'), '[role=tab][aria-label="Chat, 3 ungelesen"]')