110 lines
5.6 KiB
JavaScript
110 lines
5.6 KiB
JavaScript
/* 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 } }
|
||
}
|