From af9d214e37a909d7f326653dc3c2051a3ab699b6 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Mon, 4 May 2026 07:57:47 +0000 Subject: [PATCH] Fix PDF formatting: subheading groups, vertical centering, "#" header (Task #361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed attendee subheading formatting from inline join to grouped with newline separators between subheading sections. Each subheading now appears on its own line above its group of attendees. - Added vertical centering for short cells (# column, time column, single-line titles) using vOffset calculation based on content height vs row height. - "#" column header label already applied in pdf-labels.ts (from earlier edit). - Verified: logo embeds correctly when uploaded, row colors render from ROW_COLOR_FILL map, footer shows "مقيد" + date, no gaps between rows. - Code review PASSED, e2e tests PASSED (both AR and EN PDF generation). --- artifacts/api-server/src/lib/pdf-labels.ts | 2 +- artifacts/api-server/src/lib/pdf-renderer.ts | 49 +++++++++----------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/artifacts/api-server/src/lib/pdf-labels.ts b/artifacts/api-server/src/lib/pdf-labels.ts index 70b4d554..1ac23092 100644 --- a/artifacts/api-server/src/lib/pdf-labels.ts +++ b/artifacts/api-server/src/lib/pdf-labels.ts @@ -25,7 +25,7 @@ export const PDF_LABELS: Record = { ar: { // Verbatim from the printed sample the user supplied. title: "قائمة بأسماء حضور الاجتماعات", - no: "م", + no: "#", meeting: "الاجتماع", attendees: "الحضور", time: "الوقت", diff --git a/artifacts/api-server/src/lib/pdf-renderer.ts b/artifacts/api-server/src/lib/pdf-renderer.ts index bbad9735..35e79a94 100644 --- a/artifacts/api-server/src/lib/pdf-renderer.ts +++ b/artifacts/api-server/src/lib/pdf-renderer.ts @@ -778,35 +778,29 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise meeting.attendees.length === 0 ? "—" : (() => { - // Walking person counter — each subheading starts a - // fresh numbered section, so persons restart at 1- after - // every subheading. Same logic as the on-screen - // AttendeeFlow and the client print page. let personIdx = 0; - return meeting.attendees - .map((a) => { - const isSub = (a.kind ?? "person") === "subheading"; - const name = htmlToPlain(a.name); - if (isSub) { - // Reset the in-section counter so the next person - // starts at 1. - personIdx = 0; - // Brackets keep subheadings visually distinct in the - // monospace plain-text PDF cell where bold/colour - // can't carry. Adapter prefix is non-numeric so a - // human reader can scan it as "section label". - return `— ${name} —`; + const parts: string[] = []; + let currentGroup: string[] = []; + for (const a of meeting.attendees) { + const isSub = (a.kind ?? "person") === "subheading"; + const name = htmlToPlain(a.name); + if (isSub) { + if (currentGroup.length > 0) { + parts.push(currentGroup.join(" ")); } + personIdx = 0; + parts.push(`— ${name} —`); + currentGroup = []; + } else { personIdx += 1; const t = a.title?.trim(); - return `${personIdx}- ${name}${t ? ` (${t})` : ""}`; - }) - // Inline join: attendees flow as one paragraph per cell - // (wrapped naturally by pdfkit) instead of one-per-line. - // This keeps the table compact so the schedule fits on a - // single page when possible. Subheadings still get their - // own line via the leading "\n" below. - .join(" "); + currentGroup.push(`${personIdx}- ${name}${t ? ` (${t})` : ""}`); + } + } + if (currentGroup.length > 0) { + parts.push(currentGroup.join(" ")); + } + return parts.join("\n"); })(), align: alignment(input.font, isRtl), }, @@ -900,13 +894,14 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise } doc.restore(); - // Draw cell contents. cx = tableX; for (let i = 0; i < cells.length; i++) { const c = cells[i]; const cellW = widths[i] - cellPadX * 2; const lines = c.text.split("\n"); - let y = rowTop + cellPadY; + const contentH = cellHeights[i] - cellPadY * 2; + const vOffset = Math.max(0, (rowHeight - cellPadY * 2 - contentH) / 2); + let y = rowTop + cellPadY + vOffset; for (const line of lines) { if (!line) { y += lineHeight;