Skip to content
Merged
15 changes: 12 additions & 3 deletions src/components/my-day/my-day-page-parts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/components/my-day/my-day-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
28 changes: 25 additions & 3 deletions tests/my-day-updated-ago.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Loading