From 1d0a2a812845eb556ae27ee40aa5c4b0824b9e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 19:45:12 +0100 Subject: [PATCH] fix(doctor-report): print the date of birth on its own day west of UTC The date of birth is a calendar date stored at UTC midnight. The PDF cover and the share page formatted it as an instant in the owner's zone, so everyone in the Americas read the day before; settings and the FHIR export, which take the date part, already showed the right day. Both now print it through a calendar-date formatter (read in UTC, the owner's date order), which is right in every zone, unlike a noon anchor that moves the day east of UTC+12. --- .../__tests__/clinician-view.test.tsx | 18 +++++++++++++ src/components/clinician/clinician-view.tsx | 7 +++++- .../clinician/identity-sections.tsx | 6 ++--- .../__tests__/doctor-report-pdf-core.test.ts | 25 +++++++++++++++++++ src/lib/doctor-report-pdf-core.ts | 4 +++ .../header-profile-section.ts | 3 ++- src/lib/doctor-report-pdf/render-context.ts | 6 +++++ 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/components/clinician/__tests__/clinician-view.test.tsx b/src/components/clinician/__tests__/clinician-view.test.tsx index 7b16bb750..c6ef44d2f 100644 --- a/src/components/clinician/__tests__/clinician-view.test.tsx +++ b/src/components/clinician/__tests__/clinician-view.test.tsx @@ -72,6 +72,7 @@ function render( | "selection" | "timeFormat" | "dateFormat" + | "timezone" > >, ) { @@ -501,6 +502,23 @@ describe("", () => { expect(html).not.toContain(' { + const html = render( + makeReport({ + patient: { + name: "A. Patient", + // Stored at UTC midnight: 13 May, 20:00 in New York. + dateOfBirth: "1960-05-14T00:00:00.000Z", + gender: null, + heightCm: null, + }, + }), + { timezone: "America/New_York", locale: "en" }, + ); + expect(html).toContain("05/14/1960"); + expect(html).not.toContain("05/13/1960"); + }); }); /** diff --git a/src/components/clinician/clinician-view.tsx b/src/components/clinician/clinician-view.tsx index 2be46e88b..141456606 100644 --- a/src/components/clinician/clinician-view.tsx +++ b/src/components/clinician/clinician-view.tsx @@ -30,6 +30,7 @@ * document list in `./documents-list`, the downloads in `./download-actions`. */ import type { DoctorReportData } from "@/lib/doctor-report-data"; +import { formatDate } from "@/lib/date-format"; import { formatLabValue, type LabNumberFormat } from "@/lib/labs/format-value"; import { makeFormatters, @@ -153,6 +154,10 @@ export function ClinicianView({ // Owner hour cycle + date order ride along (issue #922). const fmt = makeFormatters(locale, timezone, timeFormat, dateFormat); const fmtDate = (iso: string) => fmt.date(new Date(iso)); + // A date-only value (a date of birth, a dose date) is its calendar date in + // every zone; read as an instant it fell on the previous day west of UTC. + const fmtCalendarDate = (iso: string) => + formatDate(iso.slice(0, 10), dateFormat, locale); const fmtDateTime = (iso: string) => fmt.dateTime(new Date(iso)); // Two decimals at most, in the owner's locale: a German reader gets // "80,5", not the "80.5" a bare number prints. @@ -199,7 +204,7 @@ export function ClinicianView({ t={t} report={report} scope={scope} - fmtDate={fmtDate} + fmtCalendarDate={fmtCalendarDate} /> {/* ── measurements ─────────────────────────────────────── */} diff --git a/src/components/clinician/identity-sections.tsx b/src/components/clinician/identity-sections.tsx index 3eacd8e81..01debc6e0 100644 --- a/src/components/clinician/identity-sections.tsx +++ b/src/components/clinician/identity-sections.tsx @@ -48,12 +48,12 @@ export function PatientIdentitySection({ t, report, scope, - fmtDate, + fmtCalendarDate, }: { t: Translate; report: DoctorReportData; scope: LeafScope; - fmtDate: (iso: string) => string; + fmtCalendarDate: (iso: string) => string; }) { const patient = report.patient; const name = patient.name; @@ -67,7 +67,7 @@ export function PatientIdentitySection({ if (patient.dateOfBirth) { rows.push({ label: t("doctorReport.dateOfBirth"), - value: fmtDate(patient.dateOfBirth), + value: fmtCalendarDate(patient.dateOfBirth), }); } if (genderKey) { diff --git a/src/lib/__tests__/doctor-report-pdf-core.test.ts b/src/lib/__tests__/doctor-report-pdf-core.test.ts index 4547d92e5..f237ec2c0 100644 --- a/src/lib/__tests__/doctor-report-pdf-core.test.ts +++ b/src/lib/__tests__/doctor-report-pdf-core.test.ts @@ -1176,6 +1176,31 @@ describe("doctor report — the user's date order", () => { ); expect(text).toContain("05/03/2026"); }); + + it("prints the date of birth on its own day west of UTC", async () => { + const { t } = getServerTranslator("en"); + const text = await extractText( + renderDoctorReportPdfBytes( + makeData({ + patient: { + ...makeData().patient, + // Stored at UTC midnight: 13 May, 20:00 in New York. + dateOfBirth: "1960-05-14T00:00:00.000Z", + }, + }), + { + t, + locale: "en", + now: FIXED_NOW, + userTz: "America/New_York", + timeFormat: "AUTO", + dateFormat: "AUTO", + }, + ), + ); + expect(text).toContain("05/14/1960"); + expect(text).not.toContain("05/13/1960"); + }); }); // ── script coverage ── the report is read in every shipped locale and carries diff --git a/src/lib/doctor-report-pdf-core.ts b/src/lib/doctor-report-pdf-core.ts index 3a511be97..825f9d2b2 100644 --- a/src/lib/doctor-report-pdf-core.ts +++ b/src/lib/doctor-report-pdf-core.ts @@ -31,6 +31,7 @@ import { } from "./format-locale"; import type { Locale } from "./i18n/config"; import { isValidTimezone } from "./tz/format"; +import { formatDate } from "./date-format"; import type { DoctorReportData } from "./doctor-report-data"; import { buildEmergencyFirstPageSection } from "./doctor-report-pdf/emergency-first-page-section"; import { buildHeaderProfileSection } from "./doctor-report-pdf/header-profile-section"; @@ -146,6 +147,8 @@ export function buildDoctorReportPdfDocument( const num = (value: number, decimals = 1) => formatters.number(value, decimals); const fmtDate = (iso: string) => formatters.date(iso); + const fmtCalendarDate = (iso: string) => + formatDate(iso.slice(0, 10), dateFormat, locale); const footerTz = userTz && isValidTimezone(userTz) ? userTz : DEFAULT_TIMEZONE; @@ -187,6 +190,7 @@ export function buildDoctorReportPdfDocument( t, num, fmtDate, + fmtCalendarDate, dateShort: formatters.dateShort, dateTime: formatters.dateTime, now, diff --git a/src/lib/doctor-report-pdf/header-profile-section.ts b/src/lib/doctor-report-pdf/header-profile-section.ts index f4a3ca0d4..93328d8a6 100644 --- a/src/lib/doctor-report-pdf/header-profile-section.ts +++ b/src/lib/doctor-report-pdf/header-profile-section.ts @@ -16,6 +16,7 @@ export function buildHeaderProfileSection( t, num, fmtDate, + fmtCalendarDate, now, insuranceNumber, margin, @@ -68,7 +69,7 @@ export function buildHeaderProfileSection( } if (data.patient.dateOfBirth) { patientInfo.push( - `${t("doctorReport.dateOfBirth")}: ${fmtDate(data.patient.dateOfBirth)}`, + `${t("doctorReport.dateOfBirth")}: ${fmtCalendarDate(data.patient.dateOfBirth)}`, ); } if (data.patient.gender) { diff --git a/src/lib/doctor-report-pdf/render-context.ts b/src/lib/doctor-report-pdf/render-context.ts index f5f1c2d85..213972cc7 100644 --- a/src/lib/doctor-report-pdf/render-context.ts +++ b/src/lib/doctor-report-pdf/render-context.ts @@ -22,6 +22,12 @@ export interface DoctorReportPdfRenderContext { t: DoctorReportTranslator; num: DoctorReportNumberFormatter; fmtDate: (iso: string) => string; + /** + * A date-only value (a date of birth, a dose date): its calendar date, the + * same in every zone. `fmtDate` reads an instant in the report zone, which + * put a UTC-midnight date on the previous day west of UTC. + */ + fmtCalendarDate: (iso: string) => string; dateShort: (iso: string) => string; dateTime: (date: Date) => string; now: Date;