129 lines
6.5 KiB
JavaScript
129 lines
6.5 KiB
JavaScript
/* 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
|
||
}
|