mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(clinician): print every shared statistic with its unit
The clinician view printed bare numbers for every measurement group and the per-context glucose rows in raw mg/dL, so a recipient read glucose without a unit and in the wrong one for an account on mmol/L. The PDF's measurement table had the same gap for the raw glucose series. Both artefacts now take the unit and the displayed number from one helper (doctor-report/stat-display.ts): SI for mass, length and temperature as before, hours for sleep, and the record owner's unit for glucose. Refs #1067
This commit is contained in:
1 parent
1b694ec222
commit
d00e106ce1
13 files changed
+207
-41
No files matched your search
@@ -225,6 +225,49 @@ describe("<ClinicianView>", () => {
|
||||
expect(html).not.toContain(">WEIGHT<");
|
||||
});
|
||||
|
||||
it("prints every measurement group with its unit", () => {
|
||||
const html = render(
|
||||
makeReport({
|
||||
stats: {
|
||||
WEIGHT: { avg: 80, min: 78, max: 82, count: 12, latest: 79 },
|
||||
// Per-night asleep minutes; the report reads them in hours.
|
||||
SLEEP_DURATION: {
|
||||
avg: 450,
|
||||
min: 420,
|
||||
max: 480,
|
||||
count: 7,
|
||||
latest: 450,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(html).toContain("latest 79 kg (avg 80 kg, range 78–82 kg)");
|
||||
expect(html).toContain("latest 7.5 h (avg 7.5 h, range 7–8 h)");
|
||||
});
|
||||
|
||||
it("shows glucose in the record owner's unit, never raw mg/dL", () => {
|
||||
const html = render(
|
||||
makeReport({
|
||||
glucoseUnit: "mmol/L",
|
||||
stats: {
|
||||
BLOOD_GLUCOSE: { avg: 108, min: 90, max: 126, count: 9, latest: 99 },
|
||||
},
|
||||
glucoseStats: {
|
||||
FASTING: { avg: 99, min: 90, max: 108, count: 5, latest: 95 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
// 99 mg/dL = 5.5 mmol/L, 108 = 6.0, 90 = 5.0, 126 = 7.0, 95 = 5.3.
|
||||
expect(html).toContain(
|
||||
"latest 5.5 mmol/L (avg 6 mmol/L, range 5–7 mmol/L)",
|
||||
);
|
||||
expect(html).toContain(
|
||||
"latest 5.3 mmol/L (avg 5.5 mmol/L, range 5–6 mmol/L)",
|
||||
);
|
||||
expect(html).not.toContain("mg/dL");
|
||||
expect(html).not.toContain("latest 99");
|
||||
});
|
||||
|
||||
it("omits the wellness card when there are no scores", () => {
|
||||
const html = render(makeReport({ wellnessScores: [] }));
|
||||
expect(html).not.toContain("Wellness scores");
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
*/
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import type { DoctorReportData } from "@/lib/doctor-report-data";
|
||||
import {
|
||||
reportStatUnit,
|
||||
reportStatValue,
|
||||
} from "@/lib/doctor-report/stat-display";
|
||||
import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
|
||||
import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/lib/measurements/type-label-keys";
|
||||
import {
|
||||
isReportLeafId,
|
||||
@@ -212,6 +217,13 @@ export function MeasurementGroups({
|
||||
|
||||
if (groups.length === 0) return null;
|
||||
|
||||
// The same unit and the same number the PDF prints for the row: SI for
|
||||
// mass, length and temperature, the owner's unit for glucose, hours for
|
||||
// sleep. A clinician must never read a figure without knowing its unit.
|
||||
const glucoseUnit = resolveGlucoseUnit(report.glucoseUnit ?? null);
|
||||
const show = (type: string, value: number) =>
|
||||
fmtNum(reportStatValue(type, value, glucoseUnit));
|
||||
|
||||
return (
|
||||
<>
|
||||
{groups.map((group) => (
|
||||
@@ -224,11 +236,11 @@ export function MeasurementGroups({
|
||||
<StatRow
|
||||
key={type}
|
||||
label={t(MEASUREMENT_TYPE_LABEL_KEYS[type])}
|
||||
value={t("clinicianView.statSummary", {
|
||||
latest: fmtNum(stat!.latest),
|
||||
avg: fmtNum(stat!.avg),
|
||||
min: fmtNum(stat!.min),
|
||||
max: fmtNum(stat!.max),
|
||||
value={statSummary(t, reportStatUnit(type, glucoseUnit, t), {
|
||||
latest: show(type, stat!.latest),
|
||||
avg: show(type, stat!.avg),
|
||||
min: show(type, stat!.min),
|
||||
max: show(type, stat!.max),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
@@ -238,6 +250,20 @@ export function MeasurementGroups({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* "latest 79 kg (avg 80 kg, range 78–82 kg)". A type with no unit (a score, a
|
||||
* count) keeps the unit-less sentence rather than printing a dangling space.
|
||||
*/
|
||||
function statSummary(
|
||||
t: Translate,
|
||||
unit: string,
|
||||
values: { latest: number; avg: number; min: number; max: number },
|
||||
): string {
|
||||
return unit
|
||||
? t("clinicianView.statSummaryWithUnit", { ...values, unit })
|
||||
: t("clinicianView.statSummary", values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-context glucose stats. The heading used to read "Lab values", which was
|
||||
* the wrong name for it even before the record's actual lab results reached
|
||||
@@ -258,6 +284,9 @@ export function GlucoseSection({
|
||||
const entries = Object.entries(report.glucoseStats).filter(
|
||||
([, s]) => s.count > 0,
|
||||
);
|
||||
// Stored in mg/dL; shown in the record owner's unit, as the PDF shows it.
|
||||
const glucoseUnit = resolveGlucoseUnit(report.glucoseUnit ?? null);
|
||||
const show = (value: number) => fmtNum(convertGlucose(value, glucoseUnit));
|
||||
return (
|
||||
<LeafSection
|
||||
t={t}
|
||||
@@ -270,11 +299,11 @@ export function GlucoseSection({
|
||||
<StatRow
|
||||
key={ctx}
|
||||
label={t(`clinicianView.glucose.${ctx}`)}
|
||||
value={t("clinicianView.statSummary", {
|
||||
latest: fmtNum(s.latest),
|
||||
avg: fmtNum(s.avg),
|
||||
min: fmtNum(s.min),
|
||||
max: fmtNum(s.max),
|
||||
value={statSummary(t, glucoseUnit, {
|
||||
latest: show(s.latest),
|
||||
avg: show(s.avg),
|
||||
min: show(s.min),
|
||||
max: show(s.max),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user