mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
chore(release): v1.28.20 — estimated drug level in efficacy + insights
The GLP-1 estimated active-substance curve (the modelled drug level from logged doses) now also renders in the efficacy (Wirkung) tab and in the Insights medication overview, not only under the injection tab. Both reuse the same self-contained DrugLevelChart, gated on the GLP-1 treatment class, so the drug level sits next to the efficacy readouts. Additive — no existing surface changes; non-GLP-1 medications render nothing.
This commit is contained in:
1 parent
5f558abc33
commit
349cfa4d80
8 files changed
+159
-4
No files matched your search
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.28.20 — compact GLP-1 estimated active-substance curve for the Insights
|
||||
* medication surface. Additive to the medication detail page's Injektion and
|
||||
* Wirkung tabs: the same self-contained `DrugLevelChart` (modelled
|
||||
* one-compartment level from the user's logged intakes + dose changes),
|
||||
* surfaced where the compliance overview already lives.
|
||||
*
|
||||
* Gating: reads `GET /api/insights/glp1-timeline` — the server-authoritative
|
||||
* GLP-1 signal. When `hasGlp1` is false the block renders nothing. The set of
|
||||
* GLP-1 medication names comes from the timeline's entries (every entry is
|
||||
* from a `treatmentClass === "GLP1"` medication), and the passed medication
|
||||
* list is filtered to that set so non-GLP-1 medications never render a curve.
|
||||
*
|
||||
* The chart carries its own `MedicationDetailSection` card chrome + estimate
|
||||
* disclaimer, so it reads as one system with the surrounding Insights cards
|
||||
* without a second computation. Recharts loads through `next/dynamic`
|
||||
* (client-only) to stay out of the page's initial bundle.
|
||||
*/
|
||||
import dynamic from "next/dynamic";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
|
||||
const DrugLevelChart = dynamic(
|
||||
() =>
|
||||
import("@/components/charts/chart-runtime").then((mod) => ({
|
||||
default: mod.DrugLevelChart,
|
||||
})),
|
||||
{ ssr: false, loading: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
interface Glp1TimelineResponse {
|
||||
hasGlp1: boolean;
|
||||
entries: { medicationName?: string }[];
|
||||
}
|
||||
|
||||
export function Glp1SubstanceCurveSummary({
|
||||
medications,
|
||||
}: {
|
||||
medications: { id: string; name: string; dose: string }[];
|
||||
}) {
|
||||
const { data } = useQuery({
|
||||
queryKey: queryKeys.insightsGlp1Timeline(200),
|
||||
queryFn: () =>
|
||||
apiGet<Glp1TimelineResponse>("/api/insights/glp1-timeline?limit=200"),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
if (!data?.hasGlp1) return null;
|
||||
|
||||
// Every timeline entry is from a GLP-1 medication (server filters on
|
||||
// `treatmentClass: "GLP1"`). Medication names match the comprehensive
|
||||
// list exactly — both derive from `medication.name` server-side.
|
||||
const glp1Names = new Set(
|
||||
data.entries
|
||||
.map((e) => e.medicationName)
|
||||
.filter((n): n is string => typeof n === "string" && n.length > 0),
|
||||
);
|
||||
|
||||
const glp1Meds = medications.filter((m) => glp1Names.has(m.name));
|
||||
if (glp1Meds.length === 0) return null;
|
||||
|
||||
const showNameLabel = glp1Meds.length > 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-4" data-slot="insights-glp1-substance-curve">
|
||||
{glp1Meds.map((med) => (
|
||||
<div key={med.id} className="space-y-2">
|
||||
{showNameLabel ? (
|
||||
<p className="text-muted-foreground px-1 text-xs font-medium">
|
||||
{med.name}
|
||||
</p>
|
||||
) : null}
|
||||
<DrugLevelChart
|
||||
medication={{ id: med.id, name: med.name, dose: med.dose }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,12 +44,39 @@ const EfficacyChart = dynamic(
|
||||
{ ssr: false, loading: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
// v1.28.20 — the estimated active-substance curve (Wirkstoffverlauf) also
|
||||
// surfaces here, additive to the Injektion tab. Same self-contained chart,
|
||||
// same `next/dynamic` (client-only) load that keeps Recharts out of the
|
||||
// initial bundle.
|
||||
const DrugLevelChart = dynamic(
|
||||
() =>
|
||||
import("@/components/charts/chart-runtime").then((mod) => ({
|
||||
default: mod.DrugLevelChart,
|
||||
})),
|
||||
{ ssr: false, loading: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
export function EfficacyTab({
|
||||
medicationId,
|
||||
active,
|
||||
isGlp1 = false,
|
||||
medicationName,
|
||||
medicationDose,
|
||||
}: {
|
||||
medicationId: string;
|
||||
active: boolean;
|
||||
/**
|
||||
* True when the parent already knows the medication is a (non-one-shot)
|
||||
* GLP-1 drug (`treatmentClass === "GLP1"`). Threaded from the detail-tabs
|
||||
* parent because the efficacy DTO carries the target class, not the raw
|
||||
* treatment class. Gates the estimated active-substance curve exactly the
|
||||
* way the Injektion tab does — no Research-Mode wrapper, GLP-1 alone.
|
||||
*/
|
||||
isGlp1?: boolean;
|
||||
/** Brand/generic name the substance-curve chart resolves to a catalog id. */
|
||||
medicationName?: string;
|
||||
/** Headline dose string, the chart's per-intake dose fallback. */
|
||||
medicationDose?: string;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
@@ -136,6 +163,24 @@ export function EfficacyTab({
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* v1.28.20 — estimated active-substance curve for GLP-1 medications,
|
||||
mirroring the Injektion tab. The chart is self-contained (fetches
|
||||
its own intake + dose-change data and renders its own
|
||||
`MedicationDetailSection` card + estimate disclaimer); it slots
|
||||
straight into the tab's `space-y-6` rhythm. Additive: no existing
|
||||
Wirkung content changes. */}
|
||||
{isGlp1 && medicationName != null && medicationDose != null ? (
|
||||
<div data-slot="wirkung-drug-level-section">
|
||||
<DrugLevelChart
|
||||
medication={{
|
||||
id: medicationId,
|
||||
name: medicationName,
|
||||
dose: medicationDose,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p
|
||||
className="text-muted-foreground text-xs"
|
||||
data-slot="medication-wirkung-disclaimer"
|
||||
|
||||
@@ -560,7 +560,13 @@ export function MedicationDetailTabs({
|
||||
descriptive; hidden for one-shot + no-target medications. */}
|
||||
{hasEfficacyTarget && (
|
||||
<TabsContent value="wirkung" className="space-y-6 pt-2">
|
||||
<EfficacyTab medicationId={id} active={activeTab === "wirkung"} />
|
||||
<EfficacyTab
|
||||
medicationId={id}
|
||||
active={activeTab === "wirkung"}
|
||||
isGlp1={isGlp1}
|
||||
medicationName={medication.name}
|
||||
medicationDose={medication.dose}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user