Task #292: Show schedule meetings in 12h format and add LTR Start/End labels to inline editor
What changed:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- formatTime helper now uses hour: "numeric", minute: "2-digit", hour12: true
(was hour12: false). All on-page time displays — schedule cells and the
Manage tab list — flow through this single helper. Latin digits remain
enforced via the shared i18nFormatTime helper, so Arabic renders as
"5:39 م" rather than mixing Arabic-Indic numerals into the table.
- Inline time editor (InlineTimeEditor):
- Wrapper locked to dir="ltr" so start-on-left / end-on-right (and
save/cancel after end) layout never mirrors under RTL.
- Each <input type="time"> wrapped in a vertical mini-column with a
small muted <label> above ("Start"/"End" or "البدء"/"الانتهاء").
- Stable per-meeting input ids (em-time-{start,end}-input-<id>) wired
via htmlFor so click-to-focus works and screen readers don't jump
across rows. Existing aria-labels preserved for accessibility.
- All existing data-testids preserved; keyboard (Enter/Escape) and
blur-to-save behavior unchanged.
- artifacts/tx-os/src/locales/en.json + ar.json
- Added executiveMeetings.schedule.timeStartShort ("Start"/"البدء")
and timeEndShort ("End"/"الانتهاء"). Existing timeStart/timeEnd
("Start time"/"End time") were too long for the compact label slot
above the time inputs and remain in use as aria-labels.
Out of scope (verified):
- API-side PDF renderer at artifacts/api-server/src/lib/pdf-renderer.ts
already uses hour12: true (line 291) — no change needed.
- No tx-os print/PDF page exists; scope confined to the schedule page.
- Audit log timestamps, home/chat clocks, clockHour12 user setting:
out of scope, untouched.
Verification:
- TypeScript clean.
- E2E (English): visual screenshot confirmed schedule shows
"5:39 PM – 5:50 PM" / "6:20 PM – 6:25 PM"; inline editor shows Start/End
labels above inputs in correct LTR order.
- E2E (Arabic): runTest passed — time cells use Latin digits with ص/م
markers; inline editor shows البدء/الانتهاء labels with LTR layout
(start-on-left, end-on-right, save/cancel on right).
- Architect code review: PASS, no critical issues.
- No existing test asserts a specific 24h display string, so no e2e
test breakage introduced.
Notes:
- Pre-existing test workflow failures on executive-meetings*.test.mjs
(500 errors on meeting create + Reorder 400 vs 200) are unrelated to
this task — they pre-date both this task and Task #205.
- Re-applies and extends the 12h direction from earlier Task #160 which
had regressed back to 24h.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -1087,6 +1087,8 @@
|
||||
"newMeetingDefaultEn": "New meeting",
|
||||
"timeStart": "وقت البداية",
|
||||
"timeEnd": "وقت النهاية",
|
||||
"timeStartShort": "البدء",
|
||||
"timeEndShort": "الانتهاء",
|
||||
"timeOrderError": "وقت النهاية قبل وقت البداية",
|
||||
"addAttendee": "أضف حاضر",
|
||||
"addPersonHere": "+ شخص هنا",
|
||||
|
||||
@@ -1005,6 +1005,8 @@
|
||||
"newMeetingDefaultEn": "New meeting",
|
||||
"timeStart": "Start time",
|
||||
"timeEnd": "End time",
|
||||
"timeStartShort": "Start",
|
||||
"timeEndShort": "End",
|
||||
"timeOrderError": "End time is before start time",
|
||||
"addAttendee": "Add attendee",
|
||||
"addPersonHere": "+ person here",
|
||||
|
||||
@@ -357,18 +357,24 @@ function computeCurrentMeetingId(
|
||||
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 use 24h
|
||||
// (no AM/PM) so the schedule shows a clean "10:00 – 11:00" without
|
||||
// the ص/م suffix in 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).
|
||||
// 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).
|
||||
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: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3480,41 +3486,72 @@ function TimeRangeCell({
|
||||
}
|
||||
};
|
||||
|
||||
// Stable per-meeting input ids so the <label htmlFor=...> hookup is
|
||||
// unique across meeting rows in the schedule. Without this, screen
|
||||
// readers and click-to-focus on the label would jump to the first
|
||||
// matching id elsewhere in the document.
|
||||
const startInputId = `em-time-start-input-${meeting.id}`;
|
||||
const endInputId = `em-time-end-input-${meeting.id}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
onFocus={onFocusIn}
|
||||
onBlur={onFocusOut}
|
||||
className="flex items-center justify-center gap-1 font-mono whitespace-nowrap"
|
||||
// Lock the editor to LTR so start-on-the-left / end-on-the-right
|
||||
// is preserved regardless of the surrounding RTL Arabic page
|
||||
// layout. Without this, the two time inputs, the dash, and the
|
||||
// save/cancel buttons get visually mirrored, making it impossible
|
||||
// for the user to tell which input is which.
|
||||
dir="ltr"
|
||||
className="inline-flex items-end justify-center gap-1 font-mono whitespace-nowrap"
|
||||
data-testid={`em-time-${meeting.id}`}
|
||||
data-editing="true"
|
||||
>
|
||||
<input
|
||||
ref={startInputRef}
|
||||
type="time"
|
||||
value={start}
|
||||
onChange={(e) => setStart(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-label={t("executiveMeetings.schedule.timeStart")}
|
||||
className="w-[5rem] border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
|
||||
data-testid={`em-time-start-${meeting.id}`}
|
||||
/>
|
||||
<span className="text-muted-foreground">–</span>
|
||||
<input
|
||||
type="time"
|
||||
value={end}
|
||||
onChange={(e) => setEnd(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-label={t("executiveMeetings.schedule.timeEnd")}
|
||||
className="w-[5rem] border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
|
||||
data-testid={`em-time-end-${meeting.id}`}
|
||||
/>
|
||||
<div className="flex flex-col items-center">
|
||||
<label
|
||||
htmlFor={startInputId}
|
||||
className="text-[10px] leading-none text-muted-foreground mb-0.5 select-none"
|
||||
>
|
||||
{t("executiveMeetings.schedule.timeStartShort")}
|
||||
</label>
|
||||
<input
|
||||
ref={startInputRef}
|
||||
id={startInputId}
|
||||
type="time"
|
||||
value={start}
|
||||
onChange={(e) => setStart(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-label={t("executiveMeetings.schedule.timeStart")}
|
||||
className="w-[5rem] border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
|
||||
data-testid={`em-time-start-${meeting.id}`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-muted-foreground pb-0.5">–</span>
|
||||
<div className="flex flex-col items-center">
|
||||
<label
|
||||
htmlFor={endInputId}
|
||||
className="text-[10px] leading-none text-muted-foreground mb-0.5 select-none"
|
||||
>
|
||||
{t("executiveMeetings.schedule.timeEndShort")}
|
||||
</label>
|
||||
<input
|
||||
id={endInputId}
|
||||
type="time"
|
||||
value={end}
|
||||
onChange={(e) => setEnd(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-label={t("executiveMeetings.schedule.timeEnd")}
|
||||
className="w-[5rem] border border-blue-400 rounded px-1 py-0.5 text-xs bg-white"
|
||||
data-testid={`em-time-end-${meeting.id}`}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => void save()}
|
||||
aria-label={t("common.save")}
|
||||
className="ms-0.5 p-0.5 rounded text-green-700 hover:bg-green-50 focus:bg-green-50 focus:outline-none"
|
||||
className="ms-0.5 p-0.5 rounded text-green-700 hover:bg-green-50 focus:bg-green-50 focus:outline-none mb-0.5"
|
||||
data-testid={`em-time-save-${meeting.id}`}
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
@@ -3524,7 +3561,7 @@ function TimeRangeCell({
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={cancel}
|
||||
aria-label={t("common.cancel")}
|
||||
className="p-0.5 rounded text-red-700 hover:bg-red-50 focus:bg-red-50 focus:outline-none"
|
||||
className="p-0.5 rounded text-red-700 hover:bg-red-50 focus:bg-red-50 focus:outline-none mb-0.5"
|
||||
data-testid={`em-time-cancel-${meeting.id}`}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
|
||||
Reference in New Issue
Block a user