mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(cycle): state the cycle-phase glucose row in the reader's unit
The cycle-phase table printed the bundle's mg/dL label beside raw mg/dL values for every reader. Weight, temperature and now glucose rows take their unit and conversion from the one reading resolver; every other row keeps its bundle label. Refs #1067
This commit is contained in:
1 parent
bc2d60cce8
commit
8a11e60bb3
2 files changed
+79
-21
No files matched your search
@@ -6,12 +6,17 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
// QueryClient, so mock the hook to a real metric-preference implementation —
|
||||
// metric is the identity on values and yields the canonical display symbols,
|
||||
// so the assertions below (kg, °C, unchanged numbers) hold.
|
||||
const reader = vi.hoisted(() => ({
|
||||
glucoseUnit: "mg/dL" as "mg/dL" | "mmol/L",
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-unit-display", async () => {
|
||||
const dt = await import("@/lib/measurements/display-transform");
|
||||
const forType = (t: string) => dt.getDisplayTransform(t, "metric");
|
||||
return {
|
||||
useUnitDisplay: () => ({
|
||||
preference: "metric" as const,
|
||||
glucoseUnit: reader.glucoseUnit,
|
||||
transformFor: forType,
|
||||
toDisplay: (t: string, v: number) =>
|
||||
dt.applyDisplayTransform(v, forType(t)),
|
||||
@@ -107,4 +112,26 @@ describe("<CyclePhaseCrosstab>", () => {
|
||||
const html = render(<CyclePhaseCrosstab rows={[unknown]} />);
|
||||
expect(html).not.toContain("undefined");
|
||||
});
|
||||
|
||||
it("states a glucose row in mmol/L for an mmol/L reader", () => {
|
||||
reader.glucoseUnit = "mmol/L";
|
||||
try {
|
||||
const glucose: CyclePhaseCrosstabRow = {
|
||||
...row("bloodGlucose", "glucose"),
|
||||
lutealAvg: 108,
|
||||
follicularAvg: 90,
|
||||
delta: 18,
|
||||
};
|
||||
const html = render(<CyclePhaseCrosstab rows={[glucose]} />);
|
||||
// 18 mg/dL is 1.0 mmol/L; 108 / 90 mg/dL are 6.0 / 5.0 mmol/L.
|
||||
expect(html).toContain("+1.0 mmol/L");
|
||||
expect(html).toContain("6.0");
|
||||
expect(html).not.toContain("mg/dL");
|
||||
expect(render(<CyclePhaseHeadline headline={glucose} />)).toContain(
|
||||
"mmol/L",
|
||||
);
|
||||
} finally {
|
||||
reader.glucoseUnit = "mg/dL";
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
import { useUnitDisplay, type UnitDisplay } from "@/hooks/use-unit-display";
|
||||
import {
|
||||
applyDisplayTransform,
|
||||
applyDisplayTransformDelta,
|
||||
getReadingTransform,
|
||||
} from "@/lib/measurements/display-transform";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
@@ -88,8 +93,47 @@ function fmt(value: number, display: CyclePhaseCrosstabDisplay): string {
|
||||
const DISPLAY_TO_TYPE: Partial<Record<CyclePhaseCrosstabDisplay, string>> = {
|
||||
kg: "WEIGHT",
|
||||
celsius: "BODY_TEMPERATURE",
|
||||
glucose: "BLOOD_GLUCOSE",
|
||||
};
|
||||
|
||||
/**
|
||||
* Unit symbol and conversions for one row, from the reader's preferences.
|
||||
* Glucose follows the glucose unit (it used to print the mg/dL label from
|
||||
* the bundle beside raw mg/dL values for every reader); weight and
|
||||
* temperature follow metric/imperial; every other display keeps its bundle
|
||||
* label and its raw value.
|
||||
*/
|
||||
function rowUnits(
|
||||
display: CyclePhaseCrosstabDisplay,
|
||||
unitDisplay: UnitDisplay,
|
||||
t: (key: string) => string,
|
||||
): {
|
||||
unit: string;
|
||||
abs: (v: number) => number;
|
||||
delta: (v: number) => number;
|
||||
} {
|
||||
const type = DISPLAY_TO_TYPE[display];
|
||||
if (!type) {
|
||||
// Defensive: an unmapped display must never pass undefined to `t`
|
||||
// (whose resolver does `key.split(".")`). Empty unit on a miss.
|
||||
const unitKey = UNIT_KEY[display];
|
||||
return {
|
||||
unit: unitKey ? t(unitKey) : "",
|
||||
abs: (v) => v,
|
||||
delta: (v) => v,
|
||||
};
|
||||
}
|
||||
const transform = getReadingTransform(type, {
|
||||
system: unitDisplay.preference,
|
||||
glucoseUnit: unitDisplay.glucoseUnit,
|
||||
});
|
||||
return {
|
||||
unit: transform.displayUnit,
|
||||
abs: (v) => applyDisplayTransform(v, transform),
|
||||
delta: (v) => applyDisplayTransformDelta(v, transform),
|
||||
};
|
||||
}
|
||||
|
||||
const HEADLINE_KEY: Record<string, string> = {
|
||||
restingHeartRate: "cycle.insights.headline.restingHeartRate",
|
||||
heartRateVariability: "cycle.insights.headline.heartRateVariability",
|
||||
@@ -120,11 +164,7 @@ export function CyclePhaseHeadline({
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[headline.metricKey] ?? headline.metricKey,
|
||||
);
|
||||
// Defensive: an unmapped display must never pass undefined to `t` (whose
|
||||
// resolver does `key.split(".")`). An empty unit is the honest degrade.
|
||||
const mt = DISPLAY_TO_TYPE[headline.display];
|
||||
const unitKey = UNIT_KEY[headline.display];
|
||||
const unit = mt ? unitDisplay.unitFor(mt) : unitKey ? t(unitKey) : "";
|
||||
const { unit, delta: deltaConv } = rowUnits(headline.display, unitDisplay, t);
|
||||
const up = headline.delta >= 0;
|
||||
const dir = t(
|
||||
up
|
||||
@@ -134,12 +174,7 @@ export function CyclePhaseHeadline({
|
||||
const params = {
|
||||
metric: metricLabel,
|
||||
// A phase delta → factor-only conversion (no affine offset).
|
||||
delta: fmt(
|
||||
Math.abs(
|
||||
mt ? unitDisplay.toDisplayDelta(mt, headline.delta) : headline.delta,
|
||||
),
|
||||
headline.display,
|
||||
),
|
||||
delta: fmt(Math.abs(deltaConv(headline.delta)), headline.display),
|
||||
unit,
|
||||
dir,
|
||||
};
|
||||
@@ -175,15 +210,11 @@ export function CyclePhaseCrosstab({
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey,
|
||||
);
|
||||
// Defensive: an unmapped display must never pass undefined to `t`
|
||||
// (whose resolver does `key.split(".")`). Empty unit on a miss.
|
||||
const mt = DISPLAY_TO_TYPE[row.display];
|
||||
const unitKey = UNIT_KEY[row.display];
|
||||
const unit = mt ? unitDisplay.unitFor(mt) : unitKey ? t(unitKey) : "";
|
||||
const absConv = (v: number) =>
|
||||
mt ? unitDisplay.toDisplay(mt, v) : v;
|
||||
const deltaConv = (v: number) =>
|
||||
mt ? unitDisplay.toDisplayDelta(mt, v) : v;
|
||||
const {
|
||||
unit,
|
||||
abs: absConv,
|
||||
delta: deltaConv,
|
||||
} = rowUnits(row.display, unitDisplay, t);
|
||||
// `delta` = lutealAvg − follicularAvg: positive = the vital runs
|
||||
// higher in the luteal phase. The number stays NEUTRAL — higher is
|
||||
// good for steps but bad for resting HR, and this board is
|
||||
|
||||
Reference in new issue
Block a user