Files
Webklar-Kundenbereich/public/zeiten-bestellen.js
T
ClaudeandClaude Opus 5.5 c7cd436ec2 Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den
Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der
Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name.
Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:00:17 +00:00

398 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Oeffnungszeiten live im Kundenbereich: Bestell-, Einschalt- und Behalten-Seite und die Statuskarte (ES-Modul, wird relativ von zeiten.js
geladen; Spec 3.1 bis 3.3, Server: server/routes/funktionen.js). Alles, was hier steht, kommt vom Server: Preis, Zustimmungen, Knopftexte
und Belehrung aus GET /api/funktionen (zeiten = Angebot, features.zeiten = Register-Ansicht der bezahlten Zustimmung), nie aus einer Kopie
im Browser. Fremdtext kommt nur ueber textContent in die Seite (h aus termine-dom.js), nie ueber innerHTML.
renderZeitenBestellen(root, { ctx, angebot, projekt, modus }) zeichnet je nach Abo-Lage (angebot.inklusive, Ruling R20):
true Einschalten: Wartungs-Abo laeuft, ein Haken (Leistungsbeschreibung), Knopf "Einschalten", kein Preis und keine
Belehrung (es gibt kein Widerrufsverlangen); POST /api/funktionen/zeiten/einschalten
false Bestellen: 30 € einmalig, Preis, Widerrufsbelehrung samt Muster-Formular als Text vor dem Knopf (Ruling R32:
exakt der Text, den der Server im Nachweis speichert, angebot.belehrung), zwei Haken, Knopf aus dem Register;
POST /api/funktionen/zeiten/checkout, dann Weiterleitung zu Stripe
false, grund nicht_aktiv Hinweis "einschaltbar, sobald dein Abo aktiv ist", kein Knopf
null "Gerade nicht prüfbar – bitte später erneut" mit Knopf "Noch einmal versuchen"
modus 'behalten' (nach dem Ende des Wartungs-Abos, Spec 3.3): dieselbe bezahlte Seite, POST /api/funktionen/zeiten/behalten.
Fehlt die Belehrung oder die Zustimmung, bestellt die Seite nie (der Nachweis wuerde einen Text behaupten, den der Kunde nicht gesehen hat).
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). */
import { bestellKoerper, sicherLink } from './funktion-bestellen.js'
import { fehlerKarte, h, hakenFeld, request } from './termine-dom.js'
const WEGE = {
einschalten: '/api/funktionen/zeiten/einschalten',
bestellen: '/api/funktionen/zeiten/checkout',
behalten: '/api/funktionen/zeiten/behalten',
}
const KUNDENANSICHT = {
einschalten: 'In der Kundenansicht kann nicht eingeschaltet werden.',
bestellen: 'In der Kundenansicht kann nicht bestellt werden.',
behalten: 'In der Kundenansicht kann nicht bestellt werden.',
}
const ZAHLUNGSSEITE = 'Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuch es noch einmal oder schreib uns im Chat.'
const NICHT_GELADEN = 'Die Seite konnte gerade nicht geladen werden. Bitte lade sie neu.'
const NICHT_PRUEFBAR = 'Gerade nicht prüfbar – bitte später erneut.'
const ABGESCHALTET = 'Öffnungszeiten live wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du die Funktion wieder möchtest.'
const FEHLER = {
einschalten: 'Öffnungszeiten live konnte gerade nicht eingeschaltet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.',
bestellen: 'Die Bestellung konnte gerade nicht gestartet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.',
behalten: 'Das Behalten konnte gerade nicht gestartet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.',
}
const text = (wert) => (typeof wert === 'string' ? wert : '')
const istConsent = (wert) => wert !== null && typeof wert === 'object' && typeof wert.version === 'string' && typeof wert.knopf === 'string' && Array.isArray(wert.items)
const chatKnopf = () => h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Frage dazu? Chat öffnen')
/**
* Wo steht das Wartungs-Abo, wenn das Abo-Ende erkannt ist (Behalten)? 'bezahlen': kein Abo (inklusive false UND grund kein_abo), nur dann gibt es
* Behalten gegen 30 €. 'abo': das Abo laeuft (grund nicht_aktiv = Probezeit/Zahlung offen, gekuendigt = Kuendigung vorgemerkt; der Server antwortet
* beim Behalten dort mit 409 „Abo läuft wieder“): nie ein Bezahlknopf (Z12, R20, R25). 'inklusive': enthalten (inklusive true). 'unklar': Abo nicht
* lesbar oder unbekannter Grund (fail-closed, kein Bezahlknopf auf Verdacht). Doppelt zu zeitenBehaltenLage in funktionen.js (die Kachel importiert nichts).
*/
export function behaltenLage(angebot) {
if (angebot?.inklusive === true) return 'inklusive'
if (angebot?.inklusive !== false) return 'unklar'
if (angebot.grund === 'kein_abo') return 'bezahlen'
return angebot.grund === 'nicht_aktiv' || angebot.grund === 'gekuendigt' ? 'abo' : 'unklar'
}
/** Adresse einer Unterlage: https oder /rechtliches/ (sicherLink) oder eine Datei unter /vertragsunterlagen/ dieses Portals, sonst ''. */
function unterlagenLink(url) {
const sicher = sicherLink(url)
if (sicher) return sicher
return typeof url === 'string' && /^\/vertragsunterlagen\/[A-Za-z0-9._-]+\.(?:html|pdf)$/.test(url) && !url.includes('..') ? url : ''
}
/** Zur Zahlungsseite darf nur eine https-Adresse fuehren, nie ein /rechtliches/-Pfad. */
function zahlungsUrl(url) {
const sicher = sicherLink(url)
return sicher.startsWith('https://') ? sicher : ''
}
/** Datum in Berlin als TT.MM.JJJJ, bei fehlendem oder ungueltigem Wert ''. */
function datumTag(wert) {
const tag = new Date(wert)
if (!wert || Number.isNaN(tag.getTime())) return ''
return tag.toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' })
}
/**
* Frist zum Behalten: Berliner Kalendertag des erkannten Abo-Endes plus 30 Tage (wie fristTag in server/services/zeiten/aboAbgleich.js und die Mail
* K29) als TT.MM.JJJJ; ein unlesbares Datum gibt '' (kein erfundenes Datum). Doppelt zu zeitenFrist in funktionen.js (die Kachel importiert nichts).
*/
export function behaltenFrist(aboEndeAt) {
const ms = typeof aboEndeAt === 'string' ? Date.parse(aboEndeAt) : NaN
if (Number.isNaN(ms)) return ''
const [jahr, monat, tag] = new Date(ms).toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' }).split('-').map(Number)
return new Date(Date.UTC(jahr, monat - 1, tag + 30)).toLocaleDateString('de-DE', { timeZone: 'UTC', day: '2-digit', month: '2-digit', year: 'numeric' })
}
/** 'JJJJ-MM-TT' als TT.MM.JJJJ, sonst ''. */
export function tagText(iso) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text(iso))
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
}
/* ---------------------------------------------------------------- Bestellseite --- */
/**
* Bestell-, Einschalt- oder Behalten-Seite in root zeichnen und das Element zurueckgeben (siehe Kopf der Datei).
* Der Knopf ist gesperrt, solange ein Haken fehlt, ein Klick laeuft oder die Kundenansicht aktiv ist.
*/
export function renderZeitenBestellen(root, { ctx = {}, angebot = {}, projekt = {}, modus = 'bestellen' } = {}) {
const zeige = (element) => {
root.replaceChildren(element)
return element
}
const projectId = text(ctx.projectId) || text(projekt.projectId)
const zurueck = typeof ctx.zurueck === 'function' && modus === 'behalten'
? h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueck', onclick: () => ctx.zurueck() }, 'Zurück')
: null
const nochmal = typeof ctx.neuZeichnen === 'function' ? () => ctx.neuZeichnen({}).catch(() => {}) : undefined
// Welche Seite? (Abo-Lage laut Server, Ruling R20)
let variante
if (angebot.inklusive === null || angebot.inklusive === undefined) {
return zeige(h('div', { class: 'kb-panel tm-info zt-hinweis', 'data-hinweis': 'nicht-pruefbar', role: 'alert' }, h('p', null, NICHT_PRUEFBAR), nochmal ? h('button', { type: 'button', class: 'kb-btn-outline', onclick: nochmal }, 'Noch einmal versuchen') : null, zurueck))
}
if (modus === 'behalten' && behaltenLage(angebot) !== 'bezahlen') {
// das Abo laeuft (wieder) oder ist nicht sicher lesbar: nichts zu bezahlen, nie ein Bezahlknopf auf Verdacht
if (behaltenLage(angebot) === 'unklar') {
return zeige(h('div', { class: 'kb-panel tm-info zt-hinweis', 'data-hinweis': 'nicht-pruefbar', role: 'alert' }, h('p', null, NICHT_PRUEFBAR), nochmal ? h('button', { type: 'button', class: 'kb-btn-outline', onclick: nochmal }, 'Noch einmal versuchen') : null, zurueck))
}
return zeige(h('div', { class: 'kb-panel tm-info zt-hinweis', 'data-hinweis': 'abo-laeuft' }, h('h3', null, 'Dein Wartungs-Abo läuft wieder'), h('p', null, 'Öffnungszeiten live ist darin enthalten – du musst nichts bezahlen.'), zurueck))
}
if (modus === 'behalten') variante = 'behalten'
else if (angebot.inklusive === true) variante = 'einschalten'
else if (angebot.grund === 'nicht_aktiv') {
return zeige(h('div', { class: 'kb-panel tm-info zt-hinweis', 'data-hinweis': 'nicht-aktiv' }, h('h3', null, 'Im Wartungs-Abo enthalten'), h('p', null, 'Öffnungszeiten live ist in deinem Wartungs-Abo enthalten – einschaltbar, sobald dein Abo aktiv ist.')))
} else variante = 'bestellen'
const bezahlt = variante !== 'einschalten'
const consent = bezahlt ? ctx.features?.zeiten?.consent : angebot.einschalten?.consent
const belehrung = text(angebot.belehrung)
// Ohne Zustimmung oder (bezahlt) ohne Belehrung wird nie bestellt: der Nachweis wuerde einen Text behaupten, den der Kunde nicht gesehen hat
if (!istConsent(consent) || (bezahlt && belehrung.trim() === '')) return zeige(fehlerKarte(NICHT_GELADEN))
const eintraege = consent.items.filter((eintrag) => eintrag !== null && typeof eintrag === 'object')
const hakenEintraege = eintraege.filter((eintrag) => eintrag.haken === true)
const kundenansicht = ctx.kundenansicht === true
const preis = text(angebot.preis) || text(ctx.features?.zeiten?.priceLabel) || '30 €'
const einrichtung = text(ctx.features?.zeiten?.setupEstimate)
const gewaehlt = new Set()
let laeuft = false
const einleitung = {
einschalten: 'Öffnungszeiten live ist in deinem Wartungs-Abo ohne Aufpreis enthalten. Du schaltest die Funktion ein, danach trägst du deine Zeiten ein.',
bestellen: 'Deine Website zeigt live, ob du gerade geöffnet hast. Die Einrichtung kostet einmalig – danach läuft die Funktion ohne laufende Kosten.',
behalten: projekt.features?.zeiten?.state === 'disabled'
? `Öffnungszeiten live ist abgeschaltet, deine Eingaben sind noch gespeichert. Für einmalig ${preis} schaltest du die Funktion wieder ein.`
: `Dein Wartungs-Abo ist beendet. Für einmalig ${preis} behältst du Öffnungszeiten live – ohne neuen Einbau, an deiner Website ändert sich nichts.`,
}[variante]
// Preis: nur auf dem bezahlten Weg; die Einrichtungsdauer nur bei einer Neubestellung (beim Behalten gibt es keinen neuen Einbau)
const preisBlock = bezahlt
? h('div', { class: 'tm-bestellen-preis' },
h('h4', null, variante === 'bestellen' ? 'Preis und Einrichtung' : 'Preis'),
h('p', { class: 'tm-bestellen-betrag' }, h('strong', null, preis), ` einmalig${text(ctx.ustHinweis) ? `, ${ctx.ustHinweis}` : ''}`),
variante === 'bestellen' && einrichtung ? h('p', null, `Einrichtung: ${einrichtung}`) : null)
: null
// Widerrufsbelehrung samt Muster-Formular: ein Textbereich zum Scrollen, nie eingeklappt, genau der Text des Nachweises (R32)
const belehrungBlock = bezahlt
? h('div', { class: 'tm-bestellen-belehrung' },
h('h4', null, 'Widerrufsbelehrung'),
h('p', { class: 'tm-hinweis' }, 'Lies die Belehrung bitte ganz. Das Muster-Widerrufsformular gehört dazu.'),
h('div', { class: 'tm-rechtstext', tabindex: '0', role: 'region', 'aria-label': 'Widerrufsbelehrung mit Muster-Widerrufsformular' }, belehrung))
: null
const fassung = bezahlt ? ctx.features?.zeiten?.leistungsbeschreibung : angebot.einschalten?.leistungsbeschreibung
const link = unterlagenLink(fassung?.url)
const unterlagenBlock = link === '' ? null : h('div', { class: 'tm-bestellen-unterlagen' },
h('h4', null, 'Unterlagen'),
h('ul', { class: 'tm-bestellen-dokumente' }, h('li', null, h('a', { href: link, target: '_blank', rel: 'noopener noreferrer' }, 'Leistungsbeschreibung „Öffnungszeiten live“'))))
// Zustimmungen in Registerreihenfolge: Eintraege mit haken: true sind Haken, die anderen reiner Text
const knopf = h('button', { type: 'button', class: 'kb-btn-primary tm-bestellen-knopf', 'data-aktion': variante, disabled: true, onclick: () => absenden() }, consent.knopf)
const meldungEl = h('p', { class: 'tm-fehler', role: 'alert', 'data-meldung': 'bestellen' })
const hinweisEl = h('p', { class: 'tm-hinweis', 'data-hinweis': 'bestellen' })
const hakenZeile = (eintrag) => {
const feld = h('input', { type: 'checkbox', 'data-haken': eintrag.id })
feld.addEventListener('change', () => {
if (feld.checked) gewaehlt.add(eintrag.id)
else gewaehlt.delete(eintrag.id)
aktualisiere()
})
return hakenFeld(text(eintrag.id), feld, text(eintrag.text))
}
const zustimmungBlock = h('div', { class: 'tm-bestellen-zustimmung' },
h('h4', null, 'Deine Zustimmung'),
h('div', { class: 'tm-bestellen-haken' }, eintraege.map((eintrag) => (eintrag.haken === true ? hakenZeile(eintrag) : h('p', { class: 'tm-bestellen-text' }, text(eintrag.text))))))
const titel = { einschalten: 'Öffnungszeiten live einschalten', bestellen: 'Öffnungszeiten live bestellen', behalten: 'Öffnungszeiten live behalten' }[variante]
const seite = h('section', { class: 'kb-panel tm-bestellen zt-bestellen', 'data-variante': variante },
h('h3', null, titel),
h('p', { class: 'tm-abschnitt-text tm-hinweis' }, einleitung),
preisBlock,
belehrungBlock,
unterlagenBlock,
zustimmungBlock,
h('div', { class: 'tm-bestellen-aktion' }, meldungEl, knopf, hinweisEl, zurueck))
zeige(seite)
function meldung(nachricht) {
meldungEl.textContent = nachricht
}
function aktualisiere() {
const hakenFehlen = hakenEintraege.some((eintrag) => !gewaehlt.has(eintrag.id))
knopf.disabled = laeuft || hakenFehlen || kundenansicht
if (kundenansicht) hinweisEl.textContent = KUNDENANSICHT[variante]
else hinweisEl.textContent = hakenFehlen ? (variante === 'einschalten' ? 'Setze den Haken, um einzuschalten.' : 'Setze alle Haken, um zu bestellen.') : ''
}
async function absenden() {
if (knopf.disabled) return
laeuft = true
aktualisiere()
meldung('')
let antwort
try {
antwort = await request(WEGE[variante], { method: 'POST', body: bestellKoerper({ projectId, bestellung: { consent }, gewaehlt }) })
} catch (err) {
// 409: der Stand hat sich geaendert (Abo, schon bestellt, neue Fassung): die Ansicht neu nach dem Server zeichnen, mit seiner Meldung
if (err?.status === 409 && typeof ctx.neuZeichnen === 'function') {
try {
await ctx.neuZeichnen({ meldung: err.message })
return
} catch {
// die Meldung steht dann am Knopf
}
}
meldung(err?.message || FEHLER[variante])
laeuft = false
aktualisiere()
return
}
if (variante === 'einschalten') {
// eingeschaltet ist eingeschaltet, auch wenn das Neuladen der Ansicht danach scheitert (ein zweiter Klick waere harmlos, ist aber unnoetig)
try {
if (typeof ctx.fertig === 'function') await ctx.fertig({ danke: 'eingeschaltet', antwort })
else meldung('Öffnungszeiten live ist eingeschaltet.')
} catch {
meldung('Öffnungszeiten live ist eingeschaltet. Lade die Seite neu, dann siehst du den aktuellen Stand.')
}
return
}
const ziel = zahlungsUrl(antwort?.url)
if (ziel) {
window.location.href = ziel // die Seite wird verlassen, der Knopf bleibt gesperrt
return
}
meldung(ZAHLUNGSSEITE)
laeuft = false
aktualisiere()
}
aktualisiere()
return seite
}
/* ---------------------------------------------------------------- Statuskarte --- */
const PILLE = { ordered: ['Zahlung offen', 'wait'], setup: ['In Einrichtung', 'wait'], active: ['Aktiv', 'ok'], disabled: ['Abgeschaltet', 'off'] }
const DANKE = {
bestellt: 'Danke! Sobald die Zahlung da ist, legen wir los.',
bestelltBezahlt: 'Danke! Deine Zahlung ist bei uns angekommen.',
behalten: 'Danke! Sobald die Zahlung da ist, behältst du Öffnungszeiten live. An deiner Website ändert sich nichts.',
behaltenBezahlt: 'Danke! Du behältst Öffnungszeiten live.',
eingeschaltet: 'Öffnungszeiten live ist eingeschaltet.',
}
/** Danke-Satz zur Rueckkehr: 'bestellt' | 'behalten' | 'eingeschaltet', je nachdem, ob die Zahlung schon gebucht ist; sonst ''. */
function dankeText(danke, feature) {
if (danke === 'bestellt') return feature?.state === 'ordered' ? DANKE.bestellt : DANKE.bestelltBezahlt
if (danke === 'behalten') return feature?.aboEndeAt ? DANKE.behalten : DANKE.behaltenBezahlt
return danke === 'eingeschaltet' ? DANKE.eingeschaltet : ''
}
/**
* Karte fuer ordered („Zahlung offen“), setup („In Einrichtung“), active und disabled.
* projekt = Eintrag aus GET /api/funktionen (projectId, zeitenLeer), feature = projekt.features.zeiten (state, einbau, ticketWoid, orderedAt,
* invoiceUrl, aboEndeAt, disabledReason, loeschungAm), angebot = zeiten (inklusive, preis), kundenansicht = Sitzung in der Kundenansicht,
* danke = 'bestellt' | 'behalten' | 'eingeschaltet' (Rueckkehr von Stripe oder Einschalten), meldung = Text am Knopf.
* behalten() oeffnet die Behalten-Seite (Knopf „Für 30 € behalten“ nur ohne Abo: behaltenLage 'bezahlen', also inklusive false UND grund kein_abo; laeuft das Abo, steht nur der Hinweis);
* 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 } = {}) {
const state = text(feature?.state) || 'off'
const [pilleText, pilleArt] = PILLE[state] || PILLE.setup
const preis = text(angebot?.preis) || '30 €'
const leer = projekt?.zeitenLeer === true
const ticket = feature?.ticketWoid
const hatTicket = ticket !== null && ticket !== undefined && String(ticket) !== ''
const bestelltAm = datumTag(feature?.orderedAt)
const rechnung = sicherLink(feature?.invoiceUrl)
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,
].filter(Boolean)
const aboEnde = feature?.disabledReason === 'abo_ende'
const daten = state === 'disabled' && aboEnde && projekt?.zeitenLeer === false && !(text(feature?.loeschungAm) !== '' && feature.loeschungAm < heuteBerlin())
const frist = (state === 'setup' || state === 'active') && feature?.aboEndeAt ? behaltenFrist(feature.aboEndeAt) : ''
const titel = {
ordered: 'Zahlung wird bestätigt …',
setup: 'Wir richten Öffnungszeiten live ein',
active: 'Öffnungszeiten live ist aktiv',
disabled: 'Öffnungszeiten live ist abgeschaltet',
}[state] || 'Öffnungszeiten live'
const absatz = {
ordered: 'Sobald deine Zahlung bei uns ankommt, bauen wir Öffnungszeiten live ein.',
setup: feature?.einbau === 'angestossen'
? 'Hinweis- und Kontakt-Leiste erscheinen nach dem nächsten Veröffentlichen deiner Website (meist in wenigen Minuten).'
: 'Unser Team baut die Funktion ein.',
active: 'Deine Öffnungszeiten stehen live auf deiner Website. Was du hier änderst, übernimmt die Website automatisch.',
disabled: aboEnde ? 'Öffnungszeiten live ist abgeschaltet, weil dein Wartungs-Abo beendet ist.' : ABGESCHALTET,
}[state] || ''
const meldungEl = h('p', { class: 'tm-fehler', role: 'alert', 'data-meldung': 'karte' }, text(meldung) || null)
const sperre = kundenansicht === true
// Behalten (30 €): in Einrichtung/aktiv nach dem erkannten Abo-Ende, abgeschaltet solange die Eingaben noch da sind
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') {
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`))
} 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.'))
}
let abgeschaltetBox = null
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`))
} else if (daten && (lage === 'inklusive' || lage === 'abo')) {
abgeschaltetBox = h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'abo-laeuft' }, h('span', null, 'Dein Wartungs-Abo läuft wieder – wir schalten Öffnungszeiten live automatisch wieder ein (meist innerhalb eines Tages).'))
} else if (daten) {
abgeschaltetBox = h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'nicht-pruefbar' }, h('span', null, NICHT_PRUEFBAR))
}
// Zahlung fortsetzen (nur ordered): dieselbe Zahlungsseite, ohne neue Zustimmung; gibt es keine mehr, zeigt die Ansicht wieder die Bestellseite
const fortsetzenKnopf = state === 'ordered'
? h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'fortsetzen', disabled: sperre, onclick: () => fortsetzen() }, 'Zahlung fortsetzen')
: null
async function fortsetzen() {
if (fortsetzenKnopf.disabled) return
fortsetzenKnopf.disabled = true
meldungEl.textContent = ''
try {
const antwort = await request(WEGE.bestellen, { method: 'POST', body: { projectId: projekt?.projectId, fortsetzen: true } })
const ziel = zahlungsUrl(antwort?.url)
if (ziel) {
window.location.href = ziel
return
}
meldungEl.textContent = ZAHLUNGSSEITE
} catch (err) {
if (typeof weiter === 'function' && err?.code === 'zustimmung_fehlt') {
weiter({ bestellen: true, meldung: err.message })
return
}
meldungEl.textContent = err?.message || ZAHLUNGSSEITE
}
fortsetzenKnopf.disabled = false
}
const dankeSatz = dankeText(danke, feature)
return h('section', { class: 'kb-panel tm-statuskarte zt-status', 'data-state': state },
dankeSatz ? h('p', { class: 'tm-danke', role: 'status', 'data-danke': danke }, dankeSatz) : null,
h('span', { class: `tm-pille tm-pille-${pilleArt}` }, pilleText),
h('h3', null, titel),
absatz ? h('p', null, absatz) : null,
state === 'setup' && leer ? h('div', { class: 'tm-box tm-box-warn', role: 'note', 'data-hinweis': 'zeiten-eintragen' }, h('span', null, 'Trag jetzt deine Zeiten ein – erst dann kann das Team die Tabelle einbauen.')) : null,
state === 'active' && leer ? h('div', { class: 'tm-box tm-box-info', role: 'note', 'data-hinweis': 'zeiten-eintragen' }, h('span', null, 'Trag jetzt deine Zeiten ein.')) : null,
fakten.length > 0 ? h('dl', { class: 'tm-fakten' }, fakten) : null,
state === 'ordered' ? h('p', { class: 'tm-hinweis' }, 'Zahlst du per SEPA-Lastschrift, kann es einige Tage dauern, bis die Zahlung bei uns ankommt.') : null,
behaltenBox,
abgeschaltetBox,
meldungEl,
sperre && (fortsetzenKnopf || behaltenBox || abgeschaltetBox) ? h('p', { class: 'tm-hinweis' }, KUNDENANSICHT.bestellen) : null,
h('div', { class: 'tm-aktionen' },
fortsetzenKnopf,
state === 'setup' && rechnung ? h('a', { class: 'kb-btn-outline', href: rechnung, target: '_blank', rel: 'noopener noreferrer' }, 'Rechnung ansehen') : null,
chatKnopf()))
}
/** Heutiger Berliner Kalendertag 'JJJJ-MM-TT' (zum Vergleich mit loeschungAm). */
function heuteBerlin() {
return new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' })
}