Files
HealthLog/src/components/labs/source-range-note.tsx
T
Marc-André Bombeck fd2bd67ff3 feat(labs): keep the reference range a report prints beside a value
A lab runs its own method on its own device, so the window a physician
evaluates a value against is the one printed next to it on that report,
not a catalog band averaged over every lab. Reading a report pulled the
value and dropped that range: the extraction asked for it, the commit
handed it to the biomarker resolver, and the resolver only uses it when
it mints a NEW marker. For every analyte the user already had — the
normal case after a first panel — the printed range was read and then
thrown away.

Add `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText`
to `LabResult` and carry the range from the report to the reading. The
text is stored whether or not numbers can be read out of it, because
"bis 5,0" and "negativ" state a window too; a range that could not be
parsed is kept verbatim, never replaced with invented bounds.

`resolveEffectiveReferenceRange` is the one place the precedence lives:
the source window wins for that single reading, and the catalog stays
the net for readings without one. A printed string with no derivable
bound does not displace the catalog. Every surface that judges a
reading routes through it — the API verdict, the doctor report, the
insights blocks and per-marker assessment, the coach snapshot, the MCP
read and the FHIR `referenceRange` — so a value cannot read in range on
one surface and out on another.

The API keeps `referenceLow` / `referenceHigh` as the window in force,
so a client that reads only those two renders the right range and the
right verdict with no change of its own; the catalog band, the source
window, its origin and a divergence flag ride alongside for clients
that want to show both.

Read side: the chart draws the source window as a second, visually
distinct band with the reading it came from named beneath it, the
reading rows name the printed range and the catalog band where the two
disagree, and the doctor-report PDF prints the report's own string with
a footnote naming the saved range. Entry side: one optional field on
the lab form, folded away until asked for, and the printed range is
editable on the scan review screen.

The range parser covers the notations German and English reports
actually print — two-sided, one-sided, comma or dot decimals, thousands
separators, trailing units — reads units without ever converting them,
and refuses to guess on anything ambiguous.
2026-08-07 21:40:41 +02:00

70 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { formatLabValue } from "@/lib/labs/format-value";
import { formatReferenceRange } from "@/lib/labs/reference-range";
import { useTranslations } from "@/lib/i18n/context";
import type { LabResultDto } from "./types";
/**
* The reference window a reading's own report printed, shown wherever that
* reading is shown.
*
* Renders nothing for a reading with no printed window — most of them — so it
* costs no space on the common row. When the report DID state a window it
* names it, verbatim where the report gave words; and when that window and the
* catalog band disagree it names the catalog band too. A reader told only
* "3,9–5,4" and a reader told only "3,5–5,0" draw different conclusions from
* the same number, so a surface that shows one silently is showing a partial
* answer.
*
* Neutral by construction: no tint, no icon, muted text. The in/out verdict is
* the badge's job — this element states where the window came from, nothing
* about whether the value is good news.
*/
export function SourceRangeNote({
reading,
className,
}: {
reading: Pick<
LabResultDto,
| "unit"
| "referenceLow"
| "referenceHigh"
| "catalogReferenceLow"
| "catalogReferenceHigh"
| "sourceReferenceText"
| "referenceOrigin"
| "referenceDivergesFromCatalog"
>;
className?: string;
}) {
const { t } = useTranslations();
if (reading.referenceOrigin !== "source") return null;
const printed =
reading.sourceReferenceText ??
`${formatReferenceRange(
reading.referenceLow,
reading.referenceHigh,
formatLabValue,
)} ${reading.unit}`.trim();
const catalog = formatReferenceRange(
reading.catalogReferenceLow,
reading.catalogReferenceHigh,
formatLabValue,
);
return (
<span className={className}>
{t("labs.sourceRange.fromReport", { range: printed })}
{reading.referenceDivergesFromCatalog && catalog
? ` · ${t("labs.sourceRange.catalogDiffers", {
range: `${catalog} ${reading.unit}`.trim(),
})}`
: null}
</span>
);
}