mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(targets): convert glucose targets back to mg/dL on save
The per-metric target editor receives a glucose range already projected into the user's display unit (mmol/L) along with `unit="mmol/L"`, but it seeded, validated and saved that value verbatim. HealthLog stores glucose canonically in mg/dL, so a user on the mmol/L preference either had a valid target (e.g. 3.9–5.5 mmol/L) rejected by the 40–400 mg/dL bounds, or — when an override already existed — saw a mg/dL number rendered as if it were mmol/L and wrote it straight back. Add `mmolToMgdl` / `toCanonicalMgdl` (the inverse of `convertGlucose`) and teach the edit sheet to treat the display unit at its boundary: seed the persisted mg/dL override into the display unit, validate against bounds expressed in the display unit, and convert the typed value back to canonical mg/dL before the PUT. A no-op for every non-glucose metric and for users on the mg/dL preference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 0cc0361058a85f32d7135e5f2fd425c98f62d885)
This commit is contained in:
1 parent
09af60303d
commit
170862fdf8
4 files changed
+101
-4
No files matched your search
@@ -96,6 +96,26 @@ describe("<TargetEditSheet>", () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("mounts a glucose target seeded in mmol/L without throwing", () => {
|
||||
// The parent hands a mmol/L-converted range + unit="mmol/L"; the
|
||||
// sheet now seeds/validates in mmol/L and converts back to mg/dL on
|
||||
// save. Smoke-check the new conversion path mounts cleanly.
|
||||
expect(() =>
|
||||
renderToStaticMarkup(
|
||||
withProviders(
|
||||
<TargetEditSheet
|
||||
targetType="BLOOD_GLUCOSE_FASTING"
|
||||
targetLabel="Fasting glucose"
|
||||
unit="mmol/L"
|
||||
initialRange={{ min: 3.9, max: 5.5 }}
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
/>,
|
||||
),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("mounts the body without throwing for a derived metric (BMI)", () => {
|
||||
// BMI is derived from weight + height — the body shows the
|
||||
// explanatory hint instead of the editable inputs.
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type ThresholdMetric,
|
||||
type EffectiveRange,
|
||||
} from "@/lib/analytics/effective-range";
|
||||
import { convertGlucose, toCanonicalMgdl } from "@/lib/glucose";
|
||||
|
||||
/**
|
||||
* Per-metric target editor, mounted inline on the Insights metric
|
||||
@@ -120,6 +121,24 @@ function TargetEditSheetBody({
|
||||
const metric = TARGET_TYPE_TO_METRIC[targetType] ?? null;
|
||||
const isDerivedMetric = !isBp && metric == null;
|
||||
|
||||
// Glucose is the one metric whose display unit can differ from the
|
||||
// canonical storage unit: HealthLog stores mg/dL, but a user on the
|
||||
// mmol/L preference sees — and types — mmol/L. The parent already
|
||||
// hands us `unit="mmol/L"` and a `initialRange` pre-converted to that
|
||||
// unit. We must therefore (a) seed the mg/dL persisted override into
|
||||
// the display unit, (b) validate the typed value against bounds
|
||||
// expressed in the display unit, and (c) convert the typed value back
|
||||
// to canonical mg/dL before the PUT — otherwise a `5.5 mmol/L` target
|
||||
// is rejected by the 40–400 mg/dL bounds or stored verbatim as 5.5.
|
||||
const isGlucoseMmol =
|
||||
metric != null &&
|
||||
metric.startsWith("BLOOD_GLUCOSE") &&
|
||||
unit === "mmol/L";
|
||||
const toDisplay = (mgdl: number) =>
|
||||
isGlucoseMmol ? convertGlucose(mgdl, "mmol/L") : mgdl;
|
||||
const toCanonical = (displayValue: number) =>
|
||||
isGlucoseMmol ? toCanonicalMgdl(displayValue, "mmol/L") : displayValue;
|
||||
|
||||
// Lazy-load the thresholds payload so the dialog also catches any
|
||||
// already-persisted override even when the seeded `initialRange`
|
||||
// came from the targets API (which can paint a different in-band
|
||||
@@ -142,13 +161,18 @@ function TargetEditSheetBody({
|
||||
m: ThresholdMetric | null,
|
||||
fallback: CurrentRange | null,
|
||||
) => {
|
||||
// The persisted override is canonical (mg/dL for glucose); the
|
||||
// parent-provided `fallback` (initialRange) is already in the
|
||||
// display unit. Convert the override into the display unit so the
|
||||
// seeded inputs always read in the unit the label announces.
|
||||
if (m && thresholdsData?.overrides?.[m]) {
|
||||
return thresholdsData.overrides[m]!;
|
||||
const override = thresholdsData.overrides[m]!;
|
||||
return { min: toDisplay(override.min), max: toDisplay(override.max) };
|
||||
}
|
||||
if (fallback) return fallback;
|
||||
if (m) {
|
||||
const bounds = METRIC_BOUNDS[m];
|
||||
return { min: bounds.min, max: bounds.max };
|
||||
return { min: toDisplay(bounds.min), max: toDisplay(bounds.max) };
|
||||
}
|
||||
return { min: 0, max: 100 };
|
||||
};
|
||||
@@ -260,13 +284,24 @@ function TargetEditSheetBody({
|
||||
const diaMinNum = parseFloat(displayDiaMin);
|
||||
const diaMaxNum = parseFloat(displayDiaMax);
|
||||
|
||||
const primaryBounds = isBp
|
||||
const canonicalPrimaryBounds = isBp
|
||||
? METRIC_BOUNDS.BLOOD_PRESSURE_SYS
|
||||
: metric
|
||||
? METRIC_BOUNDS[metric]
|
||||
: null;
|
||||
const secondaryBounds = isBp ? METRIC_BOUNDS.BLOOD_PRESSURE_DIA : null;
|
||||
|
||||
// Bounds the user is validated/clamped against — expressed in the
|
||||
// display unit so a mmol/L glucose entry is checked against the
|
||||
// mmol/L-projected 40–400 mg/dL window, not the raw mg/dL numbers.
|
||||
const primaryBounds = canonicalPrimaryBounds
|
||||
? {
|
||||
min: toDisplay(canonicalPrimaryBounds.min),
|
||||
max: toDisplay(canonicalPrimaryBounds.max),
|
||||
unit,
|
||||
}
|
||||
: null;
|
||||
|
||||
const primaryValid =
|
||||
primaryBounds != null &&
|
||||
Number.isFinite(minNum) &&
|
||||
@@ -296,7 +331,11 @@ function TargetEditSheetBody({
|
||||
return;
|
||||
}
|
||||
if (!metric) return;
|
||||
updateMutation.mutate({ [metric]: { min: minNum, max: maxNum } });
|
||||
// Convert the typed display-unit value back to canonical mg/dL for
|
||||
// glucose on the mmol/L preference; a no-op for every other metric.
|
||||
updateMutation.mutate({
|
||||
[metric]: { min: toCanonical(minNum), max: toCanonical(maxNum) },
|
||||
});
|
||||
};
|
||||
|
||||
const hasOverride = isBp
|
||||
|
||||
Reference in new issue
Block a user