mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Add first-class qualitative lab findings (positive / negative / borderline serology) alongside the existing numeric path. A LabResult is now EITHER a numeric reading (value + unit, optional reference bounds) OR a qualitative one (valueText, e.g. "negativ"), never both, never neither. - schema: make `value` nullable, add `value_text`; migration 0181 is additive (DROP NOT NULL + ADD COLUMN), every existing numeric row is untouched. - validation: numeric-XOR-qualitative refine on create + update; unit/bounds apply to the numeric path only; valueText capped at 120 chars. The single resource PUT rejects a cross-type edit. - serialise: carry valueText through both DTOs; a qualitative row reports the neutral `unknown` range verdict (no number to classify). - FHIR: a qualitative result emits valueCodeableConcept — SNOMED qualifier value for confidently-known terms (Negative 260385009, Positive 10828004, Not detected 260415000, Detected 260373001), text-only for borderline / grenzwertig and any unrecognised term. The raw text always rides .text. - UI: a numeric/qualitative toggle in the lab form; list, history, detail, chart, sparkline, and the doctor-report PDF all render qualitative readings as their text and never plot or divide on a null value. - i18n: new labs.form keys across all six locales. - openapi: response carries a nullable value + valueText; regenerated.
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
/**
|
|
* v1.18.9 — qualitative lab result → SNOMED CT concept (or honest text-only).
|
|
*
|
|
* A qualitative serology result ("negativ" / "positiv" / "nicht nachweisbar")
|
|
* has no number, so its FHIR Observation carries a `valueCodeableConcept`
|
|
* instead of a `valueQuantity`. This module maps the common German / English
|
|
* result terms onto a small set of WELL-ESTABLISHED SNOMED CT qualifier-value
|
|
* concepts. The raw recorded text ALWAYS rides `.text`; a coded `coding` is
|
|
* added only when the term resolves CONFIDENTLY. When in doubt the result stays
|
|
* text-only — never a fabricated code — mirroring the conservative stance of
|
|
* the `lab-loinc` and `illness-snomed` mappers.
|
|
*
|
|
* Concepts (SNOMED CT International, qualifier value, REFERENCED not
|
|
* redistributed per the licence):
|
|
* - 260385009 "Negative" — negativ / negative
|
|
* - 10828004 "Positive" — positiv / positive
|
|
* - 260415000 "Not detected" — nicht nachweisbar / not detected
|
|
* - 260373001 "Detected" — nachweisbar / detected
|
|
*
|
|
* Borderline / grenzwertig is DELIBERATELY left text-only: the candidate
|
|
* "Borderline" qualifier concept could not be confidently verified, and an
|
|
* unverified code is worse than an honest `.text`. Any unrecognised term
|
|
* likewise stays text-only.
|
|
*
|
|
* Systems: SNOMED CT — http://snomed.info/sct
|
|
*/
|
|
|
|
import type { FhirCodeableConcept } from "@/lib/fhir/types";
|
|
|
|
/**
|
|
* SNOMED CT system URI. Inlined (rather than imported from `resources.ts`) to
|
|
* keep this leaf module free of the heavy `resources.ts` import graph and avoid
|
|
* an import cycle — `resources.ts` consumes THIS module.
|
|
*/
|
|
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
|
|
interface QualitativeConcept {
|
|
code: string;
|
|
display: string;
|
|
}
|
|
|
|
/**
|
|
* Normalised result term → SNOMED qualifier-value concept. The left side is the
|
|
* lower-cased, whitespace-collapsed recorded text. Only confidently-known terms
|
|
* appear; everything else falls through to text-only.
|
|
*/
|
|
const QUALITATIVE_SNOMED: Record<string, QualitativeConcept> = {
|
|
// Negative-like
|
|
negativ: { code: "260385009", display: "Negative" },
|
|
negative: { code: "260385009", display: "Negative" },
|
|
neg: { code: "260385009", display: "Negative" },
|
|
"nicht nachweisbar": { code: "260415000", display: "Not detected" },
|
|
"not detected": { code: "260415000", display: "Not detected" },
|
|
// Positive-like
|
|
positiv: { code: "10828004", display: "Positive" },
|
|
positive: { code: "10828004", display: "Positive" },
|
|
pos: { code: "10828004", display: "Positive" },
|
|
nachweisbar: { code: "260373001", display: "Detected" },
|
|
detected: { code: "260373001", display: "Detected" },
|
|
};
|
|
|
|
/** Collapse a recorded qualitative term to a lookup key. */
|
|
function normaliseQualitativeKey(text: string): string {
|
|
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
}
|
|
|
|
/**
|
|
* Build the `valueCodeableConcept` for a qualitative lab result. The raw text
|
|
* always rides `.text`; a SNOMED `coding` is added only for a confidently-
|
|
* recognised term. Borderline / grenzwertig and any unknown term stay
|
|
* text-only.
|
|
*/
|
|
export function qualitativeValueConcept(
|
|
valueText: string,
|
|
): FhirCodeableConcept {
|
|
const concept = QUALITATIVE_SNOMED[normaliseQualitativeKey(valueText)];
|
|
if (!concept) {
|
|
return { text: valueText };
|
|
}
|
|
return {
|
|
coding: [
|
|
{ system: SNOMED_SYSTEM, code: concept.code, display: concept.display },
|
|
],
|
|
text: valueText,
|
|
};
|
|
}
|