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;