diff --git a/app/config.yml b/app/config.yml index 749c178..3b91aff 100644 --- a/app/config.yml +++ b/app/config.yml @@ -21,33 +21,45 @@ languages: name: Norsk direction: ltr -# ── Tax / VAT options ────────────────────────────────────────────────────────── -tax-rates: - - label-en: "No Tax (0%)" - label-de: "Keine Steuer (0%)" - label-fr: "Sans taxe (0%)" - label-no: "Ingen mva (0%)" - rate: 0 - - label-en: "VAT 5%" - label-de: "MwSt. 5%" - label-fr: "TVA 5%" - label-no: "Mva 5%" - rate: 5 - - label-en: "VAT 10%" - label-de: "MwSt. 10%" - label-fr: "TVA 10%" - label-no: "Mva 10%" - rate: 10 - - label-en: "VAT 20%" - label-de: "MwSt. 20%" - label-fr: "TVA 20%" - label-no: "Mva 20%" - rate: 20 - - label-en: "VAT 25%" - label-de: "MwSt. 25%" - label-fr: "TVA 25%" - label-no: "Mva 25%" - rate: 25 +# ── Date and paper format ───────────────────────────────────────────────────── +# Date tokens: d=day, dd=day(0-padded), M=month number, MM=month(0-padded), +# MMM=short month name, MMMM=full month name, YY=2-digit year, YYYY=4-digit year +date-format: "d MMMM YYYY" +# paper-format: a4 or letter +paper-format: a4 + +# ── Tax types (user builds tax lines; these are the label options) ───────────── +tax-types: + - key: vat + labels: + en: VAT + de: MwSt. + fr: TVA + "no": MVA + - key: gst + labels: + en: GST + de: GST + fr: TPS + "no": GST + - key: sales-tax + labels: + en: Sales Tax + de: Umsatzsteuer + fr: Taxe de vente + "no": Omsetningsavgift + - key: withholding + labels: + en: Withholding Tax + de: Quellensteuer + fr: "Retenue à la source" + "no": Kildeskatt + - key: other + labels: + en: Other Tax + de: Sonstige Steuer + fr: Autre taxe + "no": Annen skatt # ── Units of measure ────────────────────────────────────────────────────────── uom: @@ -396,6 +408,21 @@ translations: de: Rechnungswährung fr: Devise de facturation "no": Fakturavaluta + add-tax: + en: "+ Add new tax" + de: "+ Neue Steuer hinzufügen" + fr: "+ Ajouter une taxe" + "no": "+ Legg til skatt" + tax-pct: + en: "%" + de: "%" + fr: "%" + "no": "%" + tax-amount: + en: Amount + de: Betrag + fr: Montant + "no": Beløp converted-from: en: Converted from de: Umgerechnet aus diff --git a/app/index.html b/app/index.html index 084457f..e691181 100644 --- a/app/index.html +++ b/app/index.html @@ -160,8 +160,15 @@ .tot-tbl .val { text-align: right; width: 150px; font-size: 13px; font-weight: 600; } .tot-tbl .final td { background: var(--navy); color: var(--white); font-size: 15px; font-weight: 700; } .tot-tbl .final .lbl { color: rgba(255,255,255,.75); } - .tax-sel { width: auto !important; display: inline-block; } .paid-inp { width: 120px !important; text-align: right; } + .tax-inputs { display: flex; align-items: center; gap: 5px; justify-content: flex-end; } + .tax-inputs input[type="number"] { width: 68px; } + .tax-inputs select { width: auto; } + .btn-rm-tax { background: none; color: var(--danger); font-size: 16px; line-height: 1; padding: 1px 4px; border-radius: 3px; } + .btn-rm-tax:hover { background: #fef2f2; } + .tax-add-cell { text-align: right; padding: 5px 16px !important; } + .btn-add-tax { background: none; color: var(--accent); font-size: 12px; font-weight: 500; padding: 3px 8px; border: 1px dashed var(--accent); border-radius: var(--radius); } + .btn-add-tax:hover { background: #eff6ff; } /* ── Generate button ────────────────────────────────────────────────────── */ #btn-generate { @@ -325,7 +332,9 @@ let cfg = null; let lang = "en"; let lid = 0; -const lines = {}; +const lines = {}; +let tlid = 0; +const tLines = {}; // ── i18n ────────────────────────────────────────────────────────────────────── function t(key) { @@ -334,12 +343,6 @@ function t(key) { return e[lang] ?? e[cfg["default-code"]] ?? key; } -function tTax(tr, l) { - const lk = "label-" + (l || lang); - const fb = "label-" + (cfg["default-code"] || "en"); - return tr[lk] ?? tr[fb] ?? `Tax ${tr.rate}%`; -} - // ── Numbers ─────────────────────────────────────────────────────────────────── function fmt(n) { if (n === "" || n == null || isNaN(+n)) return "0.00"; @@ -348,10 +351,28 @@ function fmt(n) { function pn(s) { return parseFloat(String(s ?? 0).replace(/,/g, "")) || 0; } // ── Date ───────────────────────────────────────────────────────────────────── +const MONTHS_FULL = ["January","February","March","April","May","June", + "July","August","September","October","November","December"]; +const MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"]; + function fmtDate(v) { if (!v) return ""; - const [y, m, d] = v.split("-"); - return new Date(+y, +m - 1, +d).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); + const [yr, mo, dy] = v.split("-").map(Number); + const pattern = cfg?.["date-format"] || "d MMMM YYYY"; + return pattern.replace(/YYYY|YY|MMMM|MMM|MM|M|dd|d/g, tok => { + switch (tok) { + case "YYYY": return yr; + case "YY": return String(yr).slice(-2); + case "MMMM": return MONTHS_FULL[mo - 1]; + case "MMM": return MONTHS_SHORT[mo - 1]; + case "MM": return String(mo).padStart(2, "0"); + case "M": return mo; + case "dd": return String(dy).padStart(2, "0"); + case "d": return dy; + default: return tok; + } + }); } // ── HTML escape ─────────────────────────────────────────────────────────────── @@ -440,10 +461,8 @@ function buildForm() { const curOpts = (cfg.currencies || []).map((c, i) => ``).join(""); - const pcOpts = (cfg["project-codes"] || []).map(pc => ``).join(""); - const ctOpts = (cfg["charge-to"] || []).map((ct, i) => ``).join(""); - const taxOpts = (cfg["tax-rates"] || []).map((tr, i) => - ``).join(""); + const pcOpts = (cfg["project-codes"] || []).map(pc => ``).join(""); + const ctOpts = (cfg["charge-to"] || []).map((ct, i) => ``).join(""); document.getElementById("form-root").innerHTML = `