/* 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 }