mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
Restructure layout to 2×2 grid: Sender|Payment / Charge-to|Invoice details
Form and invoice output now follow the same four-panel layout: Sender | Payment details Charge to | Invoice details (INVOICE title + meta) Invoice lines (full width) - Payment card is standalone (top-right), bank fields always visible - Bank fields saved to localStorage so sender enters them once - hide-payment-info suppresses bank details from invoice output only - fillChargeTo() no longer touches payment/bank fields - gatherData() always collects bank fields; hidePaymentOut passed to renderers - buildPreviewHTML() uses .d-4hdr grid with four .d-tl/.d-tr/.d-bl/.d-br cells - buildPDF() rewritten: row-1 (sender|payment), light divider, row-2 (charge-to|INVOICE+meta), navy rule, then lines - Removed bank config fields from Example NGO (those belong to the sender) https://claude.ai/code/session_015iyCBgoTXNNqaErR287U1u
This commit is contained in:
parent
e4cff52909
commit
275d3a3b71
2 changed files with 189 additions and 261 deletions
|
|
@ -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:
|
||||
|
|
|
|||
418
app/index.html
418
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,26 +507,28 @@ function buildForm() {
|
|||
<input id="se" type="email" data-ls="se" autocomplete="email"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title" id="sec-invdet">${t("invoice-details-section")}</div>
|
||||
<div class="fg"><label id="lbl-idate" for="idate">${t("invoice-date")}</label>
|
||||
<input id="idate" type="date" value="${dateDef}"></div>
|
||||
<div class="fg"><label id="lbl-icur" for="icur">${t("invoice-currency")}</label>
|
||||
<select id="icur" data-ls="icur">${curOpts}</select></div>
|
||||
<div class="fg"><label id="lbl-pcode" for="pcode">${t("project-code")}</label>
|
||||
<select id="pcode">
|
||||
<option value="">${t("select")}</option>
|
||||
${pcOpts}
|
||||
<option value="__other__">${t("other")}</option>
|
||||
</select></div>
|
||||
<div class="fg" id="pcode-other-wrap" style="display:none">
|
||||
<label id="lbl-pcode-other" for="pcode-other">${t("project-code")} (${t("other")})</label>
|
||||
<input id="pcode-other" type="text">
|
||||
<div class="card-title" id="lbl-pay-sec">${t("payment")}</div>
|
||||
<div class="pay-terms-row">
|
||||
<label id="lbl-pterm">${t("payment-terms")}:</label>
|
||||
<input id="pterm" type="number" min="0" value="7" oninput="calcPayBy()" data-ls="pterm">
|
||||
<span id="lbl-days">${t("payment-days")}</span>
|
||||
</div>
|
||||
<div class="fg"><label id="lbl-ino" for="ino">${t("invoice-no")}</label>
|
||||
<input id="ino" type="text" data-ls="ino"></div>
|
||||
<div class="pay-by-row">
|
||||
<label id="lbl-paybyl">${t("pay-by")}:</label>
|
||||
<span id="paybydisp"></span>
|
||||
</div>
|
||||
<div class="fg"><label id="lbl-pacct">${t("account-holder")}</label>
|
||||
<input id="pacct" type="text" data-ls="pacct"></div>
|
||||
<div class="fg"><label id="lbl-piban">${t("account-no")}</label>
|
||||
<input id="piban" type="text" data-ls="piban"></div>
|
||||
<div class="fg"><label id="lbl-pbic">${t("bank-bic")}</label>
|
||||
<input id="pbic" type="text" data-ls="pbic"></div>
|
||||
<div class="fg"><label id="lbl-pbadr">${t("bank-address")}</label>
|
||||
<input id="pbadr1" type="text" data-ls="pbadr1">
|
||||
<input id="pbadr2" type="text" style="margin-top:6px" data-ls="pbadr2"></div>
|
||||
<div class="fg"><label id="lbl-pref">${t("payment-ref")}</label>
|
||||
<input id="pref" type="text" data-ls="pref"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span id="sec-ct">${t("charge-to")}:</span>
|
||||
|
|
@ -540,7 +538,6 @@ function buildForm() {
|
|||
<option value="__other__">${t("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ct-two-col">
|
||||
<div id="ct-fields">
|
||||
<div class="fg"><label id="lbl-ctn" for="ctn">${t("charge-to-name")}</label>
|
||||
<input id="ctn" type="text"></div>
|
||||
|
|
@ -563,31 +560,25 @@ function buildForm() {
|
|||
<div class="fi"><label id="lbl-creg">${t("registration-no")}:</label>
|
||||
<input id="creg" type="text"></div>
|
||||
</div>
|
||||
<div id="payment-panel">
|
||||
<div class="pay-section-title" id="lbl-pay-sec">${t("payment")}</div>
|
||||
<div class="pay-terms-row">
|
||||
<label id="lbl-pterm">${t("payment-terms")}:</label>
|
||||
<input id="pterm" type="number" min="0" value="7" oninput="calcPayBy()">
|
||||
<span id="lbl-days">${t("payment-days")}</span>
|
||||
</div>
|
||||
<div class="pay-by-row">
|
||||
<label id="lbl-paybyl">${t("pay-by")}:</label>
|
||||
<span id="paybydisp"></span>
|
||||
</div>
|
||||
<div id="bank-section" style="display:none">
|
||||
<div class="fg"><label id="lbl-pacct">${t("account-holder")}</label>
|
||||
<input id="pacct" type="text"></div>
|
||||
<div class="fg"><label id="lbl-piban">${t("account-no")}</label>
|
||||
<input id="piban" type="text"></div>
|
||||
<div class="fg"><label id="lbl-pbic">${t("bank-bic")}</label>
|
||||
<input id="pbic" type="text"></div>
|
||||
<div class="fg"><label id="lbl-pbadr">${t("bank-address")}</label>
|
||||
<input id="pbadr1" type="text">
|
||||
<input id="pbadr2" type="text" style="margin-top:6px"></div>
|
||||
<div class="fg"><label id="lbl-pref">${t("payment-ref")}</label>
|
||||
<input id="pref" type="text"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title" id="sec-invdet">${t("invoice-details-section")}</div>
|
||||
<div class="fg"><label id="lbl-idate" for="idate">${t("invoice-date")}</label>
|
||||
<input id="idate" type="date" value="${dateDef}"></div>
|
||||
<div class="fg"><label id="lbl-icur" for="icur">${t("invoice-currency")}</label>
|
||||
<select id="icur" data-ls="icur">${curOpts}</select></div>
|
||||
<div class="fg"><label id="lbl-pcode" for="pcode">${t("project-code")}</label>
|
||||
<select id="pcode">
|
||||
<option value="">${t("select")}</option>
|
||||
${pcOpts}
|
||||
<option value="__other__">${t("other")}</option>
|
||||
</select></div>
|
||||
<div class="fg" id="pcode-other-wrap" style="display:none">
|
||||
<label id="lbl-pcode-other" for="pcode-other">${t("project-code")} (${t("other")})</label>
|
||||
<input id="pcode-other" type="text">
|
||||
</div>
|
||||
<div class="fg"><label id="lbl-ino" for="ino">${t("invoice-no")}</label>
|
||||
<input id="ino" type="text" data-ls="ino"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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() {
|
|||
</tr>`;
|
||||
}).join("") || `<tr><td colspan="5" style="text-align:center;color:#9ca3af;padding:20px">—</td></tr>`;
|
||||
|
||||
const showBank = hasBank && !hidePaymentOut;
|
||||
return `
|
||||
<div class="d-hdr">
|
||||
<div class="d-sender">
|
||||
<div class="d-4hdr">
|
||||
<div class="d-tl d-sender">
|
||||
${sName ? `<div class="name">${h(sName)}</div>` : ""}
|
||||
${sAddr.map(a=>`<p>${h(a)}</p>`).join("")}
|
||||
${sCntry ? `<p>${h(sCntry)}</p>` : ""}
|
||||
${sPh ? `<p>${h(td("sender-phone"))}: ${h(sPh)}</p>` : ""}
|
||||
${sEm ? `<p>${h(td("sender-email"))}: ${h(sEm)}</p>` : ""}
|
||||
</div>
|
||||
<div class="d-title">
|
||||
<div class="d-tr d-pay-hdr">
|
||||
<div class="ph-lbl">${h(td("payment"))}</div>
|
||||
${pTerm > 0 ? `<div class="ph-terms">${h(td("payment-terms"))}: <strong>${pTerm}</strong> ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: <strong>${h(pPayBy)}</strong>` : ""}</div>` : ""}
|
||||
${showBank ? `<div class="ph-grid">
|
||||
${pAcct ? `<span class="pl">${h(td("account-holder"))}</span><span class="pv">${h(pAcct)}</span>` : ""}
|
||||
${pIban ? `<span class="pl">${h(td("account-no"))}</span><span class="pv">${h(pIban)}</span>` : ""}
|
||||
${pBic ? `<span class="pl">${h(td("bank-bic"))}</span><span class="pv">${h(pBic)}</span>` : ""}
|
||||
${pBadr1 || pBadr2 ? `<span class="pl">${h(td("bank-address"))}</span><span class="pv">${[pBadr1,pBadr2].filter(Boolean).map(l=>h(l)).join("<br>")}</span>` : ""}
|
||||
</div>
|
||||
${pRef ? `<div class="ph-ref">${h(td("payment-ref"))}: <strong>${h(pRef)}</strong></div>` : ""}` : ""}
|
||||
</div>
|
||||
<div class="d-bl d-bill">
|
||||
<div class="bt-lbl">${h(td("charge-to"))}</div>
|
||||
${ctName ? `<div class="bt-name">${h(ctName)}</div>` : ""}
|
||||
${ctAddr.map(a=>`<p>${h(a)}</p>`).join("")}
|
||||
${ctCntry ? `<p>${h(ctCntry)}</p>` : ""}
|
||||
<div class="bt-meta">
|
||||
${ctPh ? `<span><strong>${h(td("charge-to-phone"))}:</strong> ${h(ctPh)}</span>` : ""}
|
||||
${ctEm ? `<span><strong>${h(td("charge-to-email"))}:</strong> ${h(ctEm)}</span>` : ""}
|
||||
${ctVat ? `<span><strong>${h(td("vat-id"))}:</strong> ${h(ctVat)}</span>` : ""}
|
||||
${ctReg ? `<span><strong>${h(td("registration-no"))}:</strong> ${h(ctReg)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-br d-inv-meta">
|
||||
<h1>${h(td("invoice"))}</h1>
|
||||
<table class="d-meta">
|
||||
${iNo ? `<tr><td class="ml">${h(td("invoice-no"))}</td><td class="mv">${h(iNo)}</td></tr>` : ""}
|
||||
|
|
@ -1196,19 +1175,6 @@ function buildPreviewHTML() {
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
${ctName ? `
|
||||
<div class="d-bill">
|
||||
<div class="bt-lbl">${h(td("charge-to"))}</div>
|
||||
<div class="bt-name">${h(ctName)}</div>
|
||||
${ctAddr.map(a=>`<p>${h(a)}</p>`).join("")}
|
||||
${ctCntry ? `<p>${h(ctCntry)}</p>` : ""}
|
||||
<div class="bt-meta">
|
||||
${ctPh ? `<span><strong>${h(td("charge-to-phone"))}:</strong> ${h(ctPh)}</span>` : ""}
|
||||
${ctEm ? `<span><strong>${h(td("charge-to-email"))}:</strong> ${h(ctEm)}</span>` : ""}
|
||||
${ctVat ? `<span><strong>${h(td("vat-id"))}:</strong> ${h(ctVat)}</span>` : ""}
|
||||
${ctReg ? `<span><strong>${h(td("registration-no"))}:</strong> ${h(ctReg)}</span>` : ""}
|
||||
</div>
|
||||
</div>` : ""}
|
||||
<table class="d-lines">
|
||||
<thead><tr>
|
||||
<th style="width:50px">${h(td("qty"))}</th>
|
||||
|
|
@ -1233,18 +1199,7 @@ function buildPreviewHTML() {
|
|||
<td class="tv">${fmt(toPay)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
${(pTerm > 0 || hasBank) ? `
|
||||
<div class="d-payment">
|
||||
<div class="pay-title">${h(td("payment"))}</div>
|
||||
${pTerm > 0 ? `<div class="pay-terms">${h(td("payment-terms"))}: <strong>${pTerm}</strong> ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: <strong>${h(pPayBy)}</strong>` : ""}</div>` : ""}
|
||||
${hasBank ? `<div class="pay-grid">
|
||||
${pAcct ? `<span class="pl">${h(td("account-holder"))}</span><span class="pv">${h(pAcct)}</span>` : ""}
|
||||
${pIban ? `<span class="pl">${h(td("account-no"))}</span><span class="pv">${h(pIban)}</span>` : ""}
|
||||
${pBic ? `<span class="pl">${h(td("bank-bic"))}</span><span class="pv">${h(pBic)}</span>` : ""}
|
||||
${pBadr1 || pBadr2 ? `<span class="pl">${h(td("bank-address"))}</span><span class="pv">${[pBadr1,pBadr2].filter(Boolean).map(l=>h(l)).join("<br>")}</span>` : ""}
|
||||
</div>
|
||||
${pRef ? `<div class="pay-ref">${h(td("payment-ref"))}: <strong>${h(pRef)}</strong></div>` : ""}` : ""}
|
||||
</div>` : ""}`;
|
||||
`;
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
|
||||
// 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,
|
||||
// ── 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);
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
y = Math.max(sy, ry) + 5;
|
||||
// Row 1 divider
|
||||
const row1Y = Math.max(ly, ry) + 4;
|
||||
dc(209,213,219); doc.setLineWidth(0.3);
|
||||
doc.line(ML, row1Y, XR, row1Y);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue