mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(insights): mount per-metric assessment on HealthKit sub-pages
Add an optional statusMetric prop to HealthKitMetricPage that mounts InsightStatusCard beneath the chart, pointed at the generic metric-status route. The card renders only on the data-bearing branch; the insufficient-data branch keeps the existing empty-state note and never fires an assessment fetch. Wire statusMetric on the 29 HealthKit metric sub-pages and on the sleep page, which closes the assessment slot its deferral comment had left open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 083044201af7715740ec0ee2f14c7240c4999fbf)
This commit is contained in:
1 parent
16b0c349f2
commit
b1b2e22eeb
32 files changed
+232
-9
No files matched your search
@@ -17,6 +17,7 @@ export default function InsightsActiveEnergyPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="ACTIVE_ENERGY_BURNED"
|
||||
statusMetric="ACTIVE_ENERGY_BURNED"
|
||||
insightMetric="ACTIVE_ENERGY_BURNED"
|
||||
chartKey="activeEnergy"
|
||||
i18nPrefix="insights.activeEnergy"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsLaermereignissePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="AUDIO_EXPOSURE_EVENT"
|
||||
statusMetric="AUDIO_EXPOSURE_EVENT"
|
||||
insightMetric="AUDIO_EXPOSURE_EVENT"
|
||||
chartKey="audioExposureEvent"
|
||||
i18nPrefix="insights.audioExposureEvent"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsBlutzuckerPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="BLOOD_GLUCOSE"
|
||||
statusMetric="BLOOD_GLUCOSE"
|
||||
insightMetric="BLOOD_GLUCOSE"
|
||||
chartKey="bloodGlucose"
|
||||
i18nPrefix="insights.bloodGlucose"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsBodyTemperaturePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="BODY_TEMPERATURE"
|
||||
statusMetric="BODY_TEMPERATURE"
|
||||
insightMetric="BODY_TEMPERATURE"
|
||||
chartKey="bodyTemperature"
|
||||
i18nPrefix="insights.bodyTemperature"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsKoerperwasserPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="TOTAL_BODY_WATER"
|
||||
statusMetric="TOTAL_BODY_WATER"
|
||||
insightMetric="TOTAL_BODY_WATER"
|
||||
chartKey="totalBodyWater"
|
||||
i18nPrefix="insights.totalBodyWater"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsKnochenmassePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="BONE_MASS"
|
||||
statusMetric="BONE_MASS"
|
||||
insightMetric="BONE_MASS"
|
||||
chartKey="boneMass"
|
||||
i18nPrefix="insights.boneMass"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsTageslichtPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="TIME_IN_DAYLIGHT"
|
||||
statusMetric="TIME_IN_DAYLIGHT"
|
||||
insightMetric="TIME_IN_DAYLIGHT"
|
||||
chartKey="timeInDaylight"
|
||||
i18nPrefix="insights.timeInDaylight"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsDoppelstandphasePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_DOUBLE_SUPPORT"
|
||||
statusMetric="WALKING_DOUBLE_SUPPORT"
|
||||
insightMetric="WALKING_DOUBLE_SUPPORT"
|
||||
chartKey="walkingDoubleSupport"
|
||||
i18nPrefix="insights.walkingDoubleSupport"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsLaermbelastungPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="AUDIO_EXPOSURE_ENV"
|
||||
statusMetric="AUDIO_EXPOSURE_ENV"
|
||||
insightMetric="AUDIO_EXPOSURE_ENV"
|
||||
chartKey="audioExposureEnv"
|
||||
i18nPrefix="insights.audioExposureEnv"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsFettfreieMassePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="FAT_FREE_MASS"
|
||||
statusMetric="FAT_FREE_MASS"
|
||||
insightMetric="FAT_FREE_MASS"
|
||||
chartKey="fatFreeMass"
|
||||
i18nPrefix="insights.fatFreeMass"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsFettmassePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="FAT_MASS"
|
||||
statusMetric="FAT_MASS"
|
||||
insightMetric="FAT_MASS"
|
||||
chartKey="fatMass"
|
||||
i18nPrefix="insights.fatMass"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsStockwerkePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="FLIGHTS_CLIMBED"
|
||||
statusMetric="FLIGHTS_CLIMBED"
|
||||
insightMetric="FLIGHTS_CLIMBED"
|
||||
chartKey="flightsClimbed"
|
||||
i18nPrefix="insights.flightsClimbed"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsKopfhoererpegelPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="AUDIO_EXPOSURE_HEADPHONE"
|
||||
statusMetric="AUDIO_EXPOSURE_HEADPHONE"
|
||||
insightMetric="AUDIO_EXPOSURE_HEADPHONE"
|
||||
chartKey="audioExposureHeadphone"
|
||||
i18nPrefix="insights.audioExposureHeadphone"
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function InsightsHrvPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="HEART_RATE_VARIABILITY"
|
||||
statusMetric="HEART_RATE_VARIABILITY"
|
||||
insightMetric="HEART_RATE_VARIABILITY"
|
||||
chartKey="hrv"
|
||||
i18nPrefix="insights.hrv"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsMagermassePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="LEAN_BODY_MASS"
|
||||
statusMetric="LEAN_BODY_MASS"
|
||||
insightMetric="LEAN_BODY_MASS"
|
||||
chartKey="leanBodyMass"
|
||||
i18nPrefix="insights.leanBodyMass"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsMuskelmassePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="MUSCLE_MASS"
|
||||
statusMetric="MUSCLE_MASS"
|
||||
insightMetric="MUSCLE_MASS"
|
||||
chartKey="muscleMass"
|
||||
i18nPrefix="insights.muscleMass"
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function InsightsOxygenSaturationPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="OXYGEN_SATURATION"
|
||||
statusMetric="OXYGEN_SATURATION"
|
||||
insightMetric="OXYGEN_SATURATION"
|
||||
chartKey="oxygenSaturation"
|
||||
i18nPrefix="insights.oxygenSaturation"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsPulswellengeschwindigkeitPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="PULSE_WAVE_VELOCITY"
|
||||
statusMetric="PULSE_WAVE_VELOCITY"
|
||||
insightMetric="PULSE_WAVE_VELOCITY"
|
||||
chartKey="pulseWaveVelocity"
|
||||
i18nPrefix="insights.pulseWaveVelocity"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsAtemfrequenzPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="RESPIRATORY_RATE"
|
||||
statusMetric="RESPIRATORY_RATE"
|
||||
insightMetric="RESPIRATORY_RATE"
|
||||
chartKey="respiratoryRate"
|
||||
i18nPrefix="insights.respiratoryRate"
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function InsightsRestingHrPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="RESTING_HEART_RATE"
|
||||
statusMetric="RESTING_HEART_RATE"
|
||||
insightMetric="RESTING_HEART_RATE"
|
||||
chartKey="restingHr"
|
||||
i18nPrefix="insights.restingHr"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsHauttemperaturPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="SKIN_TEMPERATURE"
|
||||
statusMetric="SKIN_TEMPERATURE"
|
||||
insightMetric="SKIN_TEMPERATURE"
|
||||
chartKey="skinTemperature"
|
||||
i18nPrefix="insights.skinTemperature"
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import Link from "next/link";
|
||||
import { Moon } from "lucide-react";
|
||||
|
||||
import { useInsightMetricStatus } from "@/hooks/use-insight-status";
|
||||
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
|
||||
import { SleepOverview } from "@/components/insights/sleep-overview";
|
||||
@@ -32,6 +34,16 @@ export default function InsightsSchlafPage() {
|
||||
|
||||
const { isEmpty } = useInsightsAnalytics("SLEEP_DURATION");
|
||||
|
||||
// v1.8.7.1 — the per-section sleep assessment now rides the generic
|
||||
// metric-status route, keyed by `SLEEP_DURATION`. The hook runs on every
|
||||
// render but only fetches once the page has data (the empty branch below
|
||||
// short-circuits before the card mounts), so a source-less account never
|
||||
// fires an assessment round-trip.
|
||||
const { data: status, isLoading: isStatusLoading } = useInsightMetricStatus(
|
||||
"SLEEP_DURATION",
|
||||
!isEmpty,
|
||||
);
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<SubPageShell
|
||||
@@ -68,16 +80,21 @@ export default function InsightsSchlafPage() {
|
||||
<MetricTargetSummary slug="sleep" />
|
||||
|
||||
{/*
|
||||
v1.4.28 (BK-UI-StatusSchlaf) — the six sibling sub-pages all
|
||||
mount `<InsightStatusCard>` underneath the chart so the user
|
||||
sees a written per-section assessment. Sleep has no
|
||||
`/api/insights/sleep-status` route yet (the v1.4.23 schema
|
||||
landed the data but the assessment-generation pass was deferred
|
||||
to the v1.5 iOS sprint where the Apple-Health sleep snapshot
|
||||
will inform the prompt). No per-section assessment yet; the
|
||||
structural slot waits for v1.5 where the route + hook key will
|
||||
wire in.
|
||||
v1.8.7.1 — the per-section sleep assessment lands via the generic
|
||||
metric-status route (`?metric=SLEEP_DURATION`), closing the slot
|
||||
the v1.4.28 deferral left open. The card consumes the same
|
||||
`InsightStatusData` envelope every sibling sub-page reads.
|
||||
*/}
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
icon={<Moon className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
/>
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export default function InsightsSchrittlaengePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_STEP_LENGTH"
|
||||
statusMetric="WALKING_STEP_LENGTH"
|
||||
insightMetric="WALKING_STEP_LENGTH"
|
||||
chartKey="walkingStepLength"
|
||||
i18nPrefix="insights.walkingStepLength"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsGefaessalterPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="VASCULAR_AGE"
|
||||
statusMetric="VASCULAR_AGE"
|
||||
insightMetric="VASCULAR_AGE"
|
||||
chartKey="vascularAge"
|
||||
i18nPrefix="insights.vascularAge"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsViszeralfettPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="VISCERAL_FAT"
|
||||
statusMetric="VISCERAL_FAT"
|
||||
insightMetric="VISCERAL_FAT"
|
||||
chartKey="visceralFat"
|
||||
i18nPrefix="insights.visceralFat"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsGangasymmetriePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_ASYMMETRY"
|
||||
statusMetric="WALKING_ASYMMETRY"
|
||||
insightMetric="WALKING_ASYMMETRY"
|
||||
chartKey="walkingAsymmetry"
|
||||
i18nPrefix="insights.walkingAsymmetry"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsGehstreckePage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_RUNNING_DISTANCE"
|
||||
statusMetric="WALKING_RUNNING_DISTANCE"
|
||||
insightMetric="WALKING_RUNNING_DISTANCE"
|
||||
chartKey="walkingRunningDistance"
|
||||
i18nPrefix="insights.walkingRunningDistance"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsGehpulsPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_HEART_RATE_AVERAGE"
|
||||
statusMetric="WALKING_HEART_RATE_AVERAGE"
|
||||
insightMetric="WALKING_HEART_RATE_AVERAGE"
|
||||
chartKey="walkingHeartRateAverage"
|
||||
i18nPrefix="insights.walkingHeartRateAverage"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsGehgeschwindigkeitPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_SPEED"
|
||||
statusMetric="WALKING_SPEED"
|
||||
insightMetric="WALKING_SPEED"
|
||||
chartKey="walkingSpeed"
|
||||
i18nPrefix="insights.walkingSpeed"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function InsightsGangstabilitaetPage() {
|
||||
return (
|
||||
<HealthKitMetricPage
|
||||
measurementType="WALKING_STEADINESS"
|
||||
statusMetric="WALKING_STEADINESS"
|
||||
insightMetric="WALKING_STEADINESS"
|
||||
chartKey="walkingSteadiness"
|
||||
i18nPrefix="insights.walkingSteadiness"
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { InsightStatusData } from "@/hooks/use-insight-status";
|
||||
|
||||
/**
|
||||
* v1.8.7.1 — `<HealthKitMetricPage statusMetric=…>` mounts the generic
|
||||
* per-metric assessment card beneath the chart when the metric has data,
|
||||
* and falls back to the existing insufficient-data empty state otherwise.
|
||||
*
|
||||
* The page leans on several hooks (`useAuth`, `useInsightsAnalytics`,
|
||||
* `useInsightMetricStatus`, …); the test mocks them so the data /
|
||||
* empty branch is deterministic. The chart is `next/dynamic`-imported,
|
||||
* so SSR renders its stub — enough to verify the assessment slot.
|
||||
*/
|
||||
|
||||
vi.mock("next/dynamic", () => ({
|
||||
default: () => {
|
||||
const Stub = () => <div data-slot="healthkit-chart-stub" />;
|
||||
Stub.displayName = "HealthChartStub";
|
||||
return Stub;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { timezone: "UTC", dateOfBirth: null, gender: null },
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-insights-layout-prefs", () => ({
|
||||
useInsightsLayoutPrefs: () => ({ layout: null, compareBaseline: false }),
|
||||
}));
|
||||
|
||||
const analyticsMock = vi.fn();
|
||||
vi.mock("@/hooks/use-insights-analytics", () => ({
|
||||
useInsightsAnalytics: () => analyticsMock(),
|
||||
}));
|
||||
|
||||
const metricStatusMock = vi.fn();
|
||||
vi.mock("@/hooks/use-insight-status", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/hooks/use-insight-status")>();
|
||||
return {
|
||||
...actual,
|
||||
useInsightMetricStatus: (...args: unknown[]) => metricStatusMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
import { HealthKitMetricPage } from "../healthkit-metric-page";
|
||||
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
|
||||
});
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
measurementType: "HEART_RATE_VARIABILITY",
|
||||
insightMetric: "HEART_RATE_VARIABILITY" as const,
|
||||
chartKey: "hrv" as const,
|
||||
i18nPrefix: "insights.hrv",
|
||||
color: "#bd93f9",
|
||||
unit: "ms",
|
||||
emptyStateIcon: null,
|
||||
};
|
||||
|
||||
const settledStatus: InsightStatusData = {
|
||||
hasProvider: true,
|
||||
text: "Your HRV has trended upward over the last month.",
|
||||
cached: false,
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
analyticsMock.mockReset();
|
||||
metricStatusMock.mockReset();
|
||||
});
|
||||
|
||||
describe("<HealthKitMetricPage> status mount", () => {
|
||||
it("mounts the assessment card when statusMetric is set and the metric has data", () => {
|
||||
analyticsMock.mockReturnValue({
|
||||
data: { summaries: { HEART_RATE_VARIABILITY: { count: 12 } } },
|
||||
isEmpty: false,
|
||||
});
|
||||
metricStatusMock.mockReturnValue({
|
||||
data: settledStatus,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const html = render(
|
||||
<HealthKitMetricPage
|
||||
{...baseProps}
|
||||
statusMetric="HEART_RATE_VARIABILITY"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain("Your HRV has trended upward");
|
||||
// The card fetch is enabled (statusMetric present + data present).
|
||||
expect(metricStatusMock).toHaveBeenCalledWith("HEART_RATE_VARIABILITY", true);
|
||||
});
|
||||
|
||||
it("does not mount the assessment card when statusMetric is omitted", () => {
|
||||
analyticsMock.mockReturnValue({
|
||||
data: { summaries: { HEART_RATE_VARIABILITY: { count: 12 } } },
|
||||
isEmpty: false,
|
||||
});
|
||||
metricStatusMock.mockReturnValue({ data: undefined, isLoading: false });
|
||||
|
||||
const html = render(<HealthKitMetricPage {...baseProps} />);
|
||||
|
||||
// No assessment heading, and the generic fetch is disabled.
|
||||
expect(html).not.toContain("Assessment");
|
||||
expect(metricStatusMock).toHaveBeenCalledWith("", false);
|
||||
});
|
||||
|
||||
it("renders the insufficient-data empty state (no card) when the metric has no data", () => {
|
||||
analyticsMock.mockReturnValue({ data: null, isEmpty: true });
|
||||
metricStatusMock.mockReturnValue({ data: undefined, isLoading: false });
|
||||
|
||||
const html = render(
|
||||
<HealthKitMetricPage
|
||||
{...baseProps}
|
||||
statusMetric="HEART_RATE_VARIABILITY"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Empty branch: no settled assessment text, and the status fetch is
|
||||
// disabled so a source-less account never fires the round-trip.
|
||||
expect(html).not.toContain("Your HRV has trended upward");
|
||||
expect(metricStatusMock).toHaveBeenCalledWith("HEART_RATE_VARIABILITY", false);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,10 @@
|
||||
import Link from "next/link";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightMetricStatus } from "@/hooks/use-insight-status";
|
||||
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -11,6 +14,7 @@ import type { InsightMetric } from "@/lib/insights/metric-availability";
|
||||
import type { ChartOverlayKey } from "@/lib/dashboard-layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
|
||||
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
|
||||
@@ -96,6 +100,17 @@ export interface HealthKitMetricPageProps {
|
||||
* rather than a bespoke module. Omit it for metrics without a target.
|
||||
*/
|
||||
targetSummarySlug?: string;
|
||||
/**
|
||||
* v1.8.7.1 — when set, mounts `<InsightStatusCard>` beneath the chart,
|
||||
* pointed at the generic per-metric assessment route
|
||||
* (`/api/insights/metric-status?metric=<statusMetric>`). The value is
|
||||
* the HealthKit metric identifier the route keys on — almost always the
|
||||
* same string as `measurementType`. The card is only rendered on the
|
||||
* data-bearing branch; the empty (insufficient-data) branch keeps the
|
||||
* existing `<MetricEmptyState>` note and never fires an assessment
|
||||
* fetch. Omit it for metrics that should not carry an assessment.
|
||||
*/
|
||||
statusMetric?: string;
|
||||
}
|
||||
|
||||
export function HealthKitMetricPage({
|
||||
@@ -113,12 +128,22 @@ export function HealthKitMetricPage({
|
||||
valueScale,
|
||||
explainerMetric,
|
||||
targetSummarySlug,
|
||||
statusMetric,
|
||||
}: HealthKitMetricPageProps) {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
const { data: analytics, isEmpty } = useInsightsAnalytics(insightMetric);
|
||||
|
||||
// v1.8.7.1 — generic per-metric assessment. The hook must run on every
|
||||
// render (rules of hooks), so it is always called; `enabled` gates the
|
||||
// actual fetch on a configured `statusMetric` AND the presence of data
|
||||
// so an insufficient-data page never fires an assessment round-trip.
|
||||
const { data: status, isLoading: isStatusLoading } = useInsightMetricStatus(
|
||||
statusMetric ?? "",
|
||||
Boolean(statusMetric) && !isEmpty,
|
||||
);
|
||||
const rawSummary = analytics?.summaries?.[measurementType] ?? null;
|
||||
// The stat strip renders display-unit values. The summary holds stored
|
||||
// values, so when the page renders a scaled unit (e.g. WALKING_SPEED
|
||||
@@ -199,6 +224,18 @@ export function HealthKitMetricPage({
|
||||
{targetSummarySlug ? (
|
||||
<MetricTargetSummary slug={targetSummarySlug} />
|
||||
) : null}
|
||||
{statusMetric ? (
|
||||
<InsightStatusCard
|
||||
title={t("insights.assessmentTitle")}
|
||||
icon={<Sparkles className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
/>
|
||||
) : null}
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user