From 7c85b7bbd7a49fe23fe165d4fc18ef1ab0169a4b Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 24 Sep 2026 19:20:34 -0400 Subject: [PATCH] Start calendar weeks on Sunday in the admin display Render the month grid header Sunday-first with a (weekdayOf + 1) % 7 leading pad, list Sunday first in the weekly-repeat picker, and order recurrence summaries Sunday-first. The stored Monday=0 .. Sunday=6 weekday numbering is unchanged; a regression test pins it. Co-Authored-By: Claude Opus 5.5 --- admin/src/definitions/DefinitionsTab.test.tsx | 33 +++++++++++++++++++ admin/src/definitions/DefinitionsTab.tsx | 7 ++-- admin/src/definitions/format.ts | 5 ++- admin/src/schedule/ScheduleTab.test.tsx | 14 ++++++-- admin/src/schedule/ScheduleTab.tsx | 5 +-- admin/src/schedule/cycle.test.ts | 18 ++++++++++ 6 files changed, 74 insertions(+), 8 deletions(-) diff --git a/admin/src/definitions/DefinitionsTab.test.tsx b/admin/src/definitions/DefinitionsTab.test.tsx index 038429b..b54475f 100644 --- a/admin/src/definitions/DefinitionsTab.test.tsx +++ b/admin/src/definitions/DefinitionsTab.test.tsx @@ -362,6 +362,39 @@ describe("DefinitionsTab", () => { expect(screen.getByText("4 definitions ยท 3 children")).toBeTruthy(); }); + it("the weekday picker lists Sunday first and Sunday still stores 6", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date(2026, 8, 23, 9, 0)); // Wed 23 Sep 2026, local + await renderReady(); + + fireEvent.click(screen.getByRole("button", { name: "New" })); + const s = sheet(); + fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), { + target: { value: "Feed the cat" }, + }); + fireEvent.click(within(s).getByRole("button", { name: /Assigned children/ })); + fireEvent.click(within(s).getByRole("checkbox", { name: "Declan" })); + fireEvent.change(within(s).getByRole("combobox", { name: "Repeats" }), { + target: { value: "weekly" }, + }); + + const days = within(s).getByRole("group", { name: "Days" }); + expect( + within(days) + .getAllByRole("button") + .map((b) => b.getAttribute("aria-label")), + ).toEqual(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]); + fireEvent.click(within(days).getByRole("button", { name: "Sunday" })); + fireEvent.click(within(s).getByRole("button", { name: "Evening" })); + + await act(async () => { + fireEvent.click(within(s).getByRole("button", { name: "Save changes" })); + }); + const body = api.writes()[0].body as { rule: DefinitionRule }; + expect(body.rule.weekday_set).toEqual([2, 6]); + expect(meta(await screen.findByTestId("definition-99"))).toContain("on Sun, Wed"); + }); + it("tapping a row opens the sheet in editing state and saving PATCHes", async () => { await renderReady(); const row = screen.getByTestId("definition-11"); diff --git a/admin/src/definitions/DefinitionsTab.tsx b/admin/src/definitions/DefinitionsTab.tsx index b4803b5..5db5f16 100644 --- a/admin/src/definitions/DefinitionsTab.tsx +++ b/admin/src/definitions/DefinitionsTab.tsx @@ -27,6 +27,7 @@ import { } from "../api/definitions"; import { MONTH_SHORT, + WEEKDAY_DISPLAY_ORDER, WEEKDAY_SHORT, WINDOW_LABELS, WINDOW_ORDER, @@ -680,9 +681,9 @@ function EditSheet({ {draft.repeats === "weekly" || draft.repeats === "custom" ? (
- {WEEKDAY_SHORT.map((label, day) => ( + {WEEKDAY_DISPLAY_ORDER.map((day) => ( ))}
diff --git a/admin/src/definitions/format.ts b/admin/src/definitions/format.ts index a2246d4..2741859 100644 --- a/admin/src/definitions/format.ts +++ b/admin/src/definitions/format.ts @@ -21,6 +21,9 @@ export const WINDOW_LABELS: Record = { /** Index 0 = Monday .. 6 = Sunday, the core's weekday numbering. */ export const WEEKDAY_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; +/** Display order for a week: Sunday first. The stored indices stay Monday=0. */ +export const WEEKDAY_DISPLAY_ORDER = [6, 0, 1, 2, 3, 4, 5]; + export const MONTH_SHORT = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", @@ -51,7 +54,7 @@ export function assigneeSummary( function weekdayList(days: number[] | null): string { return [...(days ?? [])] - .sort((a, b) => a - b) + .sort((a, b) => WEEKDAY_DISPLAY_ORDER.indexOf(a) - WEEKDAY_DISPLAY_ORDER.indexOf(b)) .map((day) => WEEKDAY_SHORT[day]) .join(", "); } diff --git a/admin/src/schedule/ScheduleTab.test.tsx b/admin/src/schedule/ScheduleTab.test.tsx index feae62c..24af4d0 100644 --- a/admin/src/schedule/ScheduleTab.test.tsx +++ b/admin/src/schedule/ScheduleTab.test.tsx @@ -131,10 +131,20 @@ describe("ScheduleTab", () => { expect(today.className).toContain("schedule-day--today"); expect(today.dataset.state).toBe("home"); expect(today.getAttribute("aria-label")).toBe("Sep 23 (today): Home"); - // September 2026 starts on a Tuesday: one leading pad after the initials. + // Sunday-first header; September 2026 starts on a Tuesday: two leading pads. const grid = screen.getByRole("group", { name: "Month grid" }); + expect(Array.from(grid.children, (c) => c.textContent).slice(0, 7)).toEqual([ + "S", + "M", + "T", + "W", + "T", + "F", + "S", + ]); expect(grid.children[7].tagName).toBe("SPAN"); - expect(grid.children[8].getAttribute("data-date")).toBe("2026-09-01"); + expect(grid.children[8].tagName).toBe("SPAN"); + expect(grid.children[9].getAttribute("data-date")).toBe("2026-09-01"); const legend = screen.getByRole("list", { name: "Legend" }); expect(within(legend).getAllByRole("listitem").map((li) => li.textContent)).toEqual([ diff --git a/admin/src/schedule/ScheduleTab.tsx b/admin/src/schedule/ScheduleTab.tsx index 6bd5eb4..3ab4384 100644 --- a/admin/src/schedule/ScheduleTab.tsx +++ b/admin/src/schedule/ScheduleTab.tsx @@ -46,7 +46,8 @@ export const SCROLL_BOTTOM_PADDING = "92px"; export const DEFAULT_CYCLE_LENGTH_WEEKS = 2; const CYCLE_LENGTH_CHOICES = [1, 2, 3, 4]; -const WEEKDAY_INITIALS = ["M", "T", "W", "T", "F", "S", "S"]; +/** Sunday-first header; `weekdayOf` stays Monday=0. */ +const WEEKDAY_INITIALS = ["S", "M", "T", "W", "T", "F", "S"]; export const STATE_LABELS: Record = { home: "Home", @@ -130,7 +131,7 @@ interface MonthGridProps { function MonthGrid({ month, today, schedule, effective, overrides, onMonth, onToggle }: MonthGridProps) { const dates = monthDates(month); - const leading = weekdayOf(month); + const leading = (weekdayOf(month) + 1) % 7; return ( <>
diff --git a/admin/src/schedule/cycle.test.ts b/admin/src/schedule/cycle.test.ts index 26217f5..6473c6a 100644 --- a/admin/src/schedule/cycle.test.ts +++ b/admin/src/schedule/cycle.test.ts @@ -58,6 +58,24 @@ describe("calendar helpers", () => { expect(weekdayOf("2026-09-06")).toBe(6); }); + it("weekdayOf keeps the stored Monday=0 .. Sunday=6 numbering (display order is separate)", () => { + // A full known week plus dates across month and year ends. + expect( + [ + "2026-09-07", + "2026-09-08", + "2026-09-09", + "2026-09-10", + "2026-09-11", + "2026-09-12", + "2026-09-13", + ].map(weekdayOf), + ).toEqual([0, 1, 2, 3, 4, 5, 6]); + expect(weekdayOf("2026-11-01")).toBe(6); // Sunday + expect(weekdayOf("2027-01-01")).toBe(4); // Friday + expect(weekdayOf("2024-02-29")).toBe(3); // Thursday + }); + it("monthStart shifts across year ends", () => { expect(monthStart("2026-12-15", 1)).toBe("2027-01-01"); expect(monthStart("2026-01-31", -1)).toBe("2025-12-01");