1import { Controller } from "@hotwired/stimulus"; 2 3function parseNumber(value) { 4 const parsed = parseFloat(String(value ?? "").replace(",", ".")); 5 return Number.isNaN(parsed) ? 0 : parsed; 6} 7 8function round(amount) { 9 return Math.round(amount * 100) / 100; 10} 11 12function formatCurrency(amount) { 13 return new Intl.NumberFormat("de-LU", { 14 style: "currency", 15 currency: "EUR", 16 minimumFractionDigits: 2, 17 maximumFractionDigits: 2, 18 }).format(amount); 19} 20 21function lineAmounts(quantity, priceInclVat, taxRate, discountPercent, discountAmount) { 22 const grossBefore = round(quantity * priceInclVat); 23 let discount = 0; 24 25 if (discountAmount > 0) { 26 discount = Math.min(discountAmount, grossBefore); 27 } else if (discountPercent > 0) { 28 discount = round((grossBefore * discountPercent) / 100); 29 } 30 31 const gross = Math.max(grossBefore - discount, 0); 32 const divisor = 1 + taxRate / 100; 33 const net = divisor > 0 ? round(gross / divisor) : gross; 34 const vat = round(gross - net); 35 36 return { gross, vat, net }; 37} 38 39export default class extends Controller { 40 connect() { 41 this.recalculate(); 42 this._onInput = () => this.recalculate(); 43 this.element.querySelectorAll("[data-offer-qty], [data-offer-price]").forEach((input) => { 44 input.addEventListener("input", this._onInput); 45 input.addEventListener("change", this._onInput); 46 }); 47 } 48 49 disconnect() { 50 if (!this._onInput) return; 51 this.element.querySelectorAll("[data-offer-qty], [data-offer-price]").forEach((input) => { 52 input.removeEventListener("input", this._onInput); 53 input.removeEventListener("change", this._onInput); 54 }); 55 } 56 57 recalculate() { 58 let invoiceGross = 0; 59 let invoiceVat = 0; 60 61 this.element.querySelectorAll("[data-offer-line]").forEach((row) => { 62 const quantity = parseNumber(row.querySelector("[data-offer-qty]")?.value); 63 const price = parseNumber(row.querySelector("[data-offer-price]")?.value); 64 const taxRate = parseNumber(row.dataset.taxRate); 65 const discountPercent = parseNumber(row.dataset.discountPercent); 66 const discountAmount = parseNumber(row.dataset.discountAmount); 67 68 const { gross, vat } = lineAmounts(quantity, price, taxRate, discountPercent, discountAmount); 69 invoiceGross += gross; 70 invoiceVat += vat; 71 72 const lineTotal = row.querySelector("[data-offer-line-total]"); 73 if (lineTotal) { 74 lineTotal.textContent = formatCurrency(gross); 75 } 76 }); 77 78 const invoiceTotal = this.element.querySelector("[data-offer-invoice-total]"); 79 if (invoiceTotal) { 80 invoiceTotal.textContent = formatCurrency(invoiceGross); 81 } 82 83 const invoiceVatEl = this.element.querySelector("[data-offer-invoice-vat]"); 84 if (invoiceVatEl) { 85 invoiceVatEl.textContent = formatCurrency(invoiceVat); 86 } 87 } 88};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.