425 lines
27 KiB
JavaScript
425 lines
27 KiB
JavaScript
/* 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, 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, renderEinschaltSeite, 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
|
||
}
|
||
|
||
/* ----------------------------------------------------- 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 = {
|
||
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, 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 €'
|
||
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(abo ? 'Eingeschaltet am' : '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 (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`))
|
||
} 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 (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`))
|
||
} 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' })
|
||
}
|