Link a measurement type to its insights sub-page

Clicking a type on the measurements list now drills into that metric's
insights sub-page when one exists, falling back to the existing
?type= filtered list when it does not. A reverse type-to-slug map backs
the navigation, preferring the dedicated single-metric page so an
ambiguous type resolves correctly (PULSE to pulse, not blood-pressure;
WEIGHT to weight, not bmi). No API change.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 13:03:09 +02:00
1 parent 7861c9b642
commit 8a7a2bc2e8
5 files changed
+148 -7

No files matched your search

@@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
import {
TYPE_TO_SUB_PAGE_SLUG,
subPageSlugForType,
} from "@/lib/insights/sub-page-metric";
/**
* v1.22 — the measurements list lets a user click a metric type to drill
* into its Insights sub-page. The reverse map underneath must resolve an
* ambiguous type to its dedicated single-metric page, never a multi-metric
* cluster page, and must stay silent for types with no card so the caller
* falls back to the filtered list.
*/
describe("type → insights sub-page slug", () => {
it("prefers the dedicated single-metric page over a multi-metric one", () => {
// PULSE lives on both `pulse` (=[PULSE]) and `blood-pressure`
// (=[SYS, DIA, PULSE]) — the single-metric page wins.
expect(subPageSlugForType("PULSE")).toBe("pulse");
// WEIGHT lives on both `weight` (=[WEIGHT]) and the derived `bmi`
// (=[WEIGHT]) — the first single-metric page in slug order wins.
expect(subPageSlugForType("WEIGHT")).toBe("weight");
});
it("falls back to the multi-metric page for its component types", () => {
expect(subPageSlugForType("BLOOD_PRESSURE_SYS")).toBe("blood-pressure");
expect(subPageSlugForType("BLOOD_PRESSURE_DIA")).toBe("blood-pressure");
// Both HRV flavours only appear on the `hrv` page.
expect(subPageSlugForType("HEART_RATE_VARIABILITY")).toBe("hrv");
expect(subPageSlugForType("HRV_RMSSD")).toBe("hrv");
});
it("maps a plain single-metric type to its page", () => {
expect(subPageSlugForType("ACTIVITY_STEPS")).toBe("steps");
expect(subPageSlugForType("MOOD")).toBe("mood");
expect(subPageSlugForType("BLOOD_GLUCOSE")).toBe("blood-glucose");
});
it("returns undefined for a type with no sub-page", () => {
expect(subPageSlugForType("BODY_FAT")).toBeUndefined();
expect(subPageSlugForType("RECOVERY_SCORE")).toBeUndefined();
expect(subPageSlugForType("NOT_A_REAL_TYPE")).toBeUndefined();
});
it("never resolves a type to an empty-metric (event-driven) page", () => {
// `workouts` and `medications` carry no measurement series, so no type
// should ever resolve to them.
for (const slug of Object.values(TYPE_TO_SUB_PAGE_SLUG)) {
expect(slug).not.toBe("workouts");
expect(slug).not.toBe("medications");
}
});
});
+46
View File
@@ -321,6 +321,52 @@ export const SUB_PAGE_MANAGER_GROUP_SLUGS: Record<
]),
) as unknown as Record<ManagerGroup, readonly SubPageSlug[]>;
/**
* v1.22 — reverse lookup: a measurement type → the routed insights sub-page
* slug that focuses it, when one exists.
*
* Built from {@link SUB_PAGE_METRIC}, preferring the slug whose metric list
* is exactly `[type]` so an ambiguous type resolves to its dedicated
* single-metric page rather than a multi-metric cluster: `PULSE` → `pulse`
* (not `blood-pressure`), `WEIGHT` → `weight` (not the derived `bmi`). When
* two single-metric pages list the same type (`weight` + `bmi` both list
* `WEIGHT`) the first in `SUB_PAGE_SLUGS` order wins (`weight`). A type that
* only ever appears on a multi-metric page (the two blood-pressure
* components, the two HRV flavours) falls back to that page. Types with no
* sub-page at all (BODY_FAT, RECOVERY_SCORE, the `*_EVENT` series, most
* device scores) are absent — the caller falls back to the filtered
* measurements list.
*/
export const TYPE_TO_SUB_PAGE_SLUG: Readonly<Record<string, SubPageSlug>> =
(() => {
const map: Record<string, SubPageSlug> = {};
// Pass 1 — exact single-metric pages win (first occurrence in slug order).
for (const slug of SUB_PAGE_SLUGS) {
const metrics = SUB_PAGE_METRIC[slug];
if (metrics.length !== 1) continue;
const type = metrics[0];
if (!(type in map)) map[type] = slug;
}
// Pass 2 — multi-metric pages fill any type a single-metric page misses.
for (const slug of SUB_PAGE_SLUGS) {
const metrics = SUB_PAGE_METRIC[slug];
if (metrics.length === 1) continue;
for (const type of metrics) {
if (!(type in map)) map[type] = slug;
}
}
return map;
})();
/**
* The insights sub-page slug for a measurement type, or `undefined` when no
* routed card focuses it. See {@link TYPE_TO_SUB_PAGE_SLUG} for the
* ambiguity-resolution rule.
*/
export function subPageSlugForType(type: string): SubPageSlug | undefined {
return TYPE_TO_SUB_PAGE_SLUG[type];
}
/**
* Mother-page route. Kept here as a named constant so call sites
* never have to hard-code the string.