mirror of
https://github.com/kbenestad/reimburse.git
synced 2026-06-18 08:04:31 +00:00
Redesign PDF expense line layout to three-line unstructured form
Each expense line now renders as: [Vendor box] LCC amount [Description box] Date: YYYY-MM-DD Receipt: See page X / No receipt FXC amount – FXC rate per LCC Account: value Program: value Replaces the previous label-row / value-row grid (Date | Vendor | Currency | FX rate, then Description | Receipt | Amount) which BPSOS found too busy. Receipt refs are backfilled with the "Receipt: " prefix as before. Form input behaviour is unchanged.
This commit is contained in:
parent
6be776ccd2
commit
ed7ea2c4de
1 changed files with 58 additions and 60 deletions
118
app/index.html
118
app/index.html
|
|
@ -965,90 +965,87 @@ async function generatePDF() {
|
|||
|
||||
// Lines
|
||||
item.lines.forEach((ln, li) => {
|
||||
needSpace(lh * 7);
|
||||
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;
|
||||
}
|
||||
const c1=0, c2=W*0.22, c3=W*0.68, c4=W*0.82;
|
||||
|
||||
// Row 1 labels: Date | Vendor | Currency | FX rate
|
||||
pg.drawText('Date', {x:M.left+c1, y, size:szSm-1, font:fontBold, color:gray});
|
||||
pg.drawText('Vendor', {x:M.left+c2, y, size:szSm-1, font:fontBold, color:gray});
|
||||
pg.drawText('Currency', {x:M.left+c3, y, size:szSm-1, font:fontBold, color:gray});
|
||||
const fxLbl = 'FX rate'; pg.drawText(fxLbl, {x:M.left+W-fontBold.widthOfTextAtSize(fxLbl,szSm-1), y, size:szSm-1, font:fontBold, color:gray});
|
||||
y -= lh;
|
||||
// Row 1 values
|
||||
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);
|
||||
|
||||
// Line 1: [Vendor box] 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(baseAmtStr, {x:M.left+W-baseAmtW, y, size:sz, font:fontBold, color:black});
|
||||
y -= boxH + 4;
|
||||
|
||||
// Line 2: [Description box] Date: YYYY-MM-DD
|
||||
const dateInPeriod = isDateInPeriod(ln.date);
|
||||
const dateColor = dateInPeriod ? black : rgb(0.9, 0.33, 0);
|
||||
pg.drawText((ln.date || '–') + (dateInPeriod ? '' : ' (!)'), {x:M.left+c1, y, size:sz, font:fontBody, color:dateColor});
|
||||
pg.drawText(truncate(ln.vendor, fontBody, sz, (c3-c2)-8), {x:M.left+c2, y, size:sz, font:fontBody, color:black});
|
||||
pg.drawText(ln.currency, {x:M.left+c3, y, size:sz, font:fontBody, color:black});
|
||||
const fxStr = ln.currency === baseCur ? '–' : parseFloat(ln.fxRate).toFixed(5);
|
||||
const fxW = fontBody.widthOfTextAtSize(fxStr, sz);
|
||||
pg.drawText(fxStr, {x:M.left+W-fxW, y, size:sz, font:fontBody, color:black});
|
||||
y -= lh + 2;
|
||||
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(dateStr, {x:M.left+W-dateStrW, y, size:szSm, font:fontBody, color:dateColor});
|
||||
y -= boxH + 4;
|
||||
|
||||
// Row 2 labels: Description | Receipt | Amount
|
||||
pg.drawText('Description', {x:M.left, y, size:szSm-1, font:fontBold, color:gray});
|
||||
pg.drawText('Receipt', {x:M.left+c3, y, size:szSm-1, font:fontBold, color:gray});
|
||||
const amtLbl = 'Amount'; pg.drawText(amtLbl, {x:M.left+W-fontBold.widthOfTextAtSize(amtLbl,szSm-1), y, size:szSm-1, font:fontBold, color:gray});
|
||||
y -= lh;
|
||||
// Row 2 values
|
||||
pg.drawText(truncate(ln.description, fontBody, sz, (c3)-8), {x:M.left, y, size:sz, font:fontBody, color:black});
|
||||
pg.drawText(ln.hasReceipt ? 'Yes' : 'No', {x:M.left+c3, y, size:sz, font:fontBody, color:black});
|
||||
const amtStr = `${ln.currency} ${fmtAmt(ln.amount)}`;
|
||||
const amtW = fontBody.widthOfTextAtSize(amtStr, sz);
|
||||
pg.drawText(amtStr, {x:M.left+W-amtW, y, size:sz, font:fontBody, color:black});
|
||||
y -= lh + 2;
|
||||
// Line 3: Receipt: … (left, backfilled) FXC amount – FXC rate per LCC (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 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: '});
|
||||
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);
|
||||
explLines.forEach(line => {
|
||||
needSpace(lh);
|
||||
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;
|
||||
}
|
||||
|
||||
// Row 3 labels
|
||||
pg.drawText('Account', {x:M.left, y, size:szSm-1, font:fontBold, color:gray});
|
||||
pg.drawText('Program', {x:M.left+W*0.5, y, size:szSm-1, font:fontBold, color:gray});
|
||||
y -= lh;
|
||||
pg.drawText(ln.account || '–', {x:M.left, y, size:sz, font:fontBody, color:black});
|
||||
// Line 4: Account: value Program: value (inline labels)
|
||||
const progs = ln.programs || [];
|
||||
pg.drawText(`Account: ${ln.account||'–'}`, {x:M.left, y, size:sz, font:fontBody, color:black});
|
||||
if (progs.length <= 1) {
|
||||
const pe = progs[0] || {};
|
||||
const progStr = pe.program === 'Other' ? `Other: ${pe.programOther}` : (pe.program || '–');
|
||||
pg.drawText(truncate(progStr, fontBody, sz, W * 0.5 - 8), {x:M.left+W*0.5, y, size:sz, font:fontBody, color:black});
|
||||
const progVal = pe.program === 'Other' ? `Other: ${pe.programOther}` : (pe.program||'–');
|
||||
pg.drawText(truncate(`Program: ${progVal}`, fontBody, sz, W*0.5-8), {x:M.left+W*0.5, y, size:sz, font:fontBody, color:black});
|
||||
y -= lh;
|
||||
} else {
|
||||
const lineBaseAmt = (() => { const a = parseFloat(ln.amount)||0, r = parseFloat(ln.fxRate)||1; return r>0?a/r:0; })();
|
||||
const lineBaseAmt = (() => { const a=parseFloat(ln.amount)||0, r=parseFloat(ln.fxRate)||1; return r>0?a/r:0; })();
|
||||
progs.forEach((pe, pi) => {
|
||||
if (pi > 0) { needSpace(lh); }
|
||||
const progStr = pe.program === 'Other' ? `Other: ${pe.programOther}` : (pe.program || '–');
|
||||
const pct = parseFloat(pe.percent) || 0;
|
||||
const progVal = pe.program === 'Other' ? `Other: ${pe.programOther}` : (pe.program||'–');
|
||||
const pct = parseFloat(pe.percent)||0;
|
||||
const progAmt = lineBaseAmt * pct / 100;
|
||||
const label = pi === 0 ? 'Program: ' : ' ';
|
||||
pg.drawText(truncate(`${label}${progVal}`, fontBody, sz, W*0.45-8), {x:M.left+W*0.5, y, size:sz, font:fontBody, color:black});
|
||||
const suffix = ` ${pct.toFixed(2)}% – ${baseCur} ${fmtAmt(progAmt)}`;
|
||||
pg.drawText(truncate(progStr, fontBody, sz, W * 0.45 - 8), {x:M.left+W*0.5, y, size:sz, font:fontBody, color:black});
|
||||
const sfxW = fontBody.widthOfTextAtSize(suffix, szSm);
|
||||
pg.drawText(suffix, {x:M.left+W-sfxW, y, size:szSm, font:fontBody, color:gray});
|
||||
y -= lh;
|
||||
});
|
||||
}
|
||||
|
||||
// Receipt reference or explanation
|
||||
if (ln.hasReceipt && ln.receipts.length > 0) {
|
||||
ln.receipts.forEach((r, ri) => {
|
||||
const key = `${ln.id}-${ri}`;
|
||||
const refX = M.left + W * 0.6;
|
||||
receiptRefs.push({ pageIdx: pages.length - 1, x: refX, y, key });
|
||||
y -= lh;
|
||||
});
|
||||
} else if (!ln.hasReceipt) {
|
||||
needSpace(lh * 2);
|
||||
pg.drawText('Explanation for no receipt:', {x:M.left, y, size:szSm-1, font:fontBold, color:gray});
|
||||
y -= lh;
|
||||
const explLines = wrapText(ln.noReceiptExplanation || '–', fontBody, sz, W);
|
||||
explLines.forEach(line => {
|
||||
needSpace(lh);
|
||||
pg.drawText(line, {x:M.left, y, size:sz, font:fontBody, color:black});
|
||||
y -= lh;
|
||||
});
|
||||
}
|
||||
y -= 6;
|
||||
y -= 4;
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1114,7 +1111,8 @@ async function generatePDF() {
|
|||
const pageNum = receiptPageMap[ref.key];
|
||||
if (pageNum != null) {
|
||||
const pg2 = pages[ref.pageIdx];
|
||||
pg2.drawText(`See page ${pageNum} for receipt`, {x:ref.x, y:ref.y, size:szSm, font:fontBody, color:gray});
|
||||
const prefix = ref.prefix || '';
|
||||
pg2.drawText(`${prefix}See page ${pageNum} for receipt`, {x:ref.x, y:ref.y, size:szSm, font:fontBody, color:gray});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue