From 52456dac760b797225b290a481749eabffbe25ab Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 4 Oct 2026 22:43:17 +0000
Subject: [PATCH] Terminplaner TP2 C10a: Nachbesserung 1 (Abo-Hinweis vom
Server, R111)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
---
public/funktionen.js | 32 +++++-------
server/routes/funktionen.js | 11 ++--
test/funktionenTermine.test.js | 46 ++++++++++++++--
test/funktionenTiles.test.js | 95 ++++++++++++++++++++++------------
test/ui/termine-portal.ui.mjs | 20 ++++++-
5 files changed, 141 insertions(+), 63 deletions(-)
diff --git a/public/funktionen.js b/public/funktionen.js
index b75d396..91e0473 100644
--- a/public/funktionen.js
+++ b/public/funktionen.js
@@ -112,6 +112,9 @@ function newsTile(projects, price = PRICE) {
`
}
+// Link in den Hinweisen ohne Wartungs-Abo (kein_abo): Analyse-Kachel, Analyse-Seite und Terminplaner-Kachel teilen ihn
+const ZU_VERTRAEGE_LINK = ''
+
/* Analyse: Kachel je nach Zustand der Websites (comingSoon = Schalter aus) */
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')
@@ -136,7 +139,7 @@ function analyseTile(projects) {
else if (state === 'disabled') { text = 'Bitte melde dich bei uns'; action = '' }
else if (state === 'active') action = ``
else if (state === 'off') action = ``
- else if (a.reason === 'kein_abo') action = '
Im Wartungs-Abo enthalten.
'
+ else if (a.reason === 'kein_abo') action = `
Im Wartungs-Abo enthalten. ${ZU_VERTRAEGE_LINK}
`
else action = `
${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}
`
const count = projects.length > 1 && active ? `
aktiv für ${active} von ${projects.length} Websites
` : ''
return `
@@ -153,29 +156,15 @@ function analyseTile(projects) {
/* 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 (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
- Abo gibt es im Zustand Aus keinen Weg in die Ansicht; wer schon bestellt hat oder den Terminplaner nutzt, behaelt seinen. */
+ grund (kein_abo | gekuendigt | nicht_aktiv | null), preis, einrichtung und hinweis (Abo-Text des Servers, aboMeldung in
+ 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 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
// mit einem umgebogenen Import). Aenderungen an beiden Stellen machen.
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. '
-
-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)
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)}` : ''
action = knopf(hatEntwurf(target) ? 'Fragebogen fortsetzen' : `Terminplaner einrichten${preis}`)
} else {
- extra = `
${termineAboHinweis(angebot)}
`
+ // 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 = `
`), 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 () => {
@@ -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 () => {
- 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')
assert.match(kachel, /fn-pill-off">Aus)
assert.doesNotMatch(kachel, /fn-tile-cta|data-view="termine"/)
- assert.match(kachel, KEIN_ABO_HINWEIS)
+ assert.ok(kachel.includes(`
Serverhinweis ohne Abo. ${ZU_VERTRAEGE}
`), kachel)
})
test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
diff --git a/test/ui/termine-portal.ui.mjs b/test/ui/termine-portal.ui.mjs
index cd9d7cd..4dd2255 100644
--- a/test/ui/termine-portal.ui.mjs
+++ b/test/ui/termine-portal.ui.mjs
@@ -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). */
async function fixtureAntwort(fx, route, req, url, json) {
const p = url.pathname
@@ -132,7 +141,14 @@ async function fixtureAntwort(fx, route, req, url, json) {
const m = req.method()
if (p === '/api/funktionen') {
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 }
return json({ projects: fx.projekte, termine })
}
@@ -324,7 +340,7 @@ async function pruefungen() {
await knopf(kachel, 'Fragebogen fortsetzen').waitFor()
// Aus ohne Abo: kein Knopf, Hinweis; die Hinweise der uebrigen Gruende
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.'],
['nicht_aktiv', 'Dein Wartungs-Abo ist gerade nicht aktiv'],
]