Task #160: 12-hour time display for executive meetings

Switch the executive-meetings schedule time display from 24-hour
(e.g. "23:47 – 15:15") to 12-hour locale-aware format with Latin
digits in both languages:
  - EN: "11:47 PM – 3:15 PM"
  - AR: "11:47 م – 3:15 م"

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  * Local formatTime() now accepts (t, lang) and routes through the
    shared i18nFormatTime helper with hour12: true.
  * TimeRangeCell now takes a lang: "ar" | "en" prop, threaded down
    from MeetingRow (isRtl ? "ar" : "en"). Fixes the runtime
    "ReferenceError: lang is not defined" from the previous attempt.
  * ManageSection list passes (isRtl ? "ar" : "en") inline.
- artifacts/tx-os/src/pages/executive-meetings-print.tsx
  * New formatPrintTime() mirrors the same conversion. Both render
    branches (full range, start-only) updated.

Out of scope (kept untouched per the plan):
- <input type="time"> form fields (still HH:mm 24h, HTML standard).
- DB schema, API, generated client.
- User clockHour12 preference, home clock, chat — all still honor
  their existing per-user setting.

Verified: tx-os tsc clean. Architect review: PASS.
This commit is contained in:
riyadhafraa
2026-04-29 14:57:50 +00:00
parent abea383133
commit 2982a28432
2 changed files with 40 additions and 6 deletions
@@ -1,6 +1,24 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { safeHtml } from "@/lib/safe-html";
import { formatTime as i18nFormatTime } from "@/lib/i18n-format";
/**
* Convert an HH:mm:ss DB string to the same 12-hour locale-aware format
* used by the on-screen schedule. Mirrors the helper in the main page so
* the print/PDF view stays in sync (e.g. "3:15 PM" / "3:15 م"). Falls
* back to the raw HH:mm slice if the string somehow fails to parse.
*/
function formatPrintTime(t: string, lang: "ar" | "en"): string {
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, {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
type Attendee = {
id: number;
@@ -209,9 +227,9 @@ export default function ExecutiveMeetingsPrintPage() {
const attendees = m.attendees ?? [];
const time =
m.startTime && m.endTime
? `${m.startTime.slice(0, 5)} ${m.endTime.slice(0, 5)}`
? `${formatPrintTime(m.startTime, lang)} ${formatPrintTime(m.endTime, lang)}`
: m.startTime
? m.startTime.slice(0, 5)
? formatPrintTime(m.startTime, lang)
: "—";
return (
<tr key={m.id} className={m.isHighlighted ? "hl" : ""}>
@@ -88,6 +88,7 @@ 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;
@@ -376,9 +377,21 @@ function computeCurrentMeetingId(
return null;
}
function formatTime(t: string | null): string {
function formatTime(t: string | null, lang: "ar" | "en"): string {
if (!t) return "";
return t.slice(0, 5);
// 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 with AM/PM
// (English) or ص/م (Arabic). Latin digits are enforced via the shared
// formatTime helper. 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, {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
function todayIso(): string {
@@ -1975,6 +1988,7 @@ function MeetingRow({
<TimeRangeCell
meeting={meeting}
t={t}
lang={isRtl ? "ar" : "en"}
canMutate={canMutate}
onSaveTimes={onSaveTimes}
/>
@@ -2213,11 +2227,13 @@ function MergeMenu({
function TimeRangeCell({
meeting,
t,
lang,
canMutate,
onSaveTimes,
}: {
meeting: Meeting;
t: (k: string) => string;
lang: "ar" | "en";
canMutate: boolean;
onSaveTimes: (
startTime: string | null,
@@ -2310,7 +2326,7 @@ function TimeRangeCell({
if (!editing) {
const hasAny = !!(meeting.startTime || meeting.endTime);
const display = hasAny
? `${formatTime(meeting.startTime)} ${formatTime(meeting.endTime)}`
? `${formatTime(meeting.startTime, lang)} ${formatTime(meeting.endTime, lang)}`
: "—";
return (
<div
@@ -2978,7 +2994,7 @@ function ManageSection({
</div>
</td>
<td className="px-3 py-2 align-middle font-mono text-xs whitespace-nowrap">
{formatTime(m.startTime)} {formatTime(m.endTime)}
{formatTime(m.startTime, isRtl ? "ar" : "en")} {formatTime(m.endTime, isRtl ? "ar" : "en")}
</td>
<td className="px-3 py-2 align-middle text-xs">
{t(`executiveMeetings.manage.status.${m.status}`)}