diff --git a/public/funktionen.js b/public/funktionen.js index 6b8efd6..a46bef6 100644 --- a/public/funktionen.js +++ b/public/funktionen.js @@ -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 = '
Noch keine Website-Projekte zugewiesen.
' return @@ -507,11 +509,38 @@ export async function initNews(root, { projectId } = {}) { ? '

Danke! Sobald die Zahlung da ist, legen wir los.

' : '' 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 = '

News-Pinnwand ist abgeschaltet

Für diese Website ist die Pinnwand im Moment nicht aktiv. , dann klären wir das.

' + 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 = '
' + 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 ? `
Ticketnummer
#${esc(feature.ticketWoid)}
` : '', - feature.orderedAt ? `
Bestellt am
${esc(fmtDay(feature.orderedAt))}
` : '', + feature.orderedAt ? `
${abo ? 'Eingeschaltet am' : 'Bestellt am'}
${esc(fmtDay(feature.orderedAt))}
` : '', setupEstimate ? `
Zeiteinschätzung
${esc(setupEstimate)}
` : '', ].join('') const invoice = safeUrl(feature.invoiceUrl) diff --git a/test/kundenansichtNeuModulNews.test.js b/test/kundenansichtNeuModulNews.test.js new file mode 100644 index 0000000..c15b66d --- /dev/null +++ b/test/kundenansichtNeuModulNews.test.js @@ -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, /
Eingeschaltet am<\/dt>
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, /30 €<\/strong>/) + assert.match(stage.innerHTML, /id="news-order" disabled>Zahlungspflichtig bestellen · 30 € { + const { stage } = await lauf({ projects: [projekt('p1', 'Muster', 'setup', { orderedAt: '2026-10-16T09:00:00.000Z' })] }) + assert.match(stage.innerHTML, /
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) +})