Files
HealthLog/src/components/documents/use-document-facts.ts
T
Marc-André Bombeck e65ada2961 feat(labs): merge markers, edit their aliases and pick a marker on review
The marker list offers a merge with the server's preview: what moves, what
is converted, which names become aliases, and a refusal when a unit cannot
be converted; it says a merge cannot be undone. The marker page shows and
edits the other spellings and follows a merged marker to its target. Scan
and document review name the matched marker or offer its suggestions and a
new marker; the choice is remembered. The biomarkerMerge feature flag is
on.
2026-10-15 22:55:08 +02:00

142 lines
4.8 KiB
TypeScript

"use client";
/**
* Client hooks for the review-then-confirm chain on a stored document's
* staged facts — the missing UI leg of the server chain that already existed:
*
* extract (`POST …/extract`, incl. `{ mode: "stored" }` over the document's
* own indexed text) → PENDING `ExtractedFact` rows → optional per-fact edit
* (`PATCH …/facts/{factId}`, clears `needsReview`) → confirm
* (`POST …/confirm`) — the ONLY write into the structured stores.
*
* Nothing here commits on its own. The confirm mutation carries the explicit
* approve/reject decisions the person made; a fact without a decision stays
* pending on the server.
*/
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { resolveAiRun, useAiRunPhase } from "@/hooks/use-ai-run";
import { apiPatch, apiPost } from "@/lib/api/api-fetch";
import type {
AiRunAccepted,
DocumentExtractRunResult,
} from "@/lib/documents/ai-runs/types";
import { invalidateKeys, queryKeys } from "@/lib/query-keys";
import type {
ExtractedFactDto,
InboundFactEdit,
} from "@/lib/validations/inbound-documents";
/** The confirm route's per-batch outcome (per-fact misses never fail it). */
export interface ConfirmFactsResult {
approved: { factId: string; recordType: string; recordId: string }[];
rejected: string[];
needsReview: string[];
failed: { factId: string; reason: string }[];
}
/**
* Structure the document's own stored extracted text into staged facts —
* the manual recovery when the automatic staging run was skipped or failed.
* Stages PENDING facts only; the review + confirm steps stay mandatory.
*
* The extraction runs in the background (v1.40): the request only queues it,
* so no proxy can cut a slow model, and `runPhase` says so while it runs. The
* run reports how many facts it staged; the facts themselves are read from
* the document, which is refetched when the run ends.
*/
export function useStoredExtract() {
const queryClient = useQueryClient();
const { phase, onQueued, onProgress, reset } = useAiRunPhase();
const mutation = useMutation<
DocumentExtractRunResult,
Error,
{ documentId: string }
>({
mutationFn: async ({ documentId }) =>
resolveAiRun<DocumentExtractRunResult>(
queryClient,
await apiPost<DocumentExtractRunResult | AiRunAccepted>(
`/api/documents/inbound/${documentId}/extract`,
{ mode: "stored" },
{
// The request only queues the read: the enqueue, no model.
signal: AbortSignal.timeout(60_000),
headers: { Prefer: "respond-async" },
},
),
{ onQueued, onProgress },
),
onSettled: reset,
onSuccess: (_data, { documentId }) => {
void invalidateKeys(queryClient, [
queryKeys.inboundDocument(documentId),
queryKeys.documents(),
]);
},
});
return { ...mutation, runPhase: mutation.isPending ? phase : "idle" };
}
/**
* Correct a staged fact before approval. A successful edit clears
* `needsReview` server-side — the values become user-asserted and the fact
* turns approvable. The fact's resource type can never change.
*/
export function useEditFact() {
const queryClient = useQueryClient();
return useMutation<
ExtractedFactDto,
Error,
{ documentId: string; factId: string; edit: InboundFactEdit }
>({
mutationFn: ({ documentId, factId, edit }) =>
apiPatch<ExtractedFactDto>(
`/api/documents/inbound/${documentId}/facts/${factId}`,
edit,
),
onSuccess: (_data, { documentId }) => {
void invalidateKeys(queryClient, [queryKeys.inboundDocument(documentId)]);
},
});
}
/**
* Commit the review decisions. Approved facts land in their structured
* stores (labs / conditions / medications) through the server's existing
* field-by-field commit; rejected facts are discarded. Invalidates every
* read the commit can touch, plus the document itself (status + fact states).
*/
export function useConfirmFacts() {
const queryClient = useQueryClient();
return useMutation<
ConfirmFactsResult,
Error,
{
documentId: string;
decisions: {
factId: string;
action: "approve" | "reject";
/** v1.43 — the marker the person chose for a lab value. */
biomarkerId?: string;
}[];
}
>({
mutationFn: ({ documentId, decisions }) =>
apiPost<ConfirmFactsResult>(
`/api/documents/inbound/${documentId}/confirm`,
{ decisions },
),
onSuccess: (_data, { documentId }) => {
void invalidateKeys(queryClient, [
queryKeys.inboundDocument(documentId),
queryKeys.documents(),
queryKeys.labResults(),
queryKeys.biomarkers(),
queryKeys.illness(),
queryKeys.medications(),
]);
},
});
}