diff --git a/app/index.html b/app/index.html index 9346135..f757bff 100644 --- a/app/index.html +++ b/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}); } });