mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
refactor(day): share one window for date-only columns
The day view, the day index and the visit preparation each built the UTC calendar window for a date-only column by hand (documents had a fourth copy). `dateOnlyWindow()` beside the other date-only helpers replaces them. The report name select becomes one spread, the share page builds its calendar formatter outside the view, and the PDF section reads it off the context, so the functions these fixes touched stay within their size budget; `DocumentRecordLinks` drops under it.
This commit is contained in:
1 parent
bdfecfa70e
commit
da671f74aa
12 files changed
+90
-74
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
<AllergiesSection t={t} report={report} scope={scope} />
|
||||
|
||||
|
||||
@@ -188,10 +188,7 @@ export function DocumentRecordLinks({
|
||||
const [visitIds, setVisitIds] = useState<string[] | null>(null);
|
||||
const [doseIds, setDoseIds] = useState<string[] | null>(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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
+10
-21
@@ -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<SectionPart> {
|
||||
@@ -591,7 +582,7 @@ async function allergiesPart(frame: DayFrame): Promise<SectionPart> {
|
||||
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<SectionPart> {
|
||||
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<SectionPart> {
|
||||
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<SectionPart> {
|
||||
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<SectionPart> {
|
||||
async function documentsPart(frame: DayFrame): Promise<SectionPart> {
|
||||
// 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,
|
||||
|
||||
@@ -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<DayChange[]> {
|
||||
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,
|
||||
|
||||
@@ -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 ?? "—"),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user