diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json
index dd83b4bdc..7861139a9 100644
--- a/src/__tests__/baselines/source-function-lines.json
+++ b/src/__tests__/baselines/source-function-lines.json
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
- "count": 344,
- "total": 111520,
+ "count": 343,
+ "total": 111318,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -94,7 +94,6 @@
"components/documents/document-facts-review.tsx :: DocumentFactsSection": 242,
"components/documents/document-facts-review.tsx :: FactRow": 261,
"components/documents/document-filter-bar.tsx :: DocumentFilterBar": 348,
- "components/documents/document-record-links.tsx :: DocumentRecordLinks": 202,
"components/documents/document-timeline.tsx :: DocumentTimeline": 314,
"components/documents/documents-view.tsx :: DocumentsView": 303,
"components/documents/sources/document-source-picker.tsx :: DocumentSourcePicker": 563,
diff --git a/src/components/clinician/clinician-view.tsx b/src/components/clinician/clinician-view.tsx
index 5480c050d..43be0b2a6 100644
--- a/src/components/clinician/clinician-view.tsx
+++ b/src/components/clinician/clinician-view.tsx
@@ -134,6 +134,15 @@ interface ClinicianViewProps {
dateFormat: DateFormatPreference;
}
+/**
+ * A date-only value (a date of birth, a dose date) as its calendar date in
+ * the owner's date order, the same in every zone. Read as an instant in the
+ * owner's zone it fell on the previous day west of UTC.
+ */
+function calendarDateIn(dateFormat: DateFormatPreference, locale: Locale) {
+ return (iso: string) => formatDate(iso.slice(0, 10), dateFormat, locale);
+}
+
export function ClinicianView({
t,
label,
@@ -154,10 +163,6 @@ 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.
@@ -204,7 +209,7 @@ export function ClinicianView({
t={t}
report={report}
scope={scope}
- fmtCalendarDate={fmtCalendarDate}
+ fmtCalendarDate={calendarDateIn(dateFormat, locale)}
/>
{/* ── measurements ─────────────────────────────────────── */}
@@ -303,7 +308,7 @@ export function ClinicianView({
t={t}
report={report}
scope={scope}
- fmtCalendarDate={fmtCalendarDate}
+ fmtCalendarDate={calendarDateIn(dateFormat, locale)}
/>
diff --git a/src/components/documents/document-record-links.tsx b/src/components/documents/document-record-links.tsx
index fcfce50e6..c4de521b8 100644
--- a/src/components/documents/document-record-links.tsx
+++ b/src/components/documents/document-record-links.tsx
@@ -188,10 +188,7 @@ export function DocumentRecordLinks({
const [visitIds, setVisitIds] = useState(null);
const [doseIds, setDoseIds] = useState(null);
- const labels = {
- suggested: t("links.picker.suggested"),
- date: (iso: string) => format.date(iso),
- };
+ const suggested = t("links.picker.suggested");
const visitOptions = recordOptions(
[...encounters.items]
@@ -205,9 +202,8 @@ export function DocumentRecordLinks({
href: visitHref(visit.id),
})),
anchor,
- labels,
+ { suggested, date: format.date },
);
- // A dose date is a calendar date at UTC midnight, never an instant.
const doseOptions = recordOptions(
(vaccinations.data?.vaccinations ?? []).map((dose) => ({
id: dose.id,
@@ -216,7 +212,7 @@ export function DocumentRecordLinks({
href: doseHref(dose.id),
})),
anchor,
- { ...labels, date: calendarDate },
+ { suggested, date: calendarDate }, // a dose date is a calendar date
);
const showDoses = vaccinationsOn && doc.vaccinationLinks !== null;
diff --git a/src/lib/account-name.ts b/src/lib/account-name.ts
index 7e39c7319..b02c371ab 100644
--- a/src/lib/account-name.ts
+++ b/src/lib/account-name.ts
@@ -12,6 +12,14 @@ export function accountName(user: {
return displayName ? displayName : user.username;
}
+/** The columns `patientName()` reads, for a Prisma `select`. */
+export const PATIENT_NAME_SELECT = {
+ username: true,
+ displayName: true,
+ fullName: true,
+ managedProfileAt: true,
+} as const;
+
/**
* The name a clinical document gives a person: the full name when one is
* set, the display name next, the login last. A managed record's login is a
diff --git a/src/lib/day/__tests__/date-only-sections.test.ts b/src/lib/day/__tests__/date-only-sections.test.ts
index 90b22fb76..1c36c036e 100644
--- a/src/lib/day/__tests__/date-only-sections.test.ts
+++ b/src/lib/day/__tests__/date-only-sections.test.ts
@@ -55,7 +55,8 @@ import { loadDayIndex } from "@/lib/day/day-index";
import { loadSinceLastVisit } from "@/lib/day/visit-preparation";
import type { DaySectionKey } from "@/lib/day/contract";
import type { DayAccess } from "@/lib/day/sections";
-import { LANE_READERS } from "@/lib/timeline/lanes";
+import { LANE_READERS, type TimelineAccess } from "@/lib/timeline/lanes";
+import { MODULE_KEYS } from "@/lib/modules/gate";
import { startOfLocalDayKey } from "@/lib/tz/local-day";
import { shiftDateKey } from "@/lib/tz/format";
@@ -167,7 +168,13 @@ describe("the timeline", () => {
const laneFrame = {
userId: "u1",
tz: TZ,
- access: {} as never,
+ access: {
+ modules: Object.fromEntries(
+ MODULE_KEYS.map((key) => [key, true]),
+ ) as TimelineAccess["modules"],
+ domainVisible: () => true,
+ owner: true,
+ },
};
const doses = await LANE_READERS.vaccinations(laneFrame);
const allergies = await LANE_READERS.allergies(laneFrame);
diff --git a/src/lib/day/day-index.ts b/src/lib/day/day-index.ts
index 28ae9d84d..7773c2379 100644
--- a/src/lib/day/day-index.ts
+++ b/src/lib/day/day-index.ts
@@ -33,7 +33,11 @@ import { SCREENER_INSTRUMENTS } from "@/lib/validations/mental-health";
import { TRACKED_INTAKE_EVENT_WHERE } from "@/lib/medications/intake-tracking";
import { effectiveMoodTz } from "@/lib/mood/date-key";
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
-import { dateOnlyKey, dayKeyAsUtcMidnight } from "@/lib/tz/date-only";
+import {
+ dateOnlyKey,
+ dateOnlyWindow,
+ dayKeyAsUtcMidnight,
+} from "@/lib/tz/date-only";
import { shiftDateKey, userDayKey } from "@/lib/tz/format";
import { startOfLocalDayKey } from "@/lib/tz/local-day";
@@ -102,15 +106,8 @@ const instantWindow = (frame: IndexFrame) => ({
lt: frame.end,
});
-/**
- * The window as UTC calendar days, for a column that holds a date rather
- * than an instant (a dose date at UTC midnight, an allergy onset or a
- * document date at noon UTC). Its date part is its day in every zone.
- */
-const calendarWindow = (frame: IndexFrame) => ({
- gte: dayKeyAsUtcMidnight(frame.from),
- lt: dayKeyAsUtcMidnight(shiftDateKey(frame.to, 1)),
-});
+const calendarWindow = (frame: IndexFrame) =>
+ dateOnlyWindow(frame.from, frame.to);
/** Mark the calendar date of every date-only value. */
function markDates(
diff --git a/src/lib/day/records.ts b/src/lib/day/records.ts
index 139cdd416..a41d23bac 100644
--- a/src/lib/day/records.ts
+++ b/src/lib/day/records.ts
@@ -39,7 +39,11 @@ import { phaseForDate } from "@/lib/cycle/engine-adapter";
import { LUTEAL_DEFAULT } from "@/lib/cycle/types";
import { resolveCycleDay } from "@/lib/cycle/verdict";
import { effectiveMoodTz } from "@/lib/mood/date-key";
-import { dateOnlyKey, dayKeyAsUtcMidnight } from "@/lib/tz/date-only";
+import {
+ dateOnlyKey,
+ dateOnlyWindow,
+ dayKeyAsUtcMidnight,
+} from "@/lib/tz/date-only";
import { daysBetweenDateKeys, shiftDateKey, userDayKey } from "@/lib/tz/format";
import { visitPath } from "@/lib/encounters/visit-links";
import {
@@ -130,19 +134,6 @@ function dateKeyOrNull(value: Date | null): string | null {
return value ? dateOnlyKey(value) : null;
}
-/**
- * The day as a window over a column that holds a date, not an instant (a
- * dose date at UTC midnight, an allergy onset or a document date at noon
- * UTC). Its date part decides the day, whatever the person's zone; the
- * local day window put a UTC-midnight dose on the previous day west of UTC.
- */
-function calendarDayWindow(day: string): { gte: Date; lt: Date } {
- return {
- gte: dayKeyAsUtcMidnight(day),
- lt: dayKeyAsUtcMidnight(shiftDateKey(day, 1)),
- };
-}
-
/* ─── medications ─────────────────────────────────────────────────────────── */
async function medicationsPart(frame: DayFrame): Promise {
@@ -591,7 +582,7 @@ async function allergiesPart(frame: DayFrame): Promise {
where: {
userId: frame.userId,
deletedAt: null,
- onsetAt: calendarDayWindow(frame.day),
+ onsetAt: dateOnlyWindow(frame.day),
},
select: { id: true, substance: true, severity: true },
});
@@ -599,8 +590,7 @@ async function allergiesPart(frame: DayFrame): Promise {
running: [],
events: rows.map((row) =>
event({
- // A date without a time of day: no clock time to print.
- at: null,
+ at: null, // a date without a time of day
kind: "allergyOnset",
section: "allergies",
id: row.id,
@@ -662,7 +652,7 @@ async function vaccinationsPart(frame: DayFrame): Promise {
where: {
userId: frame.userId,
deletedAt: null,
- occurredAt: calendarDayWindow(frame.day),
+ occurredAt: dateOnlyWindow(frame.day),
},
select: {
id: true,
@@ -683,8 +673,7 @@ async function vaccinationsPart(frame: DayFrame): Promise {
running: [],
events: rows.map((row) =>
event({
- // A dose carries a date, never a time of day.
- at: null,
+ at: null, // a date without a time of day
kind: "vaccination",
section: "vaccinations",
id: row.id,
@@ -776,7 +765,7 @@ async function checkupsPart(frame: DayFrame): Promise {
async function documentsPart(frame: DayFrame): Promise {
// A document's date is a calendar date stored at noon UTC (older rows at
// UTC midnight). The stated report date wins over the filing date.
- const day = calendarDayWindow(frame.day);
+ const day = dateOnlyWindow(frame.day);
const rows = await prisma.inboundDocument.findMany({
where: {
userId: frame.userId,
diff --git a/src/lib/day/visit-preparation.ts b/src/lib/day/visit-preparation.ts
index 533f530d9..7c83d7a0f 100644
--- a/src/lib/day/visit-preparation.ts
+++ b/src/lib/day/visit-preparation.ts
@@ -23,7 +23,11 @@ import { openText } from "@/lib/day/records";
import { measurementTypeVisible, type DayAccess } from "@/lib/day/sections";
import { prisma } from "@/lib/db";
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
-import { dateOnlyKey, dayKeyAsUtcMidnight } from "@/lib/tz/date-only";
+import {
+ dateOnlyKey,
+ dateOnlyWindow,
+ dayKeyAsUtcMidnight,
+} from "@/lib/tz/date-only";
import { daysBetweenDateKeys, shiftDateKey, userDayKey } from "@/lib/tz/format";
import { startOfLocalDayKey } from "@/lib/tz/local-day";
import { visitPath } from "@/lib/encounters/visit-links";
@@ -240,12 +244,7 @@ async function vaccinationChanges(w: Window): Promise {
where: {
userId: w.userId,
deletedAt: null,
- // A dose date is stored at UTC midnight: its date part is its day in
- // every zone, so the window is UTC calendar days, not local instants.
- occurredAt: {
- gte: dayKeyAsUtcMidnight(w.from),
- lt: dayKeyAsUtcMidnight(shiftDateKey(w.to, 1)),
- },
+ occurredAt: dateOnlyWindow(w.from, w.to), // a date, not an instant
},
select: {
id: true,
diff --git a/src/lib/doctor-report-pdf/clinical-records-notes-section.ts b/src/lib/doctor-report-pdf/clinical-records-notes-section.ts
index 40c314fd4..32a8d02ab 100644
--- a/src/lib/doctor-report-pdf/clinical-records-notes-section.ts
+++ b/src/lib/doctor-report-pdf/clinical-records-notes-section.ts
@@ -23,17 +23,8 @@ export function buildClinicalRecordsNotesSection(
context: DoctorReportPdfRenderContext,
state: DoctorReportPdfCursorState,
): DoctorReportPdfCursorState {
- const {
- doc,
- data,
- t,
- num,
- fmtDate,
- fmtCalendarDate,
- margin,
- tableBottomMargin,
- ensureSpace,
- } = context;
+ const { doc, data, t, num, fmtDate, margin, tableBottomMargin, ensureSpace } =
+ context;
let y = state.y;
// v1.17.1 — structured lab-results section. Populated when the `labs`
@@ -431,8 +422,7 @@ export function buildClinicalRecordsNotesSection(
};
const immunizationRows = data.immunizations.map((row) => [
- // A dose date is a calendar date at UTC midnight, never an instant.
- fmtCalendarDate(row.occurredAt),
+ context.fmtCalendarDate(row.occurredAt),
row.antigenSlug
? t(`vaccinations.catalog.${row.antigenSlug}`)
: (row.vaccineName ?? "—"),
diff --git a/src/lib/doctor-report/collect.ts b/src/lib/doctor-report/collect.ts
index 5d76aafd3..f55d2f66c 100644
--- a/src/lib/doctor-report/collect.ts
+++ b/src/lib/doctor-report/collect.ts
@@ -84,7 +84,7 @@ import {
loadLabResults,
} from "./clinical-records";
import { dayKeyAsUtcMidnight } from "@/lib/tz/date-only";
-import { patientName } from "@/lib/account-name";
+import { PATIENT_NAME_SELECT, patientName } from "@/lib/account-name";
import { readingsMean } from "@/lib/measurements/day-mean";
import { loadScreenerUnvalidatedTranslations } from "./screener-translations";
@@ -179,9 +179,7 @@ export async function collectDoctorReportData(
const userProfile = await prisma.user.findUnique({
where: { id: userId },
select: {
- username: true,
- displayName: true,
- managedProfileAt: true,
+ ...PATIENT_NAME_SELECT,
dateOfBirth: true,
gender: true,
heightCm: true,
@@ -189,7 +187,6 @@ export async function collectDoctorReportData(
thresholdsJson: true,
timezone: true,
sourcePriorityJson: true,
- fullName: true,
insurerName: true,
insurerIkNumber: true,
},
diff --git a/src/lib/tz/__tests__/date-only.test.ts b/src/lib/tz/__tests__/date-only.test.ts
index 541fddbce..3aca0df8c 100644
--- a/src/lib/tz/__tests__/date-only.test.ts
+++ b/src/lib/tz/__tests__/date-only.test.ts
@@ -3,6 +3,7 @@ import {
isNoonUtcAnchor,
dateOnlyAtNoonUtc,
dateOnlyKey,
+ dateOnlyWindow,
dayKeyAsUtcMidnight,
isCalendarDateKey,
isDateOnlyKey,
@@ -60,3 +61,15 @@ describe("isNoonUtcAnchor", () => {
expect(isNoonUtcAnchor(new Date("2025-10-04T20:30:00.000Z"))).toBe(false);
});
});
+
+describe("dateOnlyWindow", () => {
+ it("spans UTC midnight to the next UTC midnight, both days included", () => {
+ expect(dateOnlyWindow("2026-11-11")).toEqual({
+ gte: new Date("2026-11-11T00:00:00.000Z"),
+ lt: new Date("2026-11-12T00:00:00.000Z"),
+ });
+ expect(dateOnlyWindow("2026-12-31", "2027-01-01").lt).toEqual(
+ new Date("2027-01-02T00:00:00.000Z"),
+ );
+ });
+});
diff --git a/src/lib/tz/date-only.ts b/src/lib/tz/date-only.ts
index 5018f530a..4a94de2cd 100644
--- a/src/lib/tz/date-only.ts
+++ b/src/lib/tz/date-only.ts
@@ -98,3 +98,19 @@ export function dateOnlyKey(value: Date): string {
export function dayKeyAsUtcMidnight(key: string): Date {
return new Date(`${key}T00:00:00.000Z`);
}
+
+/**
+ * The window over a date-only column that holds the calendar days `fromKey`
+ * to `toKey`, both included: UTC midnight to the next UTC midnight, so both
+ * anchors (midnight and noon UTC) fall on their own date. A date-only value
+ * read through a person's local-day window sat on the evening before west of
+ * UTC.
+ */
+export function dateOnlyWindow(
+ fromKey: string,
+ toKey: string = fromKey,
+): { gte: Date; lt: Date } {
+ const lt = dayKeyAsUtcMidnight(toKey);
+ lt.setUTCDate(lt.getUTCDate() + 1);
+ return { gte: dayKeyAsUtcMidnight(fromKey), lt };
+}