mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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:
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user