Terminplaner: Portal-Ansicht – Nachbesserungen (2/2)
- Annehmen mit Rueckfrage in der Seite (schliesst den Vertrag, schickt G02) - Offene Anfragen: alle bis Ende des Buchungszeitraums (Fenster zu je 62 Tagen, hoechstens 6 Aufrufe), passend zur Zahl auf Kachel und Banner - Meldung ins Bild geholt, Fehler mit Fokus; einzige Live-Region - Netzwerkfehler auf Deutsch; Antwort einer frueheren Website erscheint nie unter der neuen (Zaehler je Aufruf); Datumssprung nur mit echtem Tag - Ende um Mitternacht als 24:00, Statusfilter nach Eintragen auf Alle, abgeschaltet ohne Einstellung mit Export-Link, dunkles Datumsfeld - Playwright: Zeitzone New York/Berlin, ferne Anfragen, Projektwechsel, Meldung im Bild, Datumssprung, abgeschaltet ohne Einstellung Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c07f6a8c5
commit
650919b022
10 files changed
+410
-73
No files matched your search
+54
-30
@@ -22,7 +22,7 @@ export async function initTermine(root, { projectId } = {}) {
|
||||
delete root.dataset.wunschProjekt
|
||||
if (wunsch) auswahl.projectId = wunsch
|
||||
const { projects = [], termine } = await request('/api/funktionen')
|
||||
const s = { root, projects, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, buehne: null, meldungEl: null }
|
||||
const s = { root, projects, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null }
|
||||
S = s
|
||||
if (!termine?.verfuegbar) {
|
||||
root.replaceChildren(infoKarte('Der Terminplaner ist noch nicht verfügbar', 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
||||
@@ -56,32 +56,48 @@ function projektwahl(s) {
|
||||
return h('div', { class: 'fn-project kb-panel' }, h('label', { for: 'termine-project' }, 'Website'), select)
|
||||
}
|
||||
|
||||
const ladeDaten = (s) => request(`/api/termine/einstellungen?projectId=${encodeURIComponent(s.projekt.projectId)}`)
|
||||
const ladeDaten = (projectId) => request(`/api/termine/einstellungen?projectId=${encodeURIComponent(projectId)}`)
|
||||
|
||||
/** Je nach Zustand der Funktion: nicht freigeschaltet, Vorlage waehlen oder die Reiter. */
|
||||
/** Hinweis fuer abgeschaltete Funktionen: nur noch ansehen und exportieren. */
|
||||
const hinweisAbgeschaltet = (projectId) => h('div', { class: 'tm-box tm-box-warn', role: 'note' },
|
||||
'Der Terminplaner ist für diese Website abgeschaltet. Du kannst Termine nur noch ansehen und ',
|
||||
h('a', { href: exportUrl(projectId), download: 'termine.csv' }, 'exportieren'),
|
||||
'.')
|
||||
|
||||
/**
|
||||
* Je nach Zustand der Funktion: nicht freigeschaltet, Vorlage waehlen oder die Reiter. Jeder Aufruf zaehlt s.lauf hoch: die Antwort
|
||||
* einer frueheren Website (langsam, dann zur anderen gewechselt) wird verworfen und erscheint nie unter der neuen.
|
||||
*/
|
||||
async function zeigeProjekt(s) {
|
||||
const zustand = s.projekt.features?.termine?.state || 'off'
|
||||
const projekt = s.projekt
|
||||
const lauf = ++s.lauf
|
||||
const aktuell = () => S === s && s.lauf === lauf
|
||||
const zustand = projekt.features?.termine?.state || 'off'
|
||||
s.nurLesen = zustand !== 'active'
|
||||
if (zustand !== 'active' && zustand !== 'disabled') {
|
||||
s.buehne.replaceChildren(infoKarte('Der Terminplaner ist für diese Website noch nicht freigeschaltet', 'Wir schalten den Terminplaner für dich frei.', { chat: true }))
|
||||
return
|
||||
}
|
||||
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
||||
let daten
|
||||
try {
|
||||
s.daten = await ladeDaten(s)
|
||||
daten = await ladeDaten(projekt.projectId)
|
||||
} catch (err) {
|
||||
if (S === s) s.buehne.replaceChildren(fehlerKarte(err.message, () => zeigeProjekt(s)))
|
||||
if (aktuell()) s.buehne.replaceChildren(fehlerKarte(err.message, () => zeigeProjekt(s)))
|
||||
return
|
||||
}
|
||||
if (S !== s) return
|
||||
if (s.daten.einstellung) zeigeReiter(s)
|
||||
else if (s.nurLesen) s.buehne.replaceChildren(infoKarte('Der Terminplaner ist für diese Website abgeschaltet', 'Es wurden keine Termine eingerichtet.', { chat: true }))
|
||||
else zeigeVorlagen(s)
|
||||
if (!aktuell()) return
|
||||
s.daten = daten
|
||||
if (daten.einstellung) zeigeReiter(s, { projekt, aktuell })
|
||||
else if (s.nurLesen) {
|
||||
s.buehne.replaceChildren(
|
||||
hinweisAbgeschaltet(projekt.projectId),
|
||||
infoKarte('Keine Termine eingerichtet', 'Für diese Website wurden keine Einstellungen und Termine angelegt.', { chat: true }))
|
||||
} else zeigeVorlagen(s, { projekt, aktuell })
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------- Vorlage wählen --- */
|
||||
function zeigeVorlagen(s) {
|
||||
const projektId = s.projekt.projectId
|
||||
function zeigeVorlagen(s, { projekt, aktuell }) {
|
||||
const fehler = h('p', { class: 'tm-fehler', role: 'alert' })
|
||||
const start = h('button', { type: 'button', class: 'kb-btn-primary', disabled: true }, 'Mit dieser Vorlage starten')
|
||||
let gewaehlt = null
|
||||
@@ -95,10 +111,11 @@ function zeigeVorlagen(s) {
|
||||
start.disabled = true
|
||||
fehler.textContent = ''
|
||||
try {
|
||||
await request('/api/termine/einstellungen/vorlage', { method: 'POST', body: { projectId: projektId, vorlage: gewaehlt.key } })
|
||||
s.daten = await ladeDaten(s)
|
||||
if (S !== s) return
|
||||
zeigeReiter(s)
|
||||
await request('/api/termine/einstellungen/vorlage', { method: 'POST', body: { projectId: projekt.projectId, vorlage: gewaehlt.key } })
|
||||
const daten = await ladeDaten(projekt.projectId)
|
||||
if (!aktuell()) return
|
||||
s.daten = daten
|
||||
zeigeReiter(s, { projekt, aktuell })
|
||||
zeigeMeldung(s, `Die Vorlage „${gewaehlt.name}“ ist angelegt. Alles darin ist ein Vorschlag, den du anpassen kannst.`)
|
||||
} catch (err) {
|
||||
fehler.textContent = err.message
|
||||
@@ -126,27 +143,34 @@ const REITER = [
|
||||
{ key: 'einstellungen', label: 'Einstellungen', render: platzhalter('Einstellungen', 'Hier ändern wir bald Betriebsangaben, Regeln und Texte für deine Gäste.') },
|
||||
]
|
||||
|
||||
/** Meldung ueber der Reiterleiste: art 'ok' (Standard) oder 'fehler'; leerer Text blendet sie aus. */
|
||||
/**
|
||||
* Meldung ueber der Reiterleiste: art 'ok' (Standard) oder 'fehler'; leerer Text blendet sie aus. Die Meldung ist die einzige Live-Region
|
||||
* der Ansicht. Sie erscheint dort, wo man hinschaut: liegt sie ausserhalb des Bildes, wird sie hingescrollt (block: 'nearest'), eine
|
||||
* Fehlermeldung bekommt zusaetzlich den Fokus.
|
||||
*/
|
||||
function zeigeMeldung(s, text, art = 'ok') {
|
||||
const el = s.meldungEl
|
||||
if (!el) return
|
||||
el.textContent = text || ''
|
||||
el.hidden = !text
|
||||
el.className = `tm-meldung tm-meldung-${art}`
|
||||
el.setAttribute('role', art === 'fehler' ? 'alert' : 'status')
|
||||
el.setAttribute('aria-live', art === 'fehler' ? 'assertive' : 'polite')
|
||||
if (!text) return
|
||||
if (art === 'fehler') el.focus({ preventScroll: true })
|
||||
el.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
|
||||
function zeigeReiter(s) {
|
||||
const projectId = s.projekt.projectId
|
||||
function zeigeReiter(s, { projekt, aktuell }) {
|
||||
const projectId = projekt.projectId
|
||||
const nurLesen = s.nurLesen
|
||||
s.meldungEl = h('div', { class: 'tm-meldung', hidden: true })
|
||||
s.meldungEl = h('div', { class: 'tm-meldung', role: 'status', 'aria-live': 'polite', tabindex: '-1' })
|
||||
const ctx = {
|
||||
s,
|
||||
projectId,
|
||||
daten: s.daten,
|
||||
nurLesen,
|
||||
auswahl,
|
||||
aktuell: () => S === s,
|
||||
aktuell,
|
||||
api: {
|
||||
get: (pfad, query = {}) => request(`/api/termine${pfad}?${new URLSearchParams({ projectId, ...query })}`),
|
||||
send: (method, pfad, body = {}) => (method === 'DELETE'
|
||||
@@ -156,18 +180,18 @@ function zeigeReiter(s) {
|
||||
meldung: (text, art) => zeigeMeldung(s, text, art),
|
||||
zumReiter: (key) => waehleReiter(key),
|
||||
neuLaden: async () => {
|
||||
s.daten = await ladeDaten(s)
|
||||
ctx.daten = s.daten
|
||||
return s.daten
|
||||
const daten = await ladeDaten(projectId)
|
||||
if (aktuell()) {
|
||||
s.daten = daten
|
||||
ctx.daten = daten
|
||||
}
|
||||
return daten
|
||||
},
|
||||
}
|
||||
|
||||
const hinweise = []
|
||||
if (nurLesen) {
|
||||
hinweise.push(h('div', { class: 'tm-box tm-box-warn', role: 'note' },
|
||||
'Der Terminplaner ist für diese Website abgeschaltet. Du kannst Termine nur noch ansehen und ',
|
||||
h('a', { href: exportUrl(projectId), download: 'termine.csv' }, 'exportieren'),
|
||||
'.'))
|
||||
hinweise.push(hinweisAbgeschaltet(projectId))
|
||||
} else if ((s.daten.fehlt || []).length) {
|
||||
hinweise.push(h('div', { class: 'tm-box tm-box-warn', role: 'note' },
|
||||
h('span', null, `Noch nicht buchbar: Es fehlt ${fehltText(s.daten.fehlt) || 'eine Angabe zu deinem Betrieb'}. Solange das fehlt, können Gäste über deine Website nicht buchen.`),
|
||||
@@ -202,7 +226,7 @@ function zeigeReiter(s) {
|
||||
panel.replaceChildren()
|
||||
Promise.resolve()
|
||||
.then(() => reiter.render(panel, ctx))
|
||||
.catch((err) => { if (S === s) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
|
||||
.catch((err) => { if (aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
|
||||
}
|
||||
|
||||
s.buehne.replaceChildren(...hinweise, s.meldungEl, leiste, panel)
|
||||
|
||||
Reference in new issue
Block a user