Terminplaner TP2 C10a: Nachbesserung 1 (Abo-Hinweis vom Server, R111)
GET /api/funktionen liefert bei termine.verfuegbar ein fuenftes Feld hinweis (aboMeldung mit dem Register des Terminplaners, null bei bestellbar true). Die Kachel zeigt diesen Text maskiert an; die Browser-Kopie der Abo-Texte und der Gleichheitstest entfallen. Der Link "Zu Verträge" ist eine gemeinsame Konstante fuer Analyse und Terminplaner und erscheint bei kein_abo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ae0f67457
commit
52456dac76
5 files changed
+141
-63
No files matched your search
+12
-20
@@ -112,6 +112,9 @@ function newsTile(projects, price = PRICE) {
|
|||||||
</article>`
|
</article>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Link in den Hinweisen ohne Wartungs-Abo (kein_abo): Analyse-Kachel, Analyse-Seite und Terminplaner-Kachel teilen ihn
|
||||||
|
const ZU_VERTRAEGE_LINK = '<button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button>'
|
||||||
|
|
||||||
/* Analyse: Kachel je nach Zustand der Websites (comingSoon = Schalter aus) */
|
/* Analyse: Kachel je nach Zustand der Websites (comingSoon = Schalter aus) */
|
||||||
const analyseOf = (project) => project.analyse || { comingSoon: true }
|
const analyseOf = (project) => project.analyse || { comingSoon: true }
|
||||||
const analyseState = (a) => (a.comingSoon ? 'soon' : a.state === 'unknown' ? 'unknown' : a.state === 'disabled' ? 'disabled' : a.state === 'active' ? 'active' : a.eligible ? 'off' : 'blocked')
|
const analyseState = (a) => (a.comingSoon ? 'soon' : a.state === 'unknown' ? 'unknown' : a.state === 'disabled' ? 'disabled' : a.state === 'active' ? 'active' : a.eligible ? 'off' : 'blocked')
|
||||||
@@ -136,7 +139,7 @@ function analyseTile(projects) {
|
|||||||
else if (state === 'disabled') { text = 'Bitte melde dich bei uns'; action = '<button type="button" class="kb-btn-primary fn-tile-cta" data-view="chat">Chat öffnen</button>' }
|
else if (state === 'disabled') { text = 'Bitte melde dich bei uns'; action = '<button type="button" class="kb-btn-primary fn-tile-cta" data-view="chat">Chat öffnen</button>' }
|
||||||
else if (state === 'active') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Auswertung ansehen</button>`
|
else if (state === 'active') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Auswertung ansehen</button>`
|
||||||
else if (state === 'off') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Einrichten</button>`
|
else if (state === 'off') action = `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="analyse" data-project="${esc(target.projectId)}">Einrichten</button>`
|
||||||
else if (a.reason === 'kein_abo') action = '<p class="fn-count">Im Wartungs-Abo enthalten. <button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button></p>'
|
else if (a.reason === 'kein_abo') action = `<p class="fn-count">Im Wartungs-Abo enthalten. ${ZU_VERTRAEGE_LINK}</p>`
|
||||||
else action = `<p class="fn-count">${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}</p>`
|
else action = `<p class="fn-count">${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}</p>`
|
||||||
const count = projects.length > 1 && active ? `<p class="fn-count">aktiv für ${active} von ${projects.length} Websites</p>` : ''
|
const count = projects.length > 1 && active ? `<p class="fn-count">aktiv für ${active} von ${projects.length} Websites</p>` : ''
|
||||||
return `
|
return `
|
||||||
@@ -153,29 +156,15 @@ function analyseTile(projects) {
|
|||||||
/* Terminplaner: Kachel nur, wenn der Schalter es erlaubt (termine.verfuegbar), sonst "Kommt bald". Die Ansicht selbst
|
/* Terminplaner: Kachel nur, wenn der Schalter es erlaubt (termine.verfuegbar), sonst "Kommt bald". Die Ansicht selbst
|
||||||
(termine.js) wird von app.js geladen; hier steht nur die Kachel, damit funktionen.js keinen weiteren Import braucht.
|
(termine.js) wird von app.js geladen; hier steht nur die Kachel, damit funktionen.js keinen weiteren Import braucht.
|
||||||
termine (Antwort von /api/funktionen) traegt bei verfuegbar: true bestellbar (true | false | null = Abo nicht lesbar),
|
termine (Antwort von /api/funktionen) traegt bei verfuegbar: true bestellbar (true | false | null = Abo nicht lesbar),
|
||||||
grund (kein_abo | gekuendigt | nicht_aktiv | null), preis und einrichtung. Das Abo betrifft nur die Bestellung: ohne laufendes
|
grund (kein_abo | gekuendigt | nicht_aktiv | null), preis, einrichtung und hinweis (Abo-Text des Servers, aboMeldung in
|
||||||
Abo gibt es im Zustand Aus keinen Weg in die Ansicht; wer schon bestellt hat oder den Terminplaner nutzt, behaelt seinen. */
|
server/services/featureOrder.js, bei bestellbar true null). Das Abo betrifft nur die Bestellung: ohne laufendes Abo gibt es im
|
||||||
|
Zustand Aus keinen Weg in die Ansicht; wer schon bestellt hat oder den Terminplaner nutzt, behaelt seinen. */
|
||||||
const TERMINE_TEXT = 'Deine Kundschaft bucht Termine direkt über deine Website.'
|
const TERMINE_TEXT = 'Deine Kundschaft bucht Termine direkt über deine Website.'
|
||||||
const termineOf = (project) => project.features?.termine || { state: 'off' }
|
const termineOf = (project) => project.features?.termine || { state: 'off' }
|
||||||
// Doppelt zu anfragenText in termine-view.js: funktionen.js darf dort nichts importieren (die Kachel testet test/funktionenTiles.test.js
|
// Doppelt zu anfragenText in termine-view.js: funktionen.js darf dort nichts importieren (die Kachel testet test/funktionenTiles.test.js
|
||||||
// mit einem umgebogenen Import). Aenderungen an beiden Stellen machen.
|
// mit einem umgebogenen Import). Aenderungen an beiden Stellen machen.
|
||||||
const anfragenText = (n) => (n === 1 ? '1 Anfrage wartet' : `${n} Anfragen warten`)
|
const anfragenText = (n) => (n === 1 ? '1 Anfrage wartet' : `${n} Anfragen warten`)
|
||||||
|
|
||||||
// Abo-Hinweise der Kachel. Kopie von aboMeldung (server/services/featureOrder.js, einzige Quelle der Abo-Hinweistexte) fuer den
|
|
||||||
// Terminplaner; test/funktionenTiles.test.js haelt beide gleich. kein_abo steht wie bei der Analyse im Wortlaut der Kachel.
|
|
||||||
const TERMINE_ABO_TEXTE = {
|
|
||||||
gekuendigt: 'Dein Wartungs-Abo ist gekündigt. Für den Terminplaner brauchst du ein laufendes Abo.',
|
|
||||||
nicht_aktiv: 'Dein Wartungs-Abo ist gerade nicht aktiv (zum Beispiel wegen einer offenen Zahlung). Sobald es wieder aktiv ist, kannst du den Terminplaner bestellen.',
|
|
||||||
unbekannt: 'Das Abo konnte gerade nicht geprüft werden.',
|
|
||||||
}
|
|
||||||
const KEIN_ABO_HINWEIS = 'Im Wartungs-Abo enthalten. <button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button>'
|
|
||||||
|
|
||||||
function termineAboHinweis(angebot) {
|
|
||||||
if (angebot.bestellbar !== false) return TERMINE_ABO_TEXTE.unbekannt // null (Lesefehler) oder Feld fehlt: nicht geprueft
|
|
||||||
if (angebot.grund === 'kein_abo') return KEIN_ABO_HINWEIS
|
|
||||||
return Object.hasOwn(TERMINE_ABO_TEXTE, angebot.grund) && angebot.grund !== 'unbekannt' ? TERMINE_ABO_TEXTE[angebot.grund] : TERMINE_ABO_TEXTE.unbekannt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entwurf: Funktionsdokument im Zustand Aus mit siteKey (entsteht beim ersten Speichern des Fragebogens)
|
// Entwurf: Funktionsdokument im Zustand Aus mit siteKey (entsteht beim ersten Speichern des Fragebogens)
|
||||||
const hatEntwurf = (project) => termineOf(project).state === 'off' && Boolean(termineOf(project).siteKey)
|
const hatEntwurf = (project) => termineOf(project).state === 'off' && Boolean(termineOf(project).siteKey)
|
||||||
|
|
||||||
@@ -216,7 +205,10 @@ function termineTile(projects, angebot = {}) {
|
|||||||
const preis = typeof angebot.preis === 'string' && angebot.preis.trim() ? ` · ${esc(angebot.preis)}` : ''
|
const preis = typeof angebot.preis === 'string' && angebot.preis.trim() ? ` · ${esc(angebot.preis)}` : ''
|
||||||
action = knopf(hatEntwurf(target) ? 'Fragebogen fortsetzen' : `Terminplaner einrichten${preis}`)
|
action = knopf(hatEntwurf(target) ? 'Fragebogen fortsetzen' : `Terminplaner einrichten${preis}`)
|
||||||
} else {
|
} else {
|
||||||
extra = `<p class="fn-count">${termineAboHinweis(angebot)}</p>`
|
// Hinweis des Servers (maskiert), bei kein_abo dazu der Weg in die Verträge; ohne Text bleibt nur der Link oder nichts
|
||||||
|
const hinweis = typeof angebot.hinweis === 'string' ? esc(angebot.hinweis) : ''
|
||||||
|
const zeile = [hinweis, angebot.bestellbar === false && angebot.grund === 'kein_abo' ? ZU_VERTRAEGE_LINK : ''].filter(Boolean).join(' ')
|
||||||
|
if (zeile) extra = `<p class="fn-count">${zeile}</p>`
|
||||||
}
|
}
|
||||||
const count = projects.length > 1 && active ? `<p class="fn-count">aktiv für ${active} von ${projects.length} Websites</p>` : ''
|
const count = projects.length > 1 && active ? `<p class="fn-count">aktiv für ${active} von ${projects.length} Websites</p>` : ''
|
||||||
return `
|
return `
|
||||||
@@ -695,7 +687,7 @@ export async function initAnalyse(root, { projectId } = {}) {
|
|||||||
if (a.state === 'active') renderAnalyseStats(stage, project, reload)
|
if (a.state === 'active') renderAnalyseStats(stage, project, reload)
|
||||||
else if (a.state === 'disabled') stage.innerHTML = '<div class="kb-panel"><h3>Analyse ist abgeschaltet</h3><p>Für diese Website ist die Analyse im Moment gesperrt. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></div>'
|
else if (a.state === 'disabled') stage.innerHTML = '<div class="kb-panel"><h3>Analyse ist abgeschaltet</h3><p>Für diese Website ist die Analyse im Moment gesperrt. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></div>'
|
||||||
else if (a.state === 'unknown') stage.innerHTML = '<div class="kb-panel"><p>Die Daten konnten gerade nicht geladen werden.</p></div>'
|
else if (a.state === 'unknown') stage.innerHTML = '<div class="kb-panel"><p>Die Daten konnten gerade nicht geladen werden.</p></div>'
|
||||||
else if (!a.eligible) stage.innerHTML = `<div class="kb-panel"><h3>Analyse nicht verfügbar</h3><p>${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ' <button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button>' : ''}</p></div>`
|
else if (!a.eligible) stage.innerHTML = `<div class="kb-panel"><h3>Analyse nicht verfügbar</h3><p>${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` ${ZU_VERTRAEGE_LINK}` : ''}</p></div>`
|
||||||
else renderAnalyseActivation(stage, project, reload)
|
else renderAnalyseActivation(stage, project, reload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { nachweisTexte, speichereZustimmung, zustimmungFuerSitzung, zustimmungsf
|
|||||||
import { termineFuerKunde } from '../services/termine/schalter.js'
|
import { termineFuerKunde } from '../services/termine/schalter.js'
|
||||||
import { aboMemo } from '../services/aboStatus.js'
|
import { aboMemo } from '../services/aboStatus.js'
|
||||||
import { featureConfig, featureHaken, featureKeys, featureSperre, oeffentlicheAnsicht } from '../services/featureRegistry.js'
|
import { featureConfig, featureHaken, featureKeys, featureSperre, oeffentlicheAnsicht } from '../services/featureRegistry.js'
|
||||||
import { bestellFehler, istFortsetzen, pruefeAbo, pruefeZustimmung } from '../services/featureOrder.js'
|
import { aboMeldung, bestellFehler, istFortsetzen, pruefeAbo, pruefeZustimmung } from '../services/featureOrder.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ELIGIBILITY_MESSAGES,
|
ELIGIBILITY_MESSAGES,
|
||||||
@@ -94,8 +94,10 @@ const ABO_GRUENDE = ['kein_abo', 'gekuendigt', 'nicht_aktiv']
|
|||||||
/**
|
/**
|
||||||
* Terminplaner-Teil der Antwort von GET /api/funktionen. Nicht verfuegbar (Schalter, Pilotliste): genau { verfuegbar: false },
|
* Terminplaner-Teil der Antwort von GET /api/funktionen. Nicht verfuegbar (Schalter, Pilotliste): genau { verfuegbar: false },
|
||||||
* das Abo wird dann nicht gelesen. Verfuegbar: dazu bestellbar (true | false | null), grund (kein_abo | gekuendigt | nicht_aktiv |
|
* das Abo wird dann nicht gelesen. Verfuegbar: dazu bestellbar (true | false | null), grund (kein_abo | gekuendigt | nicht_aktiv |
|
||||||
* null), preis und einrichtung aus dem Register. Ein Lesefehler im Abo ergibt bestellbar: null und kostet die Liste nichts; im
|
* null), preis und einrichtung aus dem Register sowie hinweis, der Abo-Text fuer den Kunden (aboMeldung, einzige Quelle der
|
||||||
* Log stehen nur Art und Status, nie der Text (Appwrite-Texte koennten Namen oder IDs enthalten).
|
* Abo-Hinweise; der Browser zeigt ihn nur an). hinweis ist null, solange bestellbar true ist. Ein Lesefehler im Abo ergibt
|
||||||
|
* bestellbar: null und kostet die Liste nichts; im Log stehen nur Art und Status, nie der Text (Appwrite-Texte koennten Namen
|
||||||
|
* oder IDs enthalten).
|
||||||
*/
|
*/
|
||||||
async function termineAngebot(email, customerId) {
|
async function termineAngebot(email, customerId) {
|
||||||
if (!termineFuerKunde(email)) return { verfuegbar: false }
|
if (!termineFuerKunde(email)) return { verfuegbar: false }
|
||||||
@@ -109,7 +111,8 @@ async function termineAngebot(email, customerId) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[funktionen] Abo für den Terminplaner nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
|
console.error(`[funktionen] Abo für den Terminplaner nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
|
||||||
}
|
}
|
||||||
return { verfuegbar: true, bestellbar, grund, preis: register?.priceLabel ?? '', einrichtung: register?.setupEstimate ?? '' }
|
const hinweis = bestellbar === true ? null : aboMeldung(register, grund ?? 'unbekannt')
|
||||||
|
return { verfuegbar: true, bestellbar, grund, preis: register?.priceLabel ?? '', einrichtung: register?.setupEstimate ?? '', hinweis }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zustaende der Funktionen je Projekt; geschlossene Einrichtungs-Tickets dieses Kunden werden vorher freigeschaltet
|
// Zustaende der Funktionen je Projekt; geschlossene Einrichtungs-Tickets dieses Kunden werden vorher freigeschaltet
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
|||||||
const sf = await import('../server/services/siteFeatures.js')
|
const sf = await import('../server/services/siteFeatures.js')
|
||||||
const { loeschTag } = await import('../server/services/termine/frist.js')
|
const { loeschTag } = await import('../server/services/termine/frist.js')
|
||||||
const { deleteCustomerWithPortalAccess } = await import('../server/services/customerAdmin.js')
|
const { deleteCustomerWithPortalAccess } = await import('../server/services/customerAdmin.js')
|
||||||
|
const { aboMeldung } = await import('../server/services/featureOrder.js')
|
||||||
|
|
||||||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/team': teamRoutes })
|
const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/team': teamRoutes })
|
||||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||||
@@ -165,7 +166,8 @@ test('GET /api/funktionen: termine.verfuegbar je Modus und Pilot-E-Mail; false b
|
|||||||
assert.equal(await verfuegbar(customerCookie('c2')), true)
|
assert.equal(await verfuegbar(customerCookie('c2')), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---- Abo-Felder der Kachel (C10a): bestellbar, grund, preis, einrichtung nur bei verfuegbar: true
|
// ---- Abo-Felder der Kachel (C10a): bestellbar, grund, preis, einrichtung, hinweis nur bei verfuegbar: true
|
||||||
|
// hinweis (R111) = aboMeldung des Registers: der Browser zeigt den Text nur an und hat keine Kopie; bei bestellbar true null
|
||||||
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
|
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
|
||||||
const angebot = (bestellbar, grund) => ({
|
const angebot = (bestellbar, grund) => ({
|
||||||
verfuegbar: true,
|
verfuegbar: true,
|
||||||
@@ -173,6 +175,7 @@ const angebot = (bestellbar, grund) => ({
|
|||||||
grund,
|
grund,
|
||||||
preis: config.features.termine.priceLabel,
|
preis: config.features.termine.priceLabel,
|
||||||
einrichtung: config.features.termine.setupEstimate,
|
einrichtung: config.features.termine.setupEstimate,
|
||||||
|
hinweis: bestellbar === true ? null : aboMeldung(config.features.termine, grund ?? 'unbekannt'),
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/funktionen: Kunde mit Wartungs-Abo => bestellbar true, grund null, Preis und Einrichtung aus dem Register', async (t) => {
|
test('GET /api/funktionen: Kunde mit Wartungs-Abo => bestellbar true, grund null, Preis und Einrichtung aus dem Register', async (t) => {
|
||||||
@@ -182,7 +185,38 @@ test('GET /api/funktionen: Kunde mit Wartungs-Abo => bestellbar true, grund null
|
|||||||
const termine = await termineAntwort()
|
const termine = await termineAntwort()
|
||||||
assert.deepEqual(termine, angebot(true, null))
|
assert.deepEqual(termine, angebot(true, null))
|
||||||
assert.ok(termine.preis && termine.einrichtung, 'das Register liefert beide Werte')
|
assert.ok(termine.preis && termine.einrichtung, 'das Register liefert beide Werte')
|
||||||
assert.deepEqual(Object.keys(termine), ['verfuegbar', 'bestellbar', 'grund', 'preis', 'einrichtung'])
|
assert.deepEqual(Object.keys(termine), ['verfuegbar', 'bestellbar', 'grund', 'preis', 'einrichtung', 'hinweis'])
|
||||||
|
assert.equal(termine.hinweis, null, 'bestellbar: kein Hinweis (das Feld steht da, ist aber null)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GET /api/funktionen: hinweis ist der Text von aboMeldung mit dem Register des Terminplaners, je Grund', async (t) => {
|
||||||
|
t.after(() => aw.seed('subscriptions', hostingDoc))
|
||||||
|
termineMit('pilot', ['*'])
|
||||||
|
const faelle = [
|
||||||
|
['kein_abo', () => {}, 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.'],
|
||||||
|
['gekuendigt', () => wartung({ cancelAtPeriodEnd: true }), 'Dein Wartungs-Abo ist gekündigt. Für den Terminplaner brauchst du ein laufendes Abo.'],
|
||||||
|
[
|
||||||
|
'nicht_aktiv',
|
||||||
|
() => wartung({ status: 'past_due' }),
|
||||||
|
'Dein Wartungs-Abo ist gerade nicht aktiv (zum Beispiel wegen einer offenen Zahlung). Sobald es wieder aktiv ist, kannst du den Terminplaner bestellen.',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
for (const [grund, richteEin, text] of faelle) {
|
||||||
|
aw.seed('subscriptions', hostingDoc)
|
||||||
|
richteEin()
|
||||||
|
const termine = await termineAntwort()
|
||||||
|
assert.equal(termine.bestellbar, false, grund)
|
||||||
|
assert.equal(termine.grund, grund)
|
||||||
|
assert.equal(termine.hinweis, text, grund)
|
||||||
|
assert.equal(termine.hinweis, aboMeldung(config.features.termine, grund), grund)
|
||||||
|
}
|
||||||
|
// ein Lesefehler: bestellbar null, grund null => Text zu 'unbekannt'
|
||||||
|
aw.seed('subscriptions', hostingDoc)
|
||||||
|
wartung()
|
||||||
|
t.mock.method(console, 'error', () => {})
|
||||||
|
aw.failNext({ collection: 'subscriptions', status: 500, message: 'x' })
|
||||||
|
const unbekannt = await termineAntwort()
|
||||||
|
assert.deepEqual([unbekannt.bestellbar, unbekannt.grund, unbekannt.hinweis], [null, null, 'Das Abo konnte gerade nicht geprüft werden.'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/funktionen: ohne Wartungs-Abo (nur Hosting, gar kein Abo, beendet) => bestellbar false, grund kein_abo', async (t) => {
|
test('GET /api/funktionen: ohne Wartungs-Abo (nur Hosting, gar kein Abo, beendet) => bestellbar false, grund kein_abo', async (t) => {
|
||||||
@@ -212,11 +246,15 @@ test('GET /api/funktionen: Lesefehler im Abo => bestellbar null, grund null, die
|
|||||||
const fehler = t.mock.method(console, 'error', () => {})
|
const fehler = t.mock.method(console, 'error', () => {})
|
||||||
termineMit('pilot', ['*'])
|
termineMit('pilot', ['*'])
|
||||||
wartung()
|
wartung()
|
||||||
|
// eigenes Projekt mit aktivem Terminplaner: der Test haengt nicht von den Zustaenden aus frueheren Tests ab
|
||||||
|
aw.seed('websiteProjects', { $id: 'pt_lesefehler', customerId: 'c1', projectName: 'Lesefehler Salon' })
|
||||||
|
aw.seed('portalFeatures', { $id: 'f_pt_lesefehler_termine', customerId: 'c1', projectId: 'pt_lesefehler', featureKey: 'termine', siteKey: 'abcdefghijklmnop', state: 'active', enabled: true })
|
||||||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' })
|
aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' })
|
||||||
const res = await get('/api/funktionen', { cookie })
|
const res = await get('/api/funktionen', { cookie })
|
||||||
assert.deepEqual(res.termine, angebot(null, null))
|
assert.deepEqual(res.termine, angebot(null, null))
|
||||||
for (const id of ['pt1', 'pt2']) assert.ok(res.projects.some((p) => p.projectId === id), `Projekt ${id} ist in der Liste`)
|
assert.equal(res.termine.hinweis, 'Das Abo konnte gerade nicht geprüft werden.')
|
||||||
assert.equal(featuresOf({ body: res }, 'pt1').termine.state, 'active', 'die Zustaende der Projekte bleiben')
|
assert.ok(res.projects.some((p) => p.projectId === 'pt_lesefehler'), 'das Projekt ist in der Liste')
|
||||||
|
assert.equal(featuresOf({ body: res }, 'pt_lesefehler').termine.state, 'active', 'die Zustaende der Projekte bleiben')
|
||||||
assert.ok(!JSON.stringify(res).includes('geheimer'))
|
assert.ok(!JSON.stringify(res).includes('geheimer'))
|
||||||
assert.ok(fehler.mock.calls.every((aufruf) => !String(aufruf.arguments[0]).includes('geheimer')), 'auch nicht im Log')
|
assert.ok(fehler.mock.calls.every((aufruf) => !String(aufruf.arguments[0]).includes('geheimer')), 'auch nicht im Log')
|
||||||
assert.ok(fehler.mock.calls.length >= 1, 'der Fehler steht im Log')
|
assert.ok(fehler.mock.calls.length >= 1, 'der Fehler steht im Log')
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'
|
|||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
import { aboMeldung } from '../server/services/featureOrder.js'
|
|
||||||
import { featureConfig } from '../server/services/featureRegistry.js'
|
|
||||||
|
|
||||||
// public/funktionen.js wirklich laden (die absoluten Importe /analyse-view.js und /geraete.js werden auf die Dateien umgeleitet)
|
// public/funktionen.js wirklich laden (die absoluten Importe /analyse-view.js und /geraete.js werden auf die Dateien umgeleitet)
|
||||||
const dir = mkdtempSync(join(tmpdir(), 'fn-tiles-'))
|
const dir = mkdtempSync(join(tmpdir(), 'fn-tiles-'))
|
||||||
@@ -149,7 +147,7 @@ test('statisch: News postet weiter consent: true an /api/funktionen/news/checkou
|
|||||||
const withTermine = (id, state, extra = {}) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state, ...extra } } })
|
const withTermine = (id, state, extra = {}) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state, ...extra } } })
|
||||||
const noAnalyse = { comingSoon: true }
|
const noAnalyse = { comingSoon: true }
|
||||||
// Standardantwort bei verfuegbar: true wie GET /api/funktionen (Kunde mit Wartungs-Abo); abo ueberschreibt einzelne Felder
|
// Standardantwort bei verfuegbar: true wie GET /api/funktionen (Kunde mit Wartungs-Abo); abo ueberschreibt einzelne Felder
|
||||||
const ABO_OK = { bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen' }
|
const ABO_OK = { bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen', hinweis: null }
|
||||||
async function renderTermine(projects, { verfuegbar, anfragen = {}, abo = {} } = {}) {
|
async function renderTermine(projects, { verfuegbar, anfragen = {}, abo = {} } = {}) {
|
||||||
const calls = []
|
const calls = []
|
||||||
globalThis.fetch = async (url) => {
|
globalThis.fetch = async (url) => {
|
||||||
@@ -178,10 +176,10 @@ test('Terminplaner-Kachel: fehlt termine.verfuegbar oder ist false ⇒ „Kommt
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Die Abo-Hinweise der Kachel kommen aus aboMeldung (server/services/featureOrder.js, einzige Quelle der Abo-Hinweistexte);
|
// Die Abo-Hinweise der Kachel kommen vom Server (termine.hinweis = aboMeldung in server/services/featureOrder.js, einzige Quelle der
|
||||||
// die Kachel steht im Browser und kennt den Text nur als Kopie: dieser Test haelt beide gleich.
|
// Abo-Hinweistexte, R111); der Browser hat keine Kopie und zeigt den Text nur maskiert an. Die Tests nehmen eigene Texte, damit
|
||||||
const TERMINE_REGISTER = featureConfig('termine')
|
// eine Kopie im Browser auffaellt. Der Link zu den Vertraegen steht nur bei kein_abo.
|
||||||
const KEIN_ABO_HINWEIS = /Im Wartungs-Abo enthalten\. <button type="button" class="kb-link" data-view="vertraege">Zu Verträge<\/button>/
|
const ZU_VERTRAEGE = '<button type="button" class="kb-link" data-view="vertraege">Zu Verträge</button>'
|
||||||
const knopfDerKachel = (kachel) => kachel.match(/<button[^>]*fn-tile-cta[^>]*>([^<]*)<\/button>/)
|
const knopfDerKachel = (kachel) => kachel.match(/<button[^>]*fn-tile-cta[^>]*>([^<]*)<\/button>/)
|
||||||
|
|
||||||
test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftext und Ziel', async () => {
|
test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftext und Ziel', async () => {
|
||||||
@@ -195,10 +193,10 @@ test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftex
|
|||||||
['Aktiv', [withTermine('a', 'active')], {}, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
['Aktiv', [withTermine('a', 'active')], {}, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
||||||
['Abgeschaltet', [withTermine('a', 'disabled')], {}, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'],
|
['Abgeschaltet', [withTermine('a', 'disabled')], {}, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'],
|
||||||
// das Abo betrifft nur die Bestellung: wer schon bestellt hat oder nutzt, behaelt seinen Weg in die Ansicht
|
// das Abo betrifft nur die Bestellung: wer schon bestellt hat oder nutzt, behaelt seinen Weg in die Ansicht
|
||||||
['Zahlung offen, Abo gekündigt', [withTermine('a', 'ordered')], { bestellbar: false, grund: 'gekuendigt' }, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'],
|
['Zahlung offen, Abo gekündigt', [withTermine('a', 'ordered')], { bestellbar: false, grund: 'gekuendigt', hinweis: 'Serverhinweis gekündigt.' }, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'],
|
||||||
['In Einrichtung, Abo nicht lesbar', [withTermine('a', 'setup')], { bestellbar: null, grund: null }, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'],
|
['In Einrichtung, Abo nicht lesbar', [withTermine('a', 'setup')], { bestellbar: null, grund: null, hinweis: 'Serverhinweis nicht prüfbar.' }, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'],
|
||||||
['Aktiv, Abo gekündigt', [withTermine('a', 'active')], { bestellbar: false, grund: 'gekuendigt' }, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
['Aktiv, Abo gekündigt', [withTermine('a', 'active')], { bestellbar: false, grund: 'gekuendigt', hinweis: 'Serverhinweis gekündigt.' }, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
||||||
['Abgeschaltet, Abo nicht aktiv', [withTermine('a', 'disabled')], { bestellbar: false, grund: 'nicht_aktiv' }, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'],
|
['Abgeschaltet, Abo nicht aktiv', [withTermine('a', 'disabled')], { bestellbar: false, grund: 'nicht_aktiv', hinweis: 'Serverhinweis nicht aktiv.' }, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'],
|
||||||
]
|
]
|
||||||
for (const [name, projects, abo, klasse, pille, knopfText, ziel] of zeilen) {
|
for (const [name, projects, abo, klasse, pille, knopfText, ziel] of zeilen) {
|
||||||
const { html } = await renderTermine(projects, { verfuegbar: true, abo })
|
const { html } = await renderTermine(projects, { verfuegbar: true, abo })
|
||||||
@@ -210,44 +208,75 @@ test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftex
|
|||||||
assert.ok(knopf[0].includes(`data-view="${ziel}" data-project="a"`), `${name}: Ziel ${knopf[0]}`)
|
assert.ok(knopf[0].includes(`data-view="${ziel}" data-project="a"`), `${name}: Ziel ${knopf[0]}`)
|
||||||
assert.doesNotMatch(kachel, /Wir schalten den Terminplaner für dich frei/, name)
|
assert.doesNotMatch(kachel, /Wir schalten den Terminplaner für dich frei/, name)
|
||||||
assert.doesNotMatch(kachel, /data-view="chat"/, `${name}: kein Chat-Knopf mehr`)
|
assert.doesNotMatch(kachel, /data-view="chat"/, `${name}: kein Chat-Knopf mehr`)
|
||||||
|
assert.doesNotMatch(kachel, /Serverhinweis/, `${name}: der Abo-Hinweis gilt nur im Zustand Aus`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Terminplaner-Kachel: Aus ohne Abo ⇒ kein Knopf und kein Weg in die Ansicht, Hinweis je Grund', async () => {
|
test('Terminplaner-Kachel: Aus ohne Abo => kein Knopf und kein Weg in die Ansicht, der Hinweis kommt vom Server (Link zu Verträge nur bei kein_abo)', async () => {
|
||||||
const hinweise = [
|
const faelle = [
|
||||||
['kein_abo', { bestellbar: false, grund: 'kein_abo' }, KEIN_ABO_HINWEIS],
|
['kein_abo', { bestellbar: false, grund: 'kein_abo', hinweis: 'Serverhinweis ohne Abo.' }, `Serverhinweis ohne Abo. ${ZU_VERTRAEGE}`],
|
||||||
['gekuendigt', { bestellbar: false, grund: 'gekuendigt' }, new RegExp(`${aboMeldung(TERMINE_REGISTER, 'gekuendigt')}`)],
|
['gekuendigt', { bestellbar: false, grund: 'gekuendigt', hinweis: 'Serverhinweis gekündigt & beendet.' }, 'Serverhinweis gekündigt & beendet.'],
|
||||||
['nicht_aktiv', { bestellbar: false, grund: 'nicht_aktiv' }, new RegExp(aboMeldung(TERMINE_REGISTER, 'nicht_aktiv').replace(/[()]/g, '\\$&'))],
|
['nicht_aktiv', { bestellbar: false, grund: 'nicht_aktiv', hinweis: 'Serverhinweis „nicht aktiv“ (offen).' }, 'Serverhinweis „nicht aktiv“ (offen).'],
|
||||||
['null (Lesefehler)', { bestellbar: null, grund: null }, /Das Abo konnte gerade nicht geprüft werden\./],
|
['unbekannt (Lesefehler: bestellbar null)', { bestellbar: null, grund: null, hinweis: 'Serverhinweis nicht prüfbar.' }, 'Serverhinweis nicht prüfbar.'],
|
||||||
['Feld fehlt (ältere Antwort)', { bestellbar: undefined, grund: undefined }, /Das Abo konnte gerade nicht geprüft werden\./],
|
['unbekannter Grund', { bestellbar: false, grund: 'etwas_neues', hinweis: 'Serverhinweis zu etwas Neuem.' }, 'Serverhinweis zu etwas Neuem.'],
|
||||||
['unbekannter Grund', { bestellbar: false, grund: 'etwas_neues' }, /Das Abo konnte gerade nicht geprüft werden\./],
|
|
||||||
]
|
]
|
||||||
// mit und ohne Entwurf: auch ein vorhandener Entwurf oeffnet ohne Abo keinen Weg
|
// mit und ohne Entwurf: auch ein vorhandener Entwurf oeffnet ohne Abo keinen Weg
|
||||||
for (const projekte of [[withTermine('a', 'off')], [withTermine('a', 'off', { siteKey: 'abcdefghijklmnop' })]]) {
|
for (const projekte of [[withTermine('a', 'off')], [withTermine('a', 'off', { siteKey: 'abcdefghijklmnop' })]]) {
|
||||||
for (const [name, abo, hinweis] of hinweise) {
|
for (const [name, abo, erwartet] of faelle) {
|
||||||
const { html, calls } = await renderTermine(projekte, { verfuegbar: true, abo })
|
const { html, calls } = await renderTermine(projekte, { verfuegbar: true, abo })
|
||||||
const kachel = tile(html, 'termine')
|
const kachel = tile(html, 'termine')
|
||||||
assert.match(kachel, /fn-pill-off">Aus</, name)
|
assert.match(kachel, /fn-pill-off">Aus</, name)
|
||||||
assert.match(kachel, hinweis, `${name}: ${kachel}`)
|
assert.ok(kachel.includes(`<p class="fn-count">${erwartet}</p>`), `${name}: ${kachel}`)
|
||||||
assert.doesNotMatch(kachel, /data-view="termine"/, name)
|
assert.doesNotMatch(kachel, /data-view="termine"/, name)
|
||||||
assert.doesNotMatch(kachel, /fn-tile-cta/, `${name}: kein Knopf`)
|
assert.doesNotMatch(kachel, /fn-tile-cta/, `${name}: kein Knopf`)
|
||||||
assert.doesNotMatch(html, /data-view="termine"/, `${name}: nirgends ein Weg in die Ansicht`)
|
assert.doesNotMatch(html, /data-view="termine"/, `${name}: nirgends ein Weg in die Ansicht`)
|
||||||
assert.deepEqual(calls, ['/api/funktionen'], name)
|
assert.deepEqual(calls, ['/api/funktionen'], name)
|
||||||
|
// nur kein_abo verweist auf die Vertraege; die uebrigen Hinweise sind reiner Text
|
||||||
|
assert.equal(kachel.split('data-view=').length - 1, abo.grund === 'kein_abo' ? 1 : 0, `${name}: Links`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// nur kein_abo verweist auf die Vertraege; die uebrigen Hinweise sind reiner Text
|
|
||||||
const gekuendigt = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'gekuendigt' } })).html, 'termine')
|
|
||||||
assert.doesNotMatch(gekuendigt, /data-view=/)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Terminplaner-Kachel: die Abo-Hinweise stimmen wortgleich mit aboMeldung überein (gekündigt, nicht aktiv, nicht lesbar)', async () => {
|
test('Terminplaner-Kachel: der Hinweis des Servers kommt maskiert in die Kachel (auch bei kein_abo neben dem Link)', async () => {
|
||||||
for (const grund of ['gekuendigt', 'nicht_aktiv']) {
|
const bose = '<img src=x onerror=1>"\'&'
|
||||||
const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund } })
|
const maskiert = '<img src=x onerror=1>"'&'
|
||||||
assert.ok(tile(html, 'termine').includes(`<p class="fn-count">${aboMeldung(TERMINE_REGISTER, grund)}</p>`), grund)
|
for (const grund of ['gekuendigt', 'nicht_aktiv', 'kein_abo']) {
|
||||||
|
const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund, hinweis: bose } })
|
||||||
|
const kachel = tile(html, 'termine')
|
||||||
|
assert.doesNotMatch(html, /<img/, grund)
|
||||||
|
assert.ok(kachel.includes(`<p class="fn-count">${maskiert}${grund === 'kein_abo' ? ` ${ZU_VERTRAEGE}` : ''}</p>`), `${grund}: ${kachel}`)
|
||||||
}
|
}
|
||||||
const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: null, grund: null } })
|
})
|
||||||
assert.ok(tile(html, 'termine').includes(`<p class="fn-count">${aboMeldung(TERMINE_REGISTER, 'unbekannt')}</p>`))
|
|
||||||
assert.equal(aboMeldung(TERMINE_REGISTER, 'unbekannt'), 'Das Abo konnte gerade nicht geprüft werden.')
|
test('Terminplaner-Kachel: fehlt der Hinweis (leer, null, kein Text) gibt es keine Zeile und keinen Knopf; kein_abo behält den Link', async () => {
|
||||||
|
for (const hinweis of [undefined, null, '', 5, { text: 'x' }]) {
|
||||||
|
const ohne = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'gekuendigt', hinweis } })).html, 'termine')
|
||||||
|
assert.match(ohne, /fn-pill-off">Aus</)
|
||||||
|
assert.doesNotMatch(ohne, /fn-tile-cta|data-view=|fn-count|\[object/, String(hinweis))
|
||||||
|
const keinAbo = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo', hinweis } })).html, 'termine')
|
||||||
|
assert.ok(keinAbo.includes(`<p class="fn-count">${ZU_VERTRAEGE}</p>`), String(hinweis))
|
||||||
|
assert.doesNotMatch(keinAbo, /fn-tile-cta|data-view="termine"/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Terminplaner-Kachel: mit Abo (bestellbar true) steht der Knopf da und ein Hinweis erscheint nicht', async () => {
|
||||||
|
const kachel = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { hinweis: 'Serverhinweis darf nicht erscheinen.' } })).html, 'termine')
|
||||||
|
assert.equal(knopfDerKachel(kachel)[1], 'Terminplaner einrichten · 149 €')
|
||||||
|
assert.doesNotMatch(kachel, /Serverhinweis|fn-count/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Der Browser hat keine Kopie der Abo-Hinweise; der Link zu Verträge steht einmal im Quelltext (Analyse und Terminplaner teilen ihn)', async () => {
|
||||||
|
assert.doesNotMatch(src, /TERMINE_ABO_TEXTE|termineAboHinweis|KEIN_ABO_HINWEIS|ist gekündigt\. Für|gerade nicht aktiv|nicht geprüft werden/)
|
||||||
|
assert.equal(src.split('data-view="vertraege"').length - 1, 1)
|
||||||
|
// Analyse-Kachel und -Seite (unveraendert) und Terminplaner-Kachel zeigen genau diesen Link
|
||||||
|
const analyse = tile(await render([project('a', { eligible: false, reason: 'kein_abo', reasonText: 'Kein Abo' })]), 'analyse')
|
||||||
|
assert.ok(analyse.includes(`<p class="fn-count">Im Wartungs-Abo enthalten. ${ZU_VERTRAEGE}</p>`), analyse)
|
||||||
|
const andere = tile(await render([project('a', { eligible: false, reason: 'gekuendigt', reasonText: 'Gekündigt' })]), 'analyse')
|
||||||
|
assert.ok(andere.includes('<p class="fn-count">Gekündigt</p>') && !andere.includes('data-view='), andere)
|
||||||
|
const stage = await renderActivation({ state: undefined, eligible: false, reason: 'kein_abo', reasonText: 'Kein Abo' })
|
||||||
|
assert.ok(stage.innerHTML.includes(`<p>Kein Abo ${ZU_VERTRAEGE}</p>`), stage.innerHTML)
|
||||||
|
const termine = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo', hinweis: 'T' } })).html, 'termine')
|
||||||
|
assert.ok(termine.includes(ZU_VERTRAEGE), termine)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Terminplaner-Kachel: ordered und setup fragen /api/termine nicht nach einer Zahl (nur aktive Websites)', async () => {
|
test('Terminplaner-Kachel: ordered und setup fragen /api/termine nicht nach einer Zahl (nur aktive Websites)', async () => {
|
||||||
@@ -347,11 +376,11 @@ test('Terminplaner-Kachel: Zielprojekt = aktiv, dann ordered/setup, dann abgesch
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Terminplaner-Kachel: mehrere Websites ohne Abo ⇒ die Pille bleibt „Aus“, nirgends ein Knopf (auch mit Entwurf)', async () => {
|
test('Terminplaner-Kachel: mehrere Websites ohne Abo ⇒ die Pille bleibt „Aus“, nirgends ein Knopf (auch mit Entwurf)', async () => {
|
||||||
const { html } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'off', { siteKey: 'abcdefghijklmnop' })], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo' } })
|
const { html } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'off', { siteKey: 'abcdefghijklmnop' })], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo', hinweis: 'Serverhinweis ohne Abo.' } })
|
||||||
const kachel = tile(html, 'termine')
|
const kachel = tile(html, 'termine')
|
||||||
assert.match(kachel, /fn-pill-off">Aus</)
|
assert.match(kachel, /fn-pill-off">Aus</)
|
||||||
assert.doesNotMatch(kachel, /fn-tile-cta|data-view="termine"/)
|
assert.doesNotMatch(kachel, /fn-tile-cta|data-view="termine"/)
|
||||||
assert.match(kachel, KEIN_ABO_HINWEIS)
|
assert.ok(kachel.includes(`<p class="fn-count">Serverhinweis ohne Abo. ${ZU_VERTRAEGE}</p>`), kachel)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
|
test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
|
||||||
|
|||||||
@@ -125,6 +125,15 @@ function alleStatus() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Der Abo-Text kommt in der Antwort von /api/funktionen vom Server (termine.hinweis, aboMeldung in featureOrder.js); die Fixture
|
||||||
|
// liefert feste Texte, die Kachel zeigt sie nur an
|
||||||
|
const FX_ABO_HINWEISE = {
|
||||||
|
kein_abo: 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.',
|
||||||
|
gekuendigt: 'Dein Wartungs-Abo ist gekündigt. Für den Terminplaner brauchst du ein laufendes Abo.',
|
||||||
|
nicht_aktiv: 'Dein Wartungs-Abo ist gerade nicht aktiv (zum Beispiel wegen einer offenen Zahlung). Sobald es wieder aktiv ist, kannst du den Terminplaner bestellen.',
|
||||||
|
unbekannt: 'Das Abo konnte gerade nicht geprüft werden.',
|
||||||
|
}
|
||||||
|
|
||||||
/** Antworten der Fixture auf /api/funktionen und /api/termine (Teil von page.route). */
|
/** Antworten der Fixture auf /api/funktionen und /api/termine (Teil von page.route). */
|
||||||
async function fixtureAntwort(fx, route, req, url, json) {
|
async function fixtureAntwort(fx, route, req, url, json) {
|
||||||
const p = url.pathname
|
const p = url.pathname
|
||||||
@@ -132,7 +141,14 @@ async function fixtureAntwort(fx, route, req, url, json) {
|
|||||||
const m = req.method()
|
const m = req.method()
|
||||||
if (p === '/api/funktionen') {
|
if (p === '/api/funktionen') {
|
||||||
const termine = fx.verfuegbar
|
const termine = fx.verfuegbar
|
||||||
? { verfuegbar: true, bestellbar: fx.bestellbar, grund: fx.aboGrund, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen' }
|
? {
|
||||||
|
verfuegbar: true,
|
||||||
|
bestellbar: fx.bestellbar,
|
||||||
|
grund: fx.aboGrund,
|
||||||
|
preis: '149 €',
|
||||||
|
einrichtung: 'meist innerhalb von 1–2 Wochen',
|
||||||
|
hinweis: fx.bestellbar === true ? null : FX_ABO_HINWEISE[fx.aboGrund ?? 'unbekannt'],
|
||||||
|
}
|
||||||
: { verfuegbar: false }
|
: { verfuegbar: false }
|
||||||
return json({ projects: fx.projekte, termine })
|
return json({ projects: fx.projekte, termine })
|
||||||
}
|
}
|
||||||
@@ -324,7 +340,7 @@ async function pruefungen() {
|
|||||||
await knopf(kachel, 'Fragebogen fortsetzen').waitFor()
|
await knopf(kachel, 'Fragebogen fortsetzen').waitFor()
|
||||||
// Aus ohne Abo: kein Knopf, Hinweis; die Hinweise der uebrigen Gruende
|
// Aus ohne Abo: kein Knopf, Hinweis; die Hinweise der uebrigen Gruende
|
||||||
const faelle = [
|
const faelle = [
|
||||||
['kein_abo', 'Im Wartungs-Abo enthalten.'],
|
['kein_abo', 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.'],
|
||||||
['gekuendigt', 'Dein Wartungs-Abo ist gekündigt.'],
|
['gekuendigt', 'Dein Wartungs-Abo ist gekündigt.'],
|
||||||
['nicht_aktiv', 'Dein Wartungs-Abo ist gerade nicht aktiv'],
|
['nicht_aktiv', 'Dein Wartungs-Abo ist gerade nicht aktiv'],
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in new issue
Block a user