Funktionen neu: Bestätigung als Overlay der Zurück-Geste, Erfolgsmeldung, Rückkehr nach dem Wechsel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f30e72ebbb
commit
36890c6734
2 files changed
+150
-17
No files matched your search
@@ -127,11 +127,13 @@ export function zeilenWeg(zeile) {
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs: eine ältere Antwort zeichnet nichts mehr
|
||||
let reiterGewaehlt = false // Tipp auf „Meine“: der nächste Aufruf zeigt den Leerzustand statt zu Entdecken zu springen
|
||||
let vorgabe = null // nach einer eigenen Navigation (Reiter, Sprung zu Entdecken) nimmt der nächste Aufruf die geladenen Antworten
|
||||
const wechselErledigt = new Set() // 'key:projectId' der Funktionen, in die die Rückkehr vom Wechsel schon geführt hat (je Seitenaufruf einmal)
|
||||
|
||||
/** Setzt die Merker des Moduls zurück (nur für Tests). */
|
||||
export function vergissMerker() {
|
||||
reiterGewaehlt = false
|
||||
vorgabe = null
|
||||
wechselErledigt.clear()
|
||||
}
|
||||
|
||||
const BILD = /^\/neu\/bilder\/[a-z0-9-]+\.webp$/
|
||||
@@ -160,6 +162,7 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
|
||||
|
||||
let daten
|
||||
let offen
|
||||
const frisch = !mitgebracht
|
||||
if (mitgebracht) {
|
||||
({ daten, offen } = mitgebracht)
|
||||
} else {
|
||||
@@ -183,18 +186,17 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
|
||||
const kundenart = me?.customer?.kundenartWirksam ?? daten.kundenart
|
||||
const viewAs = me?.viewAs === true
|
||||
const zeilen = meineZeilen(daten)
|
||||
if (reiterWahl === 'meine' && zeilen.length === 0 && !ausdruecklich) {
|
||||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||
A.geheZu('#entdecken', { ersetzen: true })
|
||||
return
|
||||
}
|
||||
|
||||
const projektVon = (projectId) => daten.projects.find((p) => p?.projectId === projectId)
|
||||
const weiter = (weg, ziel) => navigiere(weg, ziel, { geheZu: A.geheZu, speicher: A.speicher })
|
||||
let gezeigteMeldung = ''
|
||||
|
||||
/** Die Bestätigungsseite für diese Funktion und Website. */
|
||||
/** Die Bestätigungsseite für diese Funktion und Website als Overlay: die Zurück-Geste und „Abbrechen“ bringen die Liste ohne neuen Abruf zurück. */
|
||||
function oeffneSeite(key, projectId) {
|
||||
renderEinschalten(wurzel, { key, projectId, daten, kundenartWirksam: kundenart, viewAs, jetzt: A.jetzt, api: A.api, geheZu: A.geheZu, speicher: A.speicher })
|
||||
A.oeffneOverlay({ schliesse: () => { if (aktuell()) zeichneListe() } })
|
||||
renderEinschalten(wurzel, {
|
||||
key, projectId, daten, kundenartWirksam: kundenart, viewAs, jetzt: A.jetzt, api: A.api, geheZu: A.geheZu, speicher: A.speicher,
|
||||
bei: { abbrechen: () => A.beendeOverlay() },
|
||||
})
|
||||
}
|
||||
|
||||
/** „Einschalten“ für eine Website: die Vorprüfung wählt den Ort; eine Meldung steht im Eintrag. */
|
||||
@@ -206,6 +208,38 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
|
||||
else weiter(weg, { key: eintrag.key, projectId: kandidat.projectId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Rückkehr vom Wechsel ins Wartungs-Abo (V11): hat der Kunde den Wechsel für eine Funktion begonnen und hat die Website sie noch nicht,
|
||||
* öffnet die Ansicht ihren Ablauf, je Seitenaufruf einmal. Jeder Fehler und jede ungültige Angabe wird ignoriert.
|
||||
* @returns {Promise<boolean>} true, wenn ein Ablauf geöffnet wurde
|
||||
*/
|
||||
async function wechselRueckkehr() {
|
||||
if (daten.abo?.produkt !== 'wartung' || daten.abo.laeuft !== true) return false
|
||||
let abo
|
||||
try { abo = await A.api('/api/abo') } catch { return false }
|
||||
if (!aktuell()) return false
|
||||
const f = abo?.subscription?.wechselFunktion
|
||||
if (!f || typeof f.key !== 'string' || typeof f.projectId !== 'string') return false
|
||||
const merker = `${f.key}:${f.projectId}`
|
||||
const eintrag = verfuegbare(daten).find((e) => e.key === f.key)
|
||||
const projekt = projektVon(f.projectId)
|
||||
if (!eintrag || !projekt || wechselErledigt.has(merker) || istInMeine(eintrag, projekt)) return false
|
||||
const weg = einschaltWeg({ eintrag, projekt, abo: daten.abo, kundenart })
|
||||
if (weg.art !== 'seite' && weg.art !== 'etage') return false
|
||||
wechselErledigt.add(merker)
|
||||
if (weg.art === 'seite') oeffneSeite(f.key, f.projectId)
|
||||
else weiter(weg, { key: f.key, projectId: f.projectId })
|
||||
return true
|
||||
}
|
||||
|
||||
if (frisch && await wechselRueckkehr()) return
|
||||
if (!aktuell()) return
|
||||
if (reiterWahl === 'meine' && zeilen.length === 0 && !ausdruecklich) {
|
||||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||
A.geheZu('#entdecken', { ersetzen: true })
|
||||
return
|
||||
}
|
||||
|
||||
function meineListe() {
|
||||
if (zeilen.length === 0) {
|
||||
return [
|
||||
@@ -277,13 +311,18 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
|
||||
A.geheZu(key === 'meine' ? '#funktionen' : '#entdecken', { ersetzen: true })
|
||||
}
|
||||
|
||||
const meldung = nimmMeldung()
|
||||
const reiter = baueReiter({ eintraege: [{ key: 'meine', label: 'Meine' }, { key: 'entdecken', label: 'Entdecken' }], aktiv: reiterWahl, art: 'unterstrich', ariaLabel: 'Funktionen', bei: wechsleReiter })
|
||||
leeren(wurzel).append(
|
||||
...[
|
||||
meldung ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, meldung) : null,
|
||||
rechnungOffenZeile(offen),
|
||||
reiter,
|
||||
h('div', { class: 'kn-reiter-inhalt' }, reiterWahl === 'meine' ? meineListe() : entdeckenListe()),
|
||||
].filter(Boolean))
|
||||
/** Reiter, Rechnungszeile und Liste aus den geladenen Antworten zeichnen (auch nach dem Schließen der Bestätigung, ohne Abruf). */
|
||||
function zeichneListe() {
|
||||
const reiter = baueReiter({ eintraege: [{ key: 'meine', label: 'Meine' }, { key: 'entdecken', label: 'Entdecken' }], aktiv: reiterWahl, art: 'unterstrich', ariaLabel: 'Funktionen', bei: wechsleReiter })
|
||||
leeren(wurzel).append(
|
||||
...[
|
||||
gezeigteMeldung ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, gezeigteMeldung) : null,
|
||||
rechnungOffenZeile(offen),
|
||||
reiter,
|
||||
h('div', { class: 'kn-reiter-inhalt' }, reiterWahl === 'meine' ? meineListe() : entdeckenListe()),
|
||||
].filter(Boolean))
|
||||
}
|
||||
|
||||
gezeigteMeldung = nimmMeldung() // genau einmal; nach dem Schließen der Bestätigung bleibt sie stehen
|
||||
zeichneListe()
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { installFakeDom } from './support/fakeDom.js'
|
||||
import { zeigeFunktionen, vergissMerker } from '../public/neu/ansicht-funktionen.js'
|
||||
import { merkeMeldung } from '../public/neu/funktion-einschalten.js'
|
||||
import { daten, projekt, feature } from './support/knFunktionenDaten.js'
|
||||
|
||||
const JETZT = () => new Date('2026-10-20T10:00:00Z')
|
||||
@@ -198,3 +199,96 @@ test('Meine mit zwei Websites zeigt den Websitenamen in der zweiten Zeile, mit e
|
||||
await zwei.zeige(MEINE)
|
||||
assert.match(fx.text(fx.finde(zwei.wurzel, 'li[data-fn]')), /News-Pinnwand.*Bäckerei.*Aktiv/)
|
||||
}))
|
||||
|
||||
/* ---------------------------------------------------------------- Teil 2: Overlay, Erfolgsmeldung, Wechsel --- */
|
||||
test('Bestätigung als Overlay: Zurück-Geste bringt die Liste ohne neuen Abruf zurück, Abbrechen ebenso', mit(async (fx) => {
|
||||
const u = umgebung(fx)
|
||||
await u.zeige(ENTDECKEN)
|
||||
const geladen = () => u.aufrufe.filter(([m, p]) => m === 'GET' && p === '/api/funktionen').length
|
||||
await fx.klick(fx.finde(u.wurzel, 'section[data-fn="news"] [data-aktion="einschalten"]'))
|
||||
assert.match(fx.text(u.wurzel), /News-Pinnwand einschalten/)
|
||||
assert.equal(fx.alle(u.wurzel, 'section[data-fn]').length, 0)
|
||||
u.overlay() // Zurück-Geste des Browsers (der Router ruft schliesse)
|
||||
assert.equal(fx.alle(u.wurzel, 'section[data-fn]').length, 6)
|
||||
await fx.klick(fx.finde(u.wurzel, 'section[data-fn="news"] [data-aktion="einschalten"]'))
|
||||
await fx.klick(fx.finde(u.wurzel, '[data-aktion="abbrechen"]'))
|
||||
assert.equal(fx.alle(u.wurzel, 'section[data-fn]').length, 6)
|
||||
assert.deepEqual([geladen(), u.gegangen], [1, []])
|
||||
}))
|
||||
|
||||
test('Overlay: eine ältere Seite zeichnet nach einem neuen Aufruf nichts mehr; die Rechnungszeile und eine gezeigte Meldung bleiben', mit(async (fx) => {
|
||||
const u = umgebung(fx)
|
||||
u.rechnungen = [{ id: 'r1', number: 'RE-1', balance: 20, portalUrl: null }]
|
||||
await u.zeige(ENTDECKEN)
|
||||
await fx.klick(fx.finde(u.wurzel, 'section[data-fn="news"] [data-aktion="einschalten"]'))
|
||||
const schliesse = u.overlay
|
||||
await u.zeige(ENTDECKEN) // ein neuer Aufruf (z. B. Router) ersetzt die Anzeige
|
||||
const vorher = fx.text(u.wurzel)
|
||||
schliesse() // das alte Overlay meldet sich spät
|
||||
assert.equal(fx.text(u.wurzel), vorher)
|
||||
const v = umgebung(fx)
|
||||
v.rechnungen = [{ id: 'r1', number: 'RE-1', balance: 20, portalUrl: null }]
|
||||
await v.zeige(ENTDECKEN)
|
||||
merkeMeldung('Eine Meldung')
|
||||
await v.zeige(ENTDECKEN)
|
||||
const aufrufe = v.aufrufe.length
|
||||
await fx.klick(fx.finde(v.wurzel, 'section[data-fn="news"] [data-aktion="einschalten"]'))
|
||||
v.overlay()
|
||||
assert.ok(fx.finde(v.wurzel, '.kn-rechnung-offen'))
|
||||
assert.equal(fx.text(fx.finde(v.wurzel, '[role="status"]')), 'Eine Meldung') // nach dem Schließen noch da, aber nicht neu gemerkt
|
||||
assert.equal(v.aufrufe.length, aufrufe) // auch kein neuer Abruf der Rechnungen
|
||||
await v.zeige(ENTDECKEN)
|
||||
assert.equal(fx.finde(v.wurzel, '[role="status"]'), null)
|
||||
}))
|
||||
|
||||
test('Erfolg: Meldung merken, ersetzend zu Meine, Meldung genau einmal', mit(async (fx) => {
|
||||
const u = umgebung(fx)
|
||||
await u.zeige(ENTDECKEN)
|
||||
await fx.klick(fx.finde(u.wurzel, 'section[data-fn="news"] [data-aktion="einschalten"]'))
|
||||
await fx.klick(fx.finde(u.wurzel, '[data-aktion="einschalten"]'))
|
||||
assert.deepEqual(u.aufrufe.filter(([m]) => m === 'POST'), [['POST', '/api/funktionen/news/einschalten']])
|
||||
assert.deepEqual(u.gegangen, [['#funktionen', { ersetzen: true }]])
|
||||
u.d = daten({ projekte: [projekt('p1', 'Bäckerei Sonnenschein', { news: feature('setup', { ticketWoid: '100301' }) })] })
|
||||
await u.zeige(MEINE) // so ruft der Router danach auf
|
||||
assert.equal(fx.text(fx.finde(u.wurzel, '[role="status"]')), 'News-Pinnwand ist eingeschaltet. Die Einrichtung läuft jetzt. Ticket #100301.')
|
||||
await u.zeige(MEINE)
|
||||
assert.equal(fx.finde(u.wurzel, '[role="status"]'), null)
|
||||
}))
|
||||
|
||||
test('wechselFunktion (V11): öffnet den Ablauf der Funktion, einmal, nur mit Wartungs-Abo', mit(async (fx) => {
|
||||
const aboMit = (key) => ({ subscription: { wechselFunktion: { key, projectId: 'p1' } } })
|
||||
const abgefragt = (u) => u.aufrufe.some(([, p]) => p === '/api/abo')
|
||||
let u = umgebung(fx, { abo: aboMit('news') })
|
||||
await u.zeige(MEINE)
|
||||
assert.ok(fx.finde(u.wurzel, 'section.kn-fn-einschalten[data-key="news"]')) // Bestätigung, kein Sprung
|
||||
assert.deepEqual(u.gegangen, [])
|
||||
await u.zeige(MEINE) // zweiter Aufruf: nicht noch einmal in die Funktion
|
||||
assert.deepEqual(u.gegangen, [['#entdecken', { ersetzen: true }]])
|
||||
vergissMerker()
|
||||
u = umgebung(fx, { abo: aboMit('termine') }); await u.zeige(MEINE)
|
||||
assert.deepEqual(u.gegangen, [['#termine?projekt=p1']]) // Fragebogen zuerst
|
||||
vergissMerker()
|
||||
u = umgebung(fx, { d: daten({ abo: { produkt: 'hosting' } }), abo: aboMit('news') }); await u.zeige(MEINE)
|
||||
assert.equal(abgefragt(u), false)
|
||||
vergissMerker()
|
||||
for (const abo of [aboMit('gibtsnicht'), new Error('weg')]) {
|
||||
u = umgebung(fx, { abo }); await u.zeige(MEINE)
|
||||
assert.deepEqual(u.gegangen, [['#entdecken', { ersetzen: true }]]) // ungültig oder Fehler: wie ohne Wechsel
|
||||
}
|
||||
}))
|
||||
|
||||
test('wechselFunktion: nicht, wenn die Website die Funktion schon hat, die Website fehlt oder die Funktion nicht verfügbar ist', mit(async (fx) => {
|
||||
const abo = { subscription: { wechselFunktion: { key: 'news', projectId: 'p1' } } }
|
||||
const faelle = [
|
||||
daten({ projekte: [projekt('p1', 'B', { news: feature('active') })] }), // hat sie schon: die Liste erscheint
|
||||
daten({ projekte: [projekt('p9', 'Anders')] }), // Website nicht (mehr) da
|
||||
daten({ katalogEdit: { news: { verfuegbar: false } } }), // C9
|
||||
]
|
||||
for (const d of faelle) {
|
||||
vergissMerker()
|
||||
const u = umgebung(fx, { d, abo })
|
||||
await u.zeige(ENTDECKEN)
|
||||
assert.equal(fx.finde(u.wurzel, 'section.kn-fn-einschalten'), null)
|
||||
assert.deepEqual(u.gegangen, [])
|
||||
}
|
||||
}))
|
||||
Reference in new issue
Block a user