mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(medications): take-now / overdue pill on the GLP-1 card too
The generic medication card painted a coloured take-now / overdue / very-overdue pill at the top of the detail surface; the GLP-1 card silently omitted it. Marc reported the asymmetry during the v1.4.37 UX audit (audit item 11): Mounjaro and Ramipril cards felt "different" even though both should read as the same row shape. Wire the shared `reduceCurrentWindowStatus` reducer into the GLP-1 card, fetch the same reminder thresholds as the generic card, and re-render once a minute so the pill tracks wall-clock progress without a route reload. The pill renders only when a schedule is currently in/past its window and the user has not yet covered every overdue schedule with today's intake events — same suppression rules as the generic card.
This commit is contained in:
1 parent
6fc9c99009
commit
2e929d0afb
1 file changed
+88
-2
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -21,12 +21,17 @@ import { Progress } from "@/components/ui/progress";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys";
|
||||
import { formatDateTime, formatTime } from "@/lib/format";
|
||||
import { formatTimeWindowRange } from "@/lib/time-window-format";
|
||||
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
|
||||
import {
|
||||
describeInjectionSite,
|
||||
nextInjectionSite,
|
||||
type InjectionSiteKey,
|
||||
} from "@/lib/medications/injection-sites";
|
||||
import {
|
||||
reduceCurrentWindowStatus,
|
||||
toBerlinDate,
|
||||
} from "@/lib/medications/window-status";
|
||||
/**
|
||||
* v1.4.25 W4d — GLP-1 medication card variant.
|
||||
*
|
||||
@@ -78,6 +83,7 @@ export interface Glp1Medication {
|
||||
notificationsEnabled: boolean;
|
||||
pausedAt: string | null;
|
||||
lastTakenAt: string | null;
|
||||
todayEventCount?: number;
|
||||
schedules: ScheduleLite[];
|
||||
}
|
||||
|
||||
@@ -156,9 +162,10 @@ export function Glp1MedicationCard({
|
||||
onLogSideEffect,
|
||||
}: Glp1MedicationCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
const { t, locale } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const [intakeLoading, setIntakeLoading] = useState<string | null>(null);
|
||||
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
||||
|
||||
const { data: compliance } = useQuery({
|
||||
queryKey: ["medications", medication.id, "compliance"],
|
||||
@@ -172,6 +179,20 @@ export function Glp1MedicationCard({
|
||||
enabled: medication.active,
|
||||
});
|
||||
|
||||
// v1.4.37 W4b — same reminder-thresholds source as the generic
|
||||
// medication card so the take-now / overdue / very-overdue pill
|
||||
// tiers identically on both surfaces.
|
||||
const { data: thresholds } = useQuery({
|
||||
queryKey: ["settings", "reminder-thresholds"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/reminder-thresholds");
|
||||
if (!res.ok) return null;
|
||||
const json = await res.json();
|
||||
return json.data as { lateMinutes: number; missedMinutes: number };
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Pull GLP-1-specific extras (dose history + recent injection sites +
|
||||
// pen inventory). Lives behind a dedicated endpoint so the standard
|
||||
// medications grid loads in parallel without paying for it.
|
||||
@@ -186,6 +207,14 @@ export function Glp1MedicationCard({
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
// Re-render once a minute so the in-window / overdue pill tracks
|
||||
// wall-clock progress without a route reload — mirrors the generic
|
||||
// medication card's tick cadence.
|
||||
useEffect(() => {
|
||||
const interval = setInterval(forceUpdate, 60_000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
async function recordIntake(skipped: boolean) {
|
||||
const key = skipped ? "skip" : "take";
|
||||
setIntakeLoading(key);
|
||||
@@ -210,6 +239,28 @@ export function Glp1MedicationCard({
|
||||
const rate30 = compliance?.compliance30?.rate ?? 0;
|
||||
const streak = compliance?.compliance7?.streak ?? 0;
|
||||
|
||||
// v1.4.37 W4b — symmetric take-now / overdue pill with the generic
|
||||
// card. Sort schedules by `windowStart` so the most-actionable
|
||||
// earliest window wins when multiple GLP-1 windows overlap (rare
|
||||
// today; the parity is what matters).
|
||||
const sortedSchedules = [...medication.schedules].sort(
|
||||
(a, b) =>
|
||||
a.windowStart.localeCompare(b.windowStart) ||
|
||||
a.windowEnd.localeCompare(b.windowEnd),
|
||||
);
|
||||
const nowBerlin = toBerlinDate(now);
|
||||
const lateMinutes = thresholds?.lateMinutes ?? 120;
|
||||
const missedMinutes = thresholds?.missedMinutes ?? 240;
|
||||
const currentWindowStatus = reduceCurrentWindowStatus({
|
||||
schedules: sortedSchedules,
|
||||
nowBerlin,
|
||||
lateMinutes,
|
||||
missedMinutes,
|
||||
active: medication.active,
|
||||
lastTakenAt: medication.lastTakenAt,
|
||||
todayEventCount: medication.todayEventCount ?? 0,
|
||||
});
|
||||
|
||||
const recentInjections = details?.recentIntakes ?? [];
|
||||
const lastSite =
|
||||
recentInjections.find((i) => i.injectionSite)?.injectionSite ?? null;
|
||||
@@ -297,6 +348,41 @@ export function Glp1MedicationCard({
|
||||
/>
|
||||
|
||||
<CardContent className="space-y-3.5">
|
||||
{/* v1.4.37 W4b — take-now / overdue / very-overdue pill,
|
||||
byte-equivalent with the generic medication card. The
|
||||
GLP-1 card historically omitted this row, which made
|
||||
Mounjaro feel different from Ramipril on the medications
|
||||
grid even though the underlying schedule contract is
|
||||
the same shape. */}
|
||||
{currentWindowStatus.status && (
|
||||
<p className="text-sm">
|
||||
<span
|
||||
className={
|
||||
currentWindowStatus.status === "in_window"
|
||||
? "text-success font-medium"
|
||||
: currentWindowStatus.status === "late"
|
||||
? "text-dracula-yellow font-medium"
|
||||
: "text-warning font-medium"
|
||||
}
|
||||
>
|
||||
{currentWindowStatus.status === "in_window"
|
||||
? t("medications.takeNow")
|
||||
: currentWindowStatus.status === "late"
|
||||
? t("medications.overdue")
|
||||
: t("medications.veryOverdue")}
|
||||
</span>
|
||||
<span className="text-muted-foreground hidden sm:inline">
|
||||
{" "}
|
||||
—{" "}
|
||||
{formatTimeWindowRange(
|
||||
currentWindowStatus.schedule!.windowStart,
|
||||
currentWindowStatus.schedule!.windowEnd,
|
||||
locale,
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Injection state — last + next */}
|
||||
<div className="space-y-1 text-sm">
|
||||
{medication.lastTakenAt && (
|
||||
|
||||
Reference in new issue
Block a user