Terminplaner: Portal-Ansicht – Nachbesserungen (1/2: Module getrennt)
public/termine.js in Huelle und Module zerlegt, Verhalten unveraendert (R48): termine-dom.js (Helfer ohne Zustand), termine-tab-termine.js (Liste, Kopf, Zeilen), termine-aktionen.js (Aktionen mit Rueckfrage), termine-formulare.js (Uhrzeiten, Termin eintragen, Zeit sperren). Reiter bekommen Zustand und aktuell() nur ueber ctx; die Huelle haengt sie ueber die Registry REITER ein. Statiktest prueft alle public/termine*.js. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43e4826ae3
commit
9c07f6a8c5
7 files changed
+697
-645
No files matched your search
@@ -0,0 +1,87 @@
|
||||
/* Terminplaner im Kundenbereich: Helfer ohne Zustand (Elemente bauen, Schnittstelle, Formularfelder, Hinweiskarten).
|
||||
Alle Module der Ansicht (termine.js, termine-tab-*.js, termine-aktionen.js, termine-formulare.js) bauen die Seite nur hiermit:
|
||||
Kundentext kommt ausschliesslich ueber textContent in die Seite (h), nie ueber innerHTML. */
|
||||
|
||||
/** Element bauen: Texte nur als Textknoten, Attribute mit on… als Ereignis, true setzt ein Attribut ohne Wert, false/null/undefined lassen es weg. */
|
||||
export function h(tag, attrs, ...kinder) {
|
||||
const el = document.createElement(tag)
|
||||
for (const [name, wert] of Object.entries(attrs || {})) {
|
||||
if (wert === undefined || wert === null || wert === false) continue
|
||||
if (name === 'class') el.className = wert
|
||||
else if (name.startsWith('on')) el.addEventListener(name.slice(2), wert)
|
||||
else el.setAttribute(name, wert === true ? '' : wert)
|
||||
}
|
||||
for (const kind of kinder.flat()) if (kind !== undefined && kind !== null && kind !== false) el.append(kind)
|
||||
return el
|
||||
}
|
||||
|
||||
export async function request(path, { method = 'GET', body } = {}) {
|
||||
const response = await fetch(path, {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
const err = new Error(data.error || (response.status === 502 ? 'Server nicht erreichbar. Bitte kurz warten und noch einmal versuchen.' : `Fehler ${response.status}`))
|
||||
err.status = response.status
|
||||
err.fehler = data.fehler || {}
|
||||
err.code = data.code
|
||||
throw err
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/** Adresse des CSV-Exports (die einzige Adresse, die die Ansicht als Link setzt). */
|
||||
export const exportUrl = (projectId) => `/api/termine/export.csv?projectId=${encodeURIComponent(projectId)}`
|
||||
|
||||
let idZaehler = 0
|
||||
const neueId = () => `tm-f${++idZaehler}`
|
||||
|
||||
/** Formularfeld mit Beschriftung, Hinweis und Platz fuer den Fehler (data-feld = Feldname der Schnittstelle). */
|
||||
export function feld(name, label, steuer, hinweis) {
|
||||
const id = steuer.id || (steuer.id = neueId())
|
||||
steuer.dataset.feld = name
|
||||
return h('div', { class: 'tm-feld' },
|
||||
h('label', { for: id }, label),
|
||||
steuer,
|
||||
hinweis ? h('p', { class: 'tm-hinweis' }, hinweis) : null,
|
||||
h('p', { class: 'tm-fehler', 'data-fehler': name, role: 'alert' }))
|
||||
}
|
||||
|
||||
/** Platz fuer Fehler, die zu keinem Feld gehoeren (Feldname „_“). */
|
||||
export const allgemeinerFehler = () => h('p', { class: 'tm-fehler', 'data-fehler': '_', role: 'alert' })
|
||||
|
||||
export function setzeFehler(form, fehler) {
|
||||
for (const el of form.querySelectorAll('[data-fehler]')) el.textContent = ''
|
||||
for (const el of form.querySelectorAll('[aria-invalid]')) el.removeAttribute('aria-invalid')
|
||||
const rest = []
|
||||
for (const [name, text] of Object.entries(fehler || {})) {
|
||||
const ziel = form.querySelector(`[data-fehler="${CSS.escape(name)}"]`)
|
||||
if (ziel) {
|
||||
ziel.textContent = text
|
||||
form.querySelector(`[data-feld="${CSS.escape(name)}"]`)?.setAttribute('aria-invalid', 'true')
|
||||
} else {
|
||||
rest.push(text)
|
||||
}
|
||||
}
|
||||
const allgemein = form.querySelector('[data-fehler="_"]')
|
||||
if (allgemein) allgemein.textContent = rest.join(' ')
|
||||
}
|
||||
|
||||
/** Fehler der Schnittstelle am Formular zeigen (Feldfehler am Feld, sonst allgemein). */
|
||||
export function zeigeFehler(form, err) {
|
||||
const fehler = err.fehler && Object.keys(err.fehler).length ? err.fehler : { _: err.message }
|
||||
setzeFehler(form, fehler)
|
||||
form.querySelector('[aria-invalid]')?.focus()
|
||||
}
|
||||
|
||||
export const infoKarte = (titel, text, { chat = false } = {}) => h('div', { class: 'kb-panel tm-info' },
|
||||
h('h3', null, titel),
|
||||
text ? h('p', null, text) : null,
|
||||
chat ? h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Chat öffnen') : null)
|
||||
|
||||
export const fehlerKarte = (text, nochmal) => h('div', { class: 'kb-panel tm-info', role: 'alert' },
|
||||
h('p', null, text),
|
||||
nochmal ? h('button', { type: 'button', class: 'kb-btn-outline', onclick: nochmal }, 'Noch einmal versuchen') : null)
|
||||
Reference in new issue
Block a user