Commit Graph

23 Commits

Author SHA1 Message Date
riyadhafraa 328d867975 Improve PDF schedule layout for better readability and alignment
Adjust PDF rendering to ensure all meeting details (number, title, attendees, time) are contained within a single row, improve vertical alignment to the top, and set correct text alignment for RTL content.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 5cba573f-cf8f-4641-b2db-92b5b87be569
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Zh9QhRk
Replit-Helium-Checkpoint-Created: true
2026-05-04 09:59:03 +00:00
riyadhafraa 0114ad86d0 Fix PDF table rows splitting across lines (#370)
Root cause: the height measurement probe reimplemented wrapping logic
inline, separate from the actual drawWrappingLine renderer. The two
implementations could diverge, causing the predicted row height to be
shorter than the actual rendered content, making cells overflow their
row borders and visually bleed into the next row.

Fix:
- Extract shared computeVisualLines() function that both the probe
  and drawWrappingLine use as the single source of truth for word-
  wrapping and line counting. This guarantees the predicted height
  always matches the actual rendered height.
- Save/restore doc.y around the probe loop so PDFKit's internal
  cursor doesn't drift between measurement and rendering passes.
- Save/restore doc.y per cell render block to prevent PDFKit cursor
  side effects from affecting adjacent cells.
- Add strict overflow guard: drawWrappingLine accepts a maxY param
  and stops rendering visual lines that would exceed the row bottom.
  The render loop also breaks on y >= rowBottom before starting new
  source lines. Together these form a two-level clamp that prevents
  any content from bleeding beyond row borders.
- drawWrappingLine is now a thin wrapper that calls computeVisualLines
  then draws each line via drawBidiLine, keeping the code DRY.

Tested with Arabic RTL and English LTR PDFs containing meetings with
10+ attendees, long titles, subheadings, and mixed bidi text. All
rows stay properly aligned within their borders.
2026-05-04 09:48:56 +00:00
riyadhafraa 090451ad22 Fix PDF table rows splitting across lines (#370)
Root cause: the height measurement probe reimplemented wrapping logic
inline, separate from the actual drawWrappingLine renderer. The two
implementations could diverge, causing the predicted row height to be
shorter than the actual rendered content, making cells overflow their
row borders and visually bleed into the next row.

Fix:
- Extract shared computeVisualLines() function that both the probe
  and drawWrappingLine use as the single source of truth for word-
  wrapping and line counting. This guarantees the predicted height
  always matches the actual rendered height.
- Save/restore doc.y around the probe loop so PDFKit's internal
  cursor doesn't drift between measurement and rendering passes.
- Add Y-clamp in the render loop: if a cell's text cursor reaches
  rowBottom, stop rendering to prevent overflow as a safety net.
- drawWrappingLine is now a thin wrapper that calls computeVisualLines
  then draws each line via drawBidiLine, keeping the code DRY.

Tested with Arabic RTL and English LTR PDFs containing meetings with
10+ attendees, long titles, subheadings, and mixed bidi text. All
rows stay properly aligned within their borders.
2026-05-04 09:46:49 +00:00
riyadhafraa 771cfb5611 Improve PDF generation for meeting tables with better text handling
Update PDF rendering logic to correctly handle bidirectional text, cell merging, and text wrapping for meeting tables.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 36ae937b-a1fc-4edb-8901-b07ed7713220
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Zh9QhRk
Replit-Helium-Checkpoint-Created: true
2026-05-04 09:24:37 +00:00
riyadhafraa d47c5cf5d3 PDF: compact table with zero gaps, unified lineHeight (Task #368)
User requested maximum table compression with no gaps between rows.
Changes:
- cellPadX: 8→4, cellPadY: 6→0 (zero vertical padding)
- lineHeight: fontSize*1.5 → fontSize*1.05 (very tight)
- All columns (meeting, attendees, time, #) now center-aligned
- Added lineHeight option to DrawOpts type so draw helpers use the
  same metric as row measurement (fixes probe/draw mismatch that
  could cause row overlap with tight padding)
- drawMixedLine and drawWrappingLine now respect opts.lineHeight
  instead of hardcoded fontSize*1.2, falling back to 1.2 when
  lineHeight is not provided (backward compatible)
- Table cell draw calls pass lineHeight to drawWrappingLine
- Both AR and EN PDFs verified valid with logo embedded
2026-05-04 08:51:09 +00:00
riyadhafraa cbe7e4b748 PDF: match reference spec - column widths, padding, numbering, alignment (Task #366)
User provided detailed PDF formatting spec. Changes:
- Column widths: 6/36/39/19 → 8/30/42/20 to match reference proportions
- Attendees alignment: forced to right for RTL, left for LTR (independent
  of global font alignment setting) to always match reference layout
- Numbering format: "1- name" → "-1 name" (dash before number per reference)
- Cell padding: padX 4→8, padY 2→6 for more spacious cells
- Line height: fontSize*1.2 → fontSize*1.5 for better readability
- Removed stale column-width comment that referenced old proportions
- Both AR and EN PDFs verified valid with logo embedded
- Code review PASSED, e2e tests PASSED
2026-05-04 08:27:02 +00:00
riyadhafraa 515eddc195 PDF: match reference spec - column widths, padding, numbering, alignment (Task #366)
User provided detailed PDF formatting spec. Changes:
- Column widths: 6/36/39/19 → 8/30/42/20 to match reference proportions
- Attendees alignment: "center" → right-aligned (RTL direction-based)
- Numbering format: "1- name" → "-1 name" (dash before number per reference)
- Cell padding: padX 4→8, padY 2→6 for more spacious cells
- Line height: fontSize*1.2 → fontSize*1.5 for better readability
- Both AR and EN PDFs verified valid with logo embedded
- Code review PASSED, e2e tests PASSED
2026-05-04 08:25:14 +00:00
riyadhafraa 7a71befb62 PDF: enlarge header logo and center attendees column (Task #364)
- Increased logoBoxSize multiplier from 1.8 to 2.8 so the header logo
  renders noticeably larger in the PDF output.
- Changed attendees column text alignment from direction-based (right for
  RTL) to "center" so attendee names are centered in their column.
- Both AR and EN PDFs verified: valid PDF output, logo embedded, correct
  alignment. Code review PASSED, e2e tests PASSED.
2026-05-04 08:12:02 +00:00
riyadhafraa af9d214e37 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).
2026-05-04 07:57:47 +00:00
riyadhafraa e802a93b14 Compact PDF table layout to match reference design (Task #359)
Changes to artifacts/api-server/src/lib/pdf-renderer.ts:

- Reduced cellPadX from 6 to 4 (horizontal cell padding)
- Reduced cellPadY from 3 to 2 (vertical cell padding)
- Tightened lineHeight multiplier from 1.25 to 1.2
- Removed +2 padding buffer from heightOfString measurement loop
- Removed +2 from drawWrappingLine return value
- Tightened drawMixedLine return value (1.25→1.2)
- Consolidated header spacing from two moveDown(0.2+0.4) to one moveDown(0.3)
- Tightened title Y offset multiplier from 1.25 to 1.2
- Fixed row height probe to detect script per-line (matching draw path)
  instead of per-cell, preventing measurement/draw mismatch in
  mixed Arabic/Latin content now that the +2 cushion is removed

Result: Table rows are compact with no visible gaps between them,
matching the user's reference PDF (attached_assets/rrr1_1777879598338.pdf).
Row colors, cell borders, and footer positioning are unchanged.
Both AR and EN PDFs generate successfully (200 status).
2026-05-04 07:35:52 +00:00
riyadhafraa f9d05421f6 Use actual font files in PDF generation and fix footer display
Update pdf-renderer.ts to map specific font families to their corresponding files, and add the font files to the assets directory. Adjust footer height calculation to ensure proper display.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 32f0ec38-20de-4de2-916d-1b5b97281170
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-04 06:19:52 +00:00
riyadhafraa 78a1c5072d Task #351: Match Executive Meetings PDF to user's reference layout
User compared the rendered PDF against their reference (rrr1) and flagged
three concrete differences. This change addresses all three:

1. Time format — formatTimeRange now joins start/end with U+2011
   NON-BREAKING HYPHEN (no surrounding spaces): "9:40‑9:50" instead of
   "9:40 – 9:50". The non-breaking hyphen looks identical to ASCII "-"
   and prevents PDFKit from wrapping the range across two lines in the
   narrow Time column.

2. Date placement — removed the ISO date that was printed under the
   header title. Added a new bottom-aligned footer block on the leading
   edge (right for AR, left for EN) that prints:
     • "مقيد" / "Recorded by" (bold)
     • Long-form date "3 May 2026" (always Latin, English month name
       even on the AR PDF, mirroring the reference)
   The date row is forced to baseDirection="ltr" so RTL bidi doesn't
   visually reorder the runs into "May 2026 3" on the AR PDF.
   formatLongDate parses the ISO route param in UTC to avoid host-tz
   day drift. The footer is extracted into a drawFooter() helper and
   called in BOTH the empty-day and populated-day code paths so it
   always renders.

3. Title weight — title was already calling drawMixedLine with
   weight:"bold" but added explicit confirmation; column proportions
   tweaked (6/36/39/19) so the wider Time column doesn't force range
   wrapping while keeping enough room for Meeting/Attendees content.

Other touches:
- Added `recordedBy` to PdfLabels type + RenderPdfInput.labels type
  and to ar.json/en.json executiveMeetings.pdf blocks.
- Page-break check reserves footer height so the last row never
  overlaps the bottom footer.

Verified: PDF Arabic shaping regression test passes; live AR/EN PDFs
match the reference; one-page-fit still holds for typical days.
Pre-existing tsc errors in executive-meetings.ts and unrelated test
suite failures are not caused by this work.
2026-05-03 19:28:11 +00:00
riyadhafraa 0ef7d91cbe Task #351: Match Executive Meetings PDF to user's reference layout
User compared the rendered PDF against their reference (rrr1) and flagged
three concrete differences. This change addresses all three:

1. Time format — formatTimeRange now joins start/end with U+2011
   NON-BREAKING HYPHEN (no surrounding spaces): "9:40‑9:50" instead of
   "9:40 – 9:50". The non-breaking hyphen looks identical to ASCII "-"
   and prevents PDFKit from wrapping the range across two lines in the
   narrow Time column.

2. Date placement — removed the ISO date that was printed under the
   header title. Added a new bottom-aligned footer block on the leading
   edge (right for AR, left for EN) that prints:
     • "مقيد" / "Recorded by" (bold)
     • Long-form date "3 May 2026" (always Latin, English month name
       even on the AR PDF, mirroring the reference)
   The date row is forced to baseDirection="ltr" so RTL bidi doesn't
   visually reorder the runs into "May 2026 3" on the AR PDF.
   formatLongDate parses the ISO route param in UTC to avoid host-tz
   day drift. The footer is extracted into a drawFooter() helper and
   called in BOTH the empty-day and populated-day code paths so it
   always renders.

3. Title weight — title was already calling drawMixedLine with
   weight:"bold" but added explicit confirmation; column proportions
   tweaked (6/36/39/19) so the wider Time column doesn't force range
   wrapping while keeping enough room for Meeting/Attendees content.

Other touches:
- Added `recordedBy` to PdfLabels type + RenderPdfInput.labels type
  and to ar.json/en.json executiveMeetings.pdf blocks.
- Page-break check reserves footer height so the last row never
  overlaps the bottom footer.

Verified: PDF Arabic shaping regression test passes; live AR/EN PDFs
match the reference; one-page-fit still holds for typical days.
Pre-existing tsc errors in executive-meetings.ts and unrelated test
suite failures are not caused by this work.
2026-05-03 19:27:11 +00:00
riyadhafraa 26a869c0eb PDF: inline attendees, tighter layout — fit a typical day on one page
Follow-up to the Arabic shaping fix. The downloaded schedule PDF was
spilling onto a second page even for modest days because (a) attendees
were stacked one-per-line and (b) the page header + cell padding +
column proportions reserved more vertical space than necessary.

Changes (artifacts/api-server/src/lib/pdf-renderer.ts):

- Attendees now render as a single inline paragraph per cell (joined
  with two spaces) and wrap naturally — matches the user's reference
  layout where attendees flow as one paragraph.
- Re-balanced column widths from 6/30/44/20 → 6/38/40/16. Meeting
  titles get more room (no more 6-line wraps for typical titles) and
  attendees get less (they're inline now).
- Page margin tightened 36pt → 24pt.
- Title size cap tightened (28 → 22) so the header band doesn't eat a
  table row.
- moveDown gaps trimmed (0.4→0.2 between title and date, 0.8→0.4
  before the table).
- Cell vertical padding trimmed (5pt → 3pt).
- drawWrappingLine now passes lineGap: -1 to pdfkit's text() so wrapped
  paragraphs get tighter inter-line spacing; the row-height probe
  passes the same option so probe and draw stay symmetric.
- Row-height probe now uses heightOfString() output directly
  (Math.max(lineHeight, measured) + 2) instead of rounding up to whole
  lineHeight units, removing the half-line of empty space that was
  visible below short cells.

Verified:
- EN: all 10 meetings on one A4 page.
- AR: 9 of 10 meetings on one A4 page; the 10th row contains
  multi-line internal/external subheadings which legitimately need a
  taller cell — typical days fit on a single page.
- PDF Arabic shaping regression test still passes.
- No row overlaps anywhere.

Logo: handler already loads the brand-settings logo (logoObjectPath)
and embeds it in the header — no code change needed. If the logo isn't
visible, none has been uploaded in brand settings.
2026-05-03 19:11:49 +00:00
riyadhafraa e0824bc6a0 Fix overlapping text and improve Arabic rendering in PDFs
Update PDF rendering logic to use fontkit for Arabic shaping and RTL reordering, eliminating pre-shaping and resolving text overlap issues.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 32b8a89e-ae1f-4a09-ae50-3a53ae3f3477
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 19:01:50 +00:00
riyadhafraa 627a8fe7b1 Fix Arabic text rendering in generated PDFs
Add Arabic text shaping functionality to convert base characters to their contextual presentation forms before bidi reordering and PDF rendering. Includes a new regression test to verify correct shaping by asserting the presence of presentation form codepoints in the PDF's embedded ToUnicode CMap.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 18d38e3a-cfe8-4b69-a02c-380283320b78
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 15:26:38 +00:00
riyadhafraa 62ee62e6fa Task #349 follow-up: brand-logo embed test + label module + user-scope guard
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.

Prior mark_task_complete was rejected for three issues. This commit
closes all of them:

- Extract bilingual PDF labels into
  artifacts/api-server/src/lib/pdf-labels.ts and import it from the
  PDF route. Mirror the same keys under executiveMeetings.pdf.* in
  the tx-os ar.json / en.json locales so the frontend stays in
  sync.
- Add an end-to-end test that exercises the brand-logo path: sign
  an upload URL, PUT the bytes through the storage sidecar, save
  the path on the global font-settings row, render the PDF, and
  assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
  test caught a real silent failure: PDFKit's png-js decoder
  rejected the canonical 67-byte base64 "smallest valid PNG", so
  the renderer was logging a warning and proceeding without a
  logo. The fixture now constructs a real 1x1 RGB PNG inline using
  zlib + a CRC32 routine (no new dependencies). Skip is narrowed
  to network errors / 5xx (4xx fails loudly), and the global-row
  mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
  400 ("logo_user_scope_forbidden") instead of silently dropping
  the field. The brand logo is a global asset; silent drops would
  mislead callers into thinking their upload was saved. Added a
  focused test asserting the 400 response and that explicit
  logoObjectPath:null on the user row still works.

Also removed 12 stray backup/snapshot files at the repo root
(*.old, *-base.{ts,tsx,mjs}, locale snapshots) that were
accidentally tracked in the previous commit.

Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
2026-05-03 15:10:34 +00:00
riyadhafraa 57e8297464 Task #349: Executive Meetings PDF improvements
- Schema: add `font_color` (hex, default #000000) and `logo_object_path`
  to `executive_meeting_font_settings`. Pushed via drizzle-kit.

- PDF renderer:
  - Add `fontColor` to PdfFontPrefs (body cells only; header chrome
    and logo intentionally ignore it to preserve branding).
  - Add `rowColor` to PdfMeeting and a ROW_COLOR_FILL palette kept in
    lockstep with the on-screen swatches; deliberately stop painting
    the legacy `isHighlighted` overlay so the archived PDF reflects
    editorial state instead of the viewer's transient cursor.
  - Add optional `logo: Buffer`. Header now reserves a left-anchored
    logo box and centers the title in the remaining width; bad image
    bytes log + fall back to the no-logo layout instead of crashing.

- API route:
  - Extend fontSettingsSchema with strict #RRGGBB regex and
    /^/objects/<id>$/ regex for logoObjectPath.
  - resolveFontPrefsForUser now returns { font, logoObjectPath }.
  - loadLogoBytes downloads the brand asset via ObjectStorageService.
  - Logo only writable on the global-scope row.
  - PDF labels switched to "قائمة بأسماء حضور الاجتماعات" /
    "Meeting Attendance List" per the user's printed sample.

- Frontend (executive-meetings.tsx):
  - FontPrefs gains fontColor; FontSettingsResponse.global gains
    logoObjectPath; DEFAULT_FONT and effectiveFont updated.
  - buildFontStyle applies fontColor to on-screen rows.
  - FontSettingsSection: native color picker + hex text input;
    logo upload (PNG/JPEG) via @workspace/object-storage-web's
    useUpload, visible only at the global scope for admins.

- Locales: AR/EN keys for fontColor + logo.{label,upload,replace,
  remove,uploading,uploadFailed,globalOnly}.

- Tests: existing font-settings roundtrip extended with fontColor;
  new test rejects malformed fontColor and non-/objects logo paths.

Type-check clean for api-server and tx-os; new font-settings tests
pass. Other test failures in the suite pre-date this change.
2026-05-03 14:34:29 +00:00
riyadhafraa 471c480824 Task #303: DIN Next LT Arabic site default + working font picker
User report: "the dropdown for changing the site font does not work."
Root cause: FontSettingsSection listed Cairo / Tajawal / Noto Naskh
Arabic / Amiri, but only Tajawal had been registered with @font-face.
Picking the others was a no-op. Site default body font was also not
DIN Next LT Arabic as designed.

Changes:
- artifacts/tx-os/src/index.css: --app-font-sans now starts with
  "DIN Next LT Arabic"; dropped IBM Plex Mono.
- artifacts/tx-os/index.html: removed Google Fonts <link> + preconnect.
- artifacts/tx-os/src/pages/executive-meetings.tsx: FontSettingsSection
  dropdown replaced with the 5 actually-bundled families + system.
- artifacts/api-server/src/routes/executive-meetings.ts: FONT_FAMILIES
  Zod allowlist updated to match.
- artifacts/api-server/src/lib/sanitize.ts: FONT_NAME_PART regex now
  allows the new families (kept IBM Plex Sans Arabic for backward
  compat). Fixes a latent bug from #301 where the rich-text sanitizer
  silently stripped attendee font picks on save.
- artifacts/api-server/src/lib/pdf-renderer.ts: FAMILY_MAP and
  ARABIC_FONT_NAMES updated. Sans-style families map to the bundled
  NotoSansArabic; Naskh-style families to NotoNaskhArabic.
- artifacts/api-server/tests/executive-meetings.test.mjs: PDF
  sans-vs-naskh assertion updated from Cairo/Noto Naskh Arabic to
  DIN Next LT Arabic/Majalla, preserving its semantics. Other
  Cairo/Noto Naskh Arabic occurrences swapped to the new names.
- replit.md: documented site default font + lockstep allowlist rule.

Deviations from plan: kept DEFAULT_FONT.fontFamily = "system" on the
frontend (and the backend Zod default) instead of changing it to
"DIN Next LT Arabic". "system" semantically means "no override → use
the CSS default", which is now DIN Next LT Arabic — same end result
without forcing a migration on existing rows. The sanitizer change
was not in the original plan but was required to make the picker
actually persist.

Verification:
- Frontend tsc clean.
- Backend tsc shows the same pre-existing unrelated errors as before
  (isHighlighted boolean/number; Drizzle scope typing).
- E2E browser test verified all 7 assertions: site default font,
  no Google Fonts requests, exactly 6 dropdown options, font picker
  applies + reverts correctly, attendee font picks persist after save.
- Code review verdict: PASS.
2026-05-01 19:55:39 +00:00
riyadhafraa 4726af889d Task #295: Drop AM/PM (م/ص) from meeting schedule times
What changed:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Rewrote the local formatTime helper to call
    Intl.DateTimeFormat.formatToParts directly, filter out segments of
    type "dayPeriod", join the remaining parts and trim. Locale uses
    "ar-u-nu-latn" / "en-US-u-nu-latn" so Latin digits remain forced
    in Arabic. hour12: true, hour: "numeric", minute: "2-digit"
    preserved from Task #292 — only the AM/PM (en) and ص/م (ar)
    suffix is gone.
  - Removed the now-unused i18nFormatTime import (the shared helper
    in lib/i18n-format.ts always emits dayPeriod when hour12: true,
    and this page now intentionally diverges).
  - Touches both call sites — the schedule cell (~L3452) and the
    Manage tab list (~L4583) — via the same helper.
- artifacts/api-server/src/lib/pdf-renderer.ts
  - Applied the identical formatToParts + filter("dayPeriod") + trim
    transformation in formatTimeRange so the printed PDF Time column
    matches the on-screen schedule for both languages.

Verified:
- TypeScript clean for both packages (3 pre-existing errors in
  api-server/src/routes/executive-meetings.ts last touched in #293,
  unrelated to PDF renderer).
- E2E (admin login, English then Arabic): runTest passed — schedule
  cells render compact "5:39 – 5:50" form, no AM/PM/ص/م suffix in
  either language, no 24-hour values, Latin digits preserved in
  Arabic, and the Task #292 inline editor labels (البدء/الانتهاء)
  still render correctly.
- Architect code review: PASS, no critical issues.
- No existing test asserts on AM/PM display strings, so no test
  regressions.

Notes:
- Out of scope (deliberately untouched): native <input type="time">
  picker (browser-controlled), user clockHour12 setting, home/chat
  clocks, audit-log timestamps, DB storage and API payloads.
- Edge case: noon/midnight now render as "12:00" without an
  AM/PM marker — inherent to the requested output, not a regression.
- artifacts/tx-os/public/opengraph.jpg shows a tiny size bump in the
  diff. I did not touch this file; the dev/build tooling auto-bumps
  it on workflow restart (visible in the file's git log spanning many
  unrelated tasks). Cannot be removed without destructive git ops
  which are restricted.
2026-05-01 17:34:40 +00:00
riyadhafraa 76f0dfacb4 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 17c29c3f-e55d-4765-875c-0387b6936812
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/piUNOmy
Replit-Helium-Checkpoint-Created: true
2026-04-30 12:47:50 +00:00
riyadhafraa 89df2385a8 Task #207: custom subheadings inside executive-meeting attendee cells
Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on
`executive_meeting_attendees` so users can interleave free-text section
labels with person rows in a meeting's attendee list. Subheadings are
excluded from the running attendee number and from the per-meeting
attendee count surface, but reorder and delete identically to person
rows.

DB
- New `kind` column in `lib/db/src/schema/executive-meetings.ts`,
  defaulting to `"person"`. Applied via direct SQL because
  `drizzle-kit push` trips on a pre-existing duplicate-row issue in
  `app_permissions` (already documented in replit.md).

API (artifacts/api-server/src/routes/executive-meetings.ts)
- `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with
  default `"person"`.
- All 4 insert paths round-trip `kind`: POST create, PATCH meeting
  update (attendees replacement), PUT `/attendees`, and duplicate.
- `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind`
  typed as `string | null` to match the DB column shape.

Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx)
- `AttendeeFlow` renders subheadings on a separate full-width row
  (`basis-full`, semibold, centered) and increments the running
  person index only for `kind === "person"`. Pending ghost row branches
  on `pendingKind`.
- Inline "+ subheading" chip via `onStartAdd(type, "subheading")`.
- Manage dialog: addSubheading button, subheading rows hide the title
  field, show a kind badge, and reorder/delete identically. Manage
  list summary count filters to person rows only (architect fix).
- Print page renders subheadings as `.em-print-subheading` and skips
  them in the running counter.
- New locale keys under `executiveMeetings.schedule` and
  `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`.

PDF
- Subheadings print as `— label —` and never advance `personIdx`.

Tests
- New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds
  mixed person+subheading rows and asserts (a) the subheading row
  renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading
  wedged between persons, (c) zero-subheading meetings keep legacy
  numbering. Runs in both AR and EN. All 4 cases pass.

Code review
- Architect found one regression (Manage list summary count included
  subheadings) — fixed.
2026-04-30 11:45:59 +00:00
riyadhafraa 11aaaf2abe Replace executive-meetings print-to-PDF with server-side PDF generator
The "Download PDF" button on the executive-meetings page now hits a real
backend endpoint that returns a true PDF (no more browser print dialog),
respects each user's font preferences (family, size, weight, alignment)
with proper Arabic RTL shaping, and archives every download.

The renderer maps each saved fontFamily ("system", "Cairo", "Tajawal",
"Noto Naskh Arabic", "Amiri") to a concrete pair of bundled font files
so the chosen family genuinely changes the embedded glyphs — Cairo and
Tajawal pick Noto Sans Arabic, the Naskh-style families and the system
default pick Noto Naskh Arabic, and Latin glyphs render in DejaVu Sans
across the board. Headers, body cells, and footer all flow through the
same script-aware font selection.

Backend (artifacts/api-server)
- New GET /api/executive-meetings/pdf?date=&lang= route in
  src/routes/executive-meetings.ts that fetches the day's meetings +
  attendees, renders a PDF, uploads it to object storage, writes an
  executive_meeting_pdf_archives row (date, generated_by, byte_size,
  storage_url), and streams the file back inline.
- New src/lib/pdf-renderer.ts using pdfkit + bidi-js with bundled
  Noto Naskh Arabic and DejaVu Sans fonts in assets/fonts/.
- Added byte_size column on executive_meeting_pdf_archives (also in
  lib/db schema) and rebuilt lib/db.
- Added ambient types for bidi-js; installed @swc/helpers to satisfy
  fontkit at runtime.
- build.mjs now copies pdfkit's data/ folder (Helvetica.afm, etc.)
  into dist/data so the bundled server can construct PDFDocument.

Frontend (artifacts/tx-os)
- PdfSection in src/pages/executive-meetings.tsx now renders a single
  "Download PDF" button that fetches the endpoint, builds a Blob, and
  downloads it. Removed the print/archive-creation buttons.
- Archive list shows a Download button for new /objects/... rows and a
  read-only "Legacy snapshot" badge for older print: rows.
- Added byteSize on PdfArchive + size formatting; updated en/ar locales.

Tests
- New test "PDF GET /executive-meetings/pdf returns a real PDF and
  archives it" in tests/executive-meetings.test.mjs covers: bad-date
  400, unauthenticated 401, real %PDF body + content-type/disposition,
  archive row with byteSize/generatedBy/filePath, empty-day handling,
  and font-family mapping (Cairo embeds NotoSansArabic; Noto Naskh
  Arabic embeds NotoNaskhArabic).
- All 23 executive-meetings tests pass.

Rebase
- Rebased onto main-repl/main (37255b7 "Update the website's shared
  image"). The only conflict was the binary asset
  artifacts/tx-os/public/opengraph.jpg — accepted the incoming/main
  version since it's unrelated to this PDF work.

Drift
- Kept the legacy /executive-meetings/print SPA route and the existing
  POST /pdf-archives endpoint to preserve old archive snapshots and
  the existing snapshot test. Proposed follow-up #169 to clean these
  up once stakeholders confirm.

Replit-Task-Id: 68914058-ebd6-4670-a785-c0084fe1fc94
2026-04-29 18:01:19 +00:00