diff --git a/src/components/my-day/my-day-page-parts.tsx b/src/components/my-day/my-day-page-parts.tsx index f0a5dedee6..93a28ae7f6 100644 --- a/src/components/my-day/my-day-page-parts.tsx +++ b/src/components/my-day/my-day-page-parts.tsx @@ -9,6 +9,7 @@ import { cn } from "@/components/ui-primitives"; import { appModeDefinition } from "@/lib/app-modes"; import { formatMyDayDue } from "@/lib/my-day/merge"; import type { MyDayItem, MyDaySourceMode } from "@/lib/my-day/model"; +import { formatPerthDay, PERTH_TIME_ZONE, perthDateOf, perthTimeOf } from "@/lib/perth-time"; /* * The small pieces the My Day page, its dashboard and the home card share. @@ -32,14 +33,22 @@ export function useMyDayNow(nowProp?: Date): Date { /** * How old My Day's data is, for the "Updated … · On Call, Roster" line (Ward Flow review item 5, * owner approved 12 Oct 2026): "just now", then "4 min ago", then the clock time once it is an - * hour old. `at` is when the sources last answered; `clock` is that moment as "14:05". + * hour old, with its day ("at 14:05, Tue 13 Oct"). `at` is when the sources last answered; `clock` is that moment as "14:05". */ -export function myDayUpdatedAgo(now: Date, at: number | null, clock: string | null): string { +export function myDayUpdatedAgo( + now: Date, + at: number | null, + clock: string | null, + zone: string = PERTH_TIME_ZONE, +): string { if (at === null) return clock ? `at ${clock}` : "just now"; const minutes = Math.floor((now.getTime() - at) / 60_000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes} min ago`; - return clock ? `at ${clock}` : "over an hour ago"; + // Past the hour the clock alone is ambiguous (yesterday's 14:05 reads like today's), so give the day too. + // Time and day both come from `at` in the caller's work zone (the page passes its useWorkTimeZone zone), + // so a zone change can't mix the two and nothing reads an ambient zone. + return `at ${perthTimeOf(at, zone)}, ${formatPerthDay(perthDateOf(at, zone))}`; } export function myDayModeLabel(mode: MyDaySourceMode): string { diff --git a/src/components/my-day/my-day-page.tsx b/src/components/my-day/my-day-page.tsx index 79fbfa94ec..113d998efb 100644 --- a/src/components/my-day/my-day-page.tsx +++ b/src/components/my-day/my-day-page.tsx @@ -435,7 +435,7 @@ export function MyDayPage({ now: nowProp }: { now?: Date } = {}) { setLoaded({ key: loadedKey, at: ready ? perthTimeOf(answered) : null, atMs: ready ? answered.getTime() : null }); } // "Updated 4 min ago": re-read on the minute tick, so the age keeps moving while the time stays. - const updated = loaded.at ? myDayUpdatedAgo(now, loaded.atMs, loaded.at) : null; + const updated = loaded.at ? myDayUpdatedAgo(now, loaded.atMs, loaded.at, zone) : null; // The full list has its own address, so the phone's Back returns to the dashboard. const searchParams = useSearchParams(); const view: "dashboard" | "all" = searchParams?.get("view") === "all" ? "all" : "dashboard"; diff --git a/tests/my-day-updated-ago.test.ts b/tests/my-day-updated-ago.test.ts index 085a309b47..932eee7765 100644 --- a/tests/my-day-updated-ago.test.ts +++ b/tests/my-day-updated-ago.test.ts @@ -17,13 +17,35 @@ describe("myDayUpdatedAgo", () => { expect(myDayUpdatedAgo(after(59), at, "10:05")).toBe("59 min ago"); }); - it("gives the clock time once the data is an hour old", () => { - expect(myDayUpdatedAgo(after(60), at, "10:05")).toBe("at 10:05"); - expect(myDayUpdatedAgo(after(60), at, null)).toBe("over an hour ago"); + it("gives the clock time and date once the data is an hour old", () => { + expect(myDayUpdatedAgo(after(60), at, "10:05")).toBe("at 10:05, Tue 13 Oct"); + // A day later the date is what tells yesterday's 10:05 apart from today's. + expect(myDayUpdatedAgo(after(24 * 60), at, "10:05")).toBe("at 10:05, Tue 13 Oct"); + // The time comes from the recorded moment, not a clock string captured earlier (maybe in another zone). + expect(myDayUpdatedAgo(after(60), at, null)).toBe("at 10:05, Tue 13 Oct"); + expect(myDayUpdatedAgo(after(60), at, "stale")).toBe("at 10:05, Tue 13 Oct"); + }); + + it("dates the moment on the Perth calendar, not UTC", () => { + // 23:30 Perth on Mon 12 Oct is still 12 Oct, though it is 15:30Z. + const late = Date.parse("2026-10-12T15:30:00Z"); + expect(myDayUpdatedAgo(new Date(late + 90 * 60_000), late, "23:30")).toBe("at 23:30, Mon 12 Oct"); + // 07:30 Perth on Tue 13 Oct is 23:30Z on 12 Oct. + const early = Date.parse("2026-10-12T23:30:00Z"); + expect(myDayUpdatedAgo(new Date(early + 90 * 60_000), early, "07:30")).toBe("at 07:30, Tue 13 Oct"); }); it("falls back to the clock time, or just now, with no moment recorded", () => { expect(myDayUpdatedAgo(after(5), null, "10:05")).toBe("at 10:05"); expect(myDayUpdatedAgo(after(5), null, null)).toBe("just now"); }); + + it("formats the time and date in the zone it is given", () => { + // 02:05Z is 13:05 in Sydney (AEDT, UTC+11) on Tue 13 Oct. + expect(myDayUpdatedAgo(after(60), at, "10:05", "Australia/Sydney")).toBe("at 13:05, Tue 13 Oct"); + // 15:30Z on 12 Oct is already 13 Oct in Sydney (02:30) but still 12 Oct in Perth (23:30). + const late = Date.parse("2026-10-12T15:30:00Z"); + expect(myDayUpdatedAgo(new Date(late + 90 * 60_000), late, null, "Australia/Sydney")).toBe("at 02:30, Tue 13 Oct"); + expect(myDayUpdatedAgo(new Date(late + 90 * 60_000), late, null, "Australia/Perth")).toBe("at 23:30, Mon 12 Oct"); + }); });