/* Buchungskalender: Posten-Editor fuer die Freigabe einer Anfrage. summeLive(posten, steuerart) ist ein Spiegel von server/services/unterkunft/preis.js summiere (gleiche Rundung): Anzeige waehrend des Tippens, verbindlich rechnet der Server. Posten: { text, menge, einzelCent (negativ bei Rabatt), ustProzent }. */ import { euroText, euroZuCent, h } from './shop-dom.js' export function summeLive(posten, steuerart) { const bruttoCent = posten.reduce((a, p) => a + p.menge * p.einzelCent, 0) if (steuerart === 'kleinunternehmer') return { bruttoCent, nettoCent: bruttoCent, ust: [] } const proSatz = new Map() for (const p of posten) proSatz.set(p.ustProzent, (proSatz.get(p.ustProzent) || 0) + p.menge * p.einzelCent) const ust = [...proSatz.entries()] .sort((a, b) => a[0] - b[0]) .map(([satz, brutto]) => { const nettoCent = Math.round((brutto * 100) / (100 + satz)) return { satz, nettoCent, steuerCent: brutto - nettoCent } }) const nettoCent = ust.reduce((a, u) => a + u.nettoCent, 0) return { bruttoCent, nettoCent, ust } } /** Betrag mit Vorzeichen (Rabatt) in Euro-Schreibweise: -3850 ⇒ '-38,50 €'. */ export const euroMitVorzeichen = (cent) => (Number.isInteger(cent) ? (cent < 0 ? `-${euroText(-cent)}` : euroText(cent)) : '') /** Feldtext ('-38,50', '−38,50 €', '12,5') in Cent mit Vorzeichen; null bei allem Unlesbaren. */ export function euroZuCentMitVorzeichen(text) { if (typeof text !== 'string') return null const s = text.trim() const minus = /^[-−–]/.test(s) const c = euroZuCent(minus ? s.slice(1) : s) return c === null ? null : minus ? -c : c } const centFeld = (cent) => (Number.isInteger(cent) ? `${cent < 0 ? '-' : ''}${Math.floor(Math.abs(cent) / 100)},${String(Math.abs(cent) % 100).padStart(2, '0')}` : '') /** * Editor bauen. => { el, posten() => [{text,menge,einzelCent,ustProzent}] oder null bei Unlesbarem, summe() }. * steuerart 'kleinunternehmer': keine USt-Spalte (ustProzent 0). aenderung() wird nach jeder Eingabe gerufen. */ export function baueEditor(start, steuerart, { aenderung } = {}) { const klein = steuerart === 'kleinunternehmer' const zeilen = [] const liste = h('div', { class: 'bk-posten' }) const summenEl = h('div', { class: 'bk-summen', 'data-summen': true, 'aria-live': 'polite' }) const lies = () => { const ergebnis = [] for (const z of zeilen) { const einzelCent = euroZuCentMitVorzeichen(String(z.einzel.value)) const menge = Number(String(z.menge.value).trim()) if (einzelCent === null || !Number.isInteger(menge) || menge < 1) return null ergebnis.push({ text: String(z.text.value).trim(), menge, einzelCent, ustProzent: klein ? 0 : Number(z.ust.value) }) } return ergebnis } const zeichneSumme = () => { const p = lies() if (!p) { summenEl.replaceChildren(h('p', { class: 'sh-fehler' }, 'Bitte prüfe Menge und Preis der Posten.')) return } const s = summeLive(p, steuerart) summenEl.replaceChildren( ...(klein ? [] : [h('p', null, `Netto ${euroMitVorzeichen(s.nettoCent)}`), ...s.ust.map((u) => h('p', null, `${u.satz} % USt ${euroMitVorzeichen(u.steuerCent)}`))]), h('p', { class: 'bk-gesamt' }, h('strong', null, `Gesamt ${euroMitVorzeichen(s.bruttoCent)}`)), ...(klein ? [h('p', { class: 'sh-hinweis' }, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')] : [])) } const geaendert = () => { zeichneSumme() aenderung?.() } function fuegeZeile(p = { text: '', menge: 1, einzelCent: 0, ustProzent: 7 }) { const z = { text: h('input', { type: 'text', maxlength: '200', 'aria-label': 'Beschreibung', 'data-posten': 'text' }), menge: h('input', { type: 'text', inputmode: 'numeric', 'aria-label': 'Menge', 'data-posten': 'menge' }), einzel: h('input', { type: 'text', inputmode: 'decimal', autocomplete: 'off', 'aria-label': 'Einzelpreis in Euro (Rabatt mit Minus)', 'data-posten': 'einzel' }), ust: klein ? null : h('select', { 'aria-label': 'Umsatzsteuer', 'data-posten': 'ust' }, [7, 19, 0].map((satz) => h('option', { value: String(satz) }, `${satz} %`))), } z.text.value = p.text z.menge.value = String(p.menge) z.einzel.value = centFeld(p.einzelCent) if (z.ust) z.ust.value = String(p.ustProzent) for (const el of [z.text, z.menge, z.einzel, z.ust]) el?.addEventListener('input', geaendert) z.ust?.addEventListener('change', geaendert) z.einzel.addEventListener('change', () => { const c = euroZuCentMitVorzeichen(String(z.einzel.value)) if (c !== null) z.einzel.value = centFeld(c) }) z.zeile = h('div', { class: 'bk-posten-zeile', 'data-zeile': true }, z.text, z.menge, z.einzel, z.ust, h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'posten-weg', 'aria-label': 'Posten entfernen', onclick: () => { zeilen.splice(zeilen.indexOf(z), 1) z.zeile.remove() geaendert() }, }, 'Entfernen')) zeilen.push(z) liste.append(z.zeile) } for (const p of start) fuegeZeile(p) const kopf = h('div', { class: 'bk-posten-kopf', 'aria-hidden': 'true' }, h('span', null, 'Beschreibung'), h('span', null, 'Menge'), h('span', null, 'Einzelpreis €'), klein ? null : h('span', null, 'USt'), h('span')) const el = h('div', { class: 'bk-editor' }, kopf, liste, h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'posten-neu', onclick: () => { fuegeZeile(); geaendert() } }, 'Posten hinzufügen'), summenEl) zeichneSumme() return { el, posten: lies, summe: () => { const p = lies(); return p ? summeLive(p, steuerart) : null } } }