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"]')