Files
Webklar-Kundenbereich/public/termine-speichern.js
T

129 lines
6.5 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.
/* Terminplaner im Kundenbereich: Speichern der Einstellungen – gemeinsam fuer die Reiter Leistungen, Team & Plaetze und Einstellungen.
Jede Aenderung ist ein Speichern: PATCH /api/termine/einstellungen schickt die GANZE Einstellung mit der Version, auf der sie beruht
(server/routes/termine.js). Der Reiter gibt nur an, was sich aendert: speichere(aendere, { container, knopf }) holt eine Kopie des
aktuellen Standes (ctx.daten.einstellung), laesst aendere(entwurf) sie aendern und schickt sie ab.
- Erfolg: die zurueckgegebene Einstellung und Version gelten (ctx.daten), die Hinweisleiste der Huelle wird neu gezeichnet und die Daten
(Kalender-Links) werden neu geladen; Rueckgabe true.
- 400: Fehler stehen am Feld (data-fehler = Pfad wie 'leistungen.2.dauer' oder 'ressourcen.1.plan.3'); Pfade ohne Platz im container
erscheinen allgemein, mit dem Namen des Eintrags davor. Rueckgabe false.
- 409 EINSTELLUNG_VERALTET (jemand hat inzwischen gespeichert): Hinweis mit Knopf „Neu laden“ oben im Reiter. Rueckgabe false.
- alles andere: Meldung ueber den Reitern.
Der Reiter ruft speichere nur, wenn ctx.nurLesen nicht gesetzt ist. Die Tokens der Kalender-Links gehoeren dem Server und stehen nie in
der Einstellung des Browsers. */
import { allgemeinerFehler, frage, h, knopfZeile, setzeFehler } from './termine-dom.js'
export const VERALTET_TEXT = 'Die Einstellungen wurden inzwischen geändert. Bitte neu laden.'
const WORT = { leistungen: 'Leistung', abwesenheiten: 'Abwesenheit' }
/** „Leistung „Färben“: Text“ aus Pfad und Text eines Fehlers; Eintraege finden sich per Index oder per ID in den Einstellungen (quellen). */
export function beschrifteFehler(pfad, text, quellen) {
if (pfad.startsWith('betrieb.')) return `${text} Das änderst du im Reiter „Einstellungen“.`
const m = /^(leistungen|ressourcen|abwesenheiten)\.([^.]+)/.exec(pfad)
if (!m) return text
for (const quelle of quellen) {
const liste = Array.isArray(quelle?.[m[1]]) ? quelle[m[1]] : []
const eintrag = /^\d+$/.test(m[2]) ? liste[Number(m[2])] : liste.find((e) => e?.id === m[2])
const name = eintrag?.name || eintrag?.titel
if (!name) continue
const wort = m[1] === 'ressourcen' ? (eintrag.typ === 'platz' ? 'Platz' : 'Person') : WORT[m[1]]
return `${wort} „${name}“: ${text}`
}
return text
}
/**
* Speicher eines Reiters. wurzel = das Element des Reiters (verschwindet beim Wechsel des Reiters: danach fasst nichts mehr die Seite an),
* neuZeichnen() zeichnet den Reiter aus ctx.daten neu (nach „Neu laden“).
* => { konflikt, speichere, nochHier }: konflikt ist das Hinweis-Element (der Reiter setzt es an den Anfang), nochHier() sagt, ob der
* Reiter noch angezeigt wird (vor jedem Anfassen der Seite nach einem await pruefen).
*/
export function neuerSpeicher(ctx, wurzel, neuZeichnen) {
const nochHier = () => wurzel.isConnected && ctx.aktuell()
const text = h('span', null, VERALTET_TEXT)
const neuLadenKnopf = h('button', { type: 'button', class: 'kb-btn-outline' }, 'Neu laden')
const konflikt = h('div', { class: 'tm-box tm-box-warn', role: 'alert', hidden: true }, text, neuLadenKnopf)
neuLadenKnopf.addEventListener('click', async () => {
neuLadenKnopf.disabled = true
try {
await ctx.neuLaden()
if (!nochHier()) return
text.textContent = VERALTET_TEXT
konflikt.hidden = true
neuZeichnen()
} catch (err) {
if (nochHier()) text.textContent = err.message
} finally {
neuLadenKnopf.disabled = false
}
})
async function speichere(aendere, { container, knopf = null } = {}) {
setzeFehler(container, {})
konflikt.hidden = true
const bisher = ctx.daten.einstellung
const entwurf = structuredClone(bisher)
if (aendere(entwurf) === false) return false // der Reiter hat selbst Fehler am Feld gezeigt
if (knopf) knopf.disabled = true
try {
const antwort = await ctx.api.send('PATCH', '/einstellungen', { version: ctx.daten.version, einstellung: entwurf })
if (!ctx.aktuell()) return false
// Der Server hat streng geprueft: Telefon und Anschrift fehlen nach einem Erfolg nie mehr
Object.assign(ctx.daten, { einstellung: antwort.einstellung, version: antwort.version, fehlt: [] })
ctx.hinweise()
try {
await ctx.neuLaden() // neue Personen/Plaetze bekommen vom Server ihre Kalender-Links
} catch { /* dann zeigen neue Eintraege ihren Kalender-Link erst nach dem naechsten Laden */ }
return true
} catch (err) {
if (!nochHier()) return false
if (err.code === 'EINSTELLUNG_VERALTET') {
text.textContent = VERALTET_TEXT
konflikt.hidden = false
konflikt.scrollIntoView({ block: 'nearest' })
} else if (err.status === 400 && Object.keys(err.fehler || {}).length) {
zeigeFelder(container, err.fehler, [bisher, entwurf])
} else {
ctx.meldung(err.message, 'fehler')
}
return false
} finally {
if (knopf?.isConnected) knopf.disabled = false
}
}
return { konflikt, speichere, nochHier }
}
/** Fehler des Servers am Feld; was im container keinen Platz hat, steht allgemein (mit dem Namen des Eintrags davor). */
function zeigeFelder(container, fehler, quellen) {
const amFeld = {}
const rest = []
for (const [pfad, text] of Object.entries(fehler)) {
if (pfad !== '_' && container.querySelector(`[data-fehler="${CSS.escape(pfad)}"]`)) amFeld[pfad] = text
else rest.push(beschrifteFehler(pfad, text, quellen))
}
setzeFehler(container, rest.length ? { ...amFeld, _: rest.join(' ') } : amFeld)
container.querySelector('[aria-invalid="true"]')?.focus()
}
/**
* Rueckfrage „entfernen?“ in der Seite (kein confirm()): Titel, Erklaerung, Platz fuer den Fehler des Servers am Eintrag (fehlerPfad, z. B.
* 'leistungen.s_abcd1234': „Fuer diese Leistung gibt es noch 2 kuenftige Termine …“), Knoepfe. aendere(entwurf) entfernt den Eintrag,
* nachher() laeuft nach dem erfolgreichen Speichern (der Reiter zeichnet neu), zurueck() nach „Abbrechen“. Gibt das Formular zurueck;
* der Aufrufer setzt es in seine Box und gibt ihm den Fokus.
*/
export function entfernenFrage(sp, { titel, text, fehlerPfad, aendere, nachher, zurueck }) {
const { ok, zeile } = knopfZeile('Ja, entfernen', zurueck)
const box = frage(titel,
h('p', null, text),
fehlerPfad ? h('p', { class: 'tm-fehler', 'data-fehler': fehlerPfad, role: 'alert' }) : null,
allgemeinerFehler(),
zeile)
box.addEventListener('submit', async () => {
if (await sp.speichere(aendere, { container: box, knopf: ok }) && sp.nochHier()) nachher()
})
return box
}