127 lines
6.7 KiB
JavaScript
127 lines
6.7 KiB
JavaScript
/* Auftragsbestätigung im Kundenportal: DOM-Bausteine (Zusammenfassung, Bestätigt-Block, Unterlagen, Bestätigen-Formular).
|
|
Kundentext und Servertext kommen ausschließlich über textContent in die Seite (h), nie über innerHTML.
|
|
Alle Texte stehen in auftrag-ansicht.js; die Anrede (Sie auf der öffentlichen Seite, Du im Portal) wählt der Aufrufer. */
|
|
import { ANREDE_SIE, BESTAETIGUNG_FALLBACK, bestaetigtZeilen, fehlerMeldung, texte, zeigtFruehbeginn, zusammenfassung } from './auftrag-ansicht.js'
|
|
|
|
/** 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
|
|
}
|
|
|
|
const zeile = (label, wert) => h('div', { class: 'ab-zeile' }, h('dt', null, label), h('dd', null, wert))
|
|
const liste = (zeilen) => h('dl', { class: 'ab-liste' }, zeilen.map((z) => zeile(z.label, z.wert)))
|
|
|
|
/** Zusammenfassung der AB (Kopf, Leistung bzw. Aufwand, Zahlung, Termine); ebene = Überschriftenebene der Abschnitte. */
|
|
export function renderZusammenfassung(auftrag, { ebene = 2 } = {}) {
|
|
const t = texte(ANREDE_SIE)
|
|
const z = zusammenfassung(auftrag)
|
|
const kopfzeile = (text) => h(`h${ebene}`, { class: 'ab-h2' }, text)
|
|
const abschnitt = (name, titel, ...inhalt) => h('section', { class: 'ab-abschnitt', 'data-abschnitt': name }, kopfzeile(titel), ...inhalt)
|
|
const hinweis = z.hinweis ? h('p', { class: 'ab-hinweis', 'data-hinweis': true }, z.hinweis) : null
|
|
|
|
const teile = []
|
|
if (z.kopf.length) teile.push(liste(z.kopf))
|
|
if (z.art === 'aufwand') {
|
|
teile.push(abschnitt('aufwand', t.aufwand, h('p', { class: 'ab-text', 'data-aufwand': true }, z.aufwand), hinweis))
|
|
} else {
|
|
const tabelle = z.positionen.length
|
|
? h('table', { class: 'ab-tabelle', 'data-positionen': true },
|
|
h('thead', null, h('tr', null, t.spalten.map((spalte) => h('th', { scope: 'col' }, spalte)))),
|
|
h('tbody', null, z.positionen.map((p) => h('tr', { 'data-position': true },
|
|
[p.beschreibung, p.menge, p.einzelpreis, p.summe].map((wert, i) => h('td', { 'data-label': t.spalten[i] }, wert))))))
|
|
: null
|
|
const gesamt = z.summe ? h('p', { class: 'ab-gesamt', 'data-gesamt': true }, h('span', null, t.gesamtpreis), ' ', h('strong', null, z.summe)) : null
|
|
teile.push(abschnitt('leistung', t.leistung, tabelle, gesamt, hinweis))
|
|
}
|
|
if (z.zahlung) teile.push(abschnitt('zahlung', t.zahlung, h('p', { class: 'ab-text', 'data-zahlung': true }, z.zahlung)))
|
|
if (z.termine.length) teile.push(abschnitt('termine', t.termine, liste(z.termine)))
|
|
return h('div', { class: 'ab-zusammenfassung', 'data-art': z.art }, teile)
|
|
}
|
|
|
|
/** Block „bestätigt“ (Zeitpunkt, bei Privatpersonen Zeile zum Beginn); null ohne Bestätigung. */
|
|
export function renderBestaetigtBlock(auftrag, { anrede = ANREDE_SIE } = {}) {
|
|
const zeilen = bestaetigtZeilen(auftrag, anrede)
|
|
if (!zeilen.length) return null
|
|
return h('div', { class: 'ab-bestaetigt', 'data-bestaetigt': true, role: 'status', tabindex: '-1' },
|
|
zeilen.map((text, i) => h('p', null, i === 0 ? h('strong', null, text) : text)))
|
|
}
|
|
|
|
/** Links auf die AB als PDF und die AGB (neuer Tab); null ohne Ziele. */
|
|
export function renderDokumente({ pdfHref = '', agbHref = '' } = {}) {
|
|
const t = texte(ANREDE_SIE)
|
|
const link = (art, href, text) => h('li', null, h('a', { class: 'ab-link', 'data-link': art, href, target: '_blank', rel: 'noopener noreferrer' }, text))
|
|
const eintraege = [pdfHref ? link('pdf', pdfHref, t.linkPdf) : null, agbHref ? link('agb', agbHref, t.linkAgb) : null].filter(Boolean)
|
|
return eintraege.length ? h('ul', { class: 'ab-links' }, eintraege) : null
|
|
}
|
|
|
|
/**
|
|
* Formular zum Bestätigen: ein Häkchen, bei Privatpersonen ein zweites, freiwilliges (Frühbeginn). Sendet genau einmal:
|
|
* senden(koerper) → { ok: true, auftrag } | { ok: false, status, body }; nachErfolg(auftrag); nachEnde(status, body) bei 404/410.
|
|
* gesperrt (Text) = Ansichtsmodus: Häkchen und Knopf gesperrt, Hinweis sichtbar.
|
|
*/
|
|
export function renderBestaetigung(auftrag, { senden, nachErfolg, nachEnde, gesperrt = '', anrede = ANREDE_SIE } = {}) {
|
|
const t = texte(anrede)
|
|
let busy = false
|
|
const haken1 = h('input', { type: 'checkbox', 'data-feld': 'bestaetigung', required: true, disabled: Boolean(gesperrt) })
|
|
const haken2 = zeigtFruehbeginn(auftrag) ? h('input', { type: 'checkbox', 'data-feld': 'fruehbeginn', disabled: Boolean(gesperrt) }) : null
|
|
const fehler = h('p', { class: 'ab-fehler', 'data-fehler': true, role: 'alert', hidden: true })
|
|
const knopf = h('button', { type: 'submit', class: 'ab-knopf', 'data-aktion': 'bestaetigen', disabled: true }, t.knopf)
|
|
|
|
const aktualisiere = () => { knopf.disabled = Boolean(gesperrt) || busy || !haken1.checked }
|
|
|
|
const form = h('form', {
|
|
class: 'ab-bestaetigen',
|
|
novalidate: true,
|
|
onsubmit: async (ev) => {
|
|
ev.preventDefault()
|
|
if (busy || gesperrt || !haken1.checked) return
|
|
busy = true
|
|
fehler.hidden = true
|
|
knopf.disabled = true
|
|
knopf.setAttribute('aria-busy', 'true')
|
|
knopf.textContent = t.sendet
|
|
let ergebnis
|
|
try {
|
|
ergebnis = await senden({ bestaetigt: true, fruehbeginn: Boolean(haken2?.checked) })
|
|
} catch {
|
|
ergebnis = { ok: false, status: 0, body: {} }
|
|
}
|
|
knopf.removeAttribute('aria-busy')
|
|
knopf.textContent = t.knopf
|
|
if (ergebnis?.ok) {
|
|
nachErfolg?.(ergebnis.auftrag)
|
|
return
|
|
}
|
|
const status = ergebnis?.status ?? 0
|
|
if (status === 404 || status === 410) {
|
|
nachEnde?.(status, ergebnis?.body)
|
|
return
|
|
}
|
|
busy = false
|
|
aktualisiere()
|
|
fehler.textContent = fehlerMeldung(status, anrede)
|
|
fehler.hidden = false
|
|
},
|
|
},
|
|
h('fieldset', { class: 'ab-feldgruppe' },
|
|
h('legend', null, t.formular),
|
|
h('label', { class: 'ab-haken' }, haken1, h('span', null, auftrag?.texte?.bestaetigung || BESTAETIGUNG_FALLBACK)),
|
|
haken2 ? h('label', { class: 'ab-haken' }, haken2, h('span', null, auftrag.texte.fruehbeginn, ' ', h('small', { class: 'ab-frei' }, t.freiwillig))) : null),
|
|
fehler,
|
|
knopf,
|
|
h('p', { class: 'ab-notiz' }, t.speicherHinweis),
|
|
gesperrt ? h('p', { class: 'ab-gesperrt', 'data-gesperrt': true }, gesperrt) : null)
|
|
|
|
haken1.addEventListener('change', aktualisiere)
|
|
aktualisiere()
|
|
return form
|
|
}
|