Manual timezone is the single source of local-time truth (task 7e173881) (#285)

Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
Co-committed-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #285.
This commit is contained in:
2026-09-30 10:02:05 -04:00
committed by approver_agent
parent 69db92b7b8
commit 0e726ac64f
46 changed files with 1556 additions and 429 deletions
+52 -2
View File
@@ -6,6 +6,7 @@ import SettingsScreen, { SCROLL_BOTTOM_PADDING } from "./SettingsScreen";
import type { AdminChild } from "../api/definitions";
import type { HouseholdSettings } from "../api/settings";
import { storeTokens } from "../auth/oidc";
import { AppTimezoneProvider, useAppTimezone } from "../time/AppTimezone";
const CHILDREN_PATH = "/api/v1/admin/children";
const REORDER_PATH = `${CHILDREN_PATH}/reorder`;
@@ -23,8 +24,17 @@ const DEFAULT_SETTINGS: HouseholdSettings = {
end_of_day_report_enabled: true,
celebration_enabled: false,
timezone: "America/Chicago",
effective_timezone: "America/Chicago",
};
/** The API host's own zone: the effective zone while none is stored. */
const API_HOST_ZONE = "Europe/London";
/** The settings as the API answers: `effective_timezone` derived like the API's. */
function settingsBody(): HouseholdSettings {
return { ...settings, effective_timezone: settings.timezone || API_HOST_ZONE };
}
function child(
id: number,
display_name: string,
@@ -82,10 +92,10 @@ async function routeFetch(url: string, init: RequestInit = {}): Promise<Response
return respond();
}
if (path === CHILDREN_PATH && method === "GET") return jsonResponse({ children });
if (path === SETTINGS_PATH && method === "GET") return jsonResponse(settings);
if (path === SETTINGS_PATH && method === "GET") return jsonResponse(settingsBody());
if (path === SETTINGS_PATH && method === "PATCH") {
settings = { ...settings, ...body };
return jsonResponse(settings);
return jsonResponse(settingsBody());
}
if (path === CHILDREN_PATH && method === "POST") {
const created = child(9, body.display_name, body.sort_order ?? 0, {
@@ -791,3 +801,43 @@ describe("SettingsScreen — manual timezone control", () => {
}
});
});
describe("SettingsScreen — the saved zone applies app-wide", () => {
function ZoneProbe() {
return <output data-testid="app-zone">{useAppTimezone() || "(browser)"}</output>;
}
beforeEach(() => {
sessionStorage.clear();
storeTokens({ access_token: "at-123", expires_in: 600 });
children = [child(1, "Declan", 1)];
settings = { ...DEFAULT_SETTINGS, timezone: "" };
writeResponse = null;
fetchMock = vi.fn(async (url: string, init?: RequestInit) => routeFetch(url, init));
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it("a saved zone becomes the application timezone at once", async () => {
render(
<AppTimezoneProvider>
<ZoneProbe />
<SettingsScreen onClose={vi.fn()} />
</AppTimezoneProvider>,
);
const form = await screen.findByRole("form", { name: "Preferences" });
// No zone stored: the application runs in the API host's zone.
expect(screen.getByTestId("app-zone").textContent).toBe(API_HOST_ZONE);
fireEvent.change(within(form).getByRole("combobox", { name: "Timezone" }), {
target: { value: "America/New_York" },
});
fireEvent.click(within(form).getByRole("button", { name: "Save preferences" }));
await screen.findByText("Preferences saved.");
expect(screen.getByTestId("app-zone").textContent).toBe("America/New_York");
});
});