/* 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 } = {}) { let response try { response = await fetch(path, { method, credentials: 'same-origin', headers: body ? { 'Content-Type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined, }) } catch { // Netzwerkfehler (offline, Server nicht erreichbar): deutscher Text statt „Failed to fetch“; status 0 = nie eine Antwort bekommen throw Object.assign(new Error('Keine Verbindung. Bitte versuch es gleich noch einmal.'), { status: 0, fehler: {} }) } 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)