feat(measurements): log a resting heart rate by hand

The entry form offered pulse but not resting heart rate, which has its own
chart and series, so a value read off a watch or taken on waking could only
land among spot pulses. The form now offers it next to pulse. The server
already gates the type on the recovery module (its resting_hr domain), so
the form, the dashboard summary, the resting-pulse page, its trend slot and
its correlation channel now follow that module too.

Refs #1079
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 21:53:38 +02:00
1 parent 82ccac2fd0
commit 08d5acdcb5
3 files changed
+61

No files matched your search

@@ -0,0 +1,45 @@
/**
* The manual-entry form offers the resting heart rate (#1079).
*
* Resting heart rate has its own chart and series, separate from spot pulse.
* A value read off a watch or taken on waking had no row in the form, so it
* could only be logged as a pulse, which feeds a different chart.
*/
import { describe, it, expect } from "vitest";
import { VALUE_RANGES, getUnitForType } from "@/lib/validations/measurement";
import {
MEASUREMENT_FORM_TYPE_VALUES,
MEASUREMENT_TYPES,
} from "@/components/measurements/measurement-form";
import en from "../../../../messages/en.json";
describe("manual entry of the resting heart rate", () => {
it("offers it as its own type, next to pulse", () => {
expect(MEASUREMENT_FORM_TYPE_VALUES).toContain("RESTING_HEART_RATE");
const values = MEASUREMENT_TYPES.map((t) => t.value);
expect(values.indexOf("RESTING_HEART_RATE")).toBe(
values.indexOf("PULSE") + 1,
);
});
it("uses the server's unit, a labelled row and a plausible placeholder", () => {
const row = MEASUREMENT_TYPES.find((t) => t.value === "RESTING_HEART_RATE");
expect(row).toBeDefined();
expect(row && "unit" in row ? row.unit : undefined).toBe(
getUnitForType("RESTING_HEART_RATE"),
);
expect(
(en as { measurements: Record<string, string> }).measurements
.typeRestingHeartRate,
).toBeTruthy();
const range = VALUE_RANGES.RESTING_HEART_RATE;
const placeholder = Number(
row && "placeholder" in row ? row.placeholder : NaN,
);
if (range) {
expect(placeholder).toBeGreaterThanOrEqual(range.min);
expect(placeholder).toBeLessThanOrEqual(range.max);
}
});
});
@@ -67,6 +67,14 @@ export const MEASUREMENT_TYPES = [
unit: "bpm",
placeholder: "72",
},
// The resting heart rate has its own chart and series; a value read off a
// watch or taken on waking belongs there rather than among spot pulses.
{
value: "RESTING_HEART_RATE",
labelKey: "measurements.typeRestingHeartRate",
unit: "bpm",
placeholder: "58",
},
{
value: "BODY_FAT",
labelKey: "measurements.typeBodyFat",
+8
View File
@@ -100,6 +100,9 @@ const STATIC_SURFACE_MODULE = {
"insights-page:six-minute-walk": "recovery",
"insights-page:stair-ascent-speed": "recovery",
"insights-page:stair-descent-speed": "recovery",
// The resting heart rate is the recovery module's `resting_hr` domain; its
// page, trend slot, correlation channel and summary key follow it.
"insights-page:resting-pulse": "recovery",
// ── Insights overview blocks ──
"overview:cycle-summary": "cycle",
@@ -115,6 +118,7 @@ const STATIC_SURFACE_MODULE = {
// ── Trends row ──
"trend:mood": "mood",
"trend:sleep": "sleep",
"trend:resting_hr": "recovery",
// ── Add menu ──
"capture:mood": "mood",
@@ -141,6 +145,9 @@ const STATIC_SURFACE_MODULE = {
"summary:SLEEP_DURATION": "sleep",
"summary:BLOOD_GLUCOSE": "glucose",
"summary:CARDIO_RECOVERY": "recovery",
// The recovery module owns the resting_hr domain (`measurement-scope.ts`),
// so the server gates the type on it; the form and the dashboard follow.
"summary:RESTING_HEART_RATE": "recovery",
"summary:SIX_MINUTE_WALK_DISTANCE": "recovery",
"summary:STAIR_ASCENT_SPEED": "recovery",
"summary:STAIR_DESCENT_SPEED": "recovery",
@@ -189,6 +196,7 @@ const STATIC_SURFACE_MODULE = {
"correlation:MOOD": "mood",
"correlation:SLEEP_DURATION": "sleep",
"correlation:BLOOD_GLUCOSE": "glucose",
"correlation:RESTING_HEART_RATE": "recovery",
"correlation:MEDICATION_COMPLIANCE": "medications",
// Symptom burden is read from the illness journal.
"correlation:SYMPTOM_SEVERITY": "illness",