fix(medications): refresh the dashboard the moment an intake is recorded

The intake write invalidates the dashboard snapshot, but the default active
refetch type only refetches mounted queries — and the snapshot is unmounted
while the user is on the medication card or detail page. On navigating back it
remounts under refetchOnMount:false, so the pre-write cache was served and the
due prompt lingered until a hard reload. Force the inactive snapshot to refetch
after every intake write so the dashboard clears as soon as the dose is taken.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 20:21:31 +02:00
1 parent 6a99f72384
commit 59307760c5
2 files changed
+61 -4

No files matched your search

@@ -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,
@@ -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<void> {
await invalidateKeys(queryClient, medicationDependentKeys);
await queryClient.invalidateQueries({
queryKey: queryKeys.dashboardSnapshot(),
refetchType: "inactive",
});
}
type Translator = (
key: string,
params?: Record<string, string | number>,
@@ -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"));