From 36cb2ca86ed72d8ca495db5bebe8e38be87484bc Mon Sep 17 00:00:00 2001 From: Riyadh Date: Thu, 30 Apr 2026 12:47:50 +0000 Subject: [PATCH] Restart numbering for individuals after each subheading Modify attendee numbering logic to reset at each subheading, ensuring proper sequence and display across different views and PDF outputs. --- artifacts/api-server/src/lib/pdf-renderer.ts | 10 ++- .../src/pages/executive-meetings-print.tsx | 8 ++- .../tx-os/src/pages/executive-meetings.tsx | 66 ++++++++++++++----- ...ive-meetings-attendee-subheadings.spec.mjs | 13 ++-- 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/artifacts/api-server/src/lib/pdf-renderer.ts b/artifacts/api-server/src/lib/pdf-renderer.ts index f913c416..8db8ca8a 100644 --- a/artifacts/api-server/src/lib/pdf-renderer.ts +++ b/artifacts/api-server/src/lib/pdf-renderer.ts @@ -531,15 +531,19 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise meeting.attendees.length === 0 ? "—" : (() => { - // Walking person counter — subheadings are non-numbered - // labels, so they don't advance the index. Same logic as - // the on-screen AttendeeFlow and the print page. + // 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 diff --git a/artifacts/tx-os/src/pages/executive-meetings-print.tsx b/artifacts/tx-os/src/pages/executive-meetings-print.tsx index 44525a45..11e4051b 100644 --- a/artifacts/tx-os/src/pages/executive-meetings-print.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings-print.tsx @@ -275,14 +275,16 @@ export default function ExecutiveMeetingsPrintPage() { data-testid={`em-print-attendees-${m.id}`} > {(() => { - // Walking person counter — subheadings are - // labels, so they don't advance the print - // index. Mirrors AttendeeFlow on screen. + // Each subheading starts a fresh numbered + // section. Mirrors AttendeeFlow on screen. let personIdx = 0; return attendees.map((a, idx) => { const isSub = (a.kind ?? "person") === "subheading"; if (isSub) { + // Reset the in-section counter so the + // next person starts at 1. + personIdx = 0; return (
n + ((a.kind ?? "person") === "person" ? 1 : 0), - 0, + // Each subheading starts a fresh numbered section. Within a section, + // persons are numbered 1..N starting over after every subheading. + // Numbering rules: + // - If the cell has NO subheading: keep the legacy "don't number a + // single name" rule (only show numbers when 2+ persons exist). + // - If the cell HAS any subheading: always show numbers, even for + // solo persons inside a section, so the grouping reads clearly. + // Pre-compute per-list-position metadata so the render loop is O(n). + const hasAnySubheading = items.some( + ({ a }) => (a.kind ?? "person") === "subheading", ); + const sectionMeta = items.map(() => ({ + sectionPersonCount: 0, + idxInSection: 0, + })); + { + let secStart = 0; + let secCount = 0; + const finalize = (endExclusive: number) => { + for (let k = secStart; k < endExclusive; k++) { + sectionMeta[k].sectionPersonCount = secCount; + } + }; + items.forEach(({ a }, idx) => { + if ((a.kind ?? "person") === "subheading") { + finalize(idx); + secStart = idx + 1; + secCount = 0; + } else { + sectionMeta[idx].idxInSection = secCount; + secCount += 1; + } + }); + finalize(items.length); + } + // Person count of the trailing (last) section — drives whether the + // pending "+ name" ghost row shows a leading number. + let lastSectionPersonCount = 0; + for (let k = items.length - 1; k >= 0; k--) { + if ((items[k].a.kind ?? "person") === "subheading") break; + lastSectionPersonCount += 1; + } if (items.length === 0 && !showAdd && !isPending) { return ; } - // Walking counter — incremented only as we render person rows. - let personIdx = 0; - return (
    - {items.map(({ a, i }) => { + {items.map(({ a, i }, listIdx) => { const isSub = (a.kind ?? "person") === "subheading"; if (isSub) { // Subheading row: full-width line break inside the flex-wrap so @@ -3411,10 +3444,9 @@ function AttendeeFlow({ ); } - // Person row — capture index BEFORE incrementing so the displayed - // number matches what the user expects (1-based). - const myDisplayIdx = personIdx; - personIdx += 1; + // Person row — section-local 0-based index pre-computed above. + const meta = sectionMeta[listIdx]; + const myDisplayIdx = meta.idxInSection; return (
  • - {personCount > 1 && ( + {(meta.sectionPersonCount > 1 || hasAnySubheading) && ( - {personCount > 0 && ( - {personCount + 1}- + {(lastSectionPersonCount > 0 || hasAnySubheading) && ( + + {lastSectionPersonCount + 1}- + )} s.trim().replace(/\s+/g, "")); - expect(trimmed).toEqual(["1-", "2-", "3-"]); + expect(trimmed).toEqual(["1-", "2-", "1-"]); } finally { if (originalAdminPreferredLanguage) { await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(