Files
HealthLog/src/hooks/use-practitioners.ts
T
Marc-André Bombeck f2f40ae8b2 Pair every daily-read invalidation with its inactive refetch
Ten client mutation sites forgot a consumer: a coach-accepted checkup
stayed invisible for up to five minutes, a threshold or target edit left
the dashboard bands stale, a score-config save marked the daily reads
stale but never refetched them, a booked visit missed the Today rail, a
source-priority reorder kept every chart on the old canonical picks, an
adopted self-context note never reached the Settings read. Each site now
invalidates what its write changed and forces the unmounted snapshot +
digest reads to refetch through refetchInactiveDailyReads.

New illnessDependentKeys bundle carries the daily reads (Rest Mode
frames both), dailyDigest joins encounterDependentKeys, and the
chart-data prefix gains a factory accessor for whole-cache flushes.

The daily-reads guard learns the cycle / encounter / vaccination /
illness bundle families and a second matcher for direct snapshot/digest
invalidateQueries without a forced refetch, each with a frozen reasoned
allowlist. Both matchers were proven live by reverting a fix and
watching the guard go red (see the test doc comment).
2026-08-13 18:48:10 +02:00

81 lines
2.4 KiB
TypeScript

"use client";
/**
* Data hooks for the account's own address book of doctors and practices.
*
* The search runs in SQL over the two plaintext columns rather than in the
* browser over a downloaded list: the columns are plaintext precisely so the
* picker can search and sort them there, and a client-side filter would need
* the whole book in memory to do it.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiDelete, apiGet, apiPatch, apiPost } from "@/lib/api/api-fetch";
import {
encounterDependentKeys,
invalidateKeys,
queryKeys,
refetchInactiveDailyReads,
} from "@/lib/query-keys";
import type { PractitionerDTO } from "@/lib/practitioners/dto";
export type Practitioner = PractitionerDTO;
export interface PractitionerWriteBody {
name?: string;
specialty?: string | null;
practice?: string | null;
location?: string | null;
phone?: string | null;
note?: string | null;
}
const BASE = "/api/practitioners";
export function usePractitioners(q?: string, enabled = true) {
const term = q?.trim() || undefined;
return useQuery({
queryKey: queryKeys.practitionerList(term),
queryFn: () =>
apiGet<Practitioner[]>(
term ? `${BASE}?q=${encodeURIComponent(term)}` : BASE,
),
enabled,
});
}
export function usePractitionerMutations() {
const qc = useQueryClient();
// Renaming a practice changes the label a rail appointment renders under,
// and the daily reads are unmounted from the address book — pair the
// bundle invalidation with the forced inactive refetch.
const invalidate = () =>
Promise.all([
invalidateKeys(qc, encounterDependentKeys),
refetchInactiveDailyReads(qc),
]);
const create = useMutation({
mutationKey: queryKeys.practitionerCreate(),
mutationFn: (body: PractitionerWriteBody) =>
apiPost<Practitioner>(BASE, body),
onSuccess: invalidate,
});
const update = useMutation({
mutationKey: queryKeys.practitionerUpdate(),
mutationFn: ({ id, body }: { id: string; body: PractitionerWriteBody }) =>
apiPatch<Practitioner>(`${BASE}/${id}`, body),
onSuccess: invalidate,
});
const remove = useMutation({
mutationKey: queryKeys.practitionerDelete(),
mutationFn: (id: string) =>
apiDelete<{ deleted: boolean }>(`${BASE}/${id}`),
onSuccess: invalidate,
});
return { create, update, remove };
}