refactor(insights): extract useInsightStatus hook for sub-page status queries

Five insights sub-pages (blutdruck / bmi / gewicht / puls / stimmung)
each declared the same 13-line useQuery block plus an identical
StatusData interface. The hook routes through the existing queryKeys
factory so the cache keys stop drifting on the next typo. Medikamente
keeps its bespoke query because the compliance-status payload carries
a different shape (per-medication breakdown). Closes W10 simplifier
S12.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 21:51:26 +02:00
1 parent 7d625343f9
commit 3b5f06d6b3
6 files changed
+100 -100

No files matched your search

+4 -22
View File
@@ -1,10 +1,10 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import { HeartPulse } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useInsightStatus } from "@/hooks/use-insight-status";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
@@ -31,31 +31,13 @@ const HealthChart = dynamic(
{ ssr: false },
);
interface BloodPressureStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
export default function InsightsBlutdruckPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
const { data: status, isLoading: isStatusLoading } = useQuery({
queryKey: ["insights", "blood-pressure-status", locale],
queryFn: async () => {
const res = await fetch(
`/api/insights/blood-pressure-status?locale=${locale}`,
);
if (!res.ok) throw new Error("Failed");
const json = await res.json();
return json.data as BloodPressureStatusData;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
const { data: status, isLoading: isStatusLoading } =
useInsightStatus("blood-pressure");
const bpTargets =
user?.dateOfBirth != null ? getBpTargets(new Date(user.dateOfBirth)) : null;
+3 -20
View File
@@ -1,11 +1,11 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import Link from "next/link";
import { Ruler } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useInsightStatus } from "@/hooks/use-insight-status";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
@@ -30,13 +30,6 @@ const HealthChart = dynamic(
{ ssr: false },
);
interface BmiStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
const BMI_BANDS = [
{ min: 0, max: 17, color: "#ff5555", opacity: 0.16 },
{ min: 17, max: 18.5, color: "#ffb86c", opacity: 0.18 },
@@ -47,20 +40,10 @@ const BMI_BANDS = [
export default function InsightsBmiPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
const { data: status, isLoading: isStatusLoading } = useQuery({
queryKey: ["insights", "bmi-status", locale],
queryFn: async () => {
const res = await fetch(`/api/insights/bmi-status?locale=${locale}`);
if (!res.ok) throw new Error("Failed");
const json = await res.json();
return json.data as BmiStatusData;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
const { data: status, isLoading: isStatusLoading } = useInsightStatus("bmi");
if (!user?.heightCm) {
return (
+4 -20
View File
@@ -1,10 +1,10 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import { Scale } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useInsightStatus } from "@/hooks/use-insight-status";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
@@ -28,29 +28,13 @@ const HealthChart = dynamic(
{ ssr: false },
);
interface WeightStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
export default function InsightsGewichtPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
const { data: status, isLoading: isStatusLoading } = useQuery({
queryKey: ["insights", "weight-status", locale],
queryFn: async () => {
const res = await fetch(`/api/insights/weight-status?locale=${locale}`);
if (!res.ok) throw new Error("Failed");
const json = await res.json();
return json.data as WeightStatusData;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
const { data: status, isLoading: isStatusLoading } =
useInsightStatus("weight");
const weightBands = user?.heightCm
? buildWeightBandsFromHeight(user.heightCm, {
+4 -19
View File
@@ -5,6 +5,7 @@ import dynamic from "next/dynamic";
import { Heart } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useInsightStatus } from "@/hooks/use-insight-status";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
@@ -34,33 +35,17 @@ const HealthChart = dynamic(
{ ssr: false },
);
interface PulseStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
interface AnalyticsData {
summaries: Record<string, DataSummary>;
}
export default function InsightsPulsPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
const { data: status, isLoading: isStatusLoading } = useQuery({
queryKey: ["insights", "pulse-status", locale],
queryFn: async () => {
const res = await fetch(`/api/insights/pulse-status?locale=${locale}`);
if (!res.ok) throw new Error("Failed");
const json = await res.json();
return json.data as PulseStatusData;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
const { data: status, isLoading: isStatusLoading } =
useInsightStatus("pulse");
// v1.4.25 W16a — VO2 max chart-row consumes the same `/api/analytics`
// bundle the mother page reads. Sharing the cache key keeps the
+3 -19
View File
@@ -6,6 +6,7 @@ import Link from "next/link";
import { Smile } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useInsightStatus } from "@/hooks/use-insight-status";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
@@ -31,33 +32,16 @@ const MoodChart = dynamic(
{ ssr: false },
);
interface MoodStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
interface ComprehensiveMoodData {
moodSummary: { count: number } | null;
}
export default function InsightsStimmungPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
const { data: status, isLoading: isStatusLoading } = useQuery({
queryKey: ["insights", "mood-status", locale],
queryFn: async () => {
const res = await fetch(`/api/insights/mood-status?locale=${locale}`);
if (!res.ok) throw new Error("Failed");
const json = await res.json();
return json.data as MoodStatusData;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
const { data: status, isLoading: isStatusLoading } = useInsightStatus("mood");
// Reuse the mother-page comprehensive query — TanStack Query
// dedups so this is a free cache read for the common case.
+82
View File
@@ -0,0 +1,82 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
/**
* Shape returned by every `/api/insights/<metric>-status` endpoint.
*
* - `hasProvider` — whether the user has configured an AI provider; the
* sub-page hides the status card entirely when false.
* - `text` — the rendered status string; null when the provider has
* not yet produced one.
* - `cached` — whether the payload came from the per-locale cache or a
* fresh provider round-trip.
* - `updatedAt` — ISO timestamp of the cached payload's creation, or
* null when the route has never produced a value.
*/
export interface InsightStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
/**
* Metric slugs the sub-pages render. Each slug is paired with the
* Insights-status endpoint and the matching `queryKeys.*` factory so
* the cache keys stay aligned across the whole app — a hard-coded
* `["insights", "weight-status", locale]` array typoed once is exactly
* the class of bug `queryKeys` was introduced to defend against.
*/
type InsightStatusMetric =
| "blood-pressure"
| "weight"
| "pulse"
| "bmi"
| "mood"
| "medication-compliance";
const QUERY_KEY_FACTORY: Record<
InsightStatusMetric,
(locale: string) => readonly unknown[]
> = {
"blood-pressure": queryKeys.insightsBpStatus,
weight: queryKeys.insightsWeightStatus,
pulse: queryKeys.insightsPulseStatus,
bmi: queryKeys.insightsBmiStatus,
mood: queryKeys.insightsMoodStatus,
"medication-compliance": queryKeys.insightsMedicationComplianceStatus,
};
/**
* Shared loader for the insight-status payload backing
* `/insights/<metric>` sub-pages. Five sub-pages previously copy-pasted
* the same 13-line useQuery block plus an identical `XxxStatusData`
* interface; the hook collapses them onto one helper plus the existing
* `queryKeys` factory so the next "add a metric" PR touches one map
* entry instead of an entire page module.
*
* Returns the `useQuery` result narrowed to the `InsightStatusData`
* payload — callers stay free to use `data` / `isLoading` / refetch
* helpers without an extra unwrap.
*/
export function useInsightStatus(metric: InsightStatusMetric) {
const { isAuthenticated } = useAuth();
const { locale } = useTranslations();
return useQuery({
queryKey: QUERY_KEY_FACTORY[metric](locale),
queryFn: async (): Promise<InsightStatusData> => {
const res = await fetch(`/api/insights/${metric}-status?locale=${locale}`);
if (!res.ok) throw new Error("Failed");
const json = (await res.json()) as { data: InsightStatusData };
return json.data;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
}