Fix PDF formatting: subheading groups, vertical centering, "#" header (Task #361)

- 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).
This commit is contained in:
riyadhafraa
2026-05-04 07:57:47 +00:00
parent 57096e69a5
commit af9d214e37
2 changed files with 23 additions and 28 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ export const PDF_LABELS: Record<PdfLang, PdfLabels> = {
ar: {
// Verbatim from the printed sample the user supplied.
title: "قائمة بأسماء حضور الاجتماعات",
no: "م",
no: "#",
meeting: "الاجتماع",
attendees: "الحضور",
time: "الوقت",
+22 -27
View File
@@ -778,35 +778,29 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise<Buffer>
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<Buffer>
}
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;