From 46b1cd6f3444eebbde3d23eb0996630c460916ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 20:00:38 +0100 Subject: [PATCH] refactor(insights): classify the derived BMI through classifyBMI The derived BMI card kept its own copy of the WHO cut-offs beside `classifyBMI`, which the targets panel, the status text and the overview use. It now maps that one classifier onto its four-word vocabulary, so a later change to the cut-offs has one place to land. --- .../insights/derived/__tests__/bmi.test.ts | 6 ++++ src/lib/insights/derived/bmi.ts | 31 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/lib/insights/derived/__tests__/bmi.test.ts b/src/lib/insights/derived/__tests__/bmi.test.ts index bc329d599..3e304f452 100644 --- a/src/lib/insights/derived/__tests__/bmi.test.ts +++ b/src/lib/insights/derived/__tests__/bmi.test.ts @@ -27,6 +27,12 @@ describe("classifyBmi (WHO)", () => { it("obese ≥ 30 (red)", () => { expect(classifyBmi(31)).toEqual({ category: "obese", band: "red" }); }); + it("puts each WHO cut-off in the upper category, every obesity grade obese", () => { + expect(classifyBmi(18.5).category).toBe("normal"); + expect(classifyBmi(25).category).toBe("overweight"); + expect(classifyBmi(30).category).toBe("obese"); + expect(classifyBmi(45)).toEqual({ category: "obese", band: "red" }); + }); }); describe("computeBmi", () => { diff --git a/src/lib/insights/derived/bmi.ts b/src/lib/insights/derived/bmi.ts index 0766f01e8..51ce69881 100644 --- a/src/lib/insights/derived/bmi.ts +++ b/src/lib/insights/derived/bmi.ts @@ -25,6 +25,10 @@ */ import type { MeasurementType } from "@/generated/prisma/client"; import { prisma } from "@/lib/db"; +import { + classifyBMI, + type BmiClassification, +} from "@/lib/analytics/classifications"; import { buildInsufficient, buildOk, @@ -63,15 +67,32 @@ export interface BmiValue { series: number[]; } -/** WHO category + band for a BMI value. Pure. */ +const CATEGORY_OF: Readonly> = { + Underweight: "underweight", + Normal: "normal", + Overweight: "overweight", +}; + +const BAND_OF: Readonly> = { + normal: "green", + warning: "yellow", + danger: "red", +}; + +/** + * WHO category + band for a BMI value, in this payload's vocabulary. The + * cut-offs are `classifyBMI`'s, so the derived card and the targets panel + * cannot disagree about where a reading falls. Pure. + */ export function classifyBmi(bmi: number): { category: BmiCategory; band: BmiBand; } { - if (bmi < 18.5) return { category: "underweight", band: "yellow" }; - if (bmi < 25) return { category: "normal", band: "green" }; - if (bmi < 30) return { category: "overweight", band: "yellow" }; - return { category: "obese", band: "red" }; + const { category, severity } = classifyBMI(bmi); + return { + category: CATEGORY_OF[category] ?? "obese", + band: BAND_OF[severity], + }; } /**