mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(medications): dashboard quick-add offers only doses the server calls actionable
The dashboard and capture-picker intake form filtered the medication list on `active && trackIntake`, so a course that had ended (still active, nobody paused it) was offered for a dose while the cards, the table and "take all" already followed the server's `intakeActionable`. The picker now renders from that one answer as well. When medications exist but none offers a dose today, the empty state no longer claims there are no medications; it says nothing is due and points to the medication history for a past dose.
This commit is contained in:
1 parent
95d3676821
commit
ca10f2e5df
9 files changed
+97
-15
No files matched your search
@@ -71,6 +71,7 @@ function makeMed(
|
||||
name: `Med ${id}`,
|
||||
dose: "5 mg",
|
||||
active: true,
|
||||
intakeActionable: true,
|
||||
schedules: [],
|
||||
lastTakenAt: null,
|
||||
todayEventCount: 0,
|
||||
@@ -316,6 +317,48 @@ describe("<MedicationIntakeQuickAdd> — SSR contract", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// #1040 — the picker offers what the server calls actionable, nothing
|
||||
// it re-derives. An ended course is still `active` (nobody paused it),
|
||||
// which is exactly the case the old `active && trackIntake` filter let
|
||||
// through.
|
||||
it("does not offer an ended course, even though it is still active (#1040)", () => {
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: [
|
||||
makeMed("ended", {
|
||||
name: "Alpha",
|
||||
active: true,
|
||||
intakeActionable: false,
|
||||
}),
|
||||
makeMed("current", { name: "Zulu" }),
|
||||
],
|
||||
isLoading: false,
|
||||
});
|
||||
const html = renderSSR(<MedicationIntakeQuickAdd />);
|
||||
// Alphabetical fallback would land on "Alpha" if it were offered.
|
||||
expect(html).toMatch(
|
||||
/data-slot="select-value"[^>]*>[\s\S]*?Zulu[\s\S]*?<\/span>/,
|
||||
);
|
||||
expect(html).not.toContain("Alpha");
|
||||
});
|
||||
|
||||
it("shows the empty state when no medication is actionable today (#1040)", () => {
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: [
|
||||
makeMed("ended", { name: "Alpha", intakeActionable: false }),
|
||||
makeMed("upcoming", { name: "Beta", intakeActionable: false }),
|
||||
],
|
||||
isLoading: false,
|
||||
});
|
||||
const html = renderSSR(<MedicationIntakeQuickAdd />);
|
||||
expect(html).toContain('data-testid="medication-intake-quick-add-empty"');
|
||||
expect(html).not.toContain(
|
||||
'data-testid="medication-intake-quick-add-form"',
|
||||
);
|
||||
// The user has medications, so "no medications yet" would be untrue.
|
||||
expect(html).toContain("Nothing to take today");
|
||||
expect(html).not.toContain("No medications yet");
|
||||
});
|
||||
|
||||
it("does not leak raw i18n keys in either locale", () => {
|
||||
mockUseQuery.mockReturnValue({ data: [], isLoading: false });
|
||||
const en = renderSSR(<MedicationIntakeQuickAdd />, "en");
|
||||
|
||||
@@ -81,8 +81,13 @@ interface Schedule extends ScheduleWindowInput {
|
||||
export interface MedicationOption extends DefaultMedicationOption {
|
||||
dose: string;
|
||||
schedules: Schedule[];
|
||||
/** v1.39.1 (#1033) — false: kept as a record, not offered for a dose. */
|
||||
trackIntake?: boolean;
|
||||
/**
|
||||
* The server's answer to "does this medication offer a dose today"
|
||||
* (active, intake tracked, course includes today; see
|
||||
* `src/lib/medications/intake-actionable.ts`). The list read always
|
||||
* carries it, so the picker renders from it and re-derives nothing.
|
||||
*/
|
||||
intakeActionable: boolean;
|
||||
}
|
||||
|
||||
interface MedicationIntakeQuickAddProps {
|
||||
@@ -141,9 +146,10 @@ export function MedicationIntakeQuickAdd({
|
||||
const medications = useMemo(
|
||||
() =>
|
||||
Array.isArray(medicationsRaw)
|
||||
? // v1.39.1 (#1033) — a medication with intake tracking off is
|
||||
// kept as a record; the dose capture does not offer it.
|
||||
medicationsRaw.filter((m) => m.active && m.trackIntake !== false)
|
||||
? // #1040 — offer exactly what the cards, the table and "take
|
||||
// all" offer: a paused, record-only, not-yet-started or ended
|
||||
// course is not a dose to capture now.
|
||||
medicationsRaw.filter((m) => m.intakeActionable === true)
|
||||
: [],
|
||||
[medicationsRaw],
|
||||
);
|
||||
@@ -314,6 +320,10 @@ export function MedicationIntakeQuickAdd({
|
||||
// anlegen</Link>` so the footer-slot promise holds across both
|
||||
// branches.
|
||||
if (!medicationsLoading && medications.length === 0) {
|
||||
// Medications exist, none offers a dose today: say that, rather than
|
||||
// "no medications yet", which would be untrue.
|
||||
const noneActionable =
|
||||
Array.isArray(medicationsRaw) && medicationsRaw.length > 0;
|
||||
const emptyFooter = (
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
{onCancel && (
|
||||
@@ -328,7 +338,9 @@ export function MedicationIntakeQuickAdd({
|
||||
)}
|
||||
<Button asChild className="min-h-11 sm:min-h-9">
|
||||
<Link href="/medications">
|
||||
{t("dashboard.medicationIntakeQuickAdd.emptyCta")}
|
||||
{noneActionable
|
||||
? t("dashboard.medicationIntakeQuickAdd.noneActionableCta")
|
||||
: t("dashboard.medicationIntakeQuickAdd.emptyCta")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -344,10 +356,16 @@ export function MedicationIntakeQuickAdd({
|
||||
<Pill className="text-muted-foreground h-6 w-6" aria-hidden="true" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{t("dashboard.medicationIntakeQuickAdd.emptyTitle")}
|
||||
{noneActionable
|
||||
? t("dashboard.medicationIntakeQuickAdd.noneActionableTitle")
|
||||
: t("dashboard.medicationIntakeQuickAdd.emptyTitle")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("dashboard.medicationIntakeQuickAdd.emptyDescription")}
|
||||
{noneActionable
|
||||
? t(
|
||||
"dashboard.medicationIntakeQuickAdd.noneActionableDescription",
|
||||
)
|
||||
: t("dashboard.medicationIntakeQuickAdd.emptyDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user