Make the checkup due line say the same thing on both screens

The dashboard summary card and the checkups page each worked out "today",
"tomorrow", "in N days" and "overdue by N days" from the same due date, and
each did it with its own copy of the arithmetic. The page compares calendar
days. The card subtracted one instant from the other and divided, so a checkup
due Sunday at 01:00 read "tomorrow" when you looked on Friday evening, one due
Saturday at 23:59 read "in 2 days" when it was in fact tomorrow, and one due at
09:00 quietly stopped saying "today" as the afternoon wore on.

Both now read from one place, so they cannot drift apart again.
This commit is contained in:
Marc-André Bombeck committed 2026-07-31 07:44:20 +02:00
1 parent 340d1db6d9
commit a2497cf21d
3 files changed
+64 -63

No files matched your search

@@ -28,40 +28,15 @@ import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { MeasurementForm } from "@/components/measurements/measurement-form";
import { isScreeningReminderType } from "@/lib/validations/measurement-reminders";
import { relativeDueKey } from "@/lib/measurement-reminders/due-day";
import {
useMeasurementReminders,
useMeasurementReminderMutations,
type MeasurementReminder,
} from "@/hooks/use-measurement-reminders";
const DAY_MS = 24 * 60 * 60 * 1000;
const SUMMARY_LIMIT = 3;
/**
* v1.32.36 — returns a FULLY-QUALIFIED i18n key, not a bare tail. A call site
* that interpolates the tail onto the namespace root anchors the
* reverse-coverage guard at that bare namespace, which marks every key under
* it reachable and turns the guard into a no-op for the whole namespace. The
* guard now refuses that shape outright, so the key is built whole here.
*/
function relativeDueKey(
nextDueAt: string | null,
now: number,
): { key: string; days: number } {
if (!nextDueAt) return { key: "measurementReminders.nextDue.none", days: 0 };
const deltaDays = Math.round((new Date(nextDueAt).getTime() - now) / DAY_MS);
if (deltaDays < 0)
return {
key: "measurementReminders.overdueByDays",
days: Math.abs(deltaDays),
};
if (deltaDays === 0)
return { key: "measurementReminders.nextDue.today", days: 0 };
if (deltaDays === 1)
return { key: "measurementReminders.nextDue.tomorrow", days: 1 };
return { key: "measurementReminders.nextDue.inDays", days: deltaDays };
}
function resolveLabel(
reminder: MeasurementReminder,
t: (key: string) => string,
@@ -46,7 +46,7 @@ import {
useDisplayTimezone,
} from "@/lib/i18n/context";
import { relativeCalendarDate } from "@/lib/i18n/relative-time";
import { startOfLocalDayInTz } from "@/lib/tz/local-day";
import { relativeDueKey } from "@/lib/measurement-reminders/due-day";
import { cn } from "@/lib/utils";
import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
import { SettingsCardHeader } from "@/components/settings/_card-header";
@@ -143,42 +143,6 @@ const CADENCE_CUSTOM = "custom";
const CADENCE_RRULE = "rrule";
const DAY_MS = 24 * 60 * 60 * 1000;
/**
* v1.32.36 — returns a FULLY-QUALIFIED i18n key, not a bare tail. A call site
* that interpolates the tail onto the namespace root anchors the
* reverse-coverage guard at that bare namespace, which marks every key under
* it reachable and turns the guard into a no-op for the whole namespace. The
* guard now refuses that shape outright, so the key is built whole here.
*/
function relativeDueKey(
nextDueAt: string | null,
now: number,
): { key: string; days: number } {
if (!nextDueAt) return { key: "measurementReminders.nextDue.none", days: 0 };
const due = new Date(nextDueAt);
if (Number.isNaN(due.getTime()))
return { key: "measurementReminders.nextDue.none", days: 0 };
// v1.18.9 (recon) — compare CALENDAR-day deltas in the user's local zone,
// not a rolling-24h delta. A reminder due at 09:00 viewed the same evening
// at 20:00 must still read "heute", and one whose due calendar-day is before
// today must read "überfällig" — a raw `(due - now) / DAY_MS` round would
// mis-bucket both. Floor each instant to its local day start (host-local zone
// = the browser user's zone for this client component) before differencing.
const dueDay = startOfLocalDayInTz(due, undefined).getTime();
const nowDay = startOfLocalDayInTz(new Date(now), undefined).getTime();
const deltaDays = Math.round((dueDay - nowDay) / DAY_MS);
if (deltaDays < 0)
return {
key: "measurementReminders.overdueByDays",
days: Math.abs(deltaDays),
};
if (deltaDays === 0)
return { key: "measurementReminders.nextDue.today", days: 0 };
if (deltaDays === 1)
return { key: "measurementReminders.nextDue.tomorrow", days: 1 };
return { key: "measurementReminders.nextDue.inDays", days: deltaDays };
}
/**
* v1.18.1 — a COACH-minted reminder stores an i18n KEY in `label` (the
* cadence preset's `labelKey`), not free prose. Resolve it through `t()`;