mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(labs): pair the panel changes by marker, not by printed name
This commit is contained in:
1 parent
8ba4d1b0d0
commit
7167ba8cdc
3 files changed
+69
-5
No files matched your search
@@ -45,6 +45,7 @@ export const GET = apiHandler(async () => {
|
||||
where: { userId: user.id, deletedAt: null, value: { not: null } },
|
||||
select: {
|
||||
analyte: true,
|
||||
biomarkerId: true,
|
||||
unit: true,
|
||||
value: true,
|
||||
referenceLow: true,
|
||||
@@ -61,6 +62,7 @@ export const GET = apiHandler(async () => {
|
||||
const summary = summariseLabChanges(
|
||||
rows.map((r) => ({
|
||||
analyte: r.analyte,
|
||||
biomarkerId: r.biomarkerId,
|
||||
unit: r.unit,
|
||||
value: r.value as number,
|
||||
referenceLow: r.referenceLow,
|
||||
|
||||
@@ -26,10 +26,12 @@ function row(
|
||||
sourceLow?: number | null;
|
||||
sourceHigh?: number | null;
|
||||
sourceText?: string | null;
|
||||
biomarkerId?: string | null;
|
||||
} = {},
|
||||
): LabChangeRow {
|
||||
return {
|
||||
analyte,
|
||||
biomarkerId: opts.biomarkerId ?? null,
|
||||
unit: opts.unit ?? "mg/dL",
|
||||
value,
|
||||
referenceLow: opts.low ?? null,
|
||||
@@ -143,4 +145,48 @@ describe("summariseLabChanges", () => {
|
||||
);
|
||||
expect(s.present).toBe(true);
|
||||
});
|
||||
|
||||
it("pairs two printed names of one marker by the marker", () => {
|
||||
const out = summariseLabChanges(
|
||||
[
|
||||
row("HbA1c", 5.9, "2026-06-01", { unit: "%", biomarkerId: "bm-1" }),
|
||||
row("Glykiertes Hämoglobin", 5.6, "2026-03-01", {
|
||||
unit: "%",
|
||||
biomarkerId: "bm-1",
|
||||
}),
|
||||
],
|
||||
NOW,
|
||||
);
|
||||
expect(out.present).toBe(true);
|
||||
expect(out.changes).toEqual([
|
||||
expect.objectContaining({
|
||||
analyte: "HbA1c",
|
||||
latest: 5.9,
|
||||
previous: 5.6,
|
||||
delta: 0.3,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps two markers apart even when the printed name matches", () => {
|
||||
const out = summariseLabChanges(
|
||||
[
|
||||
row("Glucose", 95, "2026-06-01", { biomarkerId: "fasting" }),
|
||||
row("Glucose", 140, "2026-03-01", { biomarkerId: "postprandial" }),
|
||||
],
|
||||
NOW,
|
||||
);
|
||||
expect(out.present).toBe(false);
|
||||
});
|
||||
|
||||
it("reports no delta across two units", () => {
|
||||
const out = summariseLabChanges(
|
||||
[
|
||||
row("Glucose", 5.3, "2026-06-01", { unit: "mmol/L", biomarkerId: "g" }),
|
||||
row("Glucose", 95, "2026-03-01", { unit: "mg/dL", biomarkerId: "g" }),
|
||||
],
|
||||
NOW,
|
||||
);
|
||||
expect(out.present).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,12 @@ import {
|
||||
/** A single numeric lab reading, with both windows it could be judged against. */
|
||||
export interface LabChangeRow extends SourceReferenceRange {
|
||||
analyte: string;
|
||||
/**
|
||||
* The marker the reading is filed under. Two reports print the same marker
|
||||
* under different names ("HbA1c", "Glykiertes Hämoglobin"); the marker is
|
||||
* what pairs them. A reading without one pairs by its printed name.
|
||||
*/
|
||||
biomarkerId?: string | null;
|
||||
unit: string;
|
||||
value: number;
|
||||
referenceLow: number | null;
|
||||
@@ -88,9 +94,18 @@ function dayKey(d: Date): string {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** Normalised analyte name for cross-panel matching (free-text tolerant). */
|
||||
function analyteKey(analyte: string): string {
|
||||
return analyte.trim().toLowerCase();
|
||||
/**
|
||||
* Cross-panel matching key: the marker when the reading is filed under one,
|
||||
* else the normalised printed name (free-text tolerant).
|
||||
*/
|
||||
function analyteKey(row: LabChangeRow): string {
|
||||
return row.biomarkerId
|
||||
? `marker:${row.biomarkerId}`
|
||||
: `name:${row.analyte.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
function sameUnit(a: LabChangeRow, b: LabChangeRow): boolean {
|
||||
return a.unit.trim().toLowerCase() === b.unit.trim().toLowerCase();
|
||||
}
|
||||
|
||||
const round2 = (n: number): number => Math.round(n * 100) / 100;
|
||||
@@ -116,7 +131,7 @@ export function summariseLabChanges(
|
||||
analytes = new Map();
|
||||
byDay.set(day, analytes);
|
||||
}
|
||||
const key = analyteKey(row.analyte);
|
||||
const key = analyteKey(row);
|
||||
const existing = analytes.get(key);
|
||||
if (!existing || row.takenAt.getTime() >= existing.takenAt.getTime()) {
|
||||
analytes.set(key, row);
|
||||
@@ -144,7 +159,8 @@ export function summariseLabChanges(
|
||||
const changes: LabChange[] = [];
|
||||
for (const [key, latestRow] of latest) {
|
||||
const previousRow = previous.get(key);
|
||||
if (!previousRow) continue;
|
||||
// A delta across two units is no change, it is a conversion; skip it.
|
||||
if (!previousRow || !sameUnit(latestRow, previousRow)) continue;
|
||||
const delta = round2(latestRow.value - previousRow.value);
|
||||
const direction: LabChange["direction"] =
|
||||
delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
||||
|
||||
Reference in new issue
Block a user