fix(checkups): say "overdue since yesterday" instead of "overdue by 1 days"

The due phrase on the check-up card and the checkups page counted a
check-up one day past its date as "Overdue by 1 days", and the same
counted form read wrongly in German. One day over now has its own
phrase in all seven languages. Both overdue phrases keep the warning
colour through a shared `isOverdueDue` helper, so a surface no longer
compares against a single key.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 10:01:04 +02:00
1 parent 5fb5145d7a
commit f5bab3258a
12 files changed
+60 -9

No files matched your search

+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "In {days} Tagen"
},
"overdueByDays": "Überfällig seit {days} Tagen",
"overdueSinceYesterday": "Überfällig seit gestern",
"cadence": {
"everyNDays": "Alle {days} Tage",
"custom": "Eigener Zeitplan",
+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "In {days} days"
},
"overdueByDays": "Overdue by {days} days",
"overdueSinceYesterday": "Overdue since yesterday",
"cadence": {
"everyNDays": "Every {days} days",
"custom": "Custom schedule",
+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "En {days} días"
},
"overdueByDays": "Vencido hace {days} días",
"overdueSinceYesterday": "Vencido desde ayer",
"cadence": {
"everyNDays": "Cada {days} días",
"custom": "Programación personalizada",
+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "Dans {days} jours"
},
"overdueByDays": "En retard de {days} jours",
"overdueSinceYesterday": "En retard depuis hier",
"cadence": {
"everyNDays": "Tous les {days} jours",
"custom": "Planning personnalisé",
+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "Tra {days} giorni"
},
"overdueByDays": "In ritardo di {days} giorni",
"overdueSinceYesterday": "In ritardo da ieri",
"cadence": {
"everyNDays": "Ogni {days} giorni",
"custom": "Pianificazione personalizzata",
+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "{days}일 후"
},
"overdueByDays": "{days}일 지남",
"overdueSinceYesterday": "어제부터 지남",
"cadence": {
"everyNDays": "{days}일마다",
"custom": "사용자 지정 일정",
+1
View File
@@ -8527,6 +8527,7 @@
"inDays": "Za {days} dni"
},
"overdueByDays": "Zaległe od {days} dni",
"overdueSinceYesterday": "Zaległe od wczoraj",
"cadence": {
"everyNDays": "Co {days} dni",
"custom": "Własny harmonogram",
@@ -52,6 +52,7 @@ const MINTED_ONLY_BY_THE_BUCKET = [
"measurementReminders.nextDue.tomorrow",
"measurementReminders.nextDue.inDays",
"measurementReminders.nextDue.none",
"measurementReminders.overdueSinceYesterday",
];
/** The hook that yields the zone the surrounding dates are printed in. */
@@ -275,7 +275,9 @@ describe("<VorsorgeSection> loading + empty", () => {
} as MeasurementReminder;
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
const html = render(<VorsorgeSection />);
expect(html).toContain("Overdue by 1 days");
// One day over reads "since yesterday": the counted form printed
// "Overdue by 1 days", and the warning hue must follow both phrases.
expect(html).toContain("Overdue since yesterday");
expect(html).toContain("text-warning");
});
@@ -47,7 +47,10 @@ import {
useDisplayTimezone,
} from "@/lib/i18n/context";
import { relativeCalendarDate } from "@/lib/i18n/relative-time";
import { relativeDueKey } from "@/lib/measurement-reminders/due-day";
import {
isOverdueDue,
relativeDueKey,
} from "@/lib/measurement-reminders/due-day";
import { cn } from "@/lib/utils";
import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
import { EncounterSheet } from "@/components/encounters/encounter-sheet";
@@ -899,8 +902,7 @@ function VorsorgeCard({
// routes to the check-in page), never the numeric value-entry wording.
const isScreening = isScreeningReminderType(reminder.measurementType);
const isDue =
due.key === "measurementReminders.nextDue.today" ||
due.key === "measurementReminders.overdueByDays";
due.key === "measurementReminders.nextDue.today" || isOverdueDue(due);
const progress = intervalProgress(reminder, now);
// Category-style header badge: the measurement label, or "self-planned"
@@ -1361,7 +1363,7 @@ function VorsorgeCard({
// normal muted-resolved date. The card surface stays neutral per the house
// rule; only this inline label carries the status hue.
const dueIsToday = due.key === "measurementReminders.nextDue.today";
const dueIsOverdue = due.key === "measurementReminders.overdueByDays";
const dueIsOverdue = isOverdueDue(due);
const nextLastSlot = (
<div className="min-h-[2.75rem] space-y-1.5 text-sm">
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
@@ -16,7 +16,7 @@
*/
import { describe, it, expect } from "vitest";
import { relativeDueKey } from "../due-day";
import { isOverdueDue, relativeDueKey } from "../due-day";
/**
* The profile zone under test. July is deliberate: no IANA zone shifts its
@@ -84,11 +84,33 @@ describe("relativeDueKey", () => {
).toEqual(TOMORROW);
});
it("counts a checkup from yesterday as one day overdue, however recent", () => {
// Forty-five minutes in the past. It was still yesterday's checkup.
it("calls a checkup from yesterday overdue since yesterday, however recent", () => {
// Forty-five minutes in the past. It was still yesterday's checkup. Its
// own phrase, because "overdue by 1 days" is what the counted form reads
// as on the one day an open check-up is most likely to be looked at.
expect(
relativeDueKey(zoned(FRIDAY, 23, 30), at(SATURDAY, 0, 15), ZONE),
).toEqual({ key: "measurementReminders.overdueByDays", days: 1 });
).toEqual({ key: "measurementReminders.overdueSinceYesterday", days: 1 });
});
it("counts the days for a checkup overdue longer than that", () => {
expect(relativeDueKey(zoned(FRIDAY, 9), at(FRIDAY + 2, 10), ZONE)).toEqual({
key: "measurementReminders.overdueByDays",
days: 2,
});
});
it("reads both overdue phrases as overdue, and nothing else", () => {
const now = at(SATURDAY, 12);
expect(isOverdueDue(relativeDueKey(zoned(FRIDAY, 9), now, ZONE))).toBe(
true,
);
expect(isOverdueDue(relativeDueKey(zoned(FRIDAY - 3, 9), now, ZONE))).toBe(
true,
);
expect(isOverdueDue(relativeDueKey(zoned(SATURDAY, 9), now, ZONE))).toBe(
false,
);
});
it("counts whole calendar days for a checkup further out", () => {
+17
View File
@@ -86,6 +86,11 @@ export function relativeDueKey(
if (Number.isNaN(due.getTime()))
return { key: "measurementReminders.nextDue.none", days: 0 };
const deltaDays = calendarDaysUntil(due, new Date(now), timeZone);
// Its own phrase rather than the counted one: "overdue by 1 days" is what
// the counted form reads as, on the one day an open check-up (which since
// v1.39.2 stays due after its reminder) is most likely to be looked at.
if (deltaDays === -1)
return { key: "measurementReminders.overdueSinceYesterday", days: 1 };
if (deltaDays < 0)
return {
key: "measurementReminders.overdueByDays",
@@ -97,3 +102,15 @@ export function relativeDueKey(
return { key: "measurementReminders.nextDue.tomorrow", days: 1 };
return { key: "measurementReminders.nextDue.inDays", days: deltaDays };
}
/**
* Whether a phrase from {@link relativeDueKey} says "overdue". Two keys do —
* "since yesterday" and the counted one — so a surface that colours an
* overdue badge asks here rather than comparing against one of them.
*/
export function isOverdueDue(due: RelativeDue): boolean {
return (
due.key === "measurementReminders.overdueByDays" ||
due.key === "measurementReminders.overdueSinceYesterday"
);
}