diff --git a/app/index.html b/app/index.html index 2b0f525..753285f 100644 --- a/app/index.html +++ b/app/index.html @@ -1381,12 +1381,13 @@ async function generatePDF() { // Continuation header: light strip with staff + period function drawContHeader() { - const stripH = szXs + lh + 14; + const cPad = 7; + const stripH = 2 * cPad + szXs + 5 + sz; pg.drawRectangle({ x: 0, y: y - stripH, width: pageW, height: stripH, color: clrSurface2 }); pg.drawLine({ start:{x:0, y}, end:{x:pageW, y}, thickness:0.5, color:clrBorder }); pg.drawLine({ start:{x:0, y:y-stripH}, end:{x:pageW, y:y-stripH}, thickness:0.5, color:clrBorder }); - const labY = y - 7; + const labY = y - Math.round(cPad + szXs * 0.72); const valY = labY - szXs - 5; lbl('Staff', M.left, labY); lbl('Period', M.left + W * 0.45, labY); @@ -1399,47 +1400,38 @@ async function generatePDF() { // ── Page 1 ──────────────────────────────────────────────────────────────── addPage(true); - // Header: brand block left | doc title right + // Header: org name left | REIMBURSEMENT right (timesheet style) const hdrTopY = y; - const boxSize = 44; + const nowD = new Date(); + const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + + const hdrNameSize = szLg; // org name and title same size + const hdrNameY = hdrTopY - Math.round(hdrNameSize * 0.28); // cap top near hdrTopY if (logoImage) { const maxWLogo = (CFG['logo-maxwidth'] || 4) * 28.3465; - const scale = Math.min(maxWLogo / logoImage.width, boxSize / logoImage.height, 1); + const scale = Math.min(maxWLogo / logoImage.width, hdrNameSize * 1.6 / logoImage.height, 1); const lw = logoImage.width * scale, lhImg = logoImage.height * scale; pg.drawImage(logoImage, { x: M.left, y: hdrTopY - lhImg, width: lw, height: lhImg }); } else { - // Initials box (surface-2 fill, border, accent initials) - const raw = (CFG.organization || 'ORG').trim(); - const initials = raw.split(/\s+/).map(w => w[0] || '').join('').toUpperCase().slice(0, 2) || '??'; - pg.drawRectangle({ x: M.left, y: hdrTopY - boxSize, width: boxSize, height: boxSize, - color: clrSurface2, borderColor: clrBorder, borderWidth: 0.5 }); - const initW = fontBold.widthOfTextAtSize(initials, szLg); - pg.drawText(initials, { x: M.left + (boxSize - initW) / 2, y: hdrTopY - boxSize / 2 - szLg * 0.35, - size: szLg, font: fontBold, color: clrAccent }); + pg.drawText(CFG.organization || '', { x: M.left, y: hdrNameY, size: hdrNameSize, font: fontBold, color: clrText }); } + const hdrSubtY = hdrNameY - hdrNameSize - 5; + pg.drawText('Expense reimbursement', { x: M.left, y: hdrSubtY, size: szSm, font: fontBody, color: clrMuted }); - // Org name + subtitle - const orgX = M.left + boxSize + 11; - pg.drawText(CFG.organization || '', { x: orgX, y: hdrTopY - 15, size: sz + 2, font: fontBold, color: clrText }); - pg.drawText('Expense reimbursement', { x: orgX, y: hdrTopY - 15 - (sz + 2) - 4, size: szSm, font: fontBody, color: clrMuted }); + const titleStr = 'REIMBURSEMENT'; + const titleW = fontBold.widthOfTextAtSize(titleStr, hdrNameSize); + pg.drawText(titleStr, { x: M.left + W - titleW, y: hdrNameY, size: hdrNameSize, font: fontBold, color: clrText }); - // Title + claim date (right-aligned) - const titleStr = 'Reimbursement'; - const titleW = fontBold.widthOfTextAtSize(titleStr, szLg); - pg.drawText(titleStr, { x: M.left + W - titleW, y: hdrTopY - 15, size: szLg, font: fontBold, color: clrText }); - - const nowD = new Date(); - const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const claimDate = `Claim · ${nowD.getDate()} ${MONTHS[nowD.getMonth()]} ${nowD.getFullYear()}`; const claimDateW = fontBody.widthOfTextAtSize(claimDate, szSm); - pg.drawText(claimDate, { x: M.left + W - claimDateW, y: hdrTopY - 15 - szLg - 4, size: szSm, font: fontBody, color: clrMuted }); + pg.drawText(claimDate, { x: M.left + W - claimDateW, y: hdrSubtY, size: szSm, font: fontBody, color: clrMuted }); - y = hdrTopY - boxSize - 10; + y = hdrSubtY - szSm - 12; - // Hairline below header - pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:0.5, color:clrBorder }); - y -= 12; + // Thick accent divider below header + pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:2.5, color:clrAccent }); + y -= 16; // Intro text if (CFG.intro) { @@ -1448,13 +1440,14 @@ async function generatePDF() { y -= 6; } - // Staff / Period / Currency info block - const infoH = szXs + lh + 14; + // Staff / Period / Currency info block — equal visual padding top and bottom + const infoPad = 7; + const infoH = 2 * infoPad + szXs + 5 + sz; pg.drawRectangle({ x: M.left - 8, y: y - infoH, width: W + 16, height: infoH, color: clrSurface2 }); pg.drawLine({ start:{x:M.left-8, y}, end:{x:M.left+W+8, y}, thickness:0.5, color:clrBorder }); pg.drawLine({ start:{x:M.left-8, y:y-infoH}, end:{x:M.left+W+8, y:y-infoH}, thickness:0.5, color:clrBorder }); - const iLabY = y - 7; + const iLabY = y - Math.round(infoPad + szXs * 0.72); const iValY = iLabY - szXs - 5; const iC2 = W * 0.45, iC3 = W * 0.78; @@ -1472,17 +1465,18 @@ async function generatePDF() { state.items.forEach(item => { needSpace(lh * 7); - // Section header: accent stripe + item name left, subtotal right - accentStripe(y); - pg.drawText(item.name || '(no name)', { x: M.left + 8, y, size: sz, font: fontBold, color: clrTextSoft }); + // Section header strip — equal visual padding, accent left bar + const secH = sz + 16; + const secTextY = Math.round(y - secH / 2 + sz * 0.22); + pg.drawRectangle({ x: M.left, y: y - secH, width: W, height: secH, color: clrSurface2 }); + pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:0.5, color:clrBorder }); + pg.drawLine({ start:{x:M.left, y:y-secH}, end:{x:M.left+W, y:y-secH}, thickness:0.5, color:clrBorder }); + pg.drawRectangle({ x: M.left, y: y - secH, width: 3, height: secH, color: clrAccent }); + pg.drawText(item.name || '(no name)', { x: M.left + 10, y: secTextY, size: sz, font: fontBold, color: clrTextSoft }); const subStr = `${baseCur} ${fmtAmt(item._subtotal)}`; const subW = fontBold.widthOfTextAtSize(subStr, sz); - pg.drawText(subStr, { x: M.left + W - subW, y, size: sz, font: fontBold, color: clrAccent }); - y -= lh + 2; - - // Hairline below section header - pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:0.5, color:clrBorder }); - y -= 14; + pg.drawText(subStr, { x: M.left + W - subW, y: secTextY, size: sz, font: fontBold, color: clrAccent }); + y -= secH + 14; // Lines item.lines.forEach((ln, li) => { @@ -1571,15 +1565,15 @@ async function generatePDF() { explLines.forEach(line => { needSpace(lh); val(line, M.left, y); y -= lh; }); } - y -= 6; + y -= 2; }); - y -= 16; + y -= 10; }); // Grand total needSpace(lh * 3 + 6); - y -= 6; + y -= 4; pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:1.5, color:clrBorderStrong }); y -= lh;