Update PDF to kbenestad colour scheme and smart filename

- Replace dark navy (30,45,69) with accent blue #2f6fed (47,111,237) throughout:
  issuer name, invoice word, charge-to name, table header bar, divider line, to-pay bar
- Named colour constants (ACCENT, BODY, MUTED, BORDER, WHITE, STRIPE) for clarity
- Filename: [ISSUER]_[YYYYMMDD]_[INVOICENUMBER].pdf with non-alphanumeric chars sanitised

https://claude.ai/code/session_01MkM7p8Us3L8YAfLKGA13NS
This commit is contained in:
Claude 2026-06-08 16:59:34 +00:00
parent 46d2c95d94
commit a03b0e0bef
No known key found for this signature in database

View file

@ -1613,19 +1613,27 @@ function buildPDF() {
let y = MT; let y = MT;
let ly = y, ry = y; let ly = y, ry = y;
if (sName) { fb(13); tc(30,45,69); tL(sName, ML, ly); ly += 6; } // Accent: #2f6fed = rgb(47,111,237) Muted text: rgb(107,114,128) Body: rgb(17,24,39)
fn(8.5); tc(75,85,99); const ACCENT = [47,111,237];
const BODY = [17,24,39];
const MUTED = [107,114,128];
const BORDER = [209,213,219];
const WHITE = [255,255,255];
const STRIPE = [249,250,251];
if (sName) { fb(13); tc(...ACCENT); tL(sName, ML, ly); ly += 6; }
fn(8.5); tc(...MUTED);
[...sAddr, sCntry].filter(Boolean).forEach(l => { tL(l, ML, ly); ly += 4.5; }); [...sAddr, sCntry].filter(Boolean).forEach(l => { tL(l, ML, ly); ly += 4.5; });
if (sPh || sEm || sTax) { if (sPh || sEm || sTax) {
const parts = []; const parts = [];
if (sPh) parts.push(`${td("sender-phone")}: ${sPh}`); if (sPh) parts.push(`${td("sender-phone")}: ${sPh}`);
if (sEm) parts.push(`${td("sender-email")}: ${sEm}`); if (sEm) parts.push(`${td("sender-email")}: ${sEm}`);
if (sTax) parts.push(`${td("vat-id")}: ${sTax}`); if (sTax) parts.push(`${td("vat-id")}: ${sTax}`);
fn(8); tc(107,114,128); fn(8); tc(...MUTED);
sp(parts.join(" "), LW).forEach(l => { tL(l, ML, ly); ly += 4; }); ly += 1; sp(parts.join(" "), LW).forEach(l => { tL(l, ML, ly); ly += 4; }); ly += 1;
} }
fb(24); tc(30,45,69); tR(td("invoice"), XR, ry); ry += 10; fb(24); tc(...ACCENT); tR(td("invoice"), XR, ry); ry += 10;
const metaRows = [ const metaRows = [
iNo ? [td("invoice-no"), iNo] : null, iNo ? [td("invoice-no"), iNo] : null,
iDate ? [td("invoice-date"), fmtDate(iDate)] : null, iDate ? [td("invoice-date"), fmtDate(iDate)] : null,
@ -1633,20 +1641,20 @@ function buildPDF() {
iCur ? [td("invoice-currency"), iCur] : null, iCur ? [td("invoice-currency"), iCur] : null,
].filter(Boolean); ].filter(Boolean);
metaRows.forEach(([lbl, val]) => { metaRows.forEach(([lbl, val]) => {
fn(8.5); tc(107,114,128); tR(lbl + ":", XR - 42, ry); fn(8.5); tc(...MUTED); tR(lbl + ":", XR - 42, ry);
fb(8.5); tc(17,24,39); tR(val, XR, ry); fb(8.5); tc(...BODY); tR(val, XR, ry);
ry += 5; ry += 5;
}); });
const row1Y = Math.max(ly, ry) + 4; const row1Y = Math.max(ly, ry) + 4;
dc(209,213,219); doc.setLineWidth(0.3); dc(...BORDER); doc.setLineWidth(0.3);
doc.line(ML, row1Y, XR, row1Y); doc.line(ML, row1Y, XR, row1Y);
let ly2 = row1Y + 5, ry2 = row1Y + 5; let ly2 = row1Y + 5, ry2 = row1Y + 5;
fb(7); tc(107,114,128); tL(td("charge-to").toUpperCase(), ML, ly2); ly2 += 5; fb(7); tc(...MUTED); tL(td("charge-to").toUpperCase(), ML, ly2); ly2 += 5;
if (ctName) { if (ctName) {
fb(10); tc(30,45,69); tL(ctName, ML, ly2); ly2 += 5.5; fb(10); tc(...ACCENT); tL(ctName, ML, ly2); ly2 += 5.5;
fn(8.5); tc(17,24,39); fn(8.5); tc(...BODY);
[...ctAddr, ctCntry].filter(Boolean).forEach(l => { tL(l, ML, ly2); ly2 += 4.5; }); [...ctAddr, ctCntry].filter(Boolean).forEach(l => { tL(l, ML, ly2); ly2 += 4.5; });
const ctParts = []; const ctParts = [];
if (ctPh) ctParts.push(`${td("charge-to-phone")}: ${ctPh}`); if (ctPh) ctParts.push(`${td("charge-to-phone")}: ${ctPh}`);
@ -1654,16 +1662,16 @@ function buildPDF() {
if (ctVat) ctParts.push(`${td("vat-id")}: ${ctVat}`); if (ctVat) ctParts.push(`${td("vat-id")}: ${ctVat}`);
if (ctReg) ctParts.push(`${td("registration-no")}: ${ctReg}`); if (ctReg) ctParts.push(`${td("registration-no")}: ${ctReg}`);
if (ctParts.length) { if (ctParts.length) {
fn(8); tc(107,114,128); fn(8); tc(...MUTED);
sp(ctParts.join(" "), LW).forEach(l => { tL(l, ML, ly2); ly2 += 4; }); ly2 += 1; sp(ctParts.join(" "), LW).forEach(l => { tL(l, ML, ly2); ly2 += 4; }); ly2 += 1;
} }
} }
if (pTerm > 0 || showBank) { if (pTerm > 0 || showBank) {
fb(7); tc(107,114,128); tL(td("payment").toUpperCase(), XM_L, ry2); ry2 += 5; fb(7); tc(...MUTED); tL(td("payment").toUpperCase(), XM_L, ry2); ry2 += 5;
if (pTerm > 0) { if (pTerm > 0) {
const ts = `${td("payment-terms")}: ${pTerm} ${td("payment-days")}${pPayBy ? ` ${td("pay-by")}: ${pPayBy}` : ""}`; 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, ry2); ry2 += 5; fn(8.5); tc(...BODY); tL(ts, XM_L, ry2); ry2 += 5;
} }
if (showBank) { if (showBank) {
const LLBL = 46; const LLBL = 46;
@ -1674,22 +1682,22 @@ function buildPDF() {
(pBadr1||pBadr2) ? [td("bank-address"), [pBadr1,pBadr2].filter(Boolean).join(", ")] : null, (pBadr1||pBadr2) ? [td("bank-address"), [pBadr1,pBadr2].filter(Boolean).join(", ")] : null,
].filter(Boolean); ].filter(Boolean);
payRows.forEach(([lbl, val]) => { payRows.forEach(([lbl, val]) => {
fn(8); tc(107,114,128); tL(lbl + ":", XM_L, ry2); fn(8); tc(...MUTED); tL(lbl + ":", XM_L, ry2);
fn(8.5); tc(17,24,39); fn(8.5); tc(...BODY);
const wrapped = sp(val, LW - LLBL - 2); const wrapped = sp(val, LW - LLBL - 2);
wrapped.forEach((line, i) => tL(line, XM_L + LLBL, ry2 + i * 4)); wrapped.forEach((line, i) => tL(line, XM_L + LLBL, ry2 + i * 4));
ry2 += Math.max(4.5, wrapped.length * 4); ry2 += Math.max(4.5, wrapped.length * 4);
}); });
if (pRef) { if (pRef) {
fn(8); tc(107,114,128); tL(td("payment-ref") + ":", XM_L, ry2); fn(8); tc(...MUTED); tL(td("payment-ref") + ":", XM_L, ry2);
fb(8.5); tc(17,24,39); tL(pRef, XM_L + LLBL, ry2); ry2 += 5; fb(8.5); tc(...BODY); tL(pRef, XM_L + LLBL, ry2); ry2 += 5;
} }
} }
} }
y = Math.max(ly2, ry2) + 5; y = Math.max(ly2, ry2) + 5;
dc(30,45,69); doc.setLineWidth(0.6); dc(...ACCENT); doc.setLineWidth(0.6);
doc.line(ML, y, XR, y); y += 6; doc.line(ML, y, XR, y); y += 6;
// ── LINE ITEMS TABLE ────────────────────────────────────────────────────── // ── LINE ITEMS TABLE ──────────────────────────────────────────────────────
@ -1700,9 +1708,9 @@ function buildPDF() {
if (y + TH > PH - 40) { doc.addPage(); y = MT; } if (y + TH > PH - 40) { doc.addPage(); y = MT; }
fc(30,45,69); dc(255,255,255); doc.setLineWidth(0); fc(...ACCENT); dc(...WHITE); doc.setLineWidth(0);
doc.rect(ML, y, CW, TH, "F"); doc.rect(ML, y, CW, TH, "F");
fb(8); tc(255,255,255); fb(8); tc(...WHITE);
tL(td("qty"), xQ+2, y+4.8); tL(td("qty"), xQ+2, y+4.8);
tL(td("uom"), xU+2, y+4.8); tL(td("uom"), xU+2, y+4.8);
tL(td("description"), xD+2, y+4.8); tL(td("description"), xD+2, y+4.8);
@ -1727,27 +1735,27 @@ function buildPDF() {
if (y + rh > PH - 30) { doc.addPage(); y = MT; } if (y + rh > PH - 30) { doc.addPage(); y = MT; }
if (idx % 2 === 1) { if (idx % 2 === 1) {
fc(249,250,251); dc(255,255,255); doc.setLineWidth(0); fc(...STRIPE); dc(...WHITE); doc.setLineWidth(0);
doc.rect(ML, y, CW, rh, "F"); doc.rect(ML, y, CW, rh, "F");
} }
dc(209,213,219); doc.setLineWidth(0.1); dc(...BORDER); doc.setLineWidth(0.1);
doc.line(ML, y+rh, XR, y+rh); doc.line(ML, y+rh, XR, y+rh);
const yt = y + 5; const yt = y + 5;
const qStr = row.qty % 1 === 0 ? String(row.qty) : fmt(row.qty); const qStr = row.qty % 1 === 0 ? String(row.qty) : fmt(row.qty);
fn(8.5); tc(17,24,39); fn(8.5); tc(...BODY);
tL(qStr, xQ+2, yt); tL(qStr, xQ+2, yt);
tL(row.uomLbl, xU+2, yt); tL(row.uomLbl, xU+2, yt);
dLines.forEach((dline, li) => tL(dline, xD+2, yt + li*3.8)); dLines.forEach((dline, li) => tL(dline, xD+2, yt + li*3.8));
fn(8.5); tc(17,24,39); tR(fmt(row.price), xP+CP-2, yt); fn(8.5); tc(...BODY); tR(fmt(row.price), xP+CP-2, yt);
fb(8.5); tc(17,24,39); tR(fmt(row.tot), XR-2, yt); fb(8.5); tc(...BODY); tR(fmt(row.tot), XR-2, yt);
if (row.fxNote) { if (row.fxNote) {
const fxStr = `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)} ` const fxStr = `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)} `
+ `(${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur})`; + `(${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur})`;
fn(7); tc(107,114,128); fn(7); tc(...MUTED);
const fxLines = sp(fxStr, CD + CP - 4); const fxLines = sp(fxStr, CD + CP - 4);
fxLines.forEach((fl, fi) => tL(fl, xD+2, y + ROW_H + descH + 3.2 + fi * 3.5)); fxLines.forEach((fl, fi) => tL(fl, xD+2, y + ROW_H + descH + 3.2 + fi * 3.5));
} }
@ -1771,21 +1779,26 @@ function buildPDF() {
totRows.forEach(([lbl, val]) => { totRows.forEach(([lbl, val]) => {
if (y + TRH > PH - 20) { doc.addPage(); y = MT; } if (y + TRH > PH - 20) { doc.addPage(); y = MT; }
fn(8.5); tc(107,114,128); tR(lbl, TLBX, y+4.5); fn(8.5); tc(...MUTED); tR(lbl, TLBX, y+4.5);
fn(8.5); tc(17,24,39); tR(val, XR-2, y+4.5); fn(8.5); tc(...BODY); tR(val, XR-2, y+4.5);
dc(209,213,219); doc.setLineWidth(0.1); dc(...BORDER); doc.setLineWidth(0.1);
doc.line(TX, y+TRH, XR, y+TRH); doc.line(TX, y+TRH, XR, y+TRH);
y += TRH; y += TRH;
}); });
if (y + 9 > PH - 10) { doc.addPage(); y = MT; } if (y + 9 > PH - 10) { doc.addPage(); y = MT; }
fc(30,45,69); dc(255,255,255); doc.setLineWidth(0); fc(...ACCENT); dc(...WHITE); doc.setLineWidth(0);
doc.rect(TX, y, TW, 9, "F"); doc.rect(TX, y, TW, 9, "F");
fn(9); tc(180,195,215); tR(td("to-pay"), TLBX, y+5.8); fn(9); tc(180,210,255); tR(td("to-pay"), TLBX, y+5.8);
fb(11); tc(255,255,255); tR(fmt(toPay), XR-2, y+5.8); fb(11); tc(...WHITE); tR(fmt(toPay), XR-2, y+5.8);
y += 9; y += 9;
doc.save(iNo ? `invoice-${iNo}.pdf` : "invoice.pdf"); const safeName = s => (s || "").replace(/[^a-zA-Z0-9_\-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
const fnIssuer = safeName(sName);
const fnDate = (iDate || "").replace(/-/g, "");
const fnNo = safeName(iNo);
const parts = [fnIssuer, fnDate, fnNo].filter(Boolean);
doc.save(parts.length ? parts.join("_") + ".pdf" : "invoice.pdf");
} }
// ── Update FX labels when currency or invoice currency changes ──────────────── // ── Update FX labels when currency or invoice currency changes ────────────────