fix(targets): no adult weight and BMI bands on a child's record

Now that the BMI chart draws inside a managed record, a child's normal
BMI sat in the red under-17 band, and the weight page painted the adult
WHO band (about 26 to 35 kg for 118 cm), so a healthy 21 kg read as
underweight. The release's own weight-band test pinned that band.

No band here knows a child's age yet. For a record under 18, or a
managed record with no date of birth, the targets read names no WHO
weight range, no BMI range and no category; the record's own weight
target still shows, and the values and the chart stay. The BMI chart
and the weight band draw only what the server names. The weight-band
test now uses an adult height and gains the child case.

The targets builder and the vitals builder grow by two lines each for
the flag; their size baselines are raised to match.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:53:31 +01:00
1 parent 9683daf4f2
commit ce01d455be
8 files changed
+117 -19

No files matched your search

+22 -1
View File
@@ -23,6 +23,7 @@ const state = {
viewerHeight: null as number | null,
inSharedRecord: true,
recordHeight: 118 as number | null,
bmiRange: { min: 18.5, max: 24.9 } as { min: number; max: number } | null,
};
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
@@ -41,7 +42,10 @@ vi.mock("@tanstack/react-query", () => ({
data:
enabled === false
? undefined
: { profile: { heightCm: state.recordHeight } },
: {
targets: [{ type: "BMI", range: state.bmiRange }],
profile: { heightCm: state.recordHeight },
},
}),
}));
@@ -61,6 +65,7 @@ describe("/insights/bmi height gate", () => {
state.viewerHeight = null;
state.inSharedRecord = true;
state.recordHeight = 118;
state.bmiRange = { min: 18.5, max: 24.9 };
});
it("reads the record's height inside somebody else's record", () => {
@@ -77,4 +82,20 @@ describe("/insights/bmi height gate", () => {
state.viewerHeight = null;
expect(asksForHeight()).toBe(true);
});
it("draws the adult bands only where the record's BMI range is named", () => {
const chartBands = () => {
renderToStaticMarkup(<InsightsBmiPage />);
const { children } = shellProps.mock.calls.at(-1)![0] as {
children: Array<{ props?: { valueBands?: unknown } }>;
};
return children.find(
(child) => child?.props && "valueBands" in child.props,
)?.props?.valueBands;
};
expect(chartBands()).toBeDefined();
// A child's record: the server names no BMI range.
state.bmiRange = null;
expect(chartBands()).toBeUndefined();
});
});
+11 -5
View File
@@ -56,15 +56,21 @@ export default function InsightsBmiPage() {
const { data: targets } = useQuery({
queryKey: queryKeys.insightsTargets(),
queryFn: () =>
apiGet<{ profile?: { heightCm: number | null } }>(
"/api/insights/targets",
),
enabled: isAuthenticated && inSharedRecord,
apiGet<{
targets: ReadonlyArray<{ type: string; range: unknown }>;
profile?: { heightCm: number | null };
}>("/api/insights/targets"),
enabled: isAuthenticated,
});
const heightCm = inSharedRecord
? (targets?.profile?.heightCm ?? null)
: (user?.heightCm ?? null);
const heightKnown = !inSharedRecord || targets !== undefined;
// The adult bands only where the server names the BMI range: it names none
// for a child's record, which they do not describe.
const bmiBands = targets?.targets.find((entry) => entry.type === "BMI")?.range
? BMI_BANDS
: undefined;
// v1.4.27 — BMI is derived from WEIGHT. When no weight readings
// exist yet, the existing "set your height" branch can never compute
@@ -140,7 +146,7 @@ export default function InsightsBmiPage() {
colors={["var(--chart-5)"]}
unit="kg/m²"
valueMode="bmi"
valueBands={BMI_BANDS}
valueBands={bmiBands}
compareBaseline={compareBaseline}
userTimezone={user?.timezone}
showDataTable
@@ -12,11 +12,22 @@ describe("selectWeightBandRange", () => {
it("builds the WHO band from the record's height", () => {
const range = selectWeightBandRange({
targets: [
{ type: "WEIGHT", range: { min: 25.8, max: 34.7 }, source: "WHO BMI" },
{ type: "WEIGHT", range: { min: 59.9, max: 81 }, source: "WHO BMI" },
],
profile: { heightCm: 118 },
profile: { heightCm: 180 },
});
expect(range).toEqual(buildWeightRangeFromHeight(118));
expect(range).toEqual(buildWeightRangeFromHeight(180));
});
it("paints no adult band behind a child's weights", () => {
// A 118 cm child: the server names no WHO range for a record under 18,
// where the adult band read a normal 21 kg as underweight.
expect(
selectWeightBandRange({
targets: [{ type: "WEIGHT", range: null, source: "WHO BMI" }],
profile: { heightCm: 118 },
}),
).toBeNull();
});
it("paints the record's own target with the override wings", () => {
+6 -4
View File
@@ -24,14 +24,16 @@ export interface WeightTargetsSlice {
* record's own target and height, and the reference panel under the chart
* reads the same payload. The record's own target (`source: "Custom"`) gets
* the wings `getEffectiveRange` gives every override; otherwise the band is
* the height-derived WHO one, and with neither there is none.
* the height-derived WHO one. Where the server names no range there is none:
* no target and no height, or a child's record, which the adult WHO band does
* not describe.
*/
export function selectWeightBandRange(
data: WeightTargetsSlice | null,
): TrafficRange | null {
if (!data) return null;
const weight = data.targets.find((entry) => entry.type === "WEIGHT");
if (weight?.source === "Custom" && weight.range) {
const weight = data?.targets.find((entry) => entry.type === "WEIGHT");
if (!data || !weight?.range) return null;
if (weight.source === "Custom") {
return getEffectiveRange(
"WEIGHT",
{ heightCm: null, dateOfBirth: null, gender: null },