Buchungskalender: Portal-Bereich und Anfragen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:06:48 +00:00
1 parent d7b5b98187
commit 3ecea05b52
11 files changed
+811 -4

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
/* 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.') : null)
}
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 } }
}