mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Merge remote-tracking branch 'origin/main' into fix/settings-ekg-polish
This commit is contained in:
commit
4c2dfa57b4
138 files changed
+12046
-137
No files matched your search
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Data hooks for visits and the address book behind them.
|
||||
*
|
||||
* Every value the surfaces render arrives resolved from the server — the
|
||||
* practitioner as an object, the links with a label and a date, the
|
||||
* appointment's next-due instant computed. Nothing here re-derives one. A
|
||||
* client that recomputed a server-side derivation drifts from it the first
|
||||
* time the derivation changes on one side only.
|
||||
*
|
||||
* Writes reach two roots (`encounterDependentKeys`), because renaming a
|
||||
* practice changes the label every visit that names it renders.
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiDelete, apiGet, apiPatch, apiPost } from "@/lib/api/api-fetch";
|
||||
import { invalidateReminderReads } from "@/hooks/use-measurement-reminders";
|
||||
import {
|
||||
encounterDependentKeys,
|
||||
invalidateKeys,
|
||||
queryKeys,
|
||||
} from "@/lib/query-keys";
|
||||
import type { EncounterDTO, EncounterListDTO } from "@/lib/encounters/dto";
|
||||
import type { EncounterSuggestionResult } from "@/lib/encounters/suggest-window";
|
||||
|
||||
export type Encounter = EncounterDTO;
|
||||
export type EncounterList = EncounterListDTO;
|
||||
|
||||
export interface EncounterWriteBody {
|
||||
occurredAt?: string;
|
||||
status?: "PLANNED" | "DONE" | "CANCELLED" | "NO_SHOW";
|
||||
kind?: string;
|
||||
practitionerId?: string | null;
|
||||
reason?: string | null;
|
||||
outcome?: string | null;
|
||||
/** The checkup this visit closes, when it was filed from a due one. */
|
||||
reminderId?: string | null;
|
||||
documentIds?: string[];
|
||||
labResultIds?: string[];
|
||||
episodeIds?: string[];
|
||||
}
|
||||
|
||||
const BASE = "/api/encounters";
|
||||
|
||||
export function useEncounters(
|
||||
enabled = true,
|
||||
window?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
status?: string;
|
||||
/** Only visits filed against this condition episode. */
|
||||
episodeId?: string;
|
||||
},
|
||||
) {
|
||||
const from = window?.from ?? null;
|
||||
const to = window?.to ?? null;
|
||||
return useQuery({
|
||||
queryKey: queryKeys.encounterList(
|
||||
from,
|
||||
to,
|
||||
window?.status,
|
||||
window?.episodeId,
|
||||
),
|
||||
queryFn: () => {
|
||||
const sp = new URLSearchParams();
|
||||
if (from) sp.set("from", from);
|
||||
if (to) sp.set("to", to);
|
||||
if (window?.status) sp.set("status", window.status);
|
||||
if (window?.episodeId) sp.set("episodeId", window.episodeId);
|
||||
const qs = sp.toString();
|
||||
return apiGet<EncounterList>(qs ? `${BASE}?${qs}` : BASE);
|
||||
},
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* "Does this belong to a visit?" for one anchor date.
|
||||
*
|
||||
* The verdict is resolved server-side so the document moment and the two lab
|
||||
* moments cannot answer the question differently. `anchor` being null means
|
||||
* the surface has no date yet and there is nothing to ask about.
|
||||
*/
|
||||
export function useEncounterSuggestion(anchor: string | null, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.encounterSuggestion(anchor ?? ""),
|
||||
queryFn: () =>
|
||||
apiGet<EncounterSuggestionResult>(
|
||||
`${BASE}/suggest?anchor=${encodeURIComponent(anchor ?? "")}`,
|
||||
),
|
||||
enabled: enabled && Boolean(anchor),
|
||||
});
|
||||
}
|
||||
|
||||
export function useEncounterMutations() {
|
||||
const qc = useQueryClient();
|
||||
/**
|
||||
* A visit write reaches further than the visit list.
|
||||
*
|
||||
* Filing one against a due checkup satisfies that reminder, which moves its
|
||||
* next-due date on the preventive-care list, on the dashboard card and on the
|
||||
* Today rail — three surfaces that read the reminder table and would
|
||||
* otherwise keep showing it due until their own poll. Booking one moves the
|
||||
* same reads the other way: a new appointment is a row the digest surfaces.
|
||||
*
|
||||
* So the fan-out is the visit roots PLUS the reminder reads, through the same
|
||||
* helper the Vorsorge surfaces use, rather than a second sweep that could
|
||||
* drift from it.
|
||||
*/
|
||||
const invalidate = () =>
|
||||
Promise.all([
|
||||
invalidateKeys(qc, encounterDependentKeys),
|
||||
invalidateReminderReads(qc),
|
||||
]);
|
||||
|
||||
const create = useMutation({
|
||||
mutationKey: queryKeys.encounterCreate(),
|
||||
mutationFn: (body: EncounterWriteBody) => apiPost<Encounter>(BASE, body),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
|
||||
const update = useMutation({
|
||||
mutationKey: queryKeys.encounterUpdate(),
|
||||
mutationFn: ({ id, body }: { id: string; body: EncounterWriteBody }) =>
|
||||
apiPatch<Encounter>(`${BASE}/${id}`, body),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
mutationKey: queryKeys.encounterDelete(),
|
||||
mutationFn: (id: string) =>
|
||||
apiDelete<{ deleted: boolean }>(`${BASE}/${id}`),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
|
||||
return { create, update, remove };
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"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,
|
||||
} 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();
|
||||
const invalidate = () => invalidateKeys(qc, encounterDependentKeys);
|
||||
|
||||
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 };
|
||||
}
|
||||
Reference in new issue
Block a user