diff --git a/src/components/medications/__tests__/use-medication-intake.test.ts b/src/components/medications/__tests__/use-medication-intake.test.ts index 0c4791549..f87903eeb 100644 --- a/src/components/medications/__tests__/use-medication-intake.test.ts +++ b/src/components/medications/__tests__/use-medication-intake.test.ts @@ -7,6 +7,7 @@ import { runRecordIntake, runUndoIntake, } from "@/components/medications/use-medication-intake"; +import { queryKeys } from "@/lib/query-keys"; /** * v1.12.2 — the shared intake orchestration consumed by both the generic @@ -144,6 +145,38 @@ describe("runRecordIntake — shared C1 failure toast + C2 Undo", () => { expect(onRecorded).toHaveBeenCalledWith("evt-99", false); }); + it("forces the inactive dashboard snapshot to refetch so the due prompt clears without a reload", async () => { + // The dashboard snapshot is unmounted while the user is on the medication + // card/detail, so a default ("active") invalidation marks it stale but + // never refetches it; on navigating back, refetchOnMount:false serves the + // pre-write cache and the "due" prompt lingers until a hard reload. + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: { id: "evt-1" } }), + text: vi + .fn() + .mockResolvedValue(JSON.stringify({ data: { id: "evt-1" } })), + }), + ); + const queryClient = fakeQueryClient(); + + await runRecordIntake({ + medication, + skipped: false, + t, + queryClient, + setIntakeLoading: vi.fn(), + undoIntake: vi.fn(), + }); + + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: queryKeys.dashboardSnapshot(), + refetchType: "inactive", + }); + }); + it("sends the displayed dose's scheduledFor on the POST so the server marks THAT slot (v1.12.3)", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, diff --git a/src/components/medications/use-medication-intake.ts b/src/components/medications/use-medication-intake.ts index 96055fdab..bde93e547 100644 --- a/src/components/medications/use-medication-intake.ts +++ b/src/components/medications/use-medication-intake.ts @@ -5,9 +5,33 @@ import { useQueryClient, type QueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { useTranslations } from "@/lib/i18n/context"; -import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys"; +import { + invalidateKeys, + medicationDependentKeys, + queryKeys, +} from "@/lib/query-keys"; import { apiDelete, apiPost } from "@/lib/api/api-fetch"; +/** + * Invalidate every read that reflects a dose's taken/due state after an + * intake write. `invalidateKeys` invalidates with the default + * `refetchType: "active"`, which only refetches mounted queries — so the + * dashboard snapshot, inactive while the user is on the medication card or + * detail page, is marked stale but never refetched. On navigating back the + * snapshot remounts under `refetchOnMount: false`, so the pre-write cache is + * served and the "due" prompt lingers until a hard reload. Force the inactive + * snapshot to refetch so the dashboard clears as soon as the dose is recorded. + */ +async function invalidateMedicationReads( + queryClient: QueryClient, +): Promise { + await invalidateKeys(queryClient, medicationDependentKeys); + await queryClient.invalidateQueries({ + queryKey: queryKeys.dashboardSnapshot(), + refetchType: "inactive", + }); +} + type Translator = ( key: string, params?: Record, @@ -136,7 +160,7 @@ export async function runRecordIntake(deps: { } : undefined, ); - await invalidateKeys(queryClient, medicationDependentKeys); + await invalidateMedicationReads(queryClient); onRecorded?.(eventId, skipped); } catch { toast.error(t("medications.intakeToastFailed", { name: medication.name })); @@ -188,7 +212,7 @@ export async function runLogIntake(deps: { if (scheduledFor) body.scheduledFor = scheduledFor; if (!skipped && doseTaken) body.doseTaken = doseTaken; await apiPost(`/api/medications/${medication.id}/intake`, body); - await invalidateKeys(queryClient, medicationDependentKeys); + await invalidateMedicationReads(queryClient); toast.success( t( skipped @@ -217,7 +241,7 @@ export async function runUndoIntake(deps: { const { medication, eventId, t, queryClient } = deps; try { await apiDelete(`/api/medications/${medication.id}/intake/${eventId}`); - await invalidateKeys(queryClient, medicationDependentKeys); + await invalidateMedicationReads(queryClient); toast.success(t("medications.intakeUndone")); } catch { toast.error(t("medications.intakeUndoFailed"));