From 72c2c9e637e63ce6262c6bf0f386cf271408589d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 08:54:41 +0000 Subject: [PATCH] Add Payment panel to charge-to section - New right-hand Payment column appears only when "Other" is selected for charge-to - config.yml: hide-payment-info key (true/yes) suppresses the panel entirely - Fields: payment terms (days), computed pay-by date, account holder, account no. (BBAN/IBAN), bank/BIC, bank address (2 lines), please use reference - calcPayBy() derives the pay-by date from invoice date + terms days using fmtDate() - relabel() keeps all payment labels in sync on language switch - gatherData() captures payment fields only when panel is visible - buildPreviewHTML() and buildPDF() render a payment section after totals when present https://claude.ai/code/session_015iyCBgoTXNNqaErR287U1u --- app/config.yml | 50 ++++++++++++++ app/index.html | 179 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 207 insertions(+), 22 deletions(-) diff --git a/app/config.yml b/app/config.yml index 38e6132..392fa25 100644 --- a/app/config.yml +++ b/app/config.yml @@ -21,6 +21,11 @@ languages: name: Norsk direction: ltr +# ── Payment info visibility ─────────────────────────────────────────────────── +# Set to true to hide the payment info panel entirely (useful if payment info +# should not appear on invoices, e.g. per company policy). +hide-payment-info: false + # ── 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 @@ -460,3 +465,48 @@ translations: de: Rechnungsdetails fr: Détails de la facture "no": Fakturaopplysninger + payment: + en: Payment + de: Zahlung + fr: Paiement + "no": Betaling + payment-terms: + en: Payment terms + de: Zahlungsbedingungen + fr: Conditions de paiement + "no": Betalingsbetingelser + payment-days: + en: days + de: Tage + fr: jours + "no": dager + pay-by: + en: Pay by + de: Zahlbar bis + fr: "À payer avant le" + "no": Betal innen + account-holder: + en: Account holder + de: Kontoinhaber + fr: Titulaire du compte + "no": Kontohaver + account-no: + en: "Account no. (BBAN/IBAN)" + de: "Kontonummer (BBAN/IBAN)" + fr: "N° de compte (BBAN/IBAN)" + "no": "Kontonr. (BBAN/IBAN)" + bank-bic: + en: "Bank / BIC (Swift code)" + de: "Bank / BIC (Swift)" + fr: "Banque / BIC (code Swift)" + "no": "Bank / BIC (Swift-kode)" + bank-address: + en: Bank address + de: Bankadresse + fr: Adresse de la banque + "no": Bankadresse + payment-ref: + en: Please use reference + de: Bitte Referenz angeben + fr: "Merci d'indiquer la référence" + "no": Vennligst oppgi referanse diff --git a/app/index.html b/app/index.html index cac1a29..9ebd3a4 100644 --- a/app/index.html +++ b/app/index.html @@ -264,6 +264,15 @@ .d-lines tbody td.b { font-weight: 600; } .d-fx-note { font-size: 9.5px; color: #6b7280; margin-top: 3px; } + .d-payment { border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 14px; } + .d-payment .pay-title { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 8px; } + .d-payment .pay-terms { font-size: 10.5px; color: #4b5563; margin-bottom: 8px; } + .d-payment .pay-grid { display: grid; grid-template-columns: 140px 1fr; gap: 3px 10px; font-size: 10.5px; margin-bottom: 4px; } + .d-payment .pay-grid .pl { color: #6b7280; } + .d-payment .pay-grid .pv { color: #111827; font-weight: 500; } + .d-payment .pay-ref { margin-top: 8px; font-size: 10.5px; color: #4b5563; } + .d-payment .pay-ref strong { color: #111827; } + .d-tots { width: 100%; border-collapse: collapse; } .d-tots td { padding: 5px 10px; font-size: 11.5px; } .d-tots .sp { width: 55%; } @@ -273,6 +282,20 @@ .d-tots .fin td { background: var(--navy); color: white; font-size: 14px; font-weight: 700; border-top: 2px solid var(--navy); } .d-tots .fin .tl { color: rgba(255,255,255,.75); } + /* ── Charge-to / Payment two-column layout ──────────────────────────────── */ + .ct-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; } + @media (max-width: 720px) { .ct-two-col { grid-template-columns: 1fr; } } + #payment-panel { border-left: 2px solid var(--border-light); padding-left: 20px; } + @media (max-width: 720px) { #payment-panel { border-left: none; padding-left: 0; border-top: 2px solid var(--border-light); padding-top: 16px; } } + .pay-section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: var(--navy); margin-bottom: 10px; } + .pay-terms-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; } + .pay-terms-row label { font-size: 12px; color: var(--text-muted); white-space: nowrap; } + .pay-terms-row input { width: 64px; } + .pay-terms-row span { font-size: 12px; color: var(--text-muted); } + .pay-by-row { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; } + .pay-by-row label { font-size: 12px; color: var(--text-muted); white-space: nowrap; } + #paybydisp { font-size: 13px; font-weight: 600; color: var(--text); } + /* ── Locked charge-to fields ────────────────────────────────────────────── */ #ct-fields.locked input, #ct-fields.locked select { @@ -516,27 +539,52 @@ function buildForm() { -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -591,6 +639,7 @@ function buildForm() { document.getElementById("pcode-other-wrap").style.display = this.value === "__other__" ? "block" : "none"; }); document.getElementById("ct-pick").addEventListener("change", e => fillChargeTo(e.target.value)); + document.getElementById("idate").addEventListener("change", calcPayBy); document.getElementById("paid-inp").addEventListener("input", calcTotals); document.getElementById("the-form").addEventListener("submit", e => { e.preventDefault(); generateInvoice(); }); document.querySelectorAll("[data-ls]").forEach(el => el.addEventListener("change", saveStorage)); @@ -601,6 +650,10 @@ function fillChargeTo(v) { const f = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ""; }; const fields = document.getElementById("ct-fields"); + const hidePayment = cfg["hide-payment-info"] === true || cfg["hide-payment-info"] === "yes"; + const ppanel = document.getElementById("payment-panel"); + if (ppanel) ppanel.style.display = (!hidePayment && v === "__other__") ? "" : "none"; + if (v === "" || v === "__other__") { if (v === "") ["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat","creg"].forEach(id => f(id, "")); fields?.classList.remove("locked"); @@ -810,6 +863,21 @@ function calcLine(i) { calcTotals(); } +function calcPayBy() { + const idate = document.getElementById("idate")?.value; + const terms = parseInt(document.getElementById("pterm")?.value) || 0; + const el = document.getElementById("paybydisp"); + if (!el) return; + if (idate && terms > 0) { + const d = new Date(idate + "T00:00:00"); + d.setDate(d.getDate() + terms); + const iso = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; + el.textContent = fmtDate(iso); + } else { + el.textContent = ""; + } +} + function calcTotals() { let sub = 0; Object.keys(lines).forEach(i => { @@ -885,6 +953,8 @@ function relabel() { "lbl-ctn":"charge-to-name","lbl-ca1":"charge-to-address1","lbl-ca2":"charge-to-address2", "lbl-ca3":"charge-to-address3","lbl-ca4":"charge-to-address4","lbl-cc":"charge-to-country", "lbl-cph":"charge-to-phone","lbl-cem":"charge-to-email","lbl-cvat":"vat-id","lbl-creg":"registration-no", + "lbl-pay-sec":"payment","lbl-pterm":"payment-terms","lbl-days":"payment-days","lbl-paybyl":"pay-by", + "lbl-pacct":"account-holder","lbl-piban":"account-no","lbl-pbic":"bank-bic","lbl-pbadr":"bank-address","lbl-pref":"payment-ref", "th-qty":"qty","th-uom":"uom","th-desc":"description","th-price":"price","th-tot":"line-total", "lbl-sub":"subtotal","lbl-paid":"paid","lbl-topay":"to-pay", }; @@ -980,6 +1050,18 @@ function gatherData(renderLang) { const ctCntry= COUNTRY_MAP[g("cc")] || ""; const ctPh = g("cph"), ctEm = g("cem"), ctVat = g("cvat"), ctReg = g("creg"); + const ppanel = document.getElementById("payment-panel"); + const payVisible = ppanel && ppanel.style.display !== "none"; + const pTerm = payVisible ? (parseInt(document.getElementById("pterm")?.value) || 0) : 0; + const pPayBy = payVisible ? (document.getElementById("paybydisp")?.textContent || "") : ""; + const pAcct = payVisible ? g("pacct") : ""; + const pIban = payVisible ? g("piban") : ""; + const pBic = payVisible ? g("pbic") : ""; + const pBadr1 = payVisible ? g("pbadr1"): ""; + const pBadr2 = payVisible ? g("pbadr2"): ""; + const pRef = payVisible ? g("pref") : ""; + const hasPayment = payVisible && !!(pAcct || pIban || pBic || pBadr1 || pRef || pTerm > 0); + let sub = 0; const rows = []; Object.keys(lines).sort((a,b)=>+a-+b).forEach(i => { @@ -1033,6 +1115,7 @@ function gatherData(renderLang) { return { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur, ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat, ctReg, + pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasPayment, rows, sub, taxes, totalTax, paid, toPay }; } @@ -1040,6 +1123,7 @@ function gatherData(renderLang) { function buildPreviewHTML() { const { td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur, ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat, ctReg, + pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasPayment, rows, sub, taxes, paid, toPay } = gatherData(); const linesHTML = rows.map(row => { @@ -1113,7 +1197,19 @@ function buildPreviewHTML() { ${h(td("to-pay"))} ${fmt(toPay)} - `; + + ${hasPayment ? ` +
+
${h(td("payment"))}
+ ${pTerm > 0 ? `
${h(td("payment-terms"))}: ${pTerm} ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: ${h(pPayBy)}` : ""}
` : ""} +
+ ${pAcct ? `${h(td("account-holder"))}${h(pAcct)}` : ""} + ${pIban ? `${h(td("account-no"))}${h(pIban)}` : ""} + ${pBic ? `${h(td("bank-bic"))}${h(pBic)}` : ""} + ${pBadr1 || pBadr2 ? `${h(td("bank-address"))}${[pBadr1,pBadr2].filter(Boolean).map(l=>h(l)).join("
")}
` : ""} +
+ ${pRef ? `
${h(td("payment-ref"))}: ${h(pRef)}
` : ""} +
` : ""}`; } // ── Build PDF with jsPDF + Helvetica ───────────────────────────────────────── @@ -1143,6 +1239,7 @@ function buildPDF() { const { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur, ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat, ctReg, + pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasPayment, rows, sub, taxes, paid, toPay } = gatherData(); // ── HEADER ──────────────────────────────────────────────────────────────── @@ -1321,6 +1418,44 @@ function buildPDF() { doc.rect(TX, y, TW, 9, "F"); fn(9); tc(180,195,215); tR(td("to-pay"), TLBX, y+5.8); fb(11); tc(255,255,255); tR(fmt(toPay), XR-2, y+5.8); + y += 9; + + // ── PAYMENT DETAILS ─────────────────────────────────────────────────────── + if (hasPayment) { + y += 8; + if (y + 8 > PH - 15) { doc.addPage(); y = MT; } + dc(209,213,219); doc.setLineWidth(0.4); + doc.line(ML, y, XR, y); y += 5; + + fb(7); tc(107,114,128); tL(td("payment").toUpperCase(), ML, y); y += 5; + + if (pTerm > 0) { + const termsStr = `${td("payment-terms")}: ${pTerm} ${td("payment-days")}${pPayBy ? ` ${td("pay-by")}: ${pPayBy}` : ""}`; + fn(8.5); tc(17,24,39); tL(termsStr, ML, y); y += 5; + } + + const LBL = 50; + const payRows = [ + pAcct ? [td("account-holder"), pAcct] : null, + pIban ? [td("account-no"), pIban] : null, + pBic ? [td("bank-bic"), pBic] : null, + (pBadr1 || pBadr2) ? [td("bank-address"), [pBadr1,pBadr2].filter(Boolean).join(", ")] : null, + ].filter(Boolean); + + payRows.forEach(([lbl, val]) => { + if (y + 5 > PH - 10) { doc.addPage(); y = MT; } + fn(8); tc(107,114,128); tL(lbl + ":", ML, y); + fn(8.5); tc(17,24,39); tL(val, ML + LBL, y); + y += 4.5; + }); + + if (pRef) { + y += 1; + if (y + 6 > PH - 10) { doc.addPage(); y = MT; } + fn(8); tc(107,114,128); tL(td("payment-ref") + ":", ML, y); + fb(9); tc(17,24,39); tL(pRef, ML + LBL, y); + } + } // ── Save ────────────────────────────────────────────────────────────────── doc.save(iNo ? `invoice-${iNo}.pdf` : "invoice.pdf");