fix(medications): preselect only a dose offered today, graded by the account's thresholds

The intake pickers chose their default from every active medication, so a
course that had ended but was not archived could be the preselected dose.
The default now reads the server's intakeActionable answer; the medications
page dialog still lists ended courses for back-filling a past dose, they are
just never the default.

Both pickers also graded "due" against the fixed 120 / 240-minute defaults
while the cards beside them used the account's own late and missed
thresholds. They read the same thresholds now.
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 09:48:48 +02:00
1 parent ef420f9e2a
commit eae46ed535
8 files changed
+127 -6

No files matched your search

@@ -98,7 +98,9 @@ describe("pickDefaultMedicationId — auto-select heuristic", () => {
it("returns null when no active medications exist", () => {
expect(pickDefaultMedicationId([])).toBeNull();
expect(
pickDefaultMedicationId([makeMed("a", { active: false })]),
pickDefaultMedicationId([
makeMed("a", { active: false, intakeActionable: false }),
]),
).toBeNull();
});
@@ -32,6 +32,7 @@ import {
runUndoIntake,
} from "@/components/medications/use-medication-intake";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { useMedicationDueThresholds } from "@/lib/queries/use-reminder-thresholds";
/**
* v1.4.37 W7b — dashboard "Hinzufügen" → "Medikamenteneinnahme" quick-add.
@@ -122,6 +123,9 @@ export function MedicationIntakeQuickAdd({
// logged-out mounts behave unchanged.
const { user } = useAuth();
const userTz = user?.timezone || DEFAULT_TIMEZONE;
// The account's own late / missed thresholds decide which dose counts as
// due for the preselection, the same tiers the medication cards show.
const dueThresholds = useMedicationDueThresholds();
// The record this dose lands in, or null in the caller's own.
const recordName = useActiveRecordName();
@@ -188,7 +192,7 @@ export function MedicationIntakeQuickAdd({
medications,
medicationOverride,
selectionNow,
undefined,
dueThresholds,
userTz,
) ?? "";
const selectedMedication = medications.find((m) => m.id === medicationId);
@@ -3,8 +3,15 @@ import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
// The reminder-thresholds read: `undefined` = not loaded (the 120 / 240
// defaults apply), otherwise the account's own tiers.
let mockThresholds:
| { lateMinutes: number; missedMinutes: number; lowStockRunwayDays: null }
| undefined;
vi.mock("@tanstack/react-query", () => ({
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useQuery: () => ({ data: mockThresholds }),
}));
vi.mock("sonner", () => ({
@@ -49,6 +56,7 @@ function medication(
name: `Medication ${id}`,
dose: "5 mg",
active: true,
intakeActionable: true,
schedules: [],
lastTakenAt: null,
todayEventCount: 0,
@@ -82,6 +90,37 @@ describe("<LogIntakeDialog> medication default", () => {
afterEach(() => {
vi.useRealTimers();
mockThresholds = undefined;
});
it("grades the due dose by the account's own missed threshold", () => {
// 10:00 in Berlin, five hours after a 04:00–05:00 window closed. Under the
// 240-minute default the dose is past missed and the alphabetical
// fallback wins; an account that keeps a six-hour missed threshold still
// sees it as very late, so it is the default.
const meds = [
medication("first", { name: "Alpha" }),
medication("late", {
name: "Zulu",
nextDueAt: undefined,
schedules: [
{
windowStart: "04:00",
windowEnd: "05:00",
daysOfWeek: null,
label: null,
dose: null,
},
],
}),
];
expectSelected(renderDialog(meds), "first");
mockThresholds = {
lateMinutes: 120,
missedMinutes: 360,
lowStockRunwayDays: null,
};
expectSelected(renderDialog(meds), "late");
});
it("selects an overdue current-window medication ahead of array order", () => {
const html = renderDialog([
@@ -45,6 +45,7 @@ import {
runUndoIntake,
} from "@/components/medications/use-medication-intake";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { useMedicationDueThresholds } from "@/lib/queries/use-reminder-thresholds";
interface LogIntakeSchedule extends ScheduleWindowInput {
label: string | null;
@@ -122,6 +123,9 @@ export function LogIntakeDialog({
const queryClient = useQueryClient();
const { user } = useAuth();
const userTz = user?.timezone || DEFAULT_TIMEZONE;
// The account's own late / missed thresholds decide which dose counts as
// due for the preselection, the same tiers the medication cards show.
const dueThresholds = useMedicationDueThresholds();
const formId = useId();
const [medicationOverride, setMedicationOverride] = useState<string | null>(
@@ -133,7 +137,7 @@ export function LogIntakeDialog({
medications,
medicationOverride,
selectionNow,
undefined,
dueThresholds,
userTz,
) ?? "";
const [slot, setSlot] = useState<string>(NO_SLOT);