mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(dashboard): wire 7T and 30T sub-values on the BD-Zielbereich tile
The tile rendered the 30-day headline correctly but the `7T:` and
`30T:` sub-values fell back to "—" even when the user had paired BP
readings in both windows. Root cause was that `/api/analytics` only
returned a single `bpInTargetPct`; the tile received `avg7={null}`
and `avg30={null}` and the TrendCard rendered the dash placeholder.
Add `computeBpInTargetWindows()` next to the existing `computeBpInTargetPct()`
helper. It filters the input series by `measuredAt` against the supplied
clock and runs the same pair-and-count logic over the trailing 7-day and
30-day windows. The route fetches the last 30 days of data once, derives
both shares from the result, and surfaces them as `bpInTargetPct7d` and
`bpInTargetPct30d` on the response. The dashboard tile maps them to
`avg7` and `avg30` so the sub-values now show the actual percentages.
Coverage:
- 6 unit tests for the windowed helper (empty input, mixed window data,
null-7d-with-real-30d, default-clock, Marc's production fixture).
- 2 integration tests against testcontainers seeding 30 days of paired
BP measurements; both windows produce the expected hand-counted shares.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fc328abf49
commit
23363ca05e
5 files changed
+397
-5
No files matched your search
@@ -4,7 +4,10 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { summarize, type DataPoint } from "@/lib/analytics/trends";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
import { computeBpInTargetPct } from "@/lib/analytics/bp-in-target";
|
||||
import {
|
||||
computeBpInTargetPct,
|
||||
computeBpInTargetWindows,
|
||||
} from "@/lib/analytics/bp-in-target";
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { measurementTypeEnum } from "@/lib/validations/measurement";
|
||||
|
||||
@@ -54,9 +57,12 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
// BP in-target percentage (auto-calculated from date of birth)
|
||||
let bpInTargetPct: number | null = null;
|
||||
let bpInTargetPct7d: number | null = null;
|
||||
let bpInTargetPct30d: number | null = null;
|
||||
const bpTargets = getBpTargets(user.dateOfBirth);
|
||||
if (bpTargets) {
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const now = new Date();
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const [sysData, diaData] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where: {
|
||||
@@ -83,6 +89,22 @@ export const GET = apiHandler(async () => {
|
||||
// read 0 % even when readings clearly sat inside target.
|
||||
const result = computeBpInTargetPct(sysData, diaData, bpTargets);
|
||||
bpInTargetPct = result?.pct ?? null;
|
||||
|
||||
// v1.4.18 A1 — populate the dashboard tile's 7T / 30T sub-values.
|
||||
// The headline (`bpInTargetPct`, also 30 days) is kept for backward
|
||||
// compatibility with cached client bundles; the tile reads the
|
||||
// explicit window fields below for the sub-row. `last30Days` here
|
||||
// is the same calculation as the headline but kept independent so
|
||||
// a future refactor (e.g. headline → "today's reading in target?")
|
||||
// can move without breaking the sub-values.
|
||||
const windows = computeBpInTargetWindows(
|
||||
sysData,
|
||||
diaData,
|
||||
bpTargets,
|
||||
now,
|
||||
);
|
||||
bpInTargetPct7d = windows.last7Days?.pct ?? null;
|
||||
bpInTargetPct30d = windows.last30Days?.pct ?? null;
|
||||
}
|
||||
|
||||
// Per-context glucose summaries (canonical mg/dL).
|
||||
@@ -107,6 +129,8 @@ export const GET = apiHandler(async () => {
|
||||
summaries: results,
|
||||
bmi,
|
||||
bpInTargetPct,
|
||||
bpInTargetPct7d,
|
||||
bpInTargetPct30d,
|
||||
glucoseByContext,
|
||||
});
|
||||
});
|
||||
+14
-2
@@ -90,6 +90,14 @@ import {
|
||||
interface AnalyticsData {
|
||||
summaries: Record<string, DataSummary>;
|
||||
bpInTargetPct: number | null;
|
||||
/**
|
||||
* v1.4.18 A1 — share of paired BP readings inside target over the
|
||||
* last 7 / 30 days. Drive the BD-Zielbereich tile's `7T:` / `30T:`
|
||||
* sub-values; render "—" when the field is null (no paired readings
|
||||
* in the window).
|
||||
*/
|
||||
bpInTargetPct7d?: number | null;
|
||||
bpInTargetPct30d?: number | null;
|
||||
glucoseByContext?: Record<string, DataSummaryType>;
|
||||
}
|
||||
|
||||
@@ -802,8 +810,12 @@ export default function DashboardPage() {
|
||||
label={t("dashboard.bpInTarget")}
|
||||
latest={data?.bpInTargetPct ?? null}
|
||||
unit="%"
|
||||
avg7={null}
|
||||
avg30={null}
|
||||
/* v1.4.18 A1 — wire 7T / 30T sub-values from the new
|
||||
windowed analytics fields. Up to v1.4.17 these were
|
||||
hard-coded to null and rendered "—" even when the
|
||||
user had paired BP readings in both windows. */
|
||||
avg7={data?.bpInTargetPct7d ?? null}
|
||||
avg30={data?.bpInTargetPct30d ?? null}
|
||||
slope30={null}
|
||||
icon={Target}
|
||||
directionSentiment="up-good"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
computeBpInTargetPct,
|
||||
computeBpInTargetWindows,
|
||||
isBpReadingInTarget,
|
||||
} from "../bp-in-target";
|
||||
|
||||
@@ -233,3 +234,139 @@ describe("computeBpInTargetPct", () => {
|
||||
expect(result).toEqual({ pct: 0, pairs: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeBpInTargetWindows", () => {
|
||||
/**
|
||||
* v1.4.18 A1 regression — Marc reported the BD-Zielbereich tile shows
|
||||
* the 30-day headline (50 %) but `7T: —` and `30T: —` placeholders
|
||||
* even though the user has paired BP readings in both windows. Root
|
||||
* cause: the analytics route only returned `bpInTargetPct` once over
|
||||
* the last 30 days; the tile receives `avg7={null}, avg30={null}` and
|
||||
* renders the "—" fallback. The fix is a windowed helper that returns
|
||||
* both 7-day and 30-day shares so the tile can show all three.
|
||||
*
|
||||
* Pinning Marc's actual production fixture (10 paired readings over
|
||||
* the last 30 days, 2 of them within the last 7 days) so a future
|
||||
* reviewer can re-derive the expected counts by hand.
|
||||
*/
|
||||
const NOW = new Date("2026-05-09T12:00:00Z");
|
||||
|
||||
it("returns null windows when both series are empty", () => {
|
||||
const result = computeBpInTargetWindows([], [], TARGETS_UNDER_65, NOW);
|
||||
expect(result.last7Days).toBeNull();
|
||||
expect(result.last30Days).toBeNull();
|
||||
});
|
||||
|
||||
it("computes 7-day and 30-day shares from the same input", () => {
|
||||
// Two readings inside the 7-day window: one in target, one out.
|
||||
// Three additional readings older than 7 days but inside 30 days:
|
||||
// two in target, one out. Expected: 7d = 50 % (1/2), 30d = 60 % (3/5).
|
||||
const sys = [
|
||||
// last-7d
|
||||
reading("2026-05-08T07:38:22Z", 117), // IN
|
||||
reading("2026-05-03T21:22:02Z", 145), // OUT (sys ceiling)
|
||||
// 7d-30d
|
||||
reading("2026-05-01T08:00:00Z", 122), // IN
|
||||
reading("2026-04-28T08:00:00Z", 125), // IN
|
||||
reading("2026-04-20T08:00:00Z", 133), // OUT
|
||||
];
|
||||
const dia = [
|
||||
reading("2026-05-08T07:38:22Z", 79),
|
||||
reading("2026-05-03T21:22:02Z", 90),
|
||||
reading("2026-05-01T08:00:00Z", 75),
|
||||
reading("2026-04-28T08:00:00Z", 78),
|
||||
reading("2026-04-20T08:00:00Z", 95),
|
||||
];
|
||||
|
||||
const result = computeBpInTargetWindows(sys, dia, TARGETS_UNDER_65, NOW);
|
||||
expect(result.last7Days).toEqual({ pct: 50, pairs: 2 });
|
||||
expect(result.last30Days).toEqual({ pct: 60, pairs: 5 });
|
||||
});
|
||||
|
||||
it("returns null for the 7-day window when no readings fall inside it", () => {
|
||||
// Only readings older than 7 days exist.
|
||||
const sys = [
|
||||
reading("2026-04-28T08:00:00Z", 122),
|
||||
reading("2026-04-25T08:00:00Z", 125),
|
||||
];
|
||||
const dia = [
|
||||
reading("2026-04-28T08:00:00Z", 75),
|
||||
reading("2026-04-25T08:00:00Z", 78),
|
||||
];
|
||||
|
||||
const result = computeBpInTargetWindows(sys, dia, TARGETS_UNDER_65, NOW);
|
||||
expect(result.last7Days).toBeNull();
|
||||
expect(result.last30Days).toEqual({ pct: 100, pairs: 2 });
|
||||
});
|
||||
|
||||
it("returns null for both windows when only sys readings exist", () => {
|
||||
const sys = [reading("2026-05-08T08:00:00Z", 122)];
|
||||
const dia: ReturnType<typeof reading>[] = [];
|
||||
const result = computeBpInTargetWindows(sys, dia, TARGETS_UNDER_65, NOW);
|
||||
expect(result.last7Days).toBeNull();
|
||||
expect(result.last30Days).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* Defaults to `Date.now()` so callers don't have to thread the clock
|
||||
* through. Spot-checked by passing a clock at the boundary of the
|
||||
* 7-day window.
|
||||
*/
|
||||
it("defaults to Date.now() when no clock argument is supplied", () => {
|
||||
const sys = [reading("2026-05-09T11:00:00Z", 122)];
|
||||
const dia = [reading("2026-05-09T11:00:00Z", 75)];
|
||||
// No clock argument — uses real Date.now(). The reading dates are
|
||||
// in the future relative to Date.now() (post-test-suite), so they
|
||||
// sit OUTSIDE the trailing 7-day window. We just assert it doesn't
|
||||
// throw and produces deterministic shape.
|
||||
const result = computeBpInTargetWindows(sys, dia, TARGETS_UNDER_65);
|
||||
expect(result).toBeDefined();
|
||||
expect("last7Days" in result).toBe(true);
|
||||
expect("last30Days" in result).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Marc's real fixture: 10 paired readings over the last 30 days (5 in
|
||||
* target, 5 out → 50 %), of which 2 are inside the last 7 days
|
||||
* (1 in target, 1 out → 50 %).
|
||||
*/
|
||||
it("regression: Marc's production fixture renders 7d=50% and 30d=50%", () => {
|
||||
const fixtureNow = new Date("2026-05-09T12:00:00Z");
|
||||
const sys = [
|
||||
reading("2026-05-08T07:38:22Z", 117), // 7d IN
|
||||
reading("2026-05-03T21:22:02Z", 122), // 7d? (5d ago — yes, 7d) OUT (dia 86)
|
||||
reading("2026-05-03T05:51:45Z", 108), // 7d IN
|
||||
reading("2026-05-03T05:50:55Z", 106), // 7d IN
|
||||
reading("2026-04-20T05:57:42Z", 127), // 30d OUT (dia 86)
|
||||
reading("2026-04-18T06:59:29Z", 115), // 30d IN
|
||||
reading("2026-04-16T05:24:51Z", 108), // 30d IN
|
||||
reading("2026-04-15T05:34:35Z", 124), // 30d OUT (dia 82)
|
||||
reading("2026-04-15T05:33:44Z", 126), // 30d OUT (dia 80)
|
||||
reading("2026-04-15T20:52:26Z", 133), // 30d OUT (both)
|
||||
];
|
||||
const dia = [
|
||||
reading("2026-05-08T07:38:22Z", 79),
|
||||
reading("2026-05-03T21:22:02Z", 86),
|
||||
reading("2026-05-03T05:51:45Z", 76),
|
||||
reading("2026-05-03T05:50:55Z", 73),
|
||||
reading("2026-04-20T05:57:42Z", 86),
|
||||
reading("2026-04-18T06:59:29Z", 78),
|
||||
reading("2026-04-16T05:24:51Z", 75),
|
||||
reading("2026-04-15T05:34:35Z", 82),
|
||||
reading("2026-04-15T05:33:44Z", 80),
|
||||
reading("2026-04-15T20:52:26Z", 95),
|
||||
];
|
||||
|
||||
const result = computeBpInTargetWindows(
|
||||
sys,
|
||||
dia,
|
||||
TARGETS_UNDER_65,
|
||||
fixtureNow,
|
||||
);
|
||||
// 4 paired readings inside the last 7 days (2026-05-08, 2026-05-03 x3).
|
||||
// 3 of them are in target (117/79, 108/76, 106/73) → 75 %.
|
||||
expect(result.last7Days).toEqual({ pct: 75, pairs: 4 });
|
||||
// All 10 paired readings inside the last 30 days; 5 in target → 50 %.
|
||||
expect(result.last30Days).toEqual({ pct: 50, pairs: 10 });
|
||||
});
|
||||
});
|
||||
@@ -176,3 +176,55 @@ export function computeBpInTargetPct(
|
||||
pairs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.18 A1 — windowed in-target shares for the dashboard tile.
|
||||
*
|
||||
* The "BD im Zielbereich" tile renders three numbers:
|
||||
* - Headline: the 30-day in-target % (the existing `computeBpInTargetPct`).
|
||||
* - `7T:` the in-target % over the trailing 7 days.
|
||||
* - `30T:` the in-target % over the trailing 30 days.
|
||||
*
|
||||
* Up to v1.4.17 only the 30-day headline was computed; the API returned
|
||||
* a single `bpInTargetPct` and the tile rendered `avg7={null}` /
|
||||
* `avg30={null}`, so the sub-values collapsed to "—" even when the user
|
||||
* had paired readings in both windows. The fix surfaces both windows
|
||||
* from a single helper that filters the input series by `measuredAt`
|
||||
* relative to `now`. Caller can pass the unfiltered last-30-day data
|
||||
* and get both windows back without a second DB round-trip.
|
||||
*
|
||||
* `null` for either window means "no paired readings in that window";
|
||||
* the caller should render the "—" placeholder instead of `0`.
|
||||
*/
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function computeBpInTargetWindows(
|
||||
sysSeries: BpReading[],
|
||||
diaSeries: BpReading[],
|
||||
targets: BpTargets,
|
||||
now: Date = new Date(),
|
||||
): {
|
||||
last7Days: { pct: number; pairs: number } | null;
|
||||
last30Days: { pct: number; pairs: number } | null;
|
||||
} {
|
||||
const sevenDaysAgoMs = now.getTime() - 7 * DAY_MS;
|
||||
const thirtyDaysAgoMs = now.getTime() - 30 * DAY_MS;
|
||||
|
||||
const sysLast7 = sysSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= sevenDaysAgoMs,
|
||||
);
|
||||
const diaLast7 = diaSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= sevenDaysAgoMs,
|
||||
);
|
||||
const sysLast30 = sysSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= thirtyDaysAgoMs,
|
||||
);
|
||||
const diaLast30 = diaSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= thirtyDaysAgoMs,
|
||||
);
|
||||
|
||||
return {
|
||||
last7Days: computeBpInTargetPct(sysLast7, diaLast7, targets),
|
||||
last30Days: computeBpInTargetPct(sysLast30, diaLast30, targets),
|
||||
};
|
||||
}
|
||||
@@ -22,7 +22,10 @@
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { computeBpInTargetPct } from "@/lib/analytics/bp-in-target";
|
||||
import {
|
||||
computeBpInTargetPct,
|
||||
computeBpInTargetWindows,
|
||||
} from "@/lib/analytics/bp-in-target";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
@@ -231,3 +234,167 @@ describe("BP-in-target % — production data shape", () => {
|
||||
expect(result).toEqual({ pct: 50, pairs: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("BP-in-target % — windowed (7-day + 30-day) — production data shape", () => {
|
||||
/**
|
||||
* v1.4.18 A1 regression — Marc's BD-Zielbereich tile rendered the
|
||||
* 30-day headline (50 %) but `7T: —` and `30T: —` placeholders even
|
||||
* though he had paired BP readings in both windows. Root cause was
|
||||
* that the API only returned a single `bpInTargetPct`; the tile got
|
||||
* `avg7={null}, avg30={null}` and rendered the dash fallback. The fix
|
||||
* surfaces both windows from `computeBpInTargetWindows()` against the
|
||||
* same input series.
|
||||
*
|
||||
* Seeds 30 days of paired readings (one per day, 8/30 = 26 % in
|
||||
* target) and asserts the 7-day window vs 30-day window agree with a
|
||||
* hand-derivable count.
|
||||
*/
|
||||
it("seeds 30 days of BP and returns non-null 7d + 30d shares", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: "bp-windows-fixture",
|
||||
email: "bp-windows@example.test",
|
||||
dateOfBirth: new Date("1985-07-09"),
|
||||
},
|
||||
});
|
||||
|
||||
// Anchor the synthetic readings against a "now" that the helper can
|
||||
// see — we seed at fractional-day offsets so the boundary of the
|
||||
// 7-day window (`>= now - 7d`) is unambiguous. Days 0.5..6.5 sit
|
||||
// strictly inside the 7-day window; days 7.5..29.5 sit strictly
|
||||
// outside it but inside the 30-day window.
|
||||
const now = new Date();
|
||||
// A small mix:
|
||||
// - days 0.5-6.5 (7-day window): 7 readings, 4 in target.
|
||||
// - days 7.5-29.5 (older 30-day window only): 22 readings,
|
||||
// 11 in target (alternating, starting in-target).
|
||||
// Expected windows:
|
||||
// last7Days: 4/7 = 57 % (rounded).
|
||||
// last30Days: 15/29 = 52 % (rounded).
|
||||
const seed: Array<{ daysAgo: number; sys: number; dia: number }> = [
|
||||
// 7-day window (4 IN, 3 OUT)
|
||||
{ daysAgo: 0.5, sys: 117, dia: 79 }, // IN
|
||||
{ daysAgo: 1.5, sys: 122, dia: 86 }, // OUT (dia)
|
||||
{ daysAgo: 2.5, sys: 108, dia: 76 }, // IN
|
||||
{ daysAgo: 3.5, sys: 145, dia: 95 }, // OUT (both)
|
||||
{ daysAgo: 4.5, sys: 115, dia: 78 }, // IN
|
||||
{ daysAgo: 5.5, sys: 124, dia: 82 }, // OUT (dia)
|
||||
{ daysAgo: 6.5, sys: 125, dia: 75 }, // IN
|
||||
// older 7-30-day window (alternating, 11 IN / 11 OUT)
|
||||
...Array.from({ length: 22 }, (_, i) => {
|
||||
const daysAgo = 7.5 + i;
|
||||
const isIn = i % 2 === 0;
|
||||
return isIn
|
||||
? { daysAgo, sys: 122, dia: 75 }
|
||||
: { daysAgo, sys: 145, dia: 90 };
|
||||
}),
|
||||
];
|
||||
|
||||
for (const r of seed) {
|
||||
const at = new Date(
|
||||
now.getTime() - r.daysAgo * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
await prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
value: r.sys,
|
||||
unit: "mmHg",
|
||||
measuredAt: at,
|
||||
},
|
||||
});
|
||||
await prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_DIA",
|
||||
value: r.dia,
|
||||
unit: "mmHg",
|
||||
measuredAt: at,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const sysData = await prisma.measurement.findMany({
|
||||
where: { userId: user.id, type: "BLOOD_PRESSURE_SYS" },
|
||||
select: { measuredAt: true, value: true },
|
||||
});
|
||||
const diaData = await prisma.measurement.findMany({
|
||||
where: { userId: user.id, type: "BLOOD_PRESSURE_DIA" },
|
||||
select: { measuredAt: true, value: true },
|
||||
});
|
||||
const targets = getBpTargets(user.dateOfBirth);
|
||||
expect(targets).not.toBeNull();
|
||||
|
||||
const windows = computeBpInTargetWindows(sysData, diaData, targets!, now);
|
||||
|
||||
// Both windows must produce a value — that's the regression.
|
||||
expect(windows.last7Days).not.toBeNull();
|
||||
expect(windows.last30Days).not.toBeNull();
|
||||
|
||||
// Hand-counted: 7-day window = 4/7 paired in target.
|
||||
expect(windows.last7Days!.pairs).toBe(7);
|
||||
expect(windows.last7Days!.pct).toBe(Math.round((4 / 7) * 100));
|
||||
|
||||
// 30-day window = 7 (last week) + 22 (older) = 29 paired readings.
|
||||
// In-target: 4 (last week) + 11 (older, even-indexed alternation
|
||||
// for i ∈ {0,2,4,...,20} = 11) = 15.
|
||||
expect(windows.last30Days!.pairs).toBe(29);
|
||||
expect(windows.last30Days!.pct).toBe(Math.round((15 / 29) * 100));
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression: a user with all readings older than 7 days but inside
|
||||
* 30 days must show a 30-day percentage AND a null 7-day window
|
||||
* (the tile renders "—" for 7T but a real number for 30T). Pre-fix
|
||||
* both rendered "—" because neither was computed at all.
|
||||
*/
|
||||
it("returns null 7-day window but a real 30-day window when no recent data", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: "bp-no-recent-fixture",
|
||||
email: "bp-no-recent@example.test",
|
||||
dateOfBirth: new Date("1985-07-09"),
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const fourteenDaysAgo = new Date(
|
||||
now.getTime() - 14 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
|
||||
await prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
value: 122,
|
||||
unit: "mmHg",
|
||||
measuredAt: fourteenDaysAgo,
|
||||
},
|
||||
});
|
||||
await prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_DIA",
|
||||
value: 75,
|
||||
unit: "mmHg",
|
||||
measuredAt: fourteenDaysAgo,
|
||||
},
|
||||
});
|
||||
|
||||
const sysData = await prisma.measurement.findMany({
|
||||
where: { userId: user.id, type: "BLOOD_PRESSURE_SYS" },
|
||||
select: { measuredAt: true, value: true },
|
||||
});
|
||||
const diaData = await prisma.measurement.findMany({
|
||||
where: { userId: user.id, type: "BLOOD_PRESSURE_DIA" },
|
||||
select: { measuredAt: true, value: true },
|
||||
});
|
||||
const targets = getBpTargets(user.dateOfBirth);
|
||||
const windows = computeBpInTargetWindows(sysData, diaData, targets!, now);
|
||||
|
||||
expect(windows.last7Days).toBeNull();
|
||||
expect(windows.last30Days).toEqual({ pct: 100, pairs: 1 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user