diff --git a/artifacts/api-server/src/lib/pdf-renderer.ts b/artifacts/api-server/src/lib/pdf-renderer.ts index 8db8ca8a..3b8ff9c7 100644 --- a/artifacts/api-server/src/lib/pdf-renderer.ts +++ b/artifacts/api-server/src/lib/pdf-renderer.ts @@ -285,15 +285,24 @@ function formatTimeRange( const hhmm = t.slice(0, 5); const parsed = new Date(`1970-01-01T${hhmm}:00`); if (Number.isNaN(parsed.getTime())) return hhmm; - return new Intl.DateTimeFormat(lang === "ar" ? "ar" : "en", { + // 12-hour with NO AM/PM (en) or ص/م (ar) suffix — matches the + // on-screen schedule. We `formatToParts` and drop the `dayPeriod`, + // then trim trailing/leading whitespace literals (Intl may use + // U+00A0 / U+202F — String.prototype.trim handles them all). + // Force Latin digits via -u-nu-latn so the PDF Time column renders + // consistently regardless of language. + const locale = lang === "ar" ? "ar-u-nu-latn" : "en-US-u-nu-latn"; + const parts = new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", hour12: true, - // Force Latin digits even for Arabic locale so the Time column - // renders consistently across the schedule and the PDF — matches - // i18nFormatTime in the frontend. numberingSystem: "latn", - } as Intl.DateTimeFormatOptions).format(parsed); + } as Intl.DateTimeFormatOptions).formatToParts(parsed); + return parts + .filter((p) => p.type !== "dayPeriod") + .map((p) => p.value) + .join("") + .trim(); }; if (startTime && endTime) return `${fmt(startTime)} – ${fmt(endTime)}`; if (startTime) return fmt(startTime); diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index 335bec3c..6904fd81 100644 Binary files a/artifacts/tx-os/public/opengraph.jpg and b/artifacts/tx-os/public/opengraph.jpg differ diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index bb8f793b..083fd35d 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -88,7 +88,6 @@ import { CSS } from "@dnd-kit/utilities"; import { GripVertical } from "lucide-react"; import { EditableCell } from "@/components/editable-cell"; import { safeHtml } from "@/lib/safe-html"; -import { formatTime as i18nFormatTime } from "@/lib/i18n-format"; type Attendee = { id?: number; @@ -358,24 +357,34 @@ function formatTime(t: string | null, lang: "ar" | "en"): string { if (!t) return ""; // DB stores HH:mm:ss (24h). Build a Date anchored to a fixed day so // Intl can format the time-of-day in the user's language. We render - // in 12-hour format with AM/PM (English) or ص/م (Arabic) — admins - // expect 12h on this page since meeting times sit in everyday work - // hours, and the previous 24h format ("17:39") was confusing. The - // shared `i18nFormatTime` helper enforces Latin digits in both - // languages so the schedule reads "5:39 PM" / "5:39 م" rather than - // mixing Arabic-Indic numerals into the table. `hour: "numeric"` - // (vs "2-digit") drops the leading zero so single-digit hours render - // as "5:39 PM" instead of "05:39 PM". Falls back to the raw HH:mm - // slice if parsing somehow fails (defensive — shouldn't happen for - // our DB shape). + // in 12-hour format BUT without the AM/PM (English) or ص/م (Arabic) + // day-period suffix — admins know meetings sit in business hours and + // requested the cleaner compact form ("5:39 – 5:50"). To do this we + // use `formatToParts` and drop the `dayPeriod` segment, then trim + // any leading/trailing whitespace literals that were glueing it on + // (Intl typically inserts a regular space, but some locales use + // U+00A0 / U+202F — JS `String.prototype.trim` handles all of those). + // `hour: "numeric"` (vs "2-digit") drops the leading zero so + // single-digit hours render as "5:39" instead of "05:39". Latin + // digits are forced via the `-u-nu-latn` locale extension so Arabic + // doesn't bleed Arabic-Indic numerals into the table. Falls back + // to the raw HH:mm slice if parsing somehow fails (defensive — + // shouldn't happen for our DB shape). const hhmm = t.slice(0, 5); const parsed = new Date(`1970-01-01T${hhmm}:00`); if (Number.isNaN(parsed.getTime())) return hhmm; - return i18nFormatTime(parsed, lang, { + const locale = lang === "ar" ? "ar-u-nu-latn" : "en-US-u-nu-latn"; + const parts = new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", hour12: true, - }); + numberingSystem: "latn", + }).formatToParts(parsed); + return parts + .filter((p) => p.type !== "dayPeriod") + .map((p) => p.value) + .join("") + .trim(); } function todayIso(): string {