import { useEffect, useMemo, useRef, useState } from "react"; import { AlertCircle } from "lucide-react"; import { IValueSet, useValueSetServiceContext } from "../../services"; import { Input } from "../ui/input.tsx"; import { Alert, AlertDescription } from "../ui/alert.tsx"; import { Button } from "../ui/button.tsx"; import { useDiagnosticCatalogue } from "../../hooks/useDiagnosticCatalogue.ts"; /** * Choosing what to order. * * WHY THIS IS NOT LoincCodeSearch. That component takes `searchLoincCodes`, which returns `[]` both * when nothing matched and when the lookup failed -- a deliberate fail-soft ruling, but one that * leaves the two indistinguishable to whoever is looking at the screen. * * THAT AMBIGUITY HAS A MEASURED COST. The terminology server answers an $expand of the US Core * diagnostic value sets with HTTP 200 and zero codes, no warning at all. A Diagnostic Report * type-ahead returned nothing for `hemoglobin A1c` and nothing for `blood` -- the deliberately broad * control -- and because it looked like a search that found nothing, it read as a data gap rather * than a defect. It went unexplained for weeks. * * So this one asks the status-aware search, and says which happened. * * AND IT NEVER BLOCKS AN ORDER. A practitioner who cannot look a code up can still order the test by * name. An uncoded order is a real order -- the code is how a result is matched back later, not * permission to draw blood -- and a practice unable to order a CBC because a code service is down is * a worse failure than an imperfectly coded order. */ const MIN_SEARCH_CHARS = 2; const DEBOUNCE_MS = 300; export interface LabTestChoice { /** Free text always. It is what the order and the queue display. */ text: string; /** Present only when a code was actually chosen. */ coding?: IValueSet; } type SearchState = | { status: "idle" } | { status: "searching" } | { status: "results"; results: IValueSet[]; filteredByKind: boolean } | { status: "none" } | { status: "unavailable" }; const LabTestPicker = ({ onChoose, disabled = false, kind, }: { onChoose: (choice: LabTestChoice) => void; disabled?: boolean; /** What the order is FOR. Passed to the search so the list is that kind only. */ kind?: string; }) => { const { searchLoincCodesWithStatus } = useValueSetServiceContext(); const { orderables } = useDiagnosticCatalogue(); const [query, setQuery] = useState(""); const [state, setState] = useState({ status: "idle" }); const searchIdRef = useRef(0); useEffect(() => { const trimmed = query.trim(); if (trimmed.length < MIN_SEARCH_CHARS) { setState({ status: "idle" }); return; } const currentId = ++searchIdRef.current; setState({ status: "searching" }); const timer = setTimeout(async () => { const outcome = await searchLoincCodesWithStatus(trimmed, kind); // Race guard: a slower earlier request must not overwrite a later one's answer. if (currentId !== searchIdRef.current) return; if (outcome.outcome === "unavailable") { setState({ status: "unavailable" }); return; } setState( outcome.results.length > 0 ? { status: "results", results: outcome.results, filteredByKind: outcome.filteredByKind } : { status: "none" }, ); }, DEBOUNCE_MS); return () => clearTimeout(timer); }, [query, kind, searchLoincCodesWithStatus]); const benchMatches = useMemo(() => { if (!kind) return []; const q = query.trim().toLowerCase(); return orderables.filter((row) => { if (!row.offered || (row.kind ?? "laboratory") !== kind) return false; if (!q) return true; const hay = [row.name, row.code, row.testCode, ...(row.analytes ?? []).map((a) => a.name)] .filter(Boolean) .join(" ") .toLowerCase(); return hay.includes(q); }); }, [kind, orderables, query]); const chooseBench = (row: (typeof orderables)[number]) => { searchIdRef.current += 1; setState({ status: "idle" }); setQuery(""); onChoose({ text: row.name ?? "Test", coding: row.testCode ? { system: row.codeSystem ?? "http://loinc.org", code: row.testCode, display: row.name ?? row.testCode, } : undefined, }); }; const chooseCoded = (coding: IValueSet) => { searchIdRef.current += 1; setState({ status: "idle" }); setQuery(""); onChoose({ text: coding.display, coding }); }; const chooseByName = () => { const text = query.trim(); if (!text) return; searchIdRef.current += 1; setState({ status: "idle" }); setQuery(""); onChoose({ text }); }; return (
setQuery(event.target.value)} /> {state.status === "searching" && (

Searching…

)} {kind && benchMatches.length > 0 && (

This practice offers

    {benchMatches.map((row) => (
  • ))}
)} {/* THE DISTINCTION THIS COMPONENT EXISTS FOR. "Nothing matched" invites a different search; "the lookup is unavailable" tells the practitioner the tool is broken and that ordering by name is the way through. Rendering one message for both is what hid the defect for weeks. */} {state.status === "unavailable" && ( The test lookup is unavailable, so no codes can be searched right now. This is not a search that found nothing. You can still order the test by name below. )} {state.status === "none" && (

No test matched that. Check the spelling, or order it by name below.

)} {/* SAYING SO, rather than handing over a quietly mixed list. The order carries a kind and the results here may not match it, and a picker that hides that is how an ultrasound was filed as laboratory work. */} {state.status === "results" && kind && !state.filteredByKind && (

These results are not narrowed to {kind === "imaging" ? "imaging" : "lab tests"}. The lookup could not filter by kind, so check the test before you place the order.

)} {state.status === "results" && ( )} {/* Always reachable, in every state including a healthy search. A practitioner who knows the test by a name the catalogue does not carry must not be stuck. */} {query.trim().length > 0 && ( )}
); }; export default LabTestPicker;