mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
refactor(medications): share the next-intake day label between card and table
The card and the table carried byte-identical copies of the day-label arithmetic. Two copies is how they drift, and neither was reachable from a test, which is why the double conversion in both survived review. Lift it into resolveNextDueDayLabel, which returns a discriminated label the callers map onto their own strings, and cover the case the old code could not get right: a browser zone that differs from the profile zone. Those tests pin the host zone explicitly — the defect was invisible whenever host and profile agree, which is the only configuration a UTC-only suite runs in. Also pin that the window-status verdict is identical from every host zone, including one whose spring-forward gap swallows the profile wall clock.
This commit is contained in:
1 parent
eb071b5dd8
commit
4ba600d908
5 files changed
+317
-39
No files matched your search
@@ -14,7 +14,7 @@ import { getDateTimeFormat } from "@/lib/intl/formatter-cache";
|
||||
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
|
||||
import { formatDose } from "@/lib/medications/format-dose";
|
||||
import { reduceCurrentWindowStatus } from "@/lib/medications/window-status";
|
||||
import { localDayIndex, wallClockInTz } from "@/lib/tz/wall-clock";
|
||||
import { resolveNextDueDayLabel } from "@/lib/medications/next-due-day-label";
|
||||
import { resolveDisplayedSlotInstant } from "@/components/medications/card-parts/displayed-slot-instant";
|
||||
import {
|
||||
estimateDailyDoseCount,
|
||||
@@ -241,7 +241,6 @@ export function MedicationCard({
|
||||
a.windowEnd.localeCompare(b.windowEnd),
|
||||
);
|
||||
const now = new Date();
|
||||
const nowParts = wallClockInTz(now, userTz);
|
||||
// v1.8.4 — the next-due instant comes from the server (`nextDueAt`,
|
||||
// computed by the canonical recurrence engine anchored on the last
|
||||
// intake). The day label below derives from it; the window-range /
|
||||
@@ -417,27 +416,15 @@ export function MedicationCard({
|
||||
// Format day label relative to today
|
||||
let dayLabel = "";
|
||||
if (nextAt) {
|
||||
// Both clocks are read in the PROFILE timezone, and the day
|
||||
// distance is a calendar-day difference — not an hour count —
|
||||
// so a 23 h / 25 h transition day cannot round "tomorrow" into
|
||||
// "today". `dateWithWeekdaySmart` receives the RAW instant: the
|
||||
// formatter applies `userTz` itself, and feeding it a
|
||||
// zone-shifted Date applied the offset a second time, which
|
||||
// rendered the wrong calendar day for anyone whose browser zone
|
||||
// differed from their profile zone.
|
||||
const nextInstant = new Date(nextAt);
|
||||
const nextParts = wallClockInTz(nextInstant, userTz);
|
||||
const dayDelta = localDayIndex(nextParts) - localDayIndex(nowParts);
|
||||
|
||||
if (dayDelta === 0) {
|
||||
dayLabel = t("medications.today");
|
||||
} else if (dayDelta === 1) {
|
||||
dayLabel = t("medications.tomorrow");
|
||||
} else if (dayDelta <= 5) {
|
||||
dayLabel = weekdayLabel(nextParts.weekday);
|
||||
} else {
|
||||
dayLabel = fmt.dateWithWeekdaySmart(nextInstant);
|
||||
}
|
||||
const label = resolveNextDueDayLabel(new Date(nextAt), now, userTz);
|
||||
dayLabel =
|
||||
label.kind === "today"
|
||||
? t("medications.today")
|
||||
: label.kind === "tomorrow"
|
||||
? t("medications.tomorrow")
|
||||
: label.kind === "weekday"
|
||||
? weekdayLabel(label.weekday)
|
||||
: fmt.dateWithWeekdaySmart(label.instant);
|
||||
}
|
||||
|
||||
// v1.16.1 — the TIME is the canonical next-due instant (the
|
||||
|
||||
@@ -29,7 +29,7 @@ import { resolveDisplayedSlotInstant } from "@/components/medications/card-parts
|
||||
import { estimateRunwayDays } from "@/components/medications/detail/supply-runway";
|
||||
import { useMedicationIntake } from "@/components/medications/use-medication-intake";
|
||||
import { reduceCurrentWindowStatus } from "@/lib/medications/window-status";
|
||||
import { localDayIndex, wallClockInTz } from "@/lib/tz/wall-clock";
|
||||
import { resolveNextDueDayLabel } from "@/lib/medications/next-due-day-label";
|
||||
import { formatTime } from "@/lib/format";
|
||||
import { formatTimeWindowRange } from "@/lib/time-window-format";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
@@ -418,7 +418,6 @@ function MedicationTableRowItem({
|
||||
a.windowEnd.localeCompare(b.windowEnd),
|
||||
);
|
||||
const now = new Date();
|
||||
const nowParts = wallClockInTz(now, userTz);
|
||||
const nextDueMs = medication.nextDueAt
|
||||
? new Date(medication.nextDueAt).getTime()
|
||||
: NaN;
|
||||
@@ -533,22 +532,15 @@ function MedicationTableRowItem({
|
||||
</span>
|
||||
);
|
||||
} else if (nextAt) {
|
||||
// Same contract as the card: both clocks read in the PROFILE
|
||||
// timezone, a calendar-day distance rather than an hour count, and
|
||||
// the RAW instant handed to `dateWithWeekdaySmart` — the formatter
|
||||
// applies `userTz` itself, so passing a zone-shifted Date converted
|
||||
// twice and could render the wrong calendar day.
|
||||
const nextInstant = new Date(nextAt);
|
||||
const nextParts = wallClockInTz(nextInstant, userTz);
|
||||
const dayDelta = localDayIndex(nextParts) - localDayIndex(nowParts);
|
||||
const label = resolveNextDueDayLabel(new Date(nextAt), now, userTz);
|
||||
const dayLabel =
|
||||
dayDelta === 0
|
||||
label.kind === "today"
|
||||
? t("medications.today")
|
||||
: dayDelta === 1
|
||||
: label.kind === "tomorrow"
|
||||
? t("medications.tomorrow")
|
||||
: dayDelta <= 5
|
||||
? weekdayLabel(nextParts.weekday)
|
||||
: fmt.dateWithWeekdaySmart(nextInstant);
|
||||
: label.kind === "weekday"
|
||||
? weekdayLabel(label.weekday)
|
||||
: fmt.dateWithWeekdaySmart(label.instant);
|
||||
nextCell = (
|
||||
<span>
|
||||
{dayLabel}, {formatTime(new Date(nextAt).toISOString())}
|
||||
|
||||
Reference in new issue
Block a user