mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
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()`;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* The one place a Vorsorge reminder's next-due instant becomes the words a
|
||||
* person reads: "heute" / "morgen" / "in N Tagen" / "überfällig seit N Tagen".
|
||||
*
|
||||
* It used to live inside the two surfaces that render it — the `/checkups`
|
||||
* page and the dashboard summary card — as two hand-copied bodies. The page
|
||||
* copy was fixed to compare CALENDAR days; the dashboard copy was not, and
|
||||
* kept differencing raw instants. So the same reminder read "morgen" on one
|
||||
* screen and "in 2 Tagen" on the other, and a reminder due this morning
|
||||
* dropped out of "heute" over the course of the afternoon. One body here, both
|
||||
* surfaces importing it, and the two cannot disagree again.
|
||||
*
|
||||
* Sits beside `scheduling.ts` (which computes the server-authoritative
|
||||
* `nextDueAt`) because this is the read side of the same value. Nothing in
|
||||
* this file touches the database or the network, so a client component can
|
||||
* import it directly.
|
||||
*/
|
||||
import { startOfLocalDayInTz } from "@/lib/tz/local-day";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** A fully-qualified i18n key plus the day count it interpolates. */
|
||||
export interface RelativeDue {
|
||||
key: string;
|
||||
days: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function relativeDueKey(
|
||||
nextDueAt: string | null,
|
||||
now: number,
|
||||
): RelativeDue {
|
||||
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 };
|
||||
}
|
||||
Reference in new issue
Block a user