mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(medications): read next-due from the server and confirm intake actions
Two fixes on the medication cards.
Next-due: the card recomputed "next intake" client-side from
`daysOfWeek` + `windowStart` alone, ignoring rolling intervals, RRULE,
one-shot doses, and the last-intake anchor. A rolling medication (e.g. a
35-day interval) showed "tomorrow" and never re-anchored after a dose.
`GET /api/medications` already returns `nextDueAt` per medication,
computed by the canonical recurrence engine anchored on the last intake.
Both card variants now render that value directly: the generic card
drops its `getNextOccurrenceTimestamp` walker, and the GLP-1 card drops
its weekly-only predictor that hardcoded a 7-day step. The day-label and
window-range rendering are unchanged; only the timestamp source moves to
the server. `nextDueAt` is threaded through the list page's medication
type into both cards.
Intake confirmation: tapping Taken / Skipped now raises a toast naming
the medication ("… recorded as taken" / "… skipped"), so the action has
clear feedback. The next-intake line refreshes on the invalidated list
query, which the next-due fix above makes accurate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
96f0b225fd
commit
0e3a8f9cd3
12 files changed
+290
-89
No files matched your search
@@ -549,6 +549,8 @@
|
||||
"dayStreak": "Tage Serie",
|
||||
"taken": "Genommen",
|
||||
"skipped": "Übersprungen",
|
||||
"intakeToastTaken": "{name} als genommen erfasst",
|
||||
"intakeToastSkipped": "{name} übersprungen",
|
||||
"weekdaySunday": "Sonntag",
|
||||
"weekdayMonday": "Montag",
|
||||
"weekdayTuesday": "Dienstag",
|
||||
|
||||
@@ -549,6 +549,8 @@
|
||||
"dayStreak": "day streak",
|
||||
"taken": "Taken",
|
||||
"skipped": "Skipped",
|
||||
"intakeToastTaken": "{name} recorded as taken",
|
||||
"intakeToastSkipped": "{name} skipped",
|
||||
"weekdaySunday": "Sunday",
|
||||
"weekdayMonday": "Monday",
|
||||
"weekdayTuesday": "Tuesday",
|
||||
|
||||
@@ -549,6 +549,8 @@
|
||||
"dayStreak": "días de racha",
|
||||
"taken": "Tomada",
|
||||
"skipped": "Omitida",
|
||||
"intakeToastTaken": "{name} registrada como tomada",
|
||||
"intakeToastSkipped": "{name} omitida",
|
||||
"weekdaySunday": "Domingo",
|
||||
"weekdayMonday": "Lunes",
|
||||
"weekdayTuesday": "Martes",
|
||||
|
||||
@@ -549,6 +549,8 @@
|
||||
"dayStreak": "jours de série",
|
||||
"taken": "Pris",
|
||||
"skipped": "Sauté",
|
||||
"intakeToastTaken": "{name} enregistré comme pris",
|
||||
"intakeToastSkipped": "{name} sauté",
|
||||
"weekdaySunday": "Dimanche",
|
||||
"weekdayMonday": "Lundi",
|
||||
"weekdayTuesday": "Mardi",
|
||||
|
||||
@@ -549,6 +549,8 @@
|
||||
"dayStreak": "giorni di serie",
|
||||
"taken": "Assunta",
|
||||
"skipped": "Saltata",
|
||||
"intakeToastTaken": "{name} registrata come assunta",
|
||||
"intakeToastSkipped": "{name} saltata",
|
||||
"weekdaySunday": "Domenica",
|
||||
"weekdayMonday": "Lunedì",
|
||||
"weekdayTuesday": "Martedì",
|
||||
|
||||
@@ -549,6 +549,8 @@
|
||||
"dayStreak": "dni serii",
|
||||
"taken": "Przyjęte",
|
||||
"skipped": "Pominięte",
|
||||
"intakeToastTaken": "{name} oznaczone jako przyjęte",
|
||||
"intakeToastSkipped": "{name} pominięte",
|
||||
"weekdaySunday": "Niedziela",
|
||||
"weekdayMonday": "Poniedziałek",
|
||||
"weekdayTuesday": "Wtorek",
|
||||
|
||||
@@ -48,6 +48,12 @@ interface Medication {
|
||||
notificationsEnabled: boolean;
|
||||
pausedAt: string | null;
|
||||
lastTakenAt: string | null;
|
||||
/**
|
||||
* v1.8.4 — server-computed next due instant (canonical recurrence
|
||||
* engine). Threaded straight through to both card variants, which render
|
||||
* it instead of re-deriving the timestamp client-side.
|
||||
*/
|
||||
nextDueAt?: string | null;
|
||||
/** v1.5 — medication-level course start date (ISO string). */
|
||||
startsOn?: string | null;
|
||||
/** v1.5 — medication-level course end date (ISO string). */
|
||||
|
||||
@@ -49,11 +49,14 @@ const med7p5: Glp1Medication = {
|
||||
// not "today/yesterday" — keeps the SSR string deterministic-ish
|
||||
// across local clocks while still exercising the with-site branch.
|
||||
lastTakenAt: "2026-05-08T08:00:00.000Z",
|
||||
// v1.8.4 — the card now reads the server-computed next-due instant
|
||||
// directly instead of re-deriving it client-side. A far-future fixed
|
||||
// date keeps the "in N days" branch deterministic across host clocks.
|
||||
nextDueAt: "2099-05-09T08:00:00.000Z",
|
||||
schedules: [
|
||||
{
|
||||
id: "s1",
|
||||
// Saturday only — one entry triggers the weekly-preset branch
|
||||
// in `predictNextWeeklyDate()`.
|
||||
// Saturday only.
|
||||
windowStart: "08:00",
|
||||
windowEnd: "20:00",
|
||||
label: null,
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { MedicationCard } from "@/components/medications/medication-card";
|
||||
|
||||
/**
|
||||
* v1.8.4 — the generic medication card must render the server-computed
|
||||
* `nextDueAt` (canonical recurrence engine, anchored on the last intake)
|
||||
* as the "next intake" timestamp, NOT a client-side daysOfWeek walker.
|
||||
*
|
||||
* The legacy walker only read `daysOfWeek` + `windowStart` and ignored
|
||||
* rolling / RRULE / one-shot cadences, so a rolling medication (e.g.
|
||||
* `rollingIntervalDays = 35`) wrongly read "tomorrow" and never
|
||||
* re-anchored after an intake. These tests pin that the card now
|
||||
* surfaces the server value, while the day-label / window-range
|
||||
* rendering for ordinary single-time schedules is unchanged.
|
||||
*/
|
||||
|
||||
function makeClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0, staleTime: Infinity },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function render(node: React.ReactNode, client: QueryClient) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<QueryClientProvider client={client}>{node}</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function seedCompliance(client: QueryClient, medId: string) {
|
||||
client.setQueryData(["medications", medId, "compliance"], {
|
||||
compliance7: { rate: 90, streak: 0, totalExpected: 7, taken: 6 },
|
||||
compliance30: { rate: 88 },
|
||||
});
|
||||
}
|
||||
|
||||
// A schedule whose window has already passed for today (01:00–02:00) so
|
||||
// the card surfaces the "next intake" line rather than the take-now
|
||||
// pill, regardless of host clock.
|
||||
const pastWindow = {
|
||||
windowStart: "01:00",
|
||||
windowEnd: "02:00",
|
||||
label: null,
|
||||
daysOfWeek: null,
|
||||
dose: null,
|
||||
};
|
||||
|
||||
describe("<MedicationCard> — next-due reads from server nextDueAt", () => {
|
||||
it("renders the server nextDueAt date for a rolling medication, not a today/tomorrow walker value", () => {
|
||||
// Rolling cadence: server anchors the next dose 35 days out. The
|
||||
// card must surface that calendar date — the legacy walker would
|
||||
// have produced "tomorrow" off the schedule window alone.
|
||||
const now = new Date();
|
||||
const due = new Date(now);
|
||||
due.setDate(due.getDate() + 35);
|
||||
// Midday so the Berlin display can't cross a day boundary regardless
|
||||
// of the host timezone.
|
||||
due.setHours(12, 0, 0, 0);
|
||||
|
||||
const rollingMed = {
|
||||
id: "med-rolling-1",
|
||||
name: "Mounjaro",
|
||||
dose: "7.5 mg",
|
||||
category: "OTHER",
|
||||
treatmentClass: undefined as string | undefined,
|
||||
active: true,
|
||||
notificationsEnabled: true,
|
||||
pausedAt: null,
|
||||
lastTakenAt: null,
|
||||
todayEventCount: 0,
|
||||
nextDueAt: due.toISOString(),
|
||||
schedules: [{ id: "s-rolling-1", ...pastWindow }],
|
||||
};
|
||||
|
||||
const client = makeClient();
|
||||
seedCompliance(client, rollingMed.id);
|
||||
|
||||
const html = render(
|
||||
<MedicationCard
|
||||
medication={rollingMed}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
// The next-intake line surfaces.
|
||||
expect(html).toContain("Next intake:");
|
||||
// 35 days out is past the 5-day threshold, so the card renders the
|
||||
// full weekday-anchored short date (e.g. "Mon, 07/06") derived from
|
||||
// the SERVER instant — NOT a today/tomorrow walker value. Pin the
|
||||
// day-of-month + month of the SERVER date (Berlin-rendered), proving
|
||||
// the date tracks `nextDueAt` rather than the schedule window alone.
|
||||
const berlin = new Intl.DateTimeFormat("en", {
|
||||
timeZone: "Europe/Berlin",
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
}).format(due);
|
||||
expect(html).toContain(berlin);
|
||||
expect(html).not.toContain("Next intake: Today,");
|
||||
expect(html).not.toContain("Next intake: Tomorrow,");
|
||||
});
|
||||
|
||||
it("falls back to the schedule window range when nextDueAt is absent", () => {
|
||||
// A medication with no server next-due (e.g. ended course) still
|
||||
// renders the window-range line off its first schedule; it just
|
||||
// omits the day label.
|
||||
const med = {
|
||||
id: "med-nodue-1",
|
||||
name: "Ramipril",
|
||||
dose: "5 mg",
|
||||
category: "BLOOD_PRESSURE",
|
||||
treatmentClass: undefined as string | undefined,
|
||||
active: true,
|
||||
notificationsEnabled: true,
|
||||
pausedAt: null,
|
||||
lastTakenAt: null,
|
||||
todayEventCount: 0,
|
||||
nextDueAt: null,
|
||||
schedules: [{ id: "s-nodue-1", ...pastWindow }],
|
||||
};
|
||||
|
||||
const client = makeClient();
|
||||
seedCompliance(client, med.id);
|
||||
|
||||
const html = render(
|
||||
<MedicationCard
|
||||
medication={med}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
expect(html).toContain("Next intake:");
|
||||
// No day label is prepended when the server gave no instant — the
|
||||
// line goes straight to the window range.
|
||||
expect(html).not.toContain("Next intake: Today,");
|
||||
expect(html).not.toContain("Next intake: Tomorrow,");
|
||||
});
|
||||
|
||||
it("renders the day label for a single-time daysOfWeek medication due tomorrow", () => {
|
||||
// A daily / weekly medication whose server next-due is tomorrow
|
||||
// still renders the relative "tomorrow" label — proving the ordinary
|
||||
// single-time path keeps working.
|
||||
const now = new Date();
|
||||
const due = new Date(now);
|
||||
due.setDate(due.getDate() + 1);
|
||||
due.setHours(8, 0, 0, 0);
|
||||
|
||||
const med = {
|
||||
id: "med-daily-1",
|
||||
name: "Ramipril",
|
||||
dose: "5 mg",
|
||||
category: "BLOOD_PRESSURE",
|
||||
treatmentClass: undefined as string | undefined,
|
||||
active: true,
|
||||
notificationsEnabled: true,
|
||||
pausedAt: null,
|
||||
lastTakenAt: null,
|
||||
todayEventCount: 0,
|
||||
nextDueAt: due.toISOString(),
|
||||
schedules: [
|
||||
{
|
||||
id: "s-daily-1",
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
label: null,
|
||||
daysOfWeek: null,
|
||||
dose: "5 mg",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const client = makeClient();
|
||||
seedCompliance(client, med.id);
|
||||
|
||||
const html = render(
|
||||
<MedicationCard
|
||||
medication={med}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onOpenAdvanced={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
expect(html).toContain("Next intake:");
|
||||
// Relative day label from the server instant (capitalised "Tomorrow").
|
||||
expect(html).toContain("Tomorrow,");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
@@ -79,6 +80,14 @@ export interface Glp1Medication {
|
||||
pausedAt: string | null;
|
||||
lastTakenAt: string | null;
|
||||
todayEventCount?: number;
|
||||
/**
|
||||
* v1.8.4 — server-computed next due instant from `GET /api/medications`
|
||||
* (the canonical recurrence engine anchored on the last intake). The card
|
||||
* renders this directly so a rolling GLP-1 with an interval other than the
|
||||
* weekly default re-anchors correctly; the old client-side predictor only
|
||||
* walked a single hardcoded weekly day-of-week.
|
||||
*/
|
||||
nextDueAt?: string | null;
|
||||
schedules: ScheduleLite[];
|
||||
}
|
||||
|
||||
@@ -126,14 +135,6 @@ const DAY_KEYS = [
|
||||
"medications.daysSat",
|
||||
] as const;
|
||||
|
||||
function parseDayList(raw: string | null): number[] {
|
||||
if (!raw) return [];
|
||||
return raw
|
||||
.split(",")
|
||||
.map((p) => Number(p.trim()))
|
||||
.filter((n) => Number.isInteger(n) && n >= 0 && n <= 6);
|
||||
}
|
||||
|
||||
function diffDays(target: Date, from: Date): number {
|
||||
const ms =
|
||||
Date.UTC(target.getFullYear(), target.getMonth(), target.getDate()) -
|
||||
@@ -141,26 +142,22 @@ function diffDays(target: Date, from: Date): number {
|
||||
return Math.round(ms / (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
function predictNextWeeklyDate(
|
||||
schedule: ScheduleLite | null,
|
||||
lastTakenAt: string | null,
|
||||
/**
|
||||
* v1.8.4 — the next injection instant now comes from the server's
|
||||
* `nextDueAt` (the canonical recurrence engine, anchored on the last
|
||||
* intake). The legacy client-side predictor only handled a single
|
||||
* hardcoded weekly day-of-week and re-anchored at +7 days, so a rolling
|
||||
* GLP-1 with a non-weekly interval rendered the wrong date and never
|
||||
* advanced after an injection.
|
||||
*/
|
||||
function nextInjectionFromServer(
|
||||
nextDueAt: string | null | undefined,
|
||||
now: Date,
|
||||
): { date: Date; daysAway: number } | null {
|
||||
if (!schedule) return null;
|
||||
const dow = parseDayList(schedule.daysOfWeek);
|
||||
if (dow.length !== 1) return null;
|
||||
const target = dow[0];
|
||||
const anchor = lastTakenAt ? new Date(lastTakenAt) : now;
|
||||
const cursor = new Date(anchor);
|
||||
cursor.setHours(0, 0, 0, 0);
|
||||
for (let i = 1; i <= 14; i += 1) {
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
if (cursor.getDay() === target) {
|
||||
const days = diffDays(cursor, now);
|
||||
return { date: cursor, daysAway: Math.max(0, days) };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
if (!nextDueAt) return null;
|
||||
const date = new Date(nextDueAt);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return { date, daysAway: Math.max(0, diffDays(date, now)) };
|
||||
}
|
||||
|
||||
export function Glp1MedicationCard({
|
||||
@@ -234,6 +231,14 @@ export function Glp1MedicationCard({
|
||||
body: JSON.stringify({ skipped }),
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success(
|
||||
t(
|
||||
skipped
|
||||
? "medications.intakeToastSkipped"
|
||||
: "medications.intakeToastTaken",
|
||||
{ name: medication.name },
|
||||
),
|
||||
);
|
||||
await invalidateKeys(queryClient, medicationDependentKeys);
|
||||
}
|
||||
} finally {
|
||||
@@ -243,7 +248,7 @@ export function Glp1MedicationCard({
|
||||
|
||||
const schedule = medication.schedules[0] ?? null;
|
||||
const now = new Date();
|
||||
const next = predictNextWeeklyDate(schedule, medication.lastTakenAt, now);
|
||||
const next = nextInjectionFromServer(medication.nextDueAt, now);
|
||||
const rate7 = compliance?.compliance7?.rate ?? 0;
|
||||
const rate30 = compliance?.compliance30?.rate ?? 0;
|
||||
const streak = compliance?.compliance7?.streak ?? 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useReducer } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
import { MedicationCardMenu } from "@/components/medications/medication-card-menu";
|
||||
@@ -9,12 +10,10 @@ import { MedicationStateBadges } from "@/components/medications/card-parts/medic
|
||||
import { MedicationStatusPill } from "@/components/medications/card-parts/medication-status-pill";
|
||||
import { MedicationComplianceBars } from "@/components/medications/card-parts/medication-compliance-bars";
|
||||
import { MedicationIntakeActions } from "@/components/medications/card-parts/medication-intake-actions";
|
||||
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
|
||||
import { formatTimeWindowRange } from "@/lib/time-window-format";
|
||||
import { formatDateTime, formatTime } from "@/lib/format";
|
||||
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
|
||||
import {
|
||||
parseTimeToMinutes,
|
||||
reduceCurrentWindowStatus,
|
||||
toBerlinDate,
|
||||
} from "@/lib/medications/window-status";
|
||||
@@ -52,6 +51,14 @@ interface Medication {
|
||||
pausedAt: string | null;
|
||||
lastTakenAt: string | null;
|
||||
todayEventCount?: number;
|
||||
/**
|
||||
* v1.8.4 — server-computed next due instant from `GET /api/medications`
|
||||
* (`computeNextDueAt` → the canonical recurrence engine, anchored on the
|
||||
* last intake). The card renders this directly instead of re-deriving the
|
||||
* timestamp client-side; the engine honours rolling / RRULE / one-shot
|
||||
* cadences that the legacy daysOfWeek-only walker ignored.
|
||||
*/
|
||||
nextDueAt?: string | null;
|
||||
schedules: Schedule[];
|
||||
}
|
||||
|
||||
@@ -86,47 +93,6 @@ interface MedicationCardProps {
|
||||
onOpenAdvanced: (med: Medication) => void;
|
||||
}
|
||||
|
||||
function getNextOccurrenceTimestamp(
|
||||
schedule: Schedule,
|
||||
nowBerlin: Date,
|
||||
): number | null {
|
||||
const recurrence = parseScheduleRecurrence(schedule.daysOfWeek);
|
||||
const baseDay = new Date(nowBerlin);
|
||||
baseDay.setHours(0, 0, 0, 0);
|
||||
|
||||
const nowMinutes = nowBerlin.getHours() * 60 + nowBerlin.getMinutes();
|
||||
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
||||
const baseWeekIndex = Math.floor(baseDay.getTime() / weekMs);
|
||||
const startMinutes = parseTimeToMinutes(schedule.windowStart);
|
||||
|
||||
for (let offset = 0; offset <= 400; offset += 1) {
|
||||
const candidate = new Date(baseDay);
|
||||
candidate.setDate(baseDay.getDate() + offset);
|
||||
|
||||
const candidateWeekIndex = Math.floor(candidate.getTime() / weekMs);
|
||||
const isAllowedWeek =
|
||||
recurrence.intervalWeeks <= 1 ||
|
||||
candidateWeekIndex % recurrence.intervalWeeks ===
|
||||
baseWeekIndex % recurrence.intervalWeeks;
|
||||
if (!isAllowedWeek) continue;
|
||||
|
||||
if (
|
||||
recurrence.daysOfWeek.length > 0 &&
|
||||
!recurrence.daysOfWeek.includes(candidate.getDay())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offset === 0 && startMinutes <= nowMinutes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return candidate.getTime() + startMinutes * 60 * 1000;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function MedicationCard({
|
||||
medication,
|
||||
onEdit,
|
||||
@@ -177,6 +143,14 @@ export function MedicationCard({
|
||||
body: JSON.stringify({ skipped }),
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success(
|
||||
t(
|
||||
skipped
|
||||
? "medications.intakeToastSkipped"
|
||||
: "medications.intakeToastTaken",
|
||||
{ name: medication.name },
|
||||
),
|
||||
);
|
||||
await invalidateKeys(queryClient, medicationDependentKeys);
|
||||
}
|
||||
} finally {
|
||||
@@ -194,19 +168,18 @@ export function MedicationCard({
|
||||
a.windowEnd.localeCompare(b.windowEnd),
|
||||
);
|
||||
const nowBerlin = toBerlinDate(new Date());
|
||||
const nextOccurrence =
|
||||
sortedSchedules.length > 0
|
||||
? (sortedSchedules
|
||||
.map((schedule) => ({
|
||||
schedule,
|
||||
nextAt: getNextOccurrenceTimestamp(schedule, nowBerlin),
|
||||
}))
|
||||
.filter((entry): entry is { schedule: Schedule; nextAt: number } =>
|
||||
Number.isFinite(entry.nextAt),
|
||||
)
|
||||
.sort((a, b) => a.nextAt - b.nextAt)[0] ?? null)
|
||||
: null;
|
||||
const nextSchedule = nextOccurrence?.schedule ?? sortedSchedules[0] ?? null;
|
||||
// v1.8.4 — the next-due instant comes from the server (`nextDueAt`,
|
||||
// computed by the canonical recurrence engine anchored on the last
|
||||
// intake). The day label below derives from it; the window-range /
|
||||
// dose / label still come from the earliest configured schedule. The
|
||||
// legacy client-side daysOfWeek walker only ever read daysOfWeek +
|
||||
// windowStart and silently mis-anchored rolling / RRULE / one-shot
|
||||
// cadences, so it is gone.
|
||||
const nextDueMs = medication.nextDueAt
|
||||
? new Date(medication.nextDueAt).getTime()
|
||||
: NaN;
|
||||
const nextAt = Number.isFinite(nextDueMs) ? nextDueMs : undefined;
|
||||
const nextSchedule = sortedSchedules[0] ?? null;
|
||||
|
||||
const lateMinutes = thresholds?.lateMinutes ?? 120;
|
||||
const missedMinutes = thresholds?.missedMinutes ?? 240;
|
||||
@@ -291,7 +264,6 @@ export function MedicationCard({
|
||||
currentWindowStatus.status !== "in_window" &&
|
||||
(() => {
|
||||
const s = nextSchedule;
|
||||
const nextAt = nextOccurrence?.nextAt;
|
||||
|
||||
// Format day label relative to today
|
||||
let dayLabel = "";
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* the v1.5.0 cut wires only the reminder worker (via
|
||||
* `worker-helpers.ts`) through it. The today-projector
|
||||
* (`expandTodayIntakes`), the cadence chart (`expandScheduleSlots`),
|
||||
* the medication card (`getNextOccurrenceTimestamp`), and the
|
||||
* form-level helpers continue on the legacy walker through v1.5.x and
|
||||
* migrate in v1.5.1 per the read-flip plan.
|
||||
* and the form-level helpers continue on the legacy walker through
|
||||
* v1.5.x and migrate per the read-flip plan. The medication card's
|
||||
* "next intake" line reads the server-computed `nextDueAt` (this engine
|
||||
* via `computeNextDueAt`) directly as of v1.8.4.
|
||||
*
|
||||
* Dispatch tiers (first matching tier wins):
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user