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.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 19:45:12 +01:00
1 parent 9a02dfff2e
commit 1d0a2a8128
7 files changed
+64 -5

No files matched your search

@@ -72,6 +72,7 @@ function render(
| "selection"
| "timeFormat"
| "dateFormat"
| "timezone"
>
>,
) {
@@ -501,6 +502,23 @@ describe("<ClinicianView>", () => {
expect(html).not.toContain('<img src="/c/hls_tok/d/doc-b"');
expect(html).toContain("Referral letter");
});
it("prints the date of birth on its own day west of UTC", () => {
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");
});
});
/**
+6 -1
View File
@@ -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 ─────────────────────────────────────── */}
@@ -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) {
@@ -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
+4
View File
@@ -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,
@@ -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) {
@@ -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;