From 88200a9273a308196293a955db057f87d7f83490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 21 Oct 2026 15:07:05 +0200 Subject: [PATCH] fix(report): cut the visit offer's end day on the record's calendar The offer took its start from the server's preparation window, cut on the record's calendar, and its end from the appointment's day in the browser's zone. In a browser in another zone a late-evening or early-morning visit moved the end a day against the start. Both ends now use the record's zone. --- .../__tests__/report-visit-offer.test.tsx | 18 ++++++++++- .../report-selection/report-visit-offer.tsx | 31 ++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/components/settings/report-selection/__tests__/report-visit-offer.test.tsx b/src/components/settings/report-selection/__tests__/report-visit-offer.test.tsx index c21b7fc1d..68376be4d 100644 --- a/src/components/settings/report-selection/__tests__/report-visit-offer.test.tsx +++ b/src/components/settings/report-selection/__tests__/report-visit-offer.test.tsx @@ -155,10 +155,26 @@ describe("", () => { occurredAt: at.toISOString(), preparation: { from: "2026-03-02", basis: "default", since: null }, }), + "Europe/Berlin", ); expect(window.from).toBe("2026-03-02"); expect(window.to).toMatch(/^\d{4}-\d{2}-\d{2}$/); // Without a window from the server, only the end moves. - expect(visitReportWindow(visit({ id: "plain" })).from).toBeNull(); + expect( + visitReportWindow(visit({ id: "plain" }), "Europe/Berlin").from, + ).toBeNull(); + }); + + it("cuts the appointment's day on the record's calendar, like the start", () => { + // 04:30 on the 12th in Berlin is still the evening of the 11th in New + // York. The start comes from the server on the record's calendar; the + // end is cut in the zone it is handed, never the browser's own. + const early = visit({ + id: "early", + occurredAt: "2026-11-12T03:30:00.000Z", + preparation: { from: "2026-08-12", basis: "default", since: null }, + }); + expect(visitReportWindow(early, "Europe/Berlin").to).toBe("2026-11-12"); + expect(visitReportWindow(early, "America/New_York").to).toBe("2026-11-11"); }); }); diff --git a/src/components/settings/report-selection/report-visit-offer.tsx b/src/components/settings/report-selection/report-visit-offer.tsx index 641099c94..62806d0aa 100644 --- a/src/components/settings/report-selection/report-visit-offer.tsx +++ b/src/components/settings/report-selection/report-visit-offer.tsx @@ -28,9 +28,13 @@ import { encounterKindText } from "@/components/encounters/encounter-labels"; import { preparationText } from "@/components/encounters/preparation-text"; import { Button } from "@/components/ui/button"; import { useEncounters, type Encounter } from "@/hooks/use-encounters"; -import { useFormatters, useTranslations } from "@/lib/i18n/context"; +import { + useDisplayTimezone, + useFormatters, + useTranslations, +} from "@/lib/i18n/context"; import type { EncounterKind } from "@/generated/prisma/client"; -import { detectBrowserTimezone, userDayKey } from "@/lib/tz/format"; +import { userDayKey } from "@/lib/tz/format"; /** * How far ahead a booked visit is treated as the reason for this report. @@ -40,11 +44,6 @@ import { detectBrowserTimezone, userDayKey } from "@/lib/tz/format"; */ export const REPORT_VISIT_HORIZON_DAYS = 14; -/** The local `yyyy-MM-dd` a `DateField` takes, in the reader's own day. */ -function localDay(iso: string): string { - return userDayKey(new Date(iso), detectBrowserTimezone()); -} - /** The report window an appointment offers, as `yyyy-MM-dd` days. */ export interface VisitReportWindow { /** The server's start of the window before the visit; null if it has none. */ @@ -53,11 +52,20 @@ export interface VisitReportWindow { to: string; } -/** What pressing the offer for `visit` applies. */ -export function visitReportWindow(visit: Encounter): VisitReportWindow { +/** + * What pressing the offer for `visit` applies. Both ends are days of the + * record's own calendar (`timeZone`, the profile zone): the server cuts + * `preparation.from` there, and the appointment's day has to be cut in the + * same zone, or a browser in another zone moved the end a day against the + * start for a late-evening or early-morning visit. + */ +export function visitReportWindow( + visit: Encounter, + timeZone: string, +): VisitReportWindow { return { from: visit.preparation?.from ?? null, - to: localDay(visit.occurredAt), + to: userDayKey(new Date(visit.occurredAt), timeZone), }; } @@ -69,6 +77,7 @@ export function ReportVisitOffer({ }) { const { t } = useTranslations(); const format = useFormatters(); + const timeZone = useDisplayTimezone(); const [dismissed, setDismissed] = useState(false); const visits = useEncounters(); @@ -131,7 +140,7 @@ export function ReportVisitOffer({ size="sm" className="ml-auto min-h-11 sm:ml-0 sm:min-h-9" data-slot="report-visit-offer-apply" - onClick={() => onUseVisitWindow(visitReportWindow(next))} + onClick={() => onUseVisitWindow(visitReportWindow(next, timeZone))} > {t("settings.healthRecord.visitOfferApplyWindow")}