mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(daily-loop): thread the overdue medication id into the digest's Log-dose link
The Today digest's dose-window rail item linked to the bare /medications list even though the overdue medication's id was already resolved server-side alongside its name — a tap landed the user on a generic list they then had to scan themselves. buildMedsTodayBlock now carries nextDueMedicationId; the digest's "Log dose" action links to /medications?highlight=<id> when the id is known, falling back to the bare list for an older cached block. The medications page reads the highlight param, scrolls the matching card into view, and rings it briefly (mirrors the vault's duplicate-upload highlight) in both the card grid and the table view. Regenerates docs/api/openapi.yaml for the new medsToday field.
This commit is contained in:
1 parent
844b24c7ba
commit
2b3ff0e58f
15 files changed
+213
-8
No files matched your search
@@ -23987,6 +23987,10 @@ components:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
nextDueMedicationId:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
required:
|
||||
- activeCount
|
||||
- scheduledToday
|
||||
@@ -23995,12 +23999,14 @@ components:
|
||||
- nextDueAt
|
||||
- nextDueOverdue
|
||||
- nextDueMedicationName
|
||||
- nextDueMedicationId
|
||||
additionalProperties: false
|
||||
description: "Today's medication block: active-medication count, today-window tally (scheduled / taken / skipped), and
|
||||
the earliest next-due slot across active medications. `nextDueOverdue: true` marks an OPEN overdue slot
|
||||
(anchor passed, still inside its catch-up band). Staleness contract: the snapshot is cache-served, so a
|
||||
`nextDueAt` in the past with `nextDueOverdue: false` means the anchor passed after the snapshot was built —
|
||||
render the plain day summary, never an overdue state."
|
||||
(anchor passed, still inside its catch-up band). `nextDueMedicationId` deep-links a consumer straight to the
|
||||
overdue medication's card. Staleness contract: the snapshot is cache-served, so a `nextDueAt` in the past
|
||||
with `nextDueOverdue: false` means the anchor passed after the snapshot was built — render the plain day
|
||||
summary, never an overdue state."
|
||||
healthScore:
|
||||
anyOf:
|
||||
- type: object
|
||||
|
||||
@@ -279,6 +279,7 @@ export function buildMockSnapshot(
|
||||
nextDueAt: null,
|
||||
nextDueOverdue: false,
|
||||
nextDueMedicationName: null,
|
||||
nextDueMedicationId: null,
|
||||
},
|
||||
healthScore: options.briefing
|
||||
? { score: 84, band: "green", delta: -2, restMode: null }
|
||||
|
||||
@@ -169,6 +169,14 @@ export default function MedicationsPage() {
|
||||
// the param from the URL so a manual close + refresh stays closed.
|
||||
const shouldOpenFromUrl = searchParams?.get("new") === "1";
|
||||
const [dialogOpen, setDialogOpen] = useState(shouldOpenFromUrl);
|
||||
// v1.29.x — the Today digest's "Log dose" action deep-links straight to
|
||||
// the overdue medication (`/medications?highlight=<id>`) instead of the
|
||||
// bare list. Read once on mount; a scroll effect below (once the list
|
||||
// resolves) brings the target card into view and rings it briefly, then
|
||||
// clears the state so the ring doesn't linger indefinitely.
|
||||
const [highlightId, setHighlightId] = useState<string | null>(
|
||||
() => searchParams?.get("highlight") ?? null,
|
||||
);
|
||||
// v1.14.0 — the medications-page "Add" choice. The top button now offers
|
||||
// two paths: log an intake (incl. a backdated one) against an existing
|
||||
// medication, or create a new medication (the existing wizard).
|
||||
@@ -213,6 +221,21 @@ export default function MedicationsPage() {
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
|
||||
// v1.29.x — once the highlighted card is in the DOM, scroll it into view
|
||||
// and clear the highlight state after a few seconds so the ring is a
|
||||
// one-time "you tapped here" cue, not a permanent tint (mirrors the
|
||||
// vault's upload-duplicate flash-highlight, `HIGHLIGHT_MS` in
|
||||
// `use-document-upload.ts`).
|
||||
useEffect(() => {
|
||||
if (!highlightId || !medications?.length) return;
|
||||
const card = document.querySelector(
|
||||
`[data-medication-id="${highlightId}"]`,
|
||||
);
|
||||
card?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
const timer = setTimeout(() => setHighlightId(null), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [highlightId, medications]);
|
||||
|
||||
// v1.16.11 — the same reminder-thresholds read the cards make (shared
|
||||
// key + cache), so the header's due-set derivation tiers late /
|
||||
// very_late exactly like the card pills. Null on failure keeps the
|
||||
@@ -468,6 +491,7 @@ export default function MedicationsPage() {
|
||||
<MedicationTable
|
||||
activeMedications={activeMeds}
|
||||
inactiveMedications={inactiveMeds}
|
||||
highlightId={highlightId}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
@@ -485,6 +509,7 @@ export default function MedicationsPage() {
|
||||
medication={med}
|
||||
onEdit={openEdit}
|
||||
onOpenHistory={openHistory}
|
||||
highlighted={highlightId === med.id}
|
||||
/>
|
||||
) : (
|
||||
<MedicationCard
|
||||
@@ -492,6 +517,7 @@ export default function MedicationsPage() {
|
||||
medication={med}
|
||||
onEdit={openEdit}
|
||||
onOpenHistory={openHistory}
|
||||
highlighted={highlightId === med.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
@@ -513,6 +539,7 @@ export default function MedicationsPage() {
|
||||
medication={med}
|
||||
onEdit={openEdit}
|
||||
onOpenHistory={openHistory}
|
||||
highlighted={highlightId === med.id}
|
||||
/>
|
||||
) : (
|
||||
<MedicationCard
|
||||
@@ -520,6 +547,7 @@ export default function MedicationsPage() {
|
||||
medication={med}
|
||||
onEdit={openEdit}
|
||||
onOpenHistory={openHistory}
|
||||
highlighted={highlightId === med.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -420,9 +420,15 @@ describe("streak-token parity — generic vs GLP-1 card", () => {
|
||||
* structural identity that makes the two variants impossible to diverge.
|
||||
*/
|
||||
describe("medication card body — shared shell + dose-state presentation", () => {
|
||||
function renderBody(doseStatus: DoseStatus, active = true) {
|
||||
function renderBody(
|
||||
doseStatus: DoseStatus,
|
||||
active = true,
|
||||
highlighted = false,
|
||||
) {
|
||||
return render(
|
||||
<MedicationCardBody
|
||||
id="m1"
|
||||
highlighted={highlighted}
|
||||
name="Ramipril"
|
||||
dose="5 mg"
|
||||
categoryLabel="Blood Pressure"
|
||||
@@ -500,6 +506,26 @@ describe("medication card body — shared shell + dose-state presentation", () =
|
||||
expect(html).toContain("opacity-60");
|
||||
});
|
||||
|
||||
// v1.29.x — the Today digest's "Log dose" deep-link
|
||||
// (`/medications?highlight=<id>`) rings the target card, mirroring
|
||||
// `<DocumentCard highlighted>`. Carries the id for the page's
|
||||
// scroll-into-view lookup regardless of highlight state.
|
||||
it("carries the medication id for the deep-link target lookup", () => {
|
||||
const html = renderBody("upcoming");
|
||||
expect(html).toContain('data-medication-id="m1"');
|
||||
});
|
||||
|
||||
it("rings the card when it is the deep-link highlight target", () => {
|
||||
const html = renderBody("upcoming", true, true);
|
||||
expect(html).toContain("ring-primary");
|
||||
expect(html).toContain("ring-2");
|
||||
});
|
||||
|
||||
it("does not ring the card when it is not the highlight target", () => {
|
||||
const html = renderBody("upcoming", true, false);
|
||||
expect(html).not.toContain("ring-primary");
|
||||
});
|
||||
|
||||
it("renders the decisive next + last lines exactly once each", () => {
|
||||
const html = renderBody("upcoming");
|
||||
expect((html.match(/Next intake:/g) ?? []).length).toBe(1);
|
||||
@@ -605,6 +631,7 @@ describe("compliance error fallback (v1.16.8)", () => {
|
||||
}) {
|
||||
return render(
|
||||
<MedicationCardBody
|
||||
id="m1"
|
||||
name="Ramipril"
|
||||
dose="5 mg"
|
||||
categoryLabel="Blood Pressure"
|
||||
|
||||
@@ -302,6 +302,45 @@ describe("<MedicationTable> — structure + shared payloads", () => {
|
||||
expect(html).not.toContain('aria-label="Take – Amoxicillin"');
|
||||
});
|
||||
|
||||
// v1.29.x — the Today digest's "Log dose" deep-link
|
||||
// (`/medications?highlight=<id>`) rings the matching row, mirroring the
|
||||
// card grid's transient highlight.
|
||||
it("carries the medication id and rings the row matching highlightId", () => {
|
||||
const client = makeClient();
|
||||
seedCompliance(client, "m1", 90, 88);
|
||||
seedCompliance(client, "m2", 90, 88);
|
||||
const html = render(
|
||||
<MedicationTable
|
||||
activeMedications={[
|
||||
med({ id: "m1", name: "Ramipril" }),
|
||||
med({ id: "m2", name: "Metformin" }),
|
||||
]}
|
||||
inactiveMedications={[]}
|
||||
highlightId="m2"
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
expect(html).toContain('data-medication-id="m1"');
|
||||
expect(html).toContain('data-medication-id="m2"');
|
||||
expect(html).toContain("ring-primary");
|
||||
});
|
||||
|
||||
it("rings no row when highlightId matches nothing", () => {
|
||||
const client = makeClient();
|
||||
seedCompliance(client, "m1", 90, 88);
|
||||
const html = render(
|
||||
<MedicationTable
|
||||
activeMedications={[med({ id: "m1", name: "Ramipril" })]}
|
||||
inactiveMedications={[]}
|
||||
highlightId="does-not-exist"
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
expect(html).not.toContain("ring-primary");
|
||||
});
|
||||
|
||||
it("renders a GLP-1 medication like any other row", () => {
|
||||
const client = makeClient();
|
||||
seedCompliance(client, "g1", 100, 100);
|
||||
|
||||
@@ -50,6 +50,12 @@ import type { MedicationWindowStatus } from "@/lib/medications/window-status";
|
||||
* - `upcoming` / `taken_*` / `skipped` → no escalation; the card stays calm.
|
||||
*/
|
||||
export interface MedicationCardBodyProps {
|
||||
/**
|
||||
* Medication id. Rendered as `data-medication-id` so a deep-link (e.g. the
|
||||
* Today digest's "Log dose" action, `/medications?highlight=<id>`) can
|
||||
* locate + scroll to the right card.
|
||||
*/
|
||||
id: string;
|
||||
/** Medication name shown on the header's line 1. */
|
||||
name: string;
|
||||
/** Dose shown beside the name on line 1. */
|
||||
@@ -140,9 +146,18 @@ export interface MedicationCardBodyProps {
|
||||
|
||||
/** GLP-1 variant mounts its post-dose injection-site dialog here. */
|
||||
children?: ReactNode;
|
||||
|
||||
/**
|
||||
* v1.29.x — briefly ringed when this card is the Today digest's
|
||||
* "Log dose" deep-link target (`/medications?highlight=<id>`), mirroring
|
||||
* the vault's `<DocumentCard highlighted>` treatment. Transient — the
|
||||
* page clears it a few seconds after scrolling the card into view.
|
||||
*/
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
export function MedicationCardBody({
|
||||
id,
|
||||
name,
|
||||
dose,
|
||||
categoryLabel,
|
||||
@@ -164,6 +179,7 @@ export function MedicationCardBody({
|
||||
intakeLoading,
|
||||
onRecordIntake,
|
||||
children,
|
||||
highlighted = false,
|
||||
}: MedicationCardBodyProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
|
||||
@@ -227,6 +243,7 @@ export function MedicationCardBody({
|
||||
|
||||
return (
|
||||
<Card
|
||||
data-medication-id={id}
|
||||
// The card surface is a constant neutral surface — dose status is never
|
||||
// expressed as a background / border tint (the maintainer, recurring): only the
|
||||
// discreet status line / pill below communicates take-now / overdue.
|
||||
@@ -236,7 +253,16 @@ export function MedicationCardBody({
|
||||
// the first content line; tightened to a uniform `gap-3` here (the
|
||||
// header keeps its own `pb-2.5`) so the name / class / next-intake block
|
||||
// reads as one tight stack without touching the shared primitive.
|
||||
className={cn("h-full gap-3 md:gap-3", active ? "" : "opacity-60")}
|
||||
//
|
||||
// v1.29.x — `highlighted` adds a transient ring (mirrors
|
||||
// `<DocumentCard highlighted>`) for the Today digest's deep-link
|
||||
// target. Never a persistent tint — the neutral-card rule above still
|
||||
// holds for dose STATUS; this is a one-time "you tapped here" cue.
|
||||
className={cn(
|
||||
"h-full gap-3 md:gap-3",
|
||||
active ? "" : "opacity-60",
|
||||
highlighted && "ring-primary ring-2",
|
||||
)}
|
||||
>
|
||||
<MedicationCardHeader
|
||||
name={name}
|
||||
|
||||
@@ -150,6 +150,11 @@ interface Glp1MedicationCardProps {
|
||||
*/
|
||||
onOpenHistory: (med: Glp1Medication) => void;
|
||||
onLogSideEffect?: (med: Glp1Medication) => void;
|
||||
/**
|
||||
* v1.29.x — briefly ringed when this card is the Today digest's "Log
|
||||
* dose" deep-link target (`/medications?highlight=<id>`).
|
||||
*/
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
function diffDays(target: Date, from: Date): number {
|
||||
@@ -182,6 +187,7 @@ export function Glp1MedicationCard({
|
||||
onEdit,
|
||||
onOpenHistory,
|
||||
onLogSideEffect,
|
||||
highlighted = false,
|
||||
}: Glp1MedicationCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
@@ -493,6 +499,8 @@ export function Glp1MedicationCard({
|
||||
|
||||
return (
|
||||
<MedicationCardBody
|
||||
id={medication.id}
|
||||
highlighted={highlighted}
|
||||
name={medication.name}
|
||||
dose={formatDose(medication.dose, t)}
|
||||
categoryLabel={categoryLabel}
|
||||
|
||||
@@ -121,12 +121,18 @@ interface MedicationCardProps {
|
||||
* (`/medications/{id}?tab=verlauf`). The parent owns the navigation.
|
||||
*/
|
||||
onOpenHistory: (med: Medication) => void;
|
||||
/**
|
||||
* v1.29.x — briefly ringed when this card is the Today digest's "Log
|
||||
* dose" deep-link target (`/medications?highlight=<id>`).
|
||||
*/
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
export function MedicationCard({
|
||||
medication,
|
||||
onEdit,
|
||||
onOpenHistory,
|
||||
highlighted = false,
|
||||
}: MedicationCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t, locale } = useTranslations();
|
||||
@@ -463,6 +469,8 @@ export function MedicationCard({
|
||||
|
||||
return (
|
||||
<MedicationCardBody
|
||||
id={medication.id}
|
||||
highlighted={highlighted}
|
||||
name={medication.name}
|
||||
dose={formatDose(medication.dose, t)}
|
||||
categoryLabel={categoryLabel}
|
||||
|
||||
@@ -172,12 +172,19 @@ interface MedicationTableProps {
|
||||
inactiveMedications: TableMedication[];
|
||||
/** SSR-pinnable initial sort (tests); the header buttons own it after mount. */
|
||||
initialSort?: MedicationTableSort | null;
|
||||
/**
|
||||
* v1.29.x — the Today digest's "Log dose" deep-link target
|
||||
* (`/medications?highlight=<id>`). The matching row rings briefly,
|
||||
* mirroring the card grid's `<MedicationCard highlighted>`.
|
||||
*/
|
||||
highlightId?: string | null;
|
||||
}
|
||||
|
||||
export function MedicationTable({
|
||||
activeMedications,
|
||||
inactiveMedications,
|
||||
initialSort = null,
|
||||
highlightId = null,
|
||||
}: MedicationTableProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
@@ -350,6 +357,7 @@ export function MedicationTable({
|
||||
lateMinutes={lateMinutes}
|
||||
missedMinutes={missedMinutes}
|
||||
lowStockRunwayDays={lowStockRunwayDays}
|
||||
highlighted={highlightId === med.id}
|
||||
/>
|
||||
))}
|
||||
{sortedInactive.map((med) => (
|
||||
@@ -360,6 +368,7 @@ export function MedicationTable({
|
||||
lateMinutes={lateMinutes}
|
||||
missedMinutes={missedMinutes}
|
||||
lowStockRunwayDays={lowStockRunwayDays}
|
||||
highlighted={highlightId === med.id}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -375,6 +384,8 @@ interface MedicationTableRowItemProps {
|
||||
missedMinutes: number;
|
||||
/** Per-user low-stock runway threshold in days; null = alert off. */
|
||||
lowStockRunwayDays: number | null;
|
||||
/** v1.29.x — the deep-link highlight target (see `MedicationTableProps`). */
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
function MedicationTableRowItem({
|
||||
@@ -383,6 +394,7 @@ function MedicationTableRowItem({
|
||||
lateMinutes,
|
||||
missedMinutes,
|
||||
lowStockRunwayDays,
|
||||
highlighted = false,
|
||||
}: MedicationTableRowItemProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
@@ -637,7 +649,15 @@ function MedicationTableRowItem({
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow className={cn(!medication.active && "opacity-60")}>
|
||||
<TableRow
|
||||
data-medication-id={medication.id}
|
||||
className={cn(
|
||||
!medication.active && "opacity-60",
|
||||
// v1.29.x — mirrors the card grid's transient highlight ring for
|
||||
// the Today digest's "Log dose" deep-link target.
|
||||
highlighted && "ring-primary ring-2",
|
||||
)}
|
||||
>
|
||||
{/* Sticky name column — opaque background so scrolled content
|
||||
passes underneath, not through. */}
|
||||
<TableCell className="bg-card sticky left-0 z-10">
|
||||
|
||||
@@ -35,6 +35,7 @@ function meds(over: Partial<MedsTodayBlock> = {}): MedsTodayBlock {
|
||||
nextDueAt: null,
|
||||
nextDueOverdue: false,
|
||||
nextDueMedicationName: null,
|
||||
nextDueMedicationId: null,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
@@ -201,6 +202,7 @@ describe("buildDailyDigest — worth-a-look rail item builders", () => {
|
||||
medsToday: meds({
|
||||
nextDueOverdue: true,
|
||||
nextDueMedicationName: "Ramipril",
|
||||
nextDueMedicationId: "med-ramipril",
|
||||
}),
|
||||
}),
|
||||
t,
|
||||
@@ -213,6 +215,24 @@ describe("buildDailyDigest — worth-a-look rail item builders", () => {
|
||||
expect(dose?.body).toBe("Ramipril is due today.");
|
||||
expect(dose?.actions).toHaveLength(1);
|
||||
expect(dose?.actions[0].intent).toBe("dose.log");
|
||||
// Deep-links straight to the overdue medication's card, not the bare
|
||||
// list — the tap should land on the right med.
|
||||
expect(dose?.actions[0].href).toBe("/medications?highlight=med-ramipril");
|
||||
});
|
||||
|
||||
it("falls back to the bare medications list when no id is known (older cached block)", () => {
|
||||
const d = buildDailyDigest(
|
||||
input({
|
||||
medsToday: meds({
|
||||
nextDueOverdue: true,
|
||||
nextDueMedicationName: "Ramipril",
|
||||
nextDueMedicationId: null,
|
||||
}),
|
||||
}),
|
||||
t,
|
||||
);
|
||||
const dose = d.worthALook.find((i) => i.kind === "dose_window");
|
||||
expect(dose?.actions[0].href).toBe("/medications");
|
||||
});
|
||||
|
||||
it("does NOT emit a dose-window item when the medications module is off", () => {
|
||||
|
||||
@@ -258,6 +258,7 @@ function buildDoseWindowItem(
|
||||
if (!moduleEnabled(modules, "medications")) return null;
|
||||
if (!meds.nextDueOverdue) return null;
|
||||
const name = meds.nextDueMedicationName;
|
||||
const id = meds.nextDueMedicationId;
|
||||
return {
|
||||
kind: "dose_window",
|
||||
title: t("daily.item.doseWindow.title"),
|
||||
@@ -269,7 +270,12 @@ function buildDoseWindowItem(
|
||||
{
|
||||
labelKey: "daily.action.logDose",
|
||||
intent: "dose.log",
|
||||
href: "/medications",
|
||||
// Deep-link straight to the overdue medication's card so the tap
|
||||
// lands on the right one instead of a generic list the user then
|
||||
// has to scan (the id is known server-side whenever the name is).
|
||||
// The bare list stays the honest fallback for an older cached
|
||||
// block that predates the id field.
|
||||
href: id ? `/medications?highlight=${id}` : "/medications",
|
||||
},
|
||||
],
|
||||
moduleKey: "medications",
|
||||
|
||||
@@ -207,6 +207,7 @@ describe("buildMedsTodayBlock — tally", () => {
|
||||
nextDueAt: null,
|
||||
nextDueOverdue: false,
|
||||
nextDueMedicationName: null,
|
||||
nextDueMedicationId: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,6 +260,7 @@ describe("buildMedsTodayBlock — next due (real engine)", () => {
|
||||
expect(block.nextDueAt).toBe("2026-06-10T16:00:00.000Z");
|
||||
expect(block.nextDueOverdue).toBe(false);
|
||||
expect(block.nextDueMedicationName).toBe("Abendmittel");
|
||||
expect(block.nextDueMedicationId).toBe("med-evening");
|
||||
});
|
||||
|
||||
it("surfaces an open overdue slot (anchor passed, band still open)", async () => {
|
||||
@@ -282,6 +284,7 @@ describe("buildMedsTodayBlock — next due (real engine)", () => {
|
||||
expect(block.nextDueAt).toBe("2026-06-10T09:00:00.000Z");
|
||||
expect(block.nextDueOverdue).toBe(true);
|
||||
expect(block.nextDueMedicationName).toBe("Mittagsmittel");
|
||||
expect(block.nextDueMedicationId).toBe("med-1");
|
||||
});
|
||||
|
||||
it("advances past a RESOLVED slot to the next future anchor", async () => {
|
||||
@@ -329,5 +332,6 @@ describe("buildMedsTodayBlock — next due (real engine)", () => {
|
||||
expect(block.nextDueAt).toBeNull();
|
||||
expect(block.nextDueOverdue).toBe(false);
|
||||
expect(block.nextDueMedicationName).toBeNull();
|
||||
expect(block.nextDueMedicationId).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,7 @@ function medsToday(partial: Partial<MedsTodayBlock> = {}): MedsTodayBlock {
|
||||
nextDueAt: null,
|
||||
nextDueOverdue: false,
|
||||
nextDueMedicationName: null,
|
||||
nextDueMedicationId: null,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@ export interface MedsTodayBlock {
|
||||
nextDueOverdue: boolean;
|
||||
/** Name of the medication carrying `nextDueAt`; null when none due. */
|
||||
nextDueMedicationName: string | null;
|
||||
/**
|
||||
* Id of the medication carrying `nextDueAt`; null when none due. Threaded
|
||||
* through so a consumer (the Today digest's dose-window rail item) can
|
||||
* deep-link straight to the overdue medication's card instead of the bare
|
||||
* list.
|
||||
*/
|
||||
nextDueMedicationId: string | null;
|
||||
}
|
||||
|
||||
export async function buildMedsTodayBlock(
|
||||
@@ -182,6 +189,7 @@ export async function buildMedsTodayBlock(
|
||||
let nextDueAt: Date | null = null;
|
||||
let nextDueOverdue = false;
|
||||
let nextDueMedicationName: string | null = null;
|
||||
let nextDueMedicationId: string | null = null;
|
||||
for (const m of medications) {
|
||||
const display = computeDisplayDue({
|
||||
medication: {
|
||||
@@ -203,6 +211,7 @@ export async function buildMedsTodayBlock(
|
||||
nextDueAt = display.at;
|
||||
nextDueOverdue = display.overdue;
|
||||
nextDueMedicationName = m.name;
|
||||
nextDueMedicationId = m.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,5 +223,6 @@ export async function buildMedsTodayBlock(
|
||||
nextDueAt: nextDueAt ? nextDueAt.toISOString() : null,
|
||||
nextDueOverdue,
|
||||
nextDueMedicationName,
|
||||
nextDueMedicationId,
|
||||
};
|
||||
}
|
||||
@@ -896,9 +896,10 @@ export const dashboardSnapshotResponse = z
|
||||
nextDueAt: z.string().nullable(),
|
||||
nextDueOverdue: z.boolean(),
|
||||
nextDueMedicationName: z.string().nullable(),
|
||||
nextDueMedicationId: z.string().nullable(),
|
||||
})
|
||||
.describe(
|
||||
"Today's medication block: active-medication count, today-window tally (scheduled / taken / skipped), and the earliest next-due slot across active medications. `nextDueOverdue: true` marks an OPEN overdue slot (anchor passed, still inside its catch-up band). Staleness contract: the snapshot is cache-served, so a `nextDueAt` in the past with `nextDueOverdue: false` means the anchor passed after the snapshot was built — render the plain day summary, never an overdue state.",
|
||||
"Today's medication block: active-medication count, today-window tally (scheduled / taken / skipped), and the earliest next-due slot across active medications. `nextDueOverdue: true` marks an OPEN overdue slot (anchor passed, still inside its catch-up band). `nextDueMedicationId` deep-links a consumer straight to the overdue medication's card. Staleness contract: the snapshot is cache-served, so a `nextDueAt` in the past with `nextDueOverdue: false` means the anchor passed after the snapshot was built — render the plain day summary, never an overdue state.",
|
||||
),
|
||||
// Dashboard hero — health score (warm phase, nullable on a
|
||||
// rollup-coverage miss). Score + band + delta only; the per-pillar
|
||||
|
||||
Reference in new issue
Block a user