mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Merge v189/ill into release/v1.18.9
Defer illness recovery-gap correlation to an opt-in detail expansion; gate the typical-gap insight behind signal-density thresholds; align the illness settings page to the canonical SettingsCard pattern.
This commit is contained in:
commit
b27d8509cd
16 files changed
+363
-101
No files matched your search
@@ -2,12 +2,18 @@
|
||||
|
||||
/**
|
||||
* v1.18.1 P3 — the cross-episode retrospective summary card ("Your illness
|
||||
* history"): how many episodes in the window + the typical recovery gap, with
|
||||
* a calm "still learning" line until enough resolved episodes back the figure.
|
||||
* Retrospective only — a count of the past, never a forecast.
|
||||
* history"): how many episodes in the window, and — on demand — the typical
|
||||
* recovery gap. Retrospective only — a count of the past, never a forecast.
|
||||
*
|
||||
* v1.18.9 — the card paints instantly on a single count query
|
||||
* (`includeRecoveryGap` off). The recovery gap is the only expensive figure
|
||||
* (a bounded per-episode correlation fan-out), so it is computed LAZILY: it
|
||||
* runs only when the user opens the "Analyse" expansion, never on list load.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { History } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -20,7 +26,18 @@ export function IllnessInsightsCard({
|
||||
windowDays?: number;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const { data, isLoading, isError } = useIllnessInsights(windowDays);
|
||||
// Lightweight count read — fires on mount, no recovery-gap fan-out.
|
||||
const counts = useIllnessInsights(windowDays);
|
||||
|
||||
// The heavy recovery-gap read stays disabled until the user opens it.
|
||||
const [analyzeOpen, setAnalyzeOpen] = useState(false);
|
||||
const gap = useIllnessInsights(windowDays, {
|
||||
includeRecoveryGap: true,
|
||||
enabled: analyzeOpen,
|
||||
});
|
||||
|
||||
const data = counts.data;
|
||||
const hasEpisodes = !counts.isError && !!data && data.episodeCount > 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -33,35 +50,52 @@ export function IllnessInsightsCard({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
{counts.isLoading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
) : isError || !data ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("illness.insights.none")}
|
||||
</p>
|
||||
) : data.episodeCount === 0 ? (
|
||||
) : !hasEpisodes ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("illness.insights.none")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="space-y-4 text-sm">
|
||||
<p className="text-foreground">
|
||||
{data.episodeCount === 1
|
||||
{data!.episodeCount === 1
|
||||
? t("illness.insights.countOne")
|
||||
: t("illness.insights.countOther", {
|
||||
count: data.episodeCount,
|
||||
count: data!.episodeCount,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{data.typicalRecoveryGapDays !== null
|
||||
? t("illness.insights.typicalGap", {
|
||||
days: data.typicalRecoveryGapDays,
|
||||
})
|
||||
: t("illness.insights.typicalGapLearning")}
|
||||
</p>
|
||||
|
||||
{/* The recovery gap is computed only when the user asks for it. */}
|
||||
{!analyzeOpen ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="min-h-9"
|
||||
onClick={() => setAnalyzeOpen(true)}
|
||||
>
|
||||
{t("illness.insights.analyze")}
|
||||
</Button>
|
||||
) : gap.isLoading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
) : gap.isError || !gap.data ? (
|
||||
<p className="text-muted-foreground">
|
||||
{t("illness.insights.typicalGapLearning")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
{gap.data.typicalRecoveryGapDays !== null
|
||||
? t("illness.insights.typicalGap", {
|
||||
days: gap.data.typicalRecoveryGapDays,
|
||||
})
|
||||
: t("illness.insights.typicalGapLearning")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -6,8 +6,18 @@
|
||||
* Reached from the wrench beside the page's "hinzufügen" button. Lets the
|
||||
* user reorder conditions/episodes + pick the card-vs-list page view. Order
|
||||
* + view persist client-side via `useModuleListPrefs("illness")`.
|
||||
*
|
||||
* v1.18.9 — restructured onto the canonical Settings pattern: each block is a
|
||||
* `<SettingsCard>` with a `<SettingsCardHeader>` (neutral icon + `text-lg`
|
||||
* title + a short muted description), matching the Stimmung / Medikamente
|
||||
* sections rather than the bespoke bare-`<section>` headings it carried
|
||||
* before.
|
||||
*/
|
||||
import { Eye, ListOrdered } from "lucide-react";
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { ModuleViewToggle } from "@/components/module-list/module-view-toggle";
|
||||
import {
|
||||
@@ -37,23 +47,22 @@ export function IllnessSettings() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold">
|
||||
{t("moduleList.viewHeading")}
|
||||
</h2>
|
||||
<ModuleViewToggle view={prefs.view} onChange={setView} />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("moduleList.viewDescription")}
|
||||
</p>
|
||||
</section>
|
||||
<div className="space-y-6">
|
||||
<SettingsCard id="illness-view" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCardHeader
|
||||
icon={Eye}
|
||||
title={t("moduleList.viewHeading")}
|
||||
description={t("illness.settings.viewDescription")}
|
||||
status={<ModuleViewToggle view={prefs.view} onChange={setView} />}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold">
|
||||
{t("moduleList.reorder.heading")}
|
||||
</h2>
|
||||
<SettingsCard id="illness-order" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCardHeader
|
||||
icon={ListOrdered}
|
||||
title={t("moduleList.reorder.heading")}
|
||||
description={t("illness.settings.orderDescription")}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
@@ -63,7 +72,7 @@ export function IllnessSettings() {
|
||||
) : (
|
||||
<ModuleOrderEditor items={reorderItems} onChange={setOrder} />
|
||||
)}
|
||||
</section>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,13 +61,26 @@ export function useIllnessCorrelation(episodeId: string | null) {
|
||||
});
|
||||
}
|
||||
|
||||
/** The cross-episode retrospective summary over a trailing window in days. */
|
||||
export function useIllnessInsights(windowDays = 365) {
|
||||
/**
|
||||
* The cross-episode retrospective summary over a trailing window in days.
|
||||
*
|
||||
* `includeRecoveryGap` is OFF by default: the count breakdown is served by a
|
||||
* single fast query and the recovery-gap stays null. The illness list mounts
|
||||
* this lightweight read; only the explicit "Analyse" expansion opts into the
|
||||
* expensive per-episode correlation (`includeRecoveryGap: true`), which caches
|
||||
* under its own key. `enabled` lets a caller defer the heavy read until opened.
|
||||
*/
|
||||
export function useIllnessInsights(
|
||||
windowDays = 365,
|
||||
options: { includeRecoveryGap?: boolean; enabled?: boolean } = {},
|
||||
) {
|
||||
const includeRecoveryGap = options.includeRecoveryGap ?? false;
|
||||
return useQuery({
|
||||
queryKey: queryKeys.illnessInsights(windowDays),
|
||||
queryKey: queryKeys.illnessInsights(windowDays, includeRecoveryGap),
|
||||
enabled: options.enabled ?? true,
|
||||
queryFn: () =>
|
||||
apiGet<IllnessInsightsResponse>(
|
||||
`/api/illness/insights?windowDays=${windowDays}`,
|
||||
`/api/illness/insights?windowDays=${windowDays}&includeRecoveryGap=${includeRecoveryGap}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user