News: im Abo-Modell Einschalten statt Bestellen, Projektwahl über dataset

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 18:40:59 +00:00
1 parent b6f70a502a
commit 1cabab2826
2 files changed
+118 -6

No files matched your search

+35 -6
View File
@@ -471,10 +471,12 @@ export async function initFunktionen(root) {
}
/* ---------------------------------------------------------------- News --- */
export async function initNews(root, { projectId } = {}) {
export async function initNews(root, { projectId, einschalten } = {}) {
if (!root) return
if (projectId) selection.projectId = projectId
const { projects = [], setupEstimate = '', consentText = '', features, ustHinweis } = await request('/api/funktionen')
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel legt ihr Projekt im Behälter ab (wie bei Terminplaner, Webshop, Buchungen)
if (root.dataset) delete root.dataset.wunschProjekt
if (wunsch) selection.projectId = wunsch
const { projects = [], setupEstimate = '', consentText = '', features, ustHinweis, modell } = await request('/api/funktionen')
if (!projects.length) {
root.innerHTML = '<div class="kb-panel kb-empty">Noch keine Website-Projekte zugewiesen.</div>'
return
@@ -507,11 +509,38 @@ export async function initNews(root, { projectId } = {}) {
? '<p class="fn-thanks" role="status">Danke! Sobald die Zahlung da ist, legen wir los.</p>'
: ''
if (feature.state === 'active') renderEditor(stage, project)
else if (isSetup(feature.state)) stage.innerHTML = thanks + statusCard(feature, setupEstimate)
else if (isSetup(feature.state)) stage.innerHTML = thanks + statusCard(feature, setupEstimate, modell === 'abo')
else if (feature.state === 'disabled') stage.innerHTML = '<div class="kb-panel"><h3>News-Pinnwand ist abgeschaltet</h3><p>Für diese Website ist die Pinnwand im Moment nicht aktiv. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></div>'
else if (modell === 'abo') renderBookingAbo(stage, project, { einschalten, neuLaden: () => initNews(root, { einschalten }) })
else renderBooking(stage, project, { setupEstimate, offer: { ...newsOffer(features, consentText), ustHinweis: textOr(ustHinweis, UST_HINWEIS) }, thanks })
}
/**
* Abo-Modell (modell === 'abo' in GET /api/funktionen), Zustand off: statt der Buchungsseite nur der Platz, in den der Ablauf des Einschaltens
* zeichnet (einschalten aus den Init-Optionen, die Hülle gibt ihn durch). Kein Preis, kein Haken, kein Checkout. onFertig lädt die Pinnwand
* einmal neu. Ohne die Funktion oder wenn sie scheitert, steht Text E4 im Platz.
*/
function renderBookingAbo(stage, project, { einschalten, neuLaden }) {
stage.innerHTML = '<div id="news-einschalten"></div>'
const platz = stage.querySelector('#news-einschalten')
const fehlt = () => { platz.textContent = 'Das Einschalten ist gerade nicht verfügbar. Bitte lade die Seite neu oder schreib uns im Chat.' }
if (typeof einschalten !== 'function') {
fehlt()
return
}
let fertig = false
const onFertig = () => {
if (fertig) return
fertig = true
Promise.resolve().then(neuLaden).catch(() => {})
}
try {
Promise.resolve(einschalten(platz, { key: 'news', projectId: project.projectId, viewAs: false, onFertig })).catch(fehlt)
} catch {
fehlt()
}
}
function renderBooking(stage, project, { setupEstimate, offer, thanks }) {
stage.innerHTML = `
${thanks}
@@ -551,10 +580,10 @@ function renderBooking(stage, project, { setupEstimate, offer, thanks }) {
})
}
function statusCard(feature, setupEstimate) {
function statusCard(feature, setupEstimate, abo = false) {
const rows = [
feature.ticketWoid ? `<div><dt>Ticketnummer</dt><dd>#${esc(feature.ticketWoid)}</dd></div>` : '',
feature.orderedAt ? `<div><dt>Bestellt am</dt><dd>${esc(fmtDay(feature.orderedAt))}</dd></div>` : '',
feature.orderedAt ? `<div><dt>${abo ? 'Eingeschaltet am' : 'Bestellt am'}</dt><dd>${esc(fmtDay(feature.orderedAt))}</dd></div>` : '',
setupEstimate ? `<div><dt>Zeiteinschätzung</dt><dd>${esc(setupEstimate)}</dd></div>` : '',
].join('')
const invoice = safeUrl(feature.invoiceUrl)
+83
View File
@@ -0,0 +1,83 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { KEIN_PREIS, einschaltenAttrappe, htmlWurzel, ladeFunktionenModul } from './support/modulAbo.js'
// News-Pinnwand im Abo-Modell (P4 Task 4): im Zustand off nur der Platz für den Ablauf von P3 (key news), kein Preis, kein Haken, kein
// Checkout. Ohne modell === 'abo' bleibt die Buchungsseite mit Preis und Bestellknopf. wunschProjekt wählt das Projekt.
const { initNews } = await ladeFunktionenModul()
globalThis.window = { location: { hash: '#news', pathname: '/dashboard.html', search: '', href: '' }, history: { replaceState() {} } }
after(() => { delete globalThis.window })
const projekt = (id, name, state = 'off', extra = {}) => ({ projectId: id, projectName: name, features: { news: { state, ...extra } } })
async function lauf({ modell, projects = [projekt('p1', 'Muster')], dataset = {}, projectId = 'p1', einschalten } = {}) {
const calls = []
globalThis.fetch = async (url) => {
calls.push(String(url))
return { ok: true, status: 200, json: async () => ({ ...(modell ? { modell } : {}), projects, features: { news: { priceLabel: '30 €' } }, consentText: 'Alt', setupEstimate: 'meist in 1–2 Wochen', ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG' }) }
}
const { root, stage } = htmlWurzel('#news-stage')
Object.assign(root.dataset, dataset)
await initNews(root, { projectId, einschalten })
return { root, stage, calls }
}
test('Abo: Seite ohne Preis, Haken und Checkout; der Ablauf bekommt den Platz mit key news', async () => {
const a = einschaltenAttrappe()
const { stage } = await lauf({ modell: 'abo', einschalten: a.fn })
assert.doesNotMatch(stage.innerHTML, KEIN_PREIS)
assert.doesNotMatch(stage.innerHTML, /news-consent|news-order|30 €/)
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId, a.aufrufe[0].viewAs], ['news', 'p1', false])
assert.ok(a.aufrufe[0].wurzel === stage.els['#news-einschalten'])
assert.equal(a.aufrufe.length, 1)
})
test('Abo: nach onFertig lädt die Pinnwand neu (genau einmal)', async () => {
const a = einschaltenAttrappe()
const { calls } = await lauf({ modell: 'abo', einschalten: a.fn })
a.fertig(0)
a.fertig(0)
await new Promise((r) => setImmediate(r))
assert.equal(calls.filter((u) => u === '/api/funktionen').length, 2)
})
test('Abo ohne Funktion einschalten: Satz E4 im Platz', async () => {
const { stage } = await lauf({ modell: 'abo' })
assert.match(stage.els['#news-einschalten'].textContent, /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
})
test('Abo, einschalten scheitert: Satz E4 im Platz statt eines unbehandelten Fehlers', async () => {
const { stage } = await lauf({ modell: 'abo', einschalten: () => Promise.reject(new Error('kaputt')) })
await new Promise((r) => setImmediate(r))
assert.match(stage.els['#news-einschalten'].textContent, /Das Einschalten ist gerade nicht verfügbar\./)
})
test('Abo, in Einrichtung: Statuskarte mit „Eingeschaltet am“ statt „Bestellt am“, der Ablauf wird nicht gerufen', async () => {
const a = einschaltenAttrappe()
const { stage } = await lauf({ modell: 'abo', projects: [projekt('p1', 'Muster', 'setup', { orderedAt: '2026-10-16T09:00:00.000Z', ticketWoid: '100150' })], einschalten: a.fn })
assert.match(stage.innerHTML, /<dt>Eingeschaltet am<\/dt><dd>16\.10\.2026<\/dd>/)
assert.doesNotMatch(stage.innerHTML, /Bestellt am/)
assert.equal(a.aufrufe.length, 0)
})
test('Ohne modell (fehlt, „Abo“): unverändert Preis, Haken und Bestellknopf; der Ablauf wird nie gerufen', async () => {
for (const modell of [undefined, 'Abo', 'abo ']) {
const a = einschaltenAttrappe()
const { stage } = await lauf({ modell, einschalten: a.fn })
assert.match(stage.innerHTML, /<strong>30 €<\/strong>/)
assert.match(stage.innerHTML, /id="news-order" disabled>Zahlungspflichtig bestellen · 30 €</)
assert.equal(a.aufrufe.length, 0)
}
})
test('Ohne modell, in Einrichtung: weiter „Bestellt am“', async () => {
const { stage } = await lauf({ projects: [projekt('p1', 'Muster', 'setup', { orderedAt: '2026-10-16T09:00:00.000Z' })] })
assert.match(stage.innerHTML, /<dt>Bestellt am<\/dt>/)
})
test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', async () => {
const a = einschaltenAttrappe()
const { root } = await lauf({ modell: 'abo', projects: [projekt('p1', 'Muster'), projekt('p2', 'Muster 2')], dataset: { wunschProjekt: 'p2' }, projectId: '', einschalten: a.fn })
assert.equal(a.aufrufe[0].projectId, 'p2')
assert.equal(root.dataset.wunschProjekt, undefined)
})