diff --git a/public/buchungen.js b/public/buchungen.js index 7cb9bf3..88e91d3 100644 --- a/public/buchungen.js +++ b/public/buchungen.js @@ -30,6 +30,8 @@ let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueber const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '') // Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js) const istAbo = (s) => s.modell === 'abo' +/** Mitarbeiter in der Kundenansicht (window.KbView, von der Hülle gesetzt): Einschalten bleibt gesperrt. */ +const inKundenansicht = () => typeof window !== 'undefined' && Boolean(window.KbView?.id?.()) /** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */ const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) } const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off' @@ -142,7 +144,7 @@ function zeigeAnfrage(s, { status, aktuell }) { s.buehne.replaceChildren(wurzel) renderEinschaltSeite(wurzel, { ctx: { projectId, aktuell }, - daten: { offen: [], kundenansicht: false }, + daten: { offen: [], kundenansicht: inKundenansicht() }, key: 'unterkunft', zusammenfassung: () => (ansicht.anfrage ? [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: anfrageZeilen(ansicht.anfrage).map(({ name, wert }) => `${name}: ${wert}`) }] : []), einschalten: s.einschalten, diff --git a/public/funktionen.js b/public/funktionen.js index a46bef6..e011b7d 100644 --- a/public/funktionen.js +++ b/public/funktionen.js @@ -535,7 +535,7 @@ function renderBookingAbo(stage, project, { einschalten, neuLaden }) { Promise.resolve().then(neuLaden).catch(() => {}) } try { - Promise.resolve(einschalten(platz, { key: 'news', projectId: project.projectId, viewAs: false, onFertig })).catch(fehlt) + Promise.resolve(einschalten(platz, { key: 'news', projectId: project.projectId, viewAs: Boolean(window.KbView?.id?.()), onFertig })).catch(fehlt) } catch { fehlt() } diff --git a/public/shop.js b/public/shop.js index dcac5a6..29ad072 100644 --- a/public/shop.js +++ b/public/shop.js @@ -53,6 +53,8 @@ const zustandVon = (projekt) => projekt?.features?.shop?.state || 'off' const text = (wert) => (typeof wert === 'string' ? wert : '') // Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js) const istAbo = (s) => s.funktionen?.modell === 'abo' +/** Mitarbeiter in der Kundenansicht (window.KbView, von der Hülle gesetzt): Einschalten bleibt gesperrt. */ +const inKundenansicht = () => typeof window !== 'undefined' && Boolean(window.KbView?.id?.()) /** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */ const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) } @@ -284,7 +286,7 @@ function zeigeBestellseite(s, { status, aktuell }) { const daten = bestelldaten(s, status) if (istAbo(s)) { // Abo-Modell: die Zusage der Anfrage fuehrt zum Einschalten (kein Preis, keine Haken, kein Checkout) - renderEinschaltSeite(wurzel, { ctx, daten, key: 'shop', zusammenfassung: zusammenfassungVon(daten.anfrage), einschalten: s.einschalten, nachEinschalten: () => ladeNeu(s) }) + renderEinschaltSeite(wurzel, { ctx, daten: { ...daten, kundenansicht: inKundenansicht() }, key: 'shop', zusammenfassung: zusammenfassungVon(daten.anfrage), einschalten: s.einschalten, nachEinschalten: () => ladeNeu(s) }) return } renderBestellen(wurzel, { ctx, daten, key: 'shop', zusammenfassung: zusammenfassungVon(daten.anfrage) }) diff --git a/public/zeiten-bestellen.js b/public/zeiten-bestellen.js index e4fbd59..a81ca68 100644 --- a/public/zeiten-bestellen.js +++ b/public/zeiten-bestellen.js @@ -17,10 +17,13 @@ ctx (baut zeiten.js): { projectId, features, ustHinweis, kundenansicht (Boolean), neuZeichnen({ meldung }), fertig({ danke }), zurueck() }. Bei 409 (inklusive, kein_abo, gekuendigt, nicht_aktiv, fassung_veraltet, schon bestellt …) holt neuZeichnen die Liste neu und zeichnet die Ansicht nach dem neuen Stand mit der Meldung des Servers; fehlt ctx.neuZeichnen oder scheitert es, bleibt die Meldung am Knopf. - zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, danke, meldung }, { behalten, weiter }) ist die Karte fuer ordered, setup, active - und disabled (Zustand off hat die Seite oben). */ + zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, danke, meldung, modell }, { behalten, weiter }) ist die Karte fuer ordered, setup, active + und disabled (Zustand off hat die Seite oben). + Abo-Modell (modell === 'abo' in GET /api/funktionen, strikt): renderZeitenEinschalten zeigt statt der Seiten oben die gemeinsame Einschalt-Seite + (funktion-bestellen.js, Ablauf von P3); die Karte kennt kein Behalten und keinen Preis: „Eingeschaltet am“, bei Abo-Ende die Frist (Z2), nach der + Abschaltung „Deine Eingaben bleiben … gespeichert“ (Z3). */ -import { bestellKoerper, sicherLink } from './funktion-bestellen.js' +import { bestellKoerper, renderEinschaltSeite, sicherLink } from './funktion-bestellen.js' import { fehlerKarte, h, hakenFeld, request } from './termine-dom.js' const WEGE = { @@ -260,6 +263,21 @@ export function renderZeitenBestellen(root, { ctx = {}, angebot = {}, projekt = return seite } +/* ----------------------------------------------------- Einschalten im Abo-Modell --- */ +/** + * Abo-Modell, Zustand off: die gemeinsame Einschalt-Seite (renderEinschaltSeite) mit key 'zeiten'. Den Ablauf (Bestätigung, Knopf, Fehlertexte) zeichnet + * ctx.einschalten (Init-Option der Hülle, P3); nach dem Einschalten zeichnet ctx.fertig({ danke }) die Ansicht neu. Kein Preis, kein Haken, kein Behalten. + */ +export function renderZeitenEinschalten(root, { ctx = {}, projekt = {} } = {}) { + return renderEinschaltSeite(root, { + ctx: { ...ctx, projectId: text(ctx.projectId) || text(projekt.projectId) }, + daten: { offen: [], kundenansicht: ctx.kundenansicht === true }, + key: 'zeiten', + einschalten: ctx.einschalten, + nachEinschalten: () => (typeof ctx.fertig === 'function' ? ctx.fertig({ danke: 'eingeschaltet' }) : undefined), + }) +} + /* ---------------------------------------------------------------- Statuskarte --- */ const PILLE = { ordered: ['Zahlung offen', 'wait'], setup: ['In Einrichtung', 'wait'], active: ['Aktiv', 'ok'], disabled: ['Abgeschaltet', 'off'] } const DANKE = { @@ -286,7 +304,8 @@ function dankeText(danke, feature) { * weiter({ bestellen, meldung }) wird gerufen, wenn „Zahlung fortsetzen“ scheitert (bestellen: es gibt keine offene Zahlungsseite mehr, die Ansicht * zeigt wieder die Bestellseite). */ -export function zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, danke, meldung } = {}, { behalten, weiter } = {}) { +export function zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, danke, meldung, modell } = {}, { behalten, weiter } = {}) { + const abo = modell === 'abo' const state = text(feature?.state) || 'off' const [pilleText, pilleArt] = PILLE[state] || PILLE.setup const preis = text(angebot?.preis) || '30 €' @@ -298,7 +317,7 @@ export function zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, da const zeile = (name, wert) => h('div', null, h('dt', null, name), h('dd', null, wert)) const fakten = [ state === 'setup' || state === 'active' ? (hatTicket ? zeile('Ticketnummer', `#${ticket}`) : null) : null, - bestelltAm ? zeile('Bestellt am', bestelltAm) : null, + bestelltAm ? zeile(abo ? 'Eingeschaltet am' : 'Bestellt am', bestelltAm) : null, ].filter(Boolean) const aboEnde = feature?.disabledReason === 'abo_ende' @@ -327,7 +346,11 @@ export function zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, da const behaltenKnopf = (beschriftung) => h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'behalten', disabled: sperre, onclick: () => { if (typeof behalten === 'function') behalten() } }, beschriftung) const lage = behaltenLage(angebot) let behaltenBox = null - if (frist && lage === 'bezahlen') { + if (abo) { + // Abo-Modell: kein Behalten gegen Geld; Öffnungszeiten live läuft nach dem Abo-Ende noch bis zur Frist (Text Z2) + if (frist && lage === 'bezahlen') behaltenBox = h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'abo-ende' }, h('span', null, `Dein Wartungs-Abo ist beendet. Öffnungszeiten live läuft noch bis ${frist} und schaltet sich dann ab.`)) + else if (frist && lage === 'abo') behaltenBox = h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'abo-laeuft' }, h('span', null, 'Dein Wartungs-Abo läuft wieder – Öffnungszeiten live ist darin enthalten.')) + } else if (frist && lage === 'bezahlen') { behaltenBox = h('div', { class: 'tm-box tm-box-warn', role: 'note', 'data-hinweis': 'behalten' }, h('span', null, `Dein Wartungs-Abo ist beendet – behalte die Funktion bis ${frist} für einmalig ${preis}.`), behaltenKnopf(`Für ${preis} behalten`)) @@ -335,7 +358,11 @@ export function zeitenStatusKarte({ projekt, feature, angebot, kundenansicht, da behaltenBox = h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'abo-laeuft' }, h('span', null, 'Dein Wartungs-Abo läuft wieder – Öffnungszeiten live ist darin enthalten.')) } let abgeschaltetBox = null - if (daten && lage === 'bezahlen') { + if (abo && daten && lage === 'bezahlen') { + // Text Z3: die Eingaben bleiben gespeichert, ein neues Wartungs-Abo schaltet die Funktion wieder ein + abgeschaltetBox = h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'daten-gespeichert' }, + h('span', null, `Deine Eingaben bleiben${tagText(feature?.loeschungAm) ? ` bis ${tagText(feature.loeschungAm)}` : ''} gespeichert. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein.`)) + } else if (daten && lage === 'bezahlen') { abgeschaltetBox = h('div', { class: 'tm-box tm-box-warn', role: 'note', 'data-hinweis': 'wieder-einschalten' }, h('span', null, `Deine Eingaben bleiben${tagText(feature?.loeschungAm) ? ` bis ${tagText(feature.loeschungAm)}` : ''} gespeichert. Für einmalig ${preis} schaltest du die Funktion wieder ein.`), behaltenKnopf(`Für ${preis} wieder einschalten`)) diff --git a/public/zeiten.js b/public/zeiten.js index d356d6f..d3af221 100644 --- a/public/zeiten.js +++ b/public/zeiten.js @@ -20,7 +20,7 @@ Kundenansicht (window.KbView.id() gesetzt): Einschalten, Bestellen und Behalten sind gesperrt, der Server antwortet dort ohnehin mit 403. */ import { fehlerKarte, h, infoKarte, request } from './termine-dom.js' -import { behaltenLage, renderZeitenBestellen, zeitenStatusKarte } from './zeiten-bestellen.js' +import { behaltenLage, renderZeitenBestellen, renderZeitenEinschalten, zeitenStatusKarte } from './zeiten-bestellen.js' import { renderEditor } from './zeiten-editor.js' // Auswahl bleibt beim Wechsel zwischen Funktionen und Zeiten erhalten @@ -52,14 +52,15 @@ function pickProjekt(projects) { * Darf der Kunde die Funktion jetzt behalten (30 €)? Nach dem erkannten Abo-Ende in Einrichtung/aktiv, abgeschaltet nur mit gespeicherten Eingaben, * und nur ohne Abo (behaltenLage 'bezahlen': inklusive false UND grund kein_abo); laeuft das Abo (nicht_aktiv, gekuendigt, inklusive), gibt es nichts zu bezahlen. */ -function behaltenMoeglich(projekt, angebot) { +function behaltenMoeglich(projekt, angebot, modell) { + if (modell === 'abo') return false // Abo-Modell: kein Behalten gegen Geld (Z2: Öffnungszeiten live läuft noch bis zur Frist) const feature = projekt?.features?.zeiten if (!feature || behaltenLage(angebot) !== 'bezahlen') return false if (feature.state === 'setup' || feature.state === 'active') return Boolean(feature.aboEndeAt) return feature.state === 'disabled' && feature.disabledReason === 'abo_ende' && projekt.zeitenLeer === false } -export async function initZeiten(root, { projectId } = {}) { +export async function initZeiten(root, { projectId, einschalten } = {}) { if (!root) return const rueckkehr = leseRueckkehr() const wunsch = rueckkehr.projekt || projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab @@ -71,7 +72,7 @@ export async function initZeiten(root, { projectId } = {}) { const projects = Array.isArray(antwort.projects) ? antwort.projects : [] const s = { root, projects, angebot: antwort.zeiten || null, features: antwort.features || {}, ustHinweis: typeof antwort.ustHinweis === 'string' ? antwort.ustHinweis : '', - projekt: null, lauf: 0, buehne: null, danke: '', editorAlt: null, + modell: antwort.modell, einschalten, projekt: null, lauf: 0, buehne: null, danke: '', editorAlt: null, } // der Editorplatz der bisherigen Ansicht (mit seinem Editor) wird in den neuen Aufbau mitgenommen, ausser nach der Rueckkehr von Stripe const alterEditor = !rueckkehr.bestellt && !rueckkehr.behalten && S?.root === root ? root.querySelector('.zt-editor') : null @@ -95,7 +96,7 @@ export async function initZeiten(root, { projectId } = {}) { const key = stand(s.projekt, s.angebot) const editorDa = root.dataset.zeitenKey === key && root.querySelector('.zt-editor') && S?.root === root if (editorDa && !rueckkehr.bestellt && !rueckkehr.behalten) { - if (wunschWeg === 'behalten' && behaltenMoeglich(S.projekt, S.angebot)) zeigeBehalten(S) + if (wunschWeg === 'behalten' && behaltenMoeglich(S.projekt, S.angebot, S.modell)) zeigeBehalten(S) return } S = s @@ -104,7 +105,7 @@ export async function initZeiten(root, { projectId } = {}) { : rueckkehr.behalten && (!rueckkehr.projekt || rueckkehr.projekt === s.projekt.projectId) ? 'behalten' : '' s.buehne = h('div', { id: 'zeiten-buehne' }) root.replaceChildren(...(projects.length > 1 ? [projektwahl(s)] : []), s.buehne) - zeichneProjekt(s, { weg: wunschWeg }) + zeichneProjekt(s, { weg: s.modell === 'abo' ? '' : wunschWeg }) } /** Alles, was die Statuskarte zeigt und der Editorplatz davon abhaengt (auch die Abo-Lage: Behalten gibt es nur ohne Abo); aendert es sich, wird neu aufgebaut. */ @@ -134,6 +135,7 @@ async function ladeListe(s) { const projects = Array.isArray(antwort.projects) ? antwort.projects : [] s.projects = projects s.angebot = antwort.zeiten || null + s.modell = antwort.modell s.features = antwort.features || {} s.ustHinweis = typeof antwort.ustHinweis === 'string' ? antwort.ustHinweis : '' s.projekt = projects.find((p) => p.projectId === s.projekt?.projectId) || s.projekt @@ -164,6 +166,7 @@ function baueCtx(s, projekt) { features: s.features, ustHinweis: s.ustHinweis, kundenansicht: kundenansicht(), + einschalten: s.einschalten, // Ablauf des Einschaltens im Abo-Modell (Init-Option der Hülle) neuZeichnen: (optionen) => neuZeichnen(s, optionen), fertig: ({ danke } = {}) => neuZeichnen(s, { danke }), } @@ -216,18 +219,22 @@ function zeichneProjekt(s, { weg = '', meldung = '', bestellformular = false } = } const statusKarte = (zusatz = {}) => zeitenStatusKarte( - { projekt, feature, angebot: s.angebot, kundenansicht: ctx.kundenansicht, danke, ...zusatz }, + { projekt, feature, angebot: s.angebot, kundenansicht: ctx.kundenansicht, danke, modell: s.modell, ...zusatz }, { behalten: () => zeigeBehalten(s), weiter: (fall) => zeichneProjekt(s, { bestellformular: fall?.bestellen === true, meldung: fall?.meldung || '' }), }) + if (zustand === 'off' && s.modell === 'abo') { + renderZeitenEinschalten(kopf, { ctx, projekt }) + return + } if (zustand === 'off' || (zustand === 'ordered' && bestellformular)) { renderZeitenBestellen(kopf, { ctx, angebot: s.angebot, projekt }) return } kopf.replaceChildren(statusKarte()) - if (weg === 'behalten' && behaltenMoeglich(projekt, s.angebot)) zeigeBehalten(s) + if (weg === 'behalten' && behaltenMoeglich(projekt, s.angebot, s.modell)) zeigeBehalten(s) } /** Behalten-Seite (30 €) statt der Statuskarte; der Editor darunter bleibt, nur versteckt, damit nichts verloren geht. „Zurück“ zeigt wieder die Karte. */ diff --git a/test/kundenansichtNeuModulBuchungen.test.js b/test/kundenansichtNeuModulBuchungen.test.js index 2d7d4f7..a240bca 100644 --- a/test/kundenansichtNeuModulBuchungen.test.js +++ b/test/kundenansichtNeuModulBuchungen.test.js @@ -78,3 +78,10 @@ test('Ohne modell (fehlt, „Abo“): Formular mit „199 € einmalig“, angen assert.equal(a.aufrufe.length, 0) } })) + +test('Abo, Kundenansicht der Mitarbeiter (window.KbView.id): der Ablauf bekommt viewAs true', mitDom(async (fx) => { + fx.window.KbView = { id: () => 'view1' } + const a = einschaltenAttrappe() + await oeffne(fx, { kopf: { modell: 'abo' }, anfrage: ZUGESAGT, einschalten: a.fn }) + assert.equal(a.aufrufe[0].viewAs, true) +})) diff --git a/test/kundenansichtNeuModulNews.test.js b/test/kundenansichtNeuModulNews.test.js index c15b66d..f57207a 100644 --- a/test/kundenansichtNeuModulNews.test.js +++ b/test/kundenansichtNeuModulNews.test.js @@ -81,3 +81,14 @@ test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', asy assert.equal(a.aufrufe[0].projectId, 'p2') assert.equal(root.dataset.wunschProjekt, undefined) }) + +test('Abo, Kundenansicht der Mitarbeiter (window.KbView.id): der Ablauf bekommt viewAs true', async () => { + globalThis.window.KbView = { id: () => 'view1' } + try { + const a = einschaltenAttrappe() + await lauf({ modell: 'abo', einschalten: a.fn }) + assert.equal(a.aufrufe[0].viewAs, true) + } finally { + delete globalThis.window.KbView + } +}) diff --git a/test/kundenansichtNeuModulShop.test.js b/test/kundenansichtNeuModulShop.test.js index bbf74e4..205b98a 100644 --- a/test/kundenansichtNeuModulShop.test.js +++ b/test/kundenansichtNeuModulShop.test.js @@ -107,3 +107,10 @@ test('Ohne modell (fehlt, „Abo“): Formular mit Unternehmer-Haken und 299 € assert.equal(a.aufrufe.length, 0) } })) + +test('Abo, Kundenansicht der Mitarbeiter (window.KbView.id): der Ablauf bekommt viewAs true', mitDom(async (fx) => { + fx.window.KbView = { id: () => 'view1' } + const a = einschaltenAttrappe() + await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'unternehmen' }, anfrage: ZUGESAGT, einschalten: a.fn }) + assert.equal(a.aufrufe[0].viewAs, true) +})) diff --git a/test/kundenansichtNeuModulZeiten.test.js b/test/kundenansichtNeuModulZeiten.test.js new file mode 100644 index 0000000..b53f92a --- /dev/null +++ b/test/kundenansichtNeuModulZeiten.test.js @@ -0,0 +1,109 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { h } from '../public/termine-dom.js' +import { initZeiten } from '../public/zeiten.js' +import { zeitenStatusKarte } from '../public/zeiten-bestellen.js' +import { KEIN_PREIS, einschaltenAttrappe, mitDom, routen } from './support/modulAbo.js' + +// Öffnungszeiten live im Abo-Modell (P4 Task 7): Einschalten über den Ablauf von P3, kein Behalten-Weg, Abo-Ende und Abschaltung ohne Preis +// (Texte Z2, Z3, E5). Ohne modell === 'abo' bleiben Bestellseite, „Für 30 € behalten“ und „Bestellt am“. + +const EDITOR = { state: 'active', daten: null, version: 0, kundenansicht: false, vorschau: null, feiertage: [], googleText: '' } // Antwort von GET /api/zeiten/p1 +const feature = (state, extra = {}) => ({ + state, siteKey: null, ticketWoid: null, orderedAt: null, activatedAt: null, invoiceUrl: null, disabledReason: null, disabledAt: null, loeschungAm: null, + unlockedBy: state === 'off' ? null : 'abo', aboEndeAt: null, einbau: null, ...extra, +}) +const liste = (modell, state = 'off', extra = {}, angebot = {}) => ({ + body: { + ...(modell ? { modell } : {}), + projects: [{ projectId: 'p1', projectName: 'Website p1', liveUrl: '', features: { news: { state: 'off' }, zeiten: feature(state, extra) }, zeitenStatus: null, zeitenLeer: false }], + features: { + zeiten: { + priceLabel: '30 €', + consent: { version: 'v', knopf: 'Zahlungspflichtig bestellen · 30 €', items: [] }, + leistungsbeschreibung: { version: '2026-10-05', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' }, + }, + }, + ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG', + zeiten: { + verfuegbar: true, + inklusive: true, + grund: null, + preis: '30 €', + einschalten: { consent: { version: 'v', knopf: 'Einschalten', items: [] }, leistungsbeschreibung: { version: '2026-10-05', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } }, + belehrung: 'Belehrung', + ...angebot, + }, + }, +}) +async function oeffne(fx, antwort, { einschalten, dataset = {} } = {}) { + routen(fx, { 'GET /api/funktionen': antwort, 'GET /api/zeiten/p1': { body: EDITOR } }) + const root = h('div') + document.body.append(root) + Object.assign(root.dataset, dataset) + await initZeiten(root, { projectId: 'p1', einschalten }) + await fx.ruhe() + return root +} +const ALT_ENDE = { aboEndeAt: '2026-09-01T00:00:00.000Z' } +const KEIN_ABO = { inklusive: false, grund: 'kein_abo' } + +test('Abo, off: Einschalt-Seite mit key zeiten, ohne Haken, Preis und Behalten; nach onFertig die Danke-Zeile', mitDom(async (fx) => { + const a = einschaltenAttrappe() + let n = 0 + const root = await oeffne(fx, () => (n === 0 ? liste('abo') : liste('abo', 'setup')), { einschalten: a.fn }) + assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId, a.aufrufe[0].viewAs], ['zeiten', 'p1', false]) + assert.equal(fx.alle(root, 'input[type="checkbox"]').length, 0) + assert.ok(fx.finde(root, '[data-aktion="behalten"], [data-aktion="bestellen"], [data-aktion="einschalten"]') === null) + assert.doesNotMatch(fx.text(root), KEIN_PREIS) + n = 1 + a.fertig(0) + await fx.ruhe() + assert.match(fx.text(fx.finde(root, '.zt-status')), /Öffnungszeiten live ist eingeschaltet\./) +})) + +test('Abo, off ohne Funktion einschalten: Fehlerkarte E4', mitDom(async (fx) => { + const root = await oeffne(fx, liste('abo')) + assert.match(fx.text(root), /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./) +})) + +test('Abo: Abo-Ende in active nennt die Frist (Z2) und „Eingeschaltet am“, ohne Preis und ohne Behalten-Knopf', mitDom(async (fx) => { + const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', orderedAt: '2026-08-01T10:00:00.000Z', ...ALT_ENDE }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {}) + assert.match(fx.text(karte), /Dein Wartungs-Abo ist beendet\. Öffnungszeiten live läuft noch bis 01\.10\.2026 und schaltet sich dann ab\./) + assert.match(fx.text(karte), /Eingeschaltet am\s*01\.08\.2026/) + assert.ok(fx.finde(karte, '[data-aktion="behalten"]') === null) + assert.doesNotMatch(fx.text(karte), KEIN_PREIS) +})) + +test('Abo: abgeschaltet mit Daten (Z3), ohne Behalten-Knopf', mitDom(async (fx) => { + const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'disabled', disabledReason: 'abo_ende', loeschungAm: '2099-12-31' }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {}) + assert.match(fx.text(karte), /Deine Eingaben bleiben bis 31\.12\.2099 gespeichert\. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein\./) + assert.ok(fx.finde(karte, '[data-aktion="behalten"]') === null) + assert.doesNotMatch(fx.text(karte), KEIN_PREIS) + const ohneDatum = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'disabled', disabledReason: 'abo_ende' }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {}) + assert.match(fx.text(ohneDatum), /Deine Eingaben bleiben gespeichert\. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein\./) +})) + +test('wunschWeg „behalten“: im Abo-Modell ohne Wirkung, ohne modell die Behalten-Seite und der Knopf „Für 30 € behalten“', mitDom(async (fx) => { + const wunsch = { dataset: { wunschWeg: 'behalten' } } + const abo = await oeffne(fx, liste('abo', 'active', ALT_ENDE, KEIN_ABO), wunsch) + assert.ok(fx.finde(abo, '[data-variante="behalten"]') === null) + assert.ok(fx.finde(abo, '[data-aktion="behalten"]') === null) + assert.ok(fx.finde(abo, '.zt-status')) + assert.equal(abo.dataset.wunschWeg, undefined) + const alt = await oeffne(fx, liste(undefined, 'active', ALT_ENDE, KEIN_ABO), wunsch) + assert.ok(fx.finde(alt, '[data-variante="behalten"]')) + const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', ...ALT_ENDE }, angebot: { ...KEIN_ABO, preis: '30 €' } }, {}) + assert.equal(fx.text(fx.finde(karte, '[data-aktion="behalten"]')), 'Für 30 € behalten') +})) + +test('Ohne modell (fehlt, „Abo“, „abo “): Bestellseite wie bisher, „Bestellt am“, der Ablauf wird nie gerufen', mitDom(async (fx) => { + for (const modell of [undefined, 'Abo', 'abo ']) { + const a = einschaltenAttrappe() + const bestellen = await oeffne(fx, liste(modell, 'off', {}, KEIN_ABO), { einschalten: a.fn }) + assert.ok(fx.finde(bestellen, '[data-aktion="bestellen"]'), String(modell)) + assert.equal(a.aufrufe.length, 0) + const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', orderedAt: '2026-08-01T10:00:00.000Z' }, angebot: { inklusive: true }, modell }, {}) + assert.match(fx.text(karte), /Bestellt am\s*01\.08\.2026/) + } +}))