mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
feat(labs): record qualitative lab results
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.
This commit is contained in:
1 parent
e631396af3
commit
a5b3715a46
30 files changed
+819
-123
No files matched your search
@@ -150,6 +150,24 @@ export const PUT = apiHandler(
|
||||
);
|
||||
}
|
||||
|
||||
// v1.18.9 — a row keeps its type. Editing `value` on a qualitative row or
|
||||
// `valueText` on a numeric row would either leave both columns populated or
|
||||
// strand the row in an ambiguous state. Reject the cross-type edit with a
|
||||
// 422 — switch types by deleting and re-adding.
|
||||
const rowIsQualitative = existing.valueText !== null;
|
||||
if (rowIsQualitative && d.value !== undefined) {
|
||||
return apiError(
|
||||
"This is a qualitative reading — edit its result text, not a numeric value",
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!rowIsQualitative && d.valueText !== undefined) {
|
||||
return apiError(
|
||||
"This is a numeric reading — edit its value, not a qualitative result",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// Build `data` field-by-field. `undefined` → leave the column untouched;
|
||||
// an explicit `null` on `panel` / `note` / a reference bound clears it.
|
||||
// A reading linked to a catalog marker is edited via VALUE / takenAt /
|
||||
@@ -158,6 +176,10 @@ export const PUT = apiHandler(
|
||||
if (d.panel !== undefined) data.panel = d.panel;
|
||||
if (d.analyte !== undefined) data.analyte = d.analyte;
|
||||
if (d.value !== undefined) data.value = d.value;
|
||||
// v1.18.9 — qualitative edit. A row stays single-typed: editing `value` on a
|
||||
// qualitative row (or `valueText` on a numeric row) crosses the row's type
|
||||
// and is rejected below. An edit of the matching field updates in place.
|
||||
if (d.valueText !== undefined) data.valueText = d.valueText;
|
||||
if (d.unit !== undefined) data.unit = d.unit;
|
||||
if (d.referenceLow !== undefined) data.referenceLow = d.referenceLow;
|
||||
if (d.referenceHigh !== undefined) data.referenceHigh = d.referenceHigh;
|
||||
|
||||
@@ -173,6 +173,7 @@ async function postLabResult(request: NextRequest) {
|
||||
panel,
|
||||
analyte,
|
||||
value,
|
||||
valueText,
|
||||
unit,
|
||||
referenceLow,
|
||||
referenceHigh,
|
||||
@@ -180,6 +181,10 @@ async function postLabResult(request: NextRequest) {
|
||||
note,
|
||||
} = parsed.data;
|
||||
|
||||
// v1.18.9 — a qualitative reading carries `valueText` ("negativ" / …) instead
|
||||
// of a number. The Zod refine guarantees exactly one of the two is present.
|
||||
const isQualitative = valueText !== undefined;
|
||||
|
||||
// Every row links a catalog marker — no `LabResult` ever persists unlinked
|
||||
// (v1.18.1 High). Two paths converge on a `biomarker`:
|
||||
// - Structured: a `biomarkerId` is supplied; resolve + verify ownership
|
||||
@@ -207,9 +212,12 @@ async function postLabResult(request: NextRequest) {
|
||||
} else {
|
||||
biomarker = await resolveOrMintBiomarker(user.id, {
|
||||
analyte: analyte as string,
|
||||
unit: unit as string,
|
||||
referenceLow: referenceLow ?? null,
|
||||
referenceHigh: referenceHigh ?? null,
|
||||
// A qualitative reading has no numeric unit / range — mint the catalog
|
||||
// marker with an empty unit label and no bounds. A numeric reading
|
||||
// supplies its unit (Zod requires it on the free-text numeric path).
|
||||
unit: isQualitative ? (unit ?? "") : (unit as string),
|
||||
referenceLow: isQualitative ? null : (referenceLow ?? null),
|
||||
referenceHigh: isQualitative ? null : (referenceHigh ?? null),
|
||||
panel: panel ?? null,
|
||||
});
|
||||
}
|
||||
@@ -224,7 +232,9 @@ async function postLabResult(request: NextRequest) {
|
||||
biomarkerId: biomarker.id,
|
||||
panel: biomarker.panel,
|
||||
analyte: biomarker.name,
|
||||
value,
|
||||
// Exactly one of value / valueText is set (the Zod XOR refine).
|
||||
value: value ?? null,
|
||||
valueText: valueText ?? null,
|
||||
unit: biomarker.unit,
|
||||
referenceLow: biomarker.lowerBound,
|
||||
referenceHigh: biomarker.upperBound,
|
||||
|
||||
Reference in new issue
Block a user