mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(report): end the visit offer on the record's day from the server
The preparation window now carries to, the booked visit's own day on the record's calendar, and the report offer uses it. Cutting the end in the reader's zone was wrong for a guardian viewing a managed profile in another zone.
This commit is contained in:
1 parent
fdecefd1d3
commit
a3f5f998cf
8 files changed
+80
-14
No files matched your search
@@ -72152,6 +72152,10 @@ components:
|
||||
type: string
|
||||
description: "First day of the window, `YYYY-MM-DD` on the record's own calendar: the day of `since`, or 90 days before
|
||||
today on `default`."
|
||||
to:
|
||||
type: string
|
||||
description: The booked visit's own day, `YYYY-MM-DD` on the same calendar as `from` (the record's, not the reader's). A
|
||||
report window offered for the appointment ends here.
|
||||
basis:
|
||||
type: string
|
||||
enum:
|
||||
@@ -72169,6 +72173,7 @@ components:
|
||||
- type: "null"
|
||||
required:
|
||||
- from
|
||||
- to
|
||||
- basis
|
||||
- since
|
||||
additionalProperties: false
|
||||
|
||||
@@ -194,6 +194,7 @@ describe("<VisitsSection> since the last visit", () => {
|
||||
occurredAt: "2026-09-20T09:00:00.000Z",
|
||||
preparation: {
|
||||
from: "2026-03-02",
|
||||
to: "2026-12-01",
|
||||
basis: "practitioner",
|
||||
since: sameDoctor,
|
||||
},
|
||||
@@ -340,7 +341,12 @@ describe("<VisitsSection> since the last visit", () => {
|
||||
upcoming: [
|
||||
visit({
|
||||
...next,
|
||||
preparation: { from: "2026-03-02", basis: "default", since: null },
|
||||
preparation: {
|
||||
from: "2026-03-02",
|
||||
to: "2026-12-01",
|
||||
basis: "default",
|
||||
since: null,
|
||||
},
|
||||
}),
|
||||
],
|
||||
past: [last],
|
||||
|
||||
@@ -118,6 +118,7 @@ describe("<ReportVisitOffer>", () => {
|
||||
id: "derm",
|
||||
preparation: {
|
||||
from: "2026-03-02",
|
||||
to: "2026-12-01",
|
||||
basis: "specialty",
|
||||
since: {
|
||||
id: "old-derm",
|
||||
@@ -138,7 +139,12 @@ describe("<ReportVisitOffer>", () => {
|
||||
const html = render([
|
||||
visit({
|
||||
id: "first",
|
||||
preparation: { from: "2026-07-23", basis: "default", since: null },
|
||||
preparation: {
|
||||
from: "2026-07-23",
|
||||
to: "2026-12-01",
|
||||
basis: "default",
|
||||
since: null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(html).toContain("The last 90 days");
|
||||
@@ -153,7 +159,12 @@ describe("<ReportVisitOffer>", () => {
|
||||
visit({
|
||||
id: "derm",
|
||||
occurredAt: at.toISOString(),
|
||||
preparation: { from: "2026-03-02", basis: "default", since: null },
|
||||
preparation: {
|
||||
from: "2026-03-02",
|
||||
to: "2026-12-01",
|
||||
basis: "default",
|
||||
since: null,
|
||||
},
|
||||
}),
|
||||
"Europe/Berlin",
|
||||
);
|
||||
@@ -165,16 +176,29 @@ describe("<ReportVisitOffer>", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("cuts the appointment's day on the record's calendar, like the start", () => {
|
||||
it("ends on the record's day the server cut, not the reader's", () => {
|
||||
// 04:30 on the 12th in Berlin is still the evening of the 11th in New
|
||||
// York. The start comes from the server on the record's calendar; the
|
||||
// end is cut in the zone it is handed, never the browser's own.
|
||||
// York. A guardian in New York viewing a Berlin record gets the record's
|
||||
// day: both ends come from the server, on the record's calendar.
|
||||
const early = visit({
|
||||
id: "early",
|
||||
occurredAt: "2026-11-12T03:30:00.000Z",
|
||||
preparation: { from: "2026-08-12", basis: "default", since: null },
|
||||
preparation: {
|
||||
from: "2026-08-12",
|
||||
to: "2026-11-12",
|
||||
basis: "default",
|
||||
since: null,
|
||||
},
|
||||
});
|
||||
expect(visitReportWindow(early, "Europe/Berlin").to).toBe("2026-11-12");
|
||||
expect(visitReportWindow(early, "America/New_York").to).toBe("2026-11-11");
|
||||
expect(visitReportWindow(early, "America/New_York")).toEqual({
|
||||
from: "2026-08-12",
|
||||
to: "2026-11-12",
|
||||
});
|
||||
// Without a preparation the zone handed in decides.
|
||||
const plain = visit({
|
||||
id: "plain",
|
||||
occurredAt: "2026-11-12T03:30:00.000Z",
|
||||
});
|
||||
expect(visitReportWindow(plain, "America/New_York").to).toBe("2026-11-11");
|
||||
});
|
||||
});
|
||||
@@ -54,10 +54,11 @@ export interface VisitReportWindow {
|
||||
|
||||
/**
|
||||
* What pressing the offer for `visit` applies. Both ends are days of the
|
||||
* record's own calendar (`timeZone`, the profile zone): the server cuts
|
||||
* `preparation.from` there, and the appointment's day has to be cut in the
|
||||
* same zone, or a browser in another zone moved the end a day against the
|
||||
* start for a late-evening or early-morning visit.
|
||||
* viewed record's own calendar, as the server cut them (`preparation.from`
|
||||
* and `preparation.to`). Cutting the end in the browser's zone moved it a day
|
||||
* against the start for a late-evening or early-morning visit, and a
|
||||
* guardian viewing a managed profile would have cut it in their own zone.
|
||||
* `timeZone` serves only a visit without a preparation.
|
||||
*/
|
||||
export function visitReportWindow(
|
||||
visit: Encounter,
|
||||
@@ -65,7 +66,8 @@ export function visitReportWindow(
|
||||
): VisitReportWindow {
|
||||
return {
|
||||
from: visit.preparation?.from ?? null,
|
||||
to: userDayKey(new Date(visit.occurredAt), timeZone),
|
||||
to:
|
||||
visit.preparation?.to ?? userDayKey(new Date(visit.occurredAt), timeZone),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ describe("the window before a booked visit", () => {
|
||||
]);
|
||||
expect(prep).toEqual({
|
||||
from: "2026-03-02",
|
||||
to: "2026-10-23",
|
||||
basis: "practitioner",
|
||||
since: {
|
||||
id: "derm",
|
||||
@@ -99,6 +100,7 @@ describe("the window before a booked visit", () => {
|
||||
const prep = resolve(next, [internist("int", "2026-10-16T09:00:00.000Z")]);
|
||||
expect(prep).toEqual({
|
||||
from: "2026-07-23",
|
||||
to: "2026-10-23",
|
||||
basis: "default",
|
||||
since: null,
|
||||
});
|
||||
@@ -164,6 +166,21 @@ describe("the window before a booked visit", () => {
|
||||
]);
|
||||
expect(prep.from).toBe("2026-05-05");
|
||||
});
|
||||
|
||||
it("ends on the booked visit's own day in the record's zone, whatever the rule", () => {
|
||||
// 23:30 UTC on the 22nd is the 23rd in Berlin.
|
||||
const late = visit("late-next", "2026-10-22T23:30:00.000Z", {
|
||||
practitionerId: "p-derm",
|
||||
});
|
||||
expect(resolve(late, []).to).toBe("2026-10-23");
|
||||
expect(
|
||||
resolve(late, [
|
||||
visit("before", "2026-05-04T08:00:00.000Z", {
|
||||
practitionerId: "p-derm",
|
||||
}),
|
||||
]).to,
|
||||
).toBe("2026-10-23");
|
||||
});
|
||||
});
|
||||
|
||||
describe("specialty comparison", () => {
|
||||
|
||||
@@ -68,6 +68,8 @@ export interface PreparationAnchorDTO {
|
||||
export interface EncounterPreparationDTO {
|
||||
/** First day of the window, `YYYY-MM-DD` on the record's own calendar. */
|
||||
from: string;
|
||||
/** The booked visit's own day, on the same calendar as `from`. */
|
||||
to: string;
|
||||
/** Which rule chose `from`. */
|
||||
basis: PreparationBasis;
|
||||
/** The visit `from` is the day of; null on `default`. */
|
||||
@@ -107,6 +109,7 @@ export function resolvePreparation(args: {
|
||||
today: string;
|
||||
}): EncounterPreparationDTO {
|
||||
const { next, tz, today } = args;
|
||||
const to = userDayKey(next.occurredAt, tz);
|
||||
const earlier = args.done.filter(
|
||||
(visit) =>
|
||||
visit.id !== next.id &&
|
||||
@@ -139,6 +142,7 @@ export function resolvePreparation(args: {
|
||||
if (!hit) continue;
|
||||
return {
|
||||
from: userDayKey(hit.occurredAt, tz),
|
||||
to,
|
||||
basis,
|
||||
since: {
|
||||
id: hit.id,
|
||||
@@ -151,6 +155,7 @@ export function resolvePreparation(args: {
|
||||
}
|
||||
return {
|
||||
from: shiftDateKey(today, -PREPARATION_DEFAULT_DAYS),
|
||||
to,
|
||||
basis: "default",
|
||||
since: null,
|
||||
};
|
||||
|
||||
@@ -191,6 +191,11 @@ const encounterPreparation = z
|
||||
.describe(
|
||||
"First day of the window, `YYYY-MM-DD` on the record's own calendar: the day of `since`, or 90 days before today on `default`.",
|
||||
),
|
||||
to: z
|
||||
.string()
|
||||
.describe(
|
||||
"The booked visit's own day, `YYYY-MM-DD` on the same calendar as `from` (the record's, not the reader's). A report window offered for the appointment ends here.",
|
||||
),
|
||||
basis: z
|
||||
.enum(PREPARATION_BASES)
|
||||
.describe(
|
||||
|
||||
@@ -142,6 +142,7 @@ describe("GET /api/encounters — the window before a booked visit", () => {
|
||||
const booked = body.upcoming.find((v) => v.id === next.id);
|
||||
expect(booked?.preparation).toEqual({
|
||||
from: berlinDay(dermVisit.occurredAt),
|
||||
to: berlinDay(next.occurredAt),
|
||||
basis: "practitioner",
|
||||
since: {
|
||||
id: dermVisit.id,
|
||||
@@ -207,6 +208,7 @@ describe("GET /api/encounters — the window before a booked visit", () => {
|
||||
const booked = (await list()).upcoming.find((v) => v.id === next.id);
|
||||
expect(booked?.preparation).toEqual({
|
||||
from: shiftDateKey(berlinDay(new Date()), -90),
|
||||
to: berlinDay(next.occurredAt),
|
||||
basis: "default",
|
||||
since: null,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user