Polish BPSOS PDF line layout: cleaner, less busy

- Header divider (under Staff/Period/Currency) now matches grand-total
  rule thickness (3pt instead of 1.5pt)
- ITEM / PROJECT / TRAVEL label rendered at body font size (sz) for
  stronger visual weight
- Thin grey rule drawn above every vendor line (including first in an
  item); replaces the between-lines-only separator
- Foreign currency amount moved to line 1, left of the bold base
  amount, in muted grey — removes it from line 3
- Line 3 right side now shows only the FX rate (e.g. THB 34.05000 per
  USD) since the amount is already on line 1
- Receipt line shows plain "See page N for receipt" with no prefix, or
  wraps the no-receipt explanation directly with no heading label
- Vendor and description rendered as plain text (no box or background)
This commit is contained in:
Claude 2026-05-24 18:42:11 +00:00
parent ed7ea2c4de
commit b6497c4d0a
No known key found for this signature in database

View file

@ -948,14 +948,14 @@ async function generatePDF() {
y -= lh + 6;
// Divider
pg.drawLine({start:{x:M.left,y}, end:{x:M.left+W,y}, thickness:1.5, color:accent});
pg.drawLine({start:{x:M.left,y}, end:{x:M.left+W,y}, thickness:3, color:accent});
y -= lh;
// Items
state.items.forEach(item => {
// Item header
needSpace(lh * 6); // need room for at least header + one line
pg.drawText('ITEM / PROJECT / TRAVEL', {x:M.left, y, size:szSm, font:fontBold, color:accent});
pg.drawText('ITEM / PROJECT / TRAVEL', {x:M.left, y, size:sz, font:fontBold, color:accent});
const subStr = `Subtotal: ${baseCur} ${fmtAmt(item._subtotal)}`;
const subW = fontBold.widthOfTextAtSize(subStr, sz);
pg.drawText(subStr, {x:M.left+W-subW, y, size:sz, font:fontBold, color:accent});
@ -966,58 +966,57 @@ async function generatePDF() {
// Lines
item.lines.forEach((ln, li) => {
needSpace(lh * 8);
if (li > 0 && !justBroke) {
y -= 4;
pg.drawLine({start:{x:M.left,y}, end:{x:M.left+W,y}, thickness:0.3, color:lineCol});
y -= 8;
// Thin grey rule above each vendor line
if (!justBroke) {
if (li > 0) y -= 6;
pg.drawLine({start:{x:M.left, y:y+lh+1}, end:{x:M.left+W, y:y+lh+1}, thickness:0.3, color:lineCol});
}
const boxW = W * 0.65; // width of vendor / description boxes
const boxH = lh; // box height matches line height
const boxFill = rgb(0.97, 0.97, 0.97);
const boxW = W * 0.65; // text width for vendor / description
// Line 1: [Vendor box] LCC amount
// Line 1: Vendor FXC amount LCC amount
const baseAmt = (() => { const a=parseFloat(ln.amount)||0, r=parseFloat(ln.fxRate)||1; return r>0?a/r:0; })();
const baseAmtStr = `${baseCur} ${fmtAmt(baseAmt)}`;
const baseAmtW = fontBold.widthOfTextAtSize(baseAmtStr, sz);
pg.drawRectangle({x:M.left, y:y-2, width:boxW, height:boxH, borderColor:lineCol, borderWidth:0.5, color:boxFill});
pg.drawText(truncate(ln.vendor||'', fontBody, sz, boxW-8), {x:M.left+4, y, size:sz, font:fontBody, color:black});
pg.drawText(truncate(ln.vendor||'', fontBody, sz, boxW-4), {x:M.left, y, size:sz, font:fontBody, color:black});
if (ln.currency && ln.currency !== baseCur) {
const fxAmtStr = `${ln.currency} ${fmtAmt(ln.amount)}`;
const fxAmtW = fontBody.widthOfTextAtSize(fxAmtStr, szSm);
pg.drawText(fxAmtStr, {x:M.left+W-baseAmtW-fxAmtW-6, y, size:szSm, font:fontBody, color:gray});
}
pg.drawText(baseAmtStr, {x:M.left+W-baseAmtW, y, size:sz, font:fontBold, color:black});
y -= boxH + 4;
y -= lh + 2;
// Line 2: [Description box] Date: YYYY-MM-DD
// Line 2: Description Date: YYYY-MM-DD
const dateInPeriod = isDateInPeriod(ln.date);
const dateColor = dateInPeriod ? black : rgb(0.9, 0.33, 0);
const dateStr = `Date: ${ln.date||''}${dateInPeriod ? '' : ' (!)'}`;
const dateStrW = fontBody.widthOfTextAtSize(dateStr, szSm);
pg.drawRectangle({x:M.left, y:y-2, width:boxW, height:boxH, borderColor:lineCol, borderWidth:0.5, color:boxFill});
pg.drawText(truncate(ln.description||'', fontBody, sz, boxW-8), {x:M.left+4, y, size:sz, font:fontBody, color:black});
pg.drawText(truncate(ln.description||'', fontBody, sz, boxW-4), {x:M.left, y, size:sz, font:fontBody, color:black});
pg.drawText(dateStr, {x:M.left+W-dateStrW, y, size:szSm, font:fontBody, color:dateColor});
y -= boxH + 4;
y -= lh + 2;
// Line 3: Receipt: … (left, backfilled) FXC amount FXC rate per LCC (right)
// Line 3: Receipt ref (backfilled) or explanation FXC rate detail (right)
if (ln.currency && ln.currency !== baseCur) {
const fxDetail = `${ln.currency} ${fmtAmt(ln.amount)} ${ln.currency} ${parseFloat(ln.fxRate||'0').toFixed(5)} per ${baseCur}`;
const fxDetail = `${ln.currency} ${parseFloat(ln.fxRate||'0').toFixed(5)} per ${baseCur}`;
const fxDetailW = fontBody.widthOfTextAtSize(fxDetail, szSm);
pg.drawText(fxDetail, {x:M.left+W-fxDetailW, y, size:szSm, font:fontBody, color:gray});
}
if (ln.hasReceipt && ln.receipts.length > 0) {
ln.receipts.forEach((r, ri) => {
const key = `${ln.id}-${ri}`;
receiptRefs.push({pageIdx: pages.length-1, x:M.left, y, key, prefix:'Receipt: '});
receiptRefs.push({pageIdx: pages.length-1, x:M.left, y, key, prefix:''});
y -= lh;
});
} else if (!ln.hasReceipt) {
pg.drawText('Receipt: No receipt', {x:M.left, y, size:sz, font:fontBody, color:black});
y -= lh;
const explLines = wrapText(ln.noReceiptExplanation||'', fontBody, szSm, boxW);
const explLines = wrapText(ln.noReceiptExplanation||'', fontBody, szSm, W * 0.6);
explLines.forEach(line => {
needSpace(lh);
pg.drawText(` ${line}`, {x:M.left, y, size:szSm, font:fontBody, color:gray});
pg.drawText(line, {x:M.left, y, size:szSm, font:fontBody, color:gray});
y -= lh;
});
} else {
pg.drawText('Receipt: ', {x:M.left, y, size:sz, font:fontBody, color:gray});
y -= lh;
}