255 lines
14 KiB
JavaScript
255 lines
14 KiB
JavaScript
/* Oeffnungszeiten live im Kundenbereich: die Ansicht #zeiten (ES-Modul, app.js laedt es beim Oeffnen der Ansicht; Styles: zeiten.css, dazu die
|
|
gemeinsamen tm-Klassen aus termine.css). Server: server/routes/funktionen.js (Liste, Einschalten, Bestellen, Behalten) und server/routes/zeiten.js
|
|
(Editor, Aufgaben 29 und 30).
|
|
initZeiten(rootEl, { projectId }): Projektauswahl (bei mehr als einer Website) und je nach Zustand der Funktion (features.zeiten.state aus
|
|
GET /api/funktionen):
|
|
off Einschalten (Wartungs-Abo laeuft), Bestellen (30 €) oder ein Hinweis (Abo noch nicht aktiv, nicht lesbar): renderZeitenBestellen
|
|
ordered Statuskarte „Zahlung wird bestätigt …“ (Zahlung fortsetzen)
|
|
setup Statuskarte (Einbau, Ticket, „Trag jetzt deine Zeiten ein“) und darunter der Platz des Editors
|
|
active Statuskarte und der Platz des Editors
|
|
disabled Statuskarte mit dem Grund; „Für 30 € wieder einschalten“, solange die Eingaben da sind; der Platz des Editors zum Lesen
|
|
Das Abo-Ende (aboEndeAt) zeigt in setup und active den Hinweis „behalte die Funktion bis <Datum> für einmalig 30 €“ mit dem Knopf zur Behalten-Seite.
|
|
Rueckkehr von Stripe: #zeiten?bestellt=1&projekt=<id> (Bestellung) und #zeiten?behalten=1&projekt=<id> (Behalten) waehlen das Projekt und zeigen
|
|
„Danke!“; #zeiten?projekt=<id> (Abbruch) waehlt nur das Projekt. Beides wird einmal gelesen und aus der Adresse genommen. Die Kachel in Funktionen
|
|
legt Projekt und Weg in den Behaelter ab (dataset.wunschProjekt, dataset.wunschWeg = 'behalten').
|
|
Der Editor haengt in .zt-editor ein (renderEditor(box, ctx) aus zeiten-editor.js). Ruft app.js initZeiten bei jedem Wechsel in die
|
|
Ansicht, baut sich nichts neu auf, solange Projekt und Stand gleich sind und der Editorplatz steht (Muster initNews): Eingaben im Editor bleiben stehen.
|
|
Aendert sich der Stand (zeitenLeer nach dem ersten Speichern, setup → active, die Abo-Lage), wird nur der Kopf (Statuskarte) neu gezeichnet: der Editorplatz
|
|
bleibt mit seinem Editor und den ungespeicherten Eingaben stehen, solange Website und Schreibrecht (abgeschaltet = nur lesen) gleich sind (data-projekt,
|
|
data-state); eine andere Website oder der Wechsel in „nur lesen“ bekommt einen frischen Editor. Die Rueckkehr von Stripe zeichnet immer ganz neu.
|
|
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, renderZeitenEinschalten, zeitenStatusKarte } from './zeiten-bestellen.js'
|
|
import { renderEditor } from './zeiten-editor.js'
|
|
|
|
// Auswahl bleibt beim Wechsel zwischen Funktionen und Zeiten erhalten
|
|
const auswahl = { projectId: '' }
|
|
let S = null // aktuelle Sitzung der Ansicht; eine alte Antwort darf eine neuere nicht ueberschreiben (S !== s)
|
|
|
|
const zustandVon = (projekt) => projekt?.features?.zeiten?.state || 'off'
|
|
const istBestellt = (zustand) => zustand === 'ordered' || zustand === 'setup'
|
|
const kundenansicht = () => Boolean(window.KbView?.id?.())
|
|
|
|
/** Rueckkehr von Stripe aus der Adresse lesen und die Adresse bereinigen (wie initNews/initTermine). */
|
|
function leseRueckkehr() {
|
|
const hash = window.location.hash || ''
|
|
if (!hash.startsWith('#zeiten?')) return { bestellt: false, behalten: false, projekt: '' }
|
|
const query = new URLSearchParams(hash.slice(hash.indexOf('?') + 1))
|
|
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#zeiten`)
|
|
return { bestellt: query.get('bestellt') === '1', behalten: query.get('behalten') === '1', projekt: query.get('projekt') || '' }
|
|
}
|
|
|
|
// Vorauswahl wie bei der Kachel: aktiv, dann bestellt/in Einrichtung, dann abgeschaltet, sonst die erste
|
|
function pickProjekt(projects) {
|
|
return projects.find((p) => zustandVon(p) === 'active')
|
|
|| projects.find((p) => istBestellt(zustandVon(p)))
|
|
|| projects.find((p) => zustandVon(p) === 'disabled')
|
|
|| projects[0]
|
|
}
|
|
|
|
/**
|
|
* 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, 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, einschalten } = {}) {
|
|
if (!root) return
|
|
const rueckkehr = leseRueckkehr()
|
|
const wunsch = rueckkehr.projekt || projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
|
const wunschWeg = root.dataset.wunschWeg === 'behalten' ? 'behalten' : ''
|
|
delete root.dataset.wunschProjekt
|
|
delete root.dataset.wunschWeg
|
|
if (wunsch) auswahl.projectId = wunsch
|
|
const antwort = await request('/api/funktionen')
|
|
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 : '',
|
|
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
|
|
if (!s.angebot?.verfuegbar) {
|
|
S = s
|
|
root.dataset.zeitenKey = ''
|
|
root.replaceChildren(infoKarte('Öffnungszeiten live ist noch nicht verfügbar', 'Sobald es für dich bereit ist, findest du es hier.'))
|
|
return
|
|
}
|
|
if (!projects.length) {
|
|
S = s
|
|
root.dataset.zeitenKey = ''
|
|
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
|
return
|
|
}
|
|
s.projekt = projects.find((p) => p.projectId === auswahl.projectId) || pickProjekt(projects)
|
|
auswahl.projectId = s.projekt.projectId
|
|
|
|
// Nicht neu aufbauen, solange Projekt und Stand gleich sind und der Editorplatz steht: ungespeicherte Eingaben bleiben (Muster initNews).
|
|
// Ein Weg (Behalten aus der Kachel) oder die Rueckkehr von Stripe zeichnen immer neu; der Wechsel in die Behalten-Seite behaelt den Editor.
|
|
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, S.modell)) zeigeBehalten(S)
|
|
return
|
|
}
|
|
S = s
|
|
s.editorAlt = alterEditor
|
|
s.danke = rueckkehr.bestellt && (!rueckkehr.projekt || rueckkehr.projekt === s.projekt.projectId) ? 'bestellt'
|
|
: 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: 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. */
|
|
function stand(projekt, angebot) {
|
|
const f = projekt?.features?.zeiten || {}
|
|
return [projekt?.projectId, f.state, f.aboEndeAt, f.einbau, f.ticketWoid, f.disabledReason, f.unlockedBy, projekt?.zeitenLeer, angebot?.inklusive, angebot?.grund].join(':')
|
|
}
|
|
|
|
function projektwahl(s) {
|
|
const select = h('select', { id: 'zeiten-project' }, s.projects.map((p) => {
|
|
const option = h('option', { value: p.projectId }, p.projectName)
|
|
option.selected = p.projectId === s.projekt.projectId
|
|
return option
|
|
}))
|
|
select.addEventListener('change', () => {
|
|
auswahl.projectId = select.value
|
|
s.projekt = s.projects.find((p) => p.projectId === select.value)
|
|
s.danke = '' // der Dank gilt dem Projekt, mit dem man von Stripe zurueckkam
|
|
zeichneProjekt(s)
|
|
})
|
|
return h('div', { class: 'fn-project kb-panel' }, h('label', { for: 'zeiten-project' }, 'Website'), select)
|
|
}
|
|
|
|
/** Liste der Funktionen neu holen (Projekte, Angebot, Register-Ansicht). Wirft bei einem Fehler und aendert dann nichts. */
|
|
async function ladeListe(s) {
|
|
const antwort = await request('/api/funktionen')
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Nach einem Konflikt (409) oder dem Einschalten: Liste frisch holen und die Ansicht nach dem neuen Stand zeichnen (optionen: meldung des Servers,
|
|
* danke). Scheitert das Laden, wirft die Funktion und die Seite bleibt mit ihrer Meldung stehen. Eine Antwort, die erst nach einem Wechsel der
|
|
* Website eintrifft, wird verworfen.
|
|
*/
|
|
async function neuZeichnen(s, optionen = {}) {
|
|
const lauf = s.lauf
|
|
await ladeListe(s)
|
|
if (S !== s || s.lauf !== lauf) return
|
|
if (!s.angebot?.verfuegbar) {
|
|
s.root.dataset.zeitenKey = ''
|
|
s.root.replaceChildren(infoKarte('Öffnungszeiten live ist noch nicht verfügbar', 'Sobald es für dich bereit ist, findest du es hier.'))
|
|
return
|
|
}
|
|
if (optionen.danke) s.danke = optionen.danke
|
|
zeichneProjekt(s, { meldung: optionen.meldung })
|
|
}
|
|
|
|
/** Kontext der Seiten in der Huelle (zeiten-bestellen.js): Projekt, Register-Ansicht, Kundenansicht und die Wege, die Ansicht nach dem Server neu zu zeichnen. */
|
|
function baueCtx(s, projekt) {
|
|
return {
|
|
projectId: projekt.projectId,
|
|
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 }),
|
|
}
|
|
}
|
|
|
|
/** Die Ansicht zum Zustand des gewaehlten Projekts zeichnen (siehe Kopf der Datei). Jeder Aufruf zaehlt s.lauf hoch. */
|
|
function zeichneProjekt(s, { weg = '', meldung = '', bestellformular = false } = {}) {
|
|
const projekt = s.projekt
|
|
++s.lauf
|
|
const ctx = baueCtx(s, projekt)
|
|
const zustand = zustandVon(projekt)
|
|
const feature = projekt.features?.zeiten || {}
|
|
const danke = s.danke
|
|
s.danke = '' // nur beim ersten Zeichnen nach der Rueckkehr
|
|
s.root.dataset.zeitenKey = ''
|
|
const kopf = h('div', { class: 'zt-kopf' })
|
|
const teile = []
|
|
const meldungEl = meldung ? h('p', { class: 'tm-meldung tm-meldung-fehler', role: 'alert', tabindex: '-1', 'data-meldung': 'ansicht' }, meldung) : null
|
|
if (meldungEl) teile.push(meldungEl)
|
|
teile.push(kopf)
|
|
|
|
// Der Editorplatz steht in setup, active und (mit Daten) disabled. Ein Platz derselben Website mit gleichem Schreibrecht bleibt samt Editor stehen
|
|
// (ungespeicherte Eingaben gehen nicht verloren, der Editor laedt nicht noch einmal); sonst gibt es einen frischen.
|
|
let editorPlatz = null
|
|
let frisch = false
|
|
if (zustand === 'setup' || zustand === 'active' || (zustand === 'disabled' && projekt.zeitenLeer === false)) {
|
|
const vorhanden = s.editorAlt || s.buehne.querySelector('.zt-editor')
|
|
if (vorhanden && vorhanden.dataset.projekt === projekt.projectId && (vorhanden.dataset.state === 'disabled') === (zustand === 'disabled')) {
|
|
editorPlatz = vorhanden
|
|
editorPlatz.dataset.state = zustand
|
|
editorPlatz.hidden = false // die Behalten-Seite versteckt ihn; sie wird bei Bedarf unten wieder geoeffnet
|
|
} else {
|
|
editorPlatz = h('div', { class: 'zt-editor', 'data-state': zustand, 'data-projekt': projekt.projectId })
|
|
frisch = true
|
|
}
|
|
teile.push(editorPlatz)
|
|
}
|
|
s.editorAlt = null
|
|
s.buehne.replaceChildren(...teile)
|
|
if (editorPlatz) s.root.dataset.zeitenKey = stand(projekt, s.angebot)
|
|
if (frisch) {
|
|
// neuZeichnen gehoert zur jeweils aktuellen Sitzung (S): der Platz kann einen Neuaufbau der Ansicht ueberleben
|
|
const editorCtx = { projectId: projekt.projectId, kundenansicht: ctx.kundenansicht, neuZeichnen: (optionen) => (S ? neuZeichnen(S, optionen) : undefined) }
|
|
Promise.resolve(renderEditor(editorPlatz, editorCtx)).catch((err) => editorPlatz.replaceChildren(fehlerKarte(err?.message || 'Der Editor konnte nicht gezeichnet werden.')))
|
|
}
|
|
// die Meldung des Servers ersetzt die Seite, auf der der Kunde geklickt hat: dorthin schauen, nicht ins Leere
|
|
if (meldungEl) {
|
|
meldungEl.focus({ preventScroll: true })
|
|
meldungEl.scrollIntoView({ block: 'nearest' })
|
|
}
|
|
|
|
const statusKarte = (zusatz = {}) => zeitenStatusKarte(
|
|
{ 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, 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. */
|
|
function zeigeBehalten(s) {
|
|
const kopf = s.buehne.querySelector('.zt-kopf')
|
|
if (!kopf) return
|
|
const editor = s.buehne.querySelector('.zt-editor')
|
|
const ctx = baueCtx(s, s.projekt)
|
|
ctx.zurueck = () => {
|
|
if (editor) editor.hidden = false
|
|
kopf.replaceChildren(zeitenStatusKarte(
|
|
{ projekt: s.projekt, feature: s.projekt.features?.zeiten, angebot: s.angebot, kundenansicht: ctx.kundenansicht },
|
|
{ behalten: () => zeigeBehalten(s) }))
|
|
}
|
|
if (editor) editor.hidden = true
|
|
renderZeitenBestellen(kopf, { ctx, angebot: s.angebot, projekt: s.projekt, modus: 'behalten' })
|
|
}
|