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.
This commit is contained in:
Riyadh
2026-05-01 17:34:40 +00:00
parent 5121d19507
commit 168f7fb479
3 changed files with 36 additions and 18 deletions
+14 -5
View File
@@ -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);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -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 {