Terminplaner: Portal-Ansicht Einstellungen – Nachbesserungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b75b31009
commit
3ad056f261
10 files changed
+568
-342
No files matched your search
+107
-45
@@ -8,10 +8,13 @@
|
||||
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.
|
||||
- Es laeuft immer nur ein Speichern je Reiter (PATCH und das Nachladen danach): solange es laeuft, ist der Reiter aria-busy, alle seine
|
||||
Knoepfe sind gesperrt, und ein weiterer Aufruf von speichere (z. B. ein Enter im Formular) wird abgewiesen (Rueckgabe false), statt mit
|
||||
einer veralteten Version gesendet zu werden. Dasselbe gilt fuer das Erzeugen eines Kalender-Links (exklusiv), das die Version ebenfalls aendert.
|
||||
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'
|
||||
import { allgemeinerFehler, frage, h, knopfZeile, setzeFehler, zeigeFehler } from './termine-dom.js'
|
||||
|
||||
export const VERALTET_TEXT = 'Die Einstellungen wurden inzwischen geändert. Bitte neu laden.'
|
||||
|
||||
@@ -59,41 +62,93 @@ export function neuerSpeicher(ctx, wurzel, neuZeichnen) {
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
// Ein Speichern zur Zeit: waehrenddessen aria-busy am Reiter und alle Knoepfe darin gesperrt (danach wieder frei, der Fokus kehrt zurueck)
|
||||
let laeuft = false
|
||||
function sperre(an, gesperrt) {
|
||||
const tafel = wurzel.closest('[role="tabpanel"]') || wurzel
|
||||
if (an) {
|
||||
tafel.setAttribute('aria-busy', 'true')
|
||||
const knoepfe = [...wurzel.querySelectorAll('button:not(:disabled)')]
|
||||
const fokus = document.activeElement
|
||||
for (const knopf of knoepfe) knopf.disabled = true
|
||||
return { knoepfe, fokus }
|
||||
}
|
||||
tafel.removeAttribute('aria-busy')
|
||||
for (const knopf of gesperrt.knoepfe) knopf.disabled = false
|
||||
// Der Fokus war auf einem gesperrten Knopf und ist weg: zurueck dorthin; hat der Reiter ihn inzwischen anders gesetzt (z. B. auf ein Feld mit Fehler), bleibt das so
|
||||
const jetzt = document.activeElement
|
||||
if (gesperrt.knoepfe.includes(gesperrt.fokus) && gesperrt.fokus.isConnected && (!jetzt || jetzt === document.body || jetzt === gesperrt.fokus)) gesperrt.fokus.focus()
|
||||
return null
|
||||
}
|
||||
|
||||
/** arbeit() exklusiv ausfuehren; laeuft schon etwas, passiert nichts (Rueckgabe undefined). */
|
||||
async function exklusiv(arbeit) {
|
||||
if (laeuft) return undefined
|
||||
laeuft = true
|
||||
const gesperrt = sperre(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
|
||||
return await arbeit()
|
||||
} finally {
|
||||
if (knopf?.isConnected) knopf.disabled = false
|
||||
laeuft = false
|
||||
sperre(false, gesperrt)
|
||||
}
|
||||
}
|
||||
|
||||
return { konflikt, speichere, nochHier }
|
||||
async function speichere(aendere, { container }) {
|
||||
const ergebnis = await exklusiv(async () => {
|
||||
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
|
||||
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
|
||||
}
|
||||
})
|
||||
return ergebnis === true
|
||||
}
|
||||
|
||||
return { konflikt, speichere, exklusiv, nochHier }
|
||||
}
|
||||
|
||||
/**
|
||||
* Stelle des Eintrags mit dieser ID in der Liste des Entwurfs. Editoren merken sich keine Stelle, sondern die ID und suchen sie beim
|
||||
* Absenden (die Liste kann sich inzwischen anders sortiert haben). Fehlt der Eintrag, steht am Formular eine Meldung und die Rueckgabe ist -1.
|
||||
*/
|
||||
export function stelleVon(liste, id, form, was) {
|
||||
const stelle = (Array.isArray(liste) ? liste : []).findIndex((eintrag) => eintrag?.id === id)
|
||||
if (stelle < 0) zeigeFehler(form, { fehler: { _: `${was} gibt es nicht mehr. Bitte lade die Seite neu.` } })
|
||||
return stelle
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach einem erfolgreichen Speichern: Reiter neu zeichnen, Meldung zeigen, Fokus auf das Element mit selektor (Regel: nach dem Speichern
|
||||
* auf den Knopf der gespeicherten Zeile, nach dem Entfernen auf den „+“-Knopf der Liste).
|
||||
*/
|
||||
export function nachSpeichern(t, text, selektor) {
|
||||
t.zeichne()
|
||||
t.ctx.meldung(text)
|
||||
t.wurzel.querySelector(selektor)?.focus()
|
||||
}
|
||||
|
||||
/** Fehler des Servers am Feld; was im container keinen Platz hat, steht allgemein (mit dem Namen des Eintrags davor). */
|
||||
@@ -109,20 +164,27 @@ function zeigeFelder(container, fehler, quellen) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* „Entfernen“-Knopf mit Rueckfrage 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). Abbrechen schliesst die Rueckfrage und der Fokus
|
||||
* geht zum Knopf zurueck. => { knopf, box }: der Aufrufer setzt beide NEBEN sein Formular, nicht hinein (ein submit der Rueckfrage
|
||||
* stiege sonst ins aeussere Formular auf).
|
||||
*/
|
||||
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()
|
||||
export function entfernenKnopf(sp, { titel, text, fehlerPfad, aendere, nachher }) {
|
||||
const box = h('div', { class: 'tm-frage' })
|
||||
const knopf = h('button', { type: 'button', class: 'kb-btn-outline tm-gefahr', 'data-aktion': 'entfernen' }, 'Entfernen')
|
||||
knopf.addEventListener('click', () => {
|
||||
const { zeile } = knopfZeile('Ja, entfernen', () => { box.replaceChildren(); knopf.focus() })
|
||||
const rueckfrage = frage(titel,
|
||||
h('p', null, text),
|
||||
fehlerPfad ? h('p', { class: 'tm-fehler', 'data-fehler': fehlerPfad, role: 'alert' }) : null,
|
||||
allgemeinerFehler(),
|
||||
zeile)
|
||||
rueckfrage.addEventListener('submit', async () => {
|
||||
if (await sp.speichere(aendere, { container: rueckfrage }) && sp.nochHier()) nachher()
|
||||
})
|
||||
box.replaceChildren(rueckfrage)
|
||||
rueckfrage.focus()
|
||||
})
|
||||
return box
|
||||
return { knopf, box }
|
||||
}
|
||||
Reference in new issue
Block a user