diff --git a/app/config.yml b/app/config.yml index e450932..392fa25 100644 --- a/app/config.yml +++ b/app/config.yml @@ -131,12 +131,6 @@ charge-to: vat-id: "GB123456789" reg-no: "01234567" currency: GBP - pay-account-holder: Example Non-Profit Organisation - pay-account-no: "GB29 NWBK 6016 1331 9268 19" - pay-bic: "NWBKGB2L" - pay-bank-address1: "NatWest, 1 Princes Street" - pay-bank-address2: "London EC2R 8PB" - pay-ref: "" # ── Project codes ────────────────────────────────────────────────────────────── project-codes: diff --git a/app/index.html b/app/index.html index 7d9a10e..0ce5ce6 100644 --- a/app/index.html +++ b/app/index.html @@ -226,33 +226,43 @@ font-size: 11.5px; color: #111827; } - .d-hdr { - display: flex; justify-content: space-between; align-items: flex-start; - padding-bottom: 20px; margin-bottom: 24px; + /* 2×2 invoice header grid */ + .d-4hdr { + display: grid; grid-template-columns: 1fr 1fr; border-bottom: 3px solid var(--navy); + margin-bottom: 22px; } + .d-4hdr > div { padding: 0; } + .d-4hdr > .d-tl { padding: 0 20px 16px 0; border-bottom: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb; } + .d-4hdr > .d-tr { padding: 0 0 16px 20px; border-bottom: 1px solid #e5e7eb; } + .d-4hdr > .d-bl { padding: 16px 20px 16px 0; border-right: 1px solid #e5e7eb; } + .d-4hdr > .d-br { padding: 16px 0 16px 20px; } + .d-sender .name { font-size: 17px; font-weight: 700; color: var(--navy); margin-bottom: 6px; } .d-sender p { font-size: 10.5px; color: #4b5563; margin-bottom: 2px; } - .d-title { text-align: right; } - .d-title h1 { font-size: 30px; font-weight: 800; letter-spacing: 5px; color: var(--navy); margin-bottom: 14px; } + + .d-pay-hdr .ph-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 6px; } + .d-pay-hdr .ph-terms { font-size: 10.5px; color: #4b5563; margin-bottom: 6px; } + .d-pay-hdr .ph-grid { display: grid; grid-template-columns: 130px 1fr; gap: 3px 8px; font-size: 10.5px; margin-top: 6px; } + .d-pay-hdr .ph-grid .pl { color: #6b7280; } + .d-pay-hdr .ph-grid .pv { font-weight: 500; word-break: break-all; } + .d-pay-hdr .ph-ref { margin-top: 6px; font-size: 10.5px; color: #4b5563; } + .d-pay-hdr .ph-ref strong { color: #111827; } + + .d-bill .bt-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 5px; } + .d-bill .bt-name { font-size: 13px; font-weight: 700; color: var(--navy); margin-bottom: 3px; } + .d-bill p { font-size: 10.5px; color: #4b5563; margin-bottom: 2px; } + .d-bill .bt-meta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 6px; } + .d-bill .bt-meta span { font-size: 10px; color: #4b5563; } + .d-bill .bt-meta strong { color: #111827; } + + .d-inv-meta { text-align: right; } + .d-inv-meta h1 { font-size: 28px; font-weight: 800; letter-spacing: 5px; color: var(--navy); margin-bottom: 14px; } .d-meta { border-collapse: collapse; margin-left: auto; } .d-meta td { font-size: 10.5px; padding: 2px 0 2px 12px; } .d-meta .ml { color: #6b7280; text-align: right; } .d-meta .mv { font-weight: 600; text-align: right; } - .d-bill { - padding: 12px 16px; margin-bottom: 22px; - background: #f8f9fa; - border-left: 4px solid var(--navy); - border-radius: 0 var(--radius) var(--radius) 0; - } - .d-bill .bt-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 5px; } - .d-bill .bt-name { font-size: 13px; font-weight: 700; color: var(--navy); margin-bottom: 3px; } - .d-bill p { font-size: 10.5px; color: #4b5563; margin-bottom: 2px; } - .d-bill .bt-meta { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 6px; } - .d-bill .bt-meta span { font-size: 10px; color: #4b5563; } - .d-bill .bt-meta strong { color: #111827; } - .d-lines { width: 100%; border-collapse: collapse; margin-bottom: 20px; } .d-lines thead tr { background: var(--navy); color: white; } .d-lines thead th { padding: 7px 10px; font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; text-align: left; } @@ -264,15 +274,6 @@ .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%; } @@ -282,24 +283,19 @@ .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; } } + /* ── Payment card form fields ────────────────────────────────────────────── */ .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 { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; } .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 / bank fields ─────────────────────────────────────── */ + /* ── Locked charge-to fields ─────────────────────────────────────────────── */ #ct-fields.locked input, - #ct-fields.locked select, - #bank-section.locked input { + #ct-fields.locked select { background: #f8f9fa; color: var(--text-muted); border-color: var(--border-light); @@ -511,36 +507,37 @@ function buildForm() {
-
${t("invoice-details-section")}
-
-
-
-
-
-
- -
- -
-
- ${t("charge-to")}: - -
-
+
+
+ ${t("charge-to")}: + +
@@ -563,31 +560,25 @@ function buildForm() {
-
-
${t("payment")}
-
- - - ${t("payment-days")} -
-
- - -
- +
+
+
${t("invoice-details-section")}
+
+
+
+
+
+
+ +
+
@@ -654,21 +645,9 @@ 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 bsec = document.getElementById("bank-section"); - if (v === "" || v === "__other__") { if (v === "") ["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat","creg"].forEach(id => f(id, "")); fields?.classList.remove("locked"); - if (bsec) { - bsec.classList.remove("locked"); - if (!hidePayment && v === "__other__") { - bsec.style.display = ""; - } else { - bsec.style.display = "none"; - ["pacct","piban","pbic","pbadr1","pbadr2","pref"].forEach(id => f(id, "")); - } - } return; } const ct = (cfg["charge-to"] || [])[+v]; @@ -680,29 +659,6 @@ function fillChargeTo(v) { f("cvat", ct["vat-id"]); f("creg", ct["reg-no"]); fields?.classList.add("locked"); - // Fill bank section from config if provided - if (bsec && !hidePayment) { - const hasBank = ct["pay-account-holder"] || ct["pay-account-no"] || ct["pay-bic"]; - if (hasBank) { - f("pacct", ct["pay-account-holder"]); - f("piban", ct["pay-account-no"]); - f("pbic", ct["pay-bic"]); - f("pbadr1", ct["pay-bank-address1"]); - f("pbadr2", ct["pay-bank-address2"]); - f("pref", ct["pay-ref"]); - bsec.style.display = ""; - bsec.classList.add("locked"); - } else { - bsec.style.display = "none"; - ["pacct","piban","pbic","pbadr1","pbadr2","pref"].forEach(id => f(id, "")); - bsec.classList.remove("locked"); - } - } else if (bsec) { - bsec.style.display = "none"; - ["pacct","piban","pbic","pbadr1","pbadr2","pref"].forEach(id => f(id, "")); - bsec.classList.remove("locked"); - } - // Auto-set invoice currency from recipient config if (ct.currency) { const icurEl = document.getElementById("icur"); @@ -1087,15 +1043,14 @@ function gatherData(renderLang) { const pTerm = parseInt(document.getElementById("pterm")?.value) || 0; const pPayBy = document.getElementById("paybydisp")?.textContent || ""; - const bsec = document.getElementById("bank-section"); - const bankVisible = bsec && bsec.style.display !== "none"; - const pAcct = bankVisible ? g("pacct") : ""; - const pIban = bankVisible ? g("piban") : ""; - const pBic = bankVisible ? g("pbic") : ""; - const pBadr1 = bankVisible ? g("pbadr1"): ""; - const pBadr2 = bankVisible ? g("pbadr2"): ""; - const pRef = bankVisible ? g("pref") : ""; + const pAcct = g("pacct"); + const pIban = g("piban"); + const pBic = g("pbic"); + const pBadr1 = g("pbadr1"); + const pBadr2 = g("pbadr2"); + const pRef = g("pref"); const hasBank = !!(pAcct || pIban || pBic || pBadr1 || pRef); + const hidePaymentOut = cfg["hide-payment-info"] === true || cfg["hide-payment-info"] === "yes"; let sub = 0; const rows = []; @@ -1150,7 +1105,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, hasBank, + pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasBank, hidePaymentOut, rows, sub, taxes, totalTax, paid, toPay }; } @@ -1158,7 +1113,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, + pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasBank, hidePaymentOut, rows, sub, taxes, paid, toPay } = gatherData(); const linesHTML = rows.map(row => { @@ -1177,16 +1132,40 @@ function buildPreviewHTML() { `; }).join("") || `—`; + const showBank = hasBank && !hidePaymentOut; return ` -
-
+
+
${sName ? `
${h(sName)}
` : ""} ${sAddr.map(a=>`

${h(a)}

`).join("")} ${sCntry ? `

${h(sCntry)}

` : ""} ${sPh ? `

${h(td("sender-phone"))}: ${h(sPh)}

` : ""} ${sEm ? `

${h(td("sender-email"))}: ${h(sEm)}

` : ""}
-
+
+
${h(td("payment"))}
+ ${pTerm > 0 ? `
${h(td("payment-terms"))}: ${pTerm} ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: ${h(pPayBy)}` : ""}
` : ""} + ${showBank ? `
+ ${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)}
` : ""}` : ""} +
+
+
${h(td("charge-to"))}
+ ${ctName ? `
${h(ctName)}
` : ""} + ${ctAddr.map(a=>`

${h(a)}

`).join("")} + ${ctCntry ? `

${h(ctCntry)}

` : ""} +
+ ${ctPh ? `${h(td("charge-to-phone"))}: ${h(ctPh)}` : ""} + ${ctEm ? `${h(td("charge-to-email"))}: ${h(ctEm)}` : ""} + ${ctVat ? `${h(td("vat-id"))}: ${h(ctVat)}` : ""} + ${ctReg ? `${h(td("registration-no"))}: ${h(ctReg)}` : ""} +
+
+

${h(td("invoice"))}

${iNo ? `` : ""} @@ -1196,19 +1175,6 @@ function buildPreviewHTML() {
${h(td("invoice-no"))}${h(iNo)}
- ${ctName ? ` -
-
${h(td("charge-to"))}
-
${h(ctName)}
- ${ctAddr.map(a=>`

${h(a)}

`).join("")} - ${ctCntry ? `

${h(ctCntry)}

` : ""} -
- ${ctPh ? `${h(td("charge-to-phone"))}: ${h(ctPh)}` : ""} - ${ctEm ? `${h(td("charge-to-email"))}: ${h(ctEm)}` : ""} - ${ctVat ? `${h(td("vat-id"))}: ${h(ctVat)}` : ""} - ${ctReg ? `${h(td("registration-no"))}: ${h(ctReg)}` : ""} -
-
` : ""} @@ -1233,18 +1199,7 @@ function buildPreviewHTML() {
${h(td("qty"))}${fmt(toPay)}
- ${(pTerm > 0 || hasBank) ? ` -
-
${h(td("payment"))}
- ${pTerm > 0 ? `
${h(td("payment-terms"))}: ${pTerm} ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: ${h(pPayBy)}` : ""}
` : ""} - ${hasBank ? `
- ${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 ───────────────────────────────────────── @@ -1274,81 +1229,99 @@ 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, + pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasBank, hidePaymentOut, rows, sub, taxes, paid, toPay } = gatherData(); - // ── HEADER ──────────────────────────────────────────────────────────────── - let y = MT; - let sy = y; // sender column bottom tracker - let ry = y; // right column bottom tracker + const showBank = hasBank && !hidePaymentOut; - // Sender name - if (sName) { - fb(13); tc(30,45,69); - tL(sName, ML, sy); sy += 6; - } - // Sender address + // ── 2×2 HEADER ─────────────────────────────────────────────────────────── + const GAP = 8; + const LW = (CW - GAP) / 2; // ~86mm each column + const XM_L = ML + LW + GAP; // left edge of right column + + let y = MT; + let ly = y, ry = y; // row-1 bottom trackers (left / right) + + // ── Row 1 left: Sender ── + if (sName) { fb(13); tc(30,45,69); tL(sName, ML, ly); ly += 6; } fn(8.5); tc(75,85,99); - [...sAddr, sCntry].filter(Boolean).forEach(line => { tL(line, ML, sy); sy += 4.5; }); - // Sender contact + [...sAddr, sCntry].filter(Boolean).forEach(l => { tL(l, ML, ly); ly += 4.5; }); if (sPh || sEm) { const parts = []; if (sPh) parts.push(`${td("sender-phone")}: ${sPh}`); if (sEm) parts.push(`${td("sender-email")}: ${sEm}`); - fn(8); tc(107,114,128); - tL(parts.join(" "), ML, sy); sy += 5; + fn(8); tc(107,114,128); tL(parts.join(" "), ML, ly); ly += 5; } - // INVOICE title (right) - fb(26); tc(30,45,69); - tR(td("invoice"), XR, ry); ry += 11; + // ── Row 1 right: Payment ── + if (pTerm > 0 || showBank) { + fb(7); tc(107,114,128); tL(td("payment").toUpperCase(), XM_L, ry); ry += 5; + if (pTerm > 0) { + const ts = `${td("payment-terms")}: ${pTerm} ${td("payment-days")}${pPayBy ? ` ${td("pay-by")}: ${pPayBy}` : ""}`; + fn(8.5); tc(17,24,39); tL(ts, XM_L, ry); ry += 5; + } + if (showBank) { + const LLBL = 46; + 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]) => { + fn(8); tc(107,114,128); tL(lbl + ":", XM_L, ry); + fn(8.5); tc(17,24,39); + const wrapped = sp(val, LW - LLBL - 2); + wrapped.forEach((line, i) => tL(line, XM_L + LLBL, ry + i * 4)); + ry += Math.max(4.5, wrapped.length * 4); + }); + if (pRef) { + fn(8); tc(107,114,128); tL(td("payment-ref") + ":", XM_L, ry); + fb(8.5); tc(17,24,39); tL(pRef, XM_L + LLBL, ry); ry += 5; + } + } + } - // Meta table (right-aligned) - const metaRows = [ - iNo ? [td("invoice-no"), iNo] : null, - iDate ? [td("invoice-date"), fmtDate(iDate)]: null, - pCode ? [td("project-code"), pCode] : null, - iCur ? [td("invoice-currency"), iCur] : null, - ].filter(Boolean); + // Row 1 divider + const row1Y = Math.max(ly, ry) + 4; + dc(209,213,219); doc.setLineWidth(0.3); + doc.line(ML, row1Y, XR, row1Y); - metaRows.forEach(([lbl, val]) => { - fn(8.5); tc(107,114,128); tR(lbl + ":", XR - 45, ry); - fb(8.5); tc(17,24,39); tR(val, XR, ry); - ry += 5; - }); - - y = Math.max(sy, ry) + 5; - - // Navy rule - dc(30,45,69); doc.setLineWidth(0.6); - doc.line(ML, y, XR, y); y += 6; - - // ── BILL-TO ────────────────────────────────────────────────────────────── + // ── Row 2 left: Charge-to ── + let ly2 = row1Y + 5, ry2 = row1Y + 5; + fb(7); tc(107,114,128); tL(td("charge-to").toUpperCase(), ML, ly2); ly2 += 5; if (ctName) { - const bLines = [...ctAddr, ctCntry].filter(Boolean); + fb(10); tc(30,45,69); tL(ctName, ML, ly2); ly2 += 5.5; + fn(8.5); tc(17,24,39); + [...ctAddr, ctCntry].filter(Boolean).forEach(l => { tL(l, ML, ly2); ly2 += 4.5; }); const ctParts = []; if (ctPh) ctParts.push(`${td("charge-to-phone")}: ${ctPh}`); if (ctEm) ctParts.push(`${td("charge-to-email")}: ${ctEm}`); if (ctVat) ctParts.push(`${td("vat-id")}: ${ctVat}`); if (ctReg) ctParts.push(`${td("registration-no")}: ${ctReg}`); - const ctContact = ctParts.join(" "); - - const boxH = 4.5 + 5 + 5.5 + bLines.length * 4.5 + (ctContact ? 4.5 : 0) + 4; - fc(248,249,250); dc(255,255,255); doc.setLineWidth(0); - doc.rect(ML, y, CW, boxH, "F"); - fc(30,45,69); - doc.rect(ML, y, 1.5, boxH, "F"); - - let by = y + 4.5; - fb(7); tc(107,114,128); tL(td("charge-to").toUpperCase(), ML+4, by); by += 5; - fb(10.5); tc(30,45,69); tL(ctName, ML+4, by); by += 5.5; - fn(8.5); tc(17,24,39); - bLines.forEach(line => { tL(line, ML+4, by); by += 4.5; }); - if (ctContact) { fn(8); tc(107,114,128); tL(ctContact, ML+4, by); } - - y += boxH + 6; + if (ctParts.length) { fn(8); tc(107,114,128); tL(ctParts.join(" "), ML, ly2); ly2 += 5; } } + // ── Row 2 right: INVOICE + meta ── + fb(24); tc(30,45,69); tR(td("invoice"), XR, ry2); ry2 += 10; + const metaRows = [ + iNo ? [td("invoice-no"), iNo] : null, + iDate ? [td("invoice-date"), fmtDate(iDate)] : null, + pCode ? [td("project-code"), pCode] : null, + iCur ? [td("invoice-currency"), iCur] : null, + ].filter(Boolean); + metaRows.forEach(([lbl, val]) => { + fn(8.5); tc(107,114,128); tR(lbl + ":", XR - 42, ry2); + fb(8.5); tc(17,24,39); tR(val, XR, ry2); + ry2 += 5; + }); + + y = Math.max(ly2, ry2) + 5; + + // Navy rule + dc(30,45,69); doc.setLineWidth(0.6); + doc.line(ML, y, XR, y); y += 6; + // ── LINE ITEMS TABLE ────────────────────────────────────────────────────── // Column widths: QTY=18, UOM=18, DESC=flex, PRICE=28, TOTAL=34 const CQ=18, CU=18, CP=28, CT=34; @@ -1455,45 +1428,6 @@ function buildPDF() { fb(11); tc(255,255,255); tR(fmt(toPay), XR-2, y+5.8); y += 9; - // ── PAYMENT DETAILS ─────────────────────────────────────────────────────── - if (pTerm > 0 || hasBank) { - 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; - } - - if (hasBank) { - 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"); }