mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +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
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "Einnahme konnte nicht gespeichert werden.",
|
||||
"emptyTitle": "Noch keine Medikamente angelegt",
|
||||
"emptyDescription": "Lege zuerst ein Medikament mit Zeitfenster an, damit du die Einnahme protokollieren kannst.",
|
||||
"emptyCta": "Medikament hinzufügen"
|
||||
"emptyCta": "Medikament hinzufügen",
|
||||
"noneActionableTitle": "Heute ist nichts einzunehmen",
|
||||
"noneActionableDescription": "Deine Medikamente sind pausiert, sind nur dokumentiert, oder ihr Einnahmezeitraum hat noch nicht begonnen oder ist schon vorbei. Eine vergangene Einnahme kannst du weiterhin im Verlauf des Medikaments nachtragen.",
|
||||
"noneActionableCta": "Zu den Medikamenten"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "Eintrag verwerfen?",
|
||||
|
||||
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "Could not save medication intake.",
|
||||
"emptyTitle": "No medications yet",
|
||||
"emptyDescription": "Create a medication with a schedule first, then come back here to log the intake.",
|
||||
"emptyCta": "Add medication"
|
||||
"emptyCta": "Add medication",
|
||||
"noneActionableTitle": "Nothing to take today",
|
||||
"noneActionableDescription": "Your medications are paused, kept as a record, or their course has not started or has already ended. A past dose can still be added from a medication's history.",
|
||||
"noneActionableCta": "Open medications"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "Discard this entry?",
|
||||
|
||||
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "No se ha podido guardar la toma.",
|
||||
"emptyTitle": "Aún no hay medicamentos",
|
||||
"emptyDescription": "Crea primero un medicamento con horario, después vuelve aquí para registrar la toma.",
|
||||
"emptyCta": "Añadir medicamento"
|
||||
"emptyCta": "Añadir medicamento",
|
||||
"noneActionableTitle": "Nada que tomar hoy",
|
||||
"noneActionableDescription": "Tus medicamentos están en pausa, se guardan solo como registro, o su tratamiento aún no ha empezado o ya ha terminado. Puedes seguir añadiendo una toma pasada desde el historial del medicamento.",
|
||||
"noneActionableCta": "Abrir medicamentos"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "¿Descartar esta entrada?",
|
||||
|
||||
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "Impossible d’enregistrer la prise.",
|
||||
"emptyTitle": "Aucun médicament pour l’instant",
|
||||
"emptyDescription": "Crée d’abord un médicament avec un horaire, puis reviens ici pour saisir la prise.",
|
||||
"emptyCta": "Ajouter un médicament"
|
||||
"emptyCta": "Ajouter un médicament",
|
||||
"noneActionableTitle": "Rien à prendre aujourd’hui",
|
||||
"noneActionableDescription": "Tes médicaments sont en pause, conservés comme simple fiche, ou leur traitement n’a pas encore commencé ou est déjà terminé. Tu peux toujours ajouter une prise passée depuis l’historique du médicament.",
|
||||
"noneActionableCta": "Ouvrir les médicaments"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "Abandonner cette saisie ?",
|
||||
|
||||
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "Impossibile salvare l’assunzione.",
|
||||
"emptyTitle": "Ancora nessun farmaco",
|
||||
"emptyDescription": "Crea prima un farmaco con un orario, poi torna qui per registrarne l’assunzione.",
|
||||
"emptyCta": "Aggiungi farmaco"
|
||||
"emptyCta": "Aggiungi farmaco",
|
||||
"noneActionableTitle": "Niente da prendere oggi",
|
||||
"noneActionableDescription": "I tuoi farmaci sono in pausa, conservati solo come scheda, oppure la terapia non è ancora iniziata o è già terminata. Puoi comunque aggiungere un’assunzione passata dalla cronologia del farmaco.",
|
||||
"noneActionableCta": "Apri i farmaci"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "Scartare questa voce?",
|
||||
|
||||
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "복용 기록을 저장하지 못했어요.",
|
||||
"emptyTitle": "등록된 약이 없어요",
|
||||
"emptyDescription": "먼저 일정이 있는 약을 만든 뒤, 여기로 돌아와 복용을 기록하세요.",
|
||||
"emptyCta": "약 추가"
|
||||
"emptyCta": "약 추가",
|
||||
"noneActionableTitle": "오늘 복용할 약이 없어요",
|
||||
"noneActionableDescription": "약이 일시 중지되었거나 기록용으로만 보관 중이거나, 복용 기간이 아직 시작되지 않았거나 이미 끝났어요. 지난 복용은 약의 기록에서 계속 추가할 수 있어요.",
|
||||
"noneActionableCta": "약 목록 열기"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "이 기록을 버릴까요?",
|
||||
|
||||
+4
-1
@@ -533,7 +533,10 @@
|
||||
"saveError": "Nie udało się zapisać przyjęcia.",
|
||||
"emptyTitle": "Brak leków",
|
||||
"emptyDescription": "Najpierw dodaj lek z harmonogramem, a potem wróć tutaj, aby zarejestrować jego przyjęcie.",
|
||||
"emptyCta": "Dodaj lek"
|
||||
"emptyCta": "Dodaj lek",
|
||||
"noneActionableTitle": "Dziś nic do przyjęcia",
|
||||
"noneActionableDescription": "Twoje leki są wstrzymane, zapisane tylko jako wpis albo ich kuracja jeszcze się nie zaczęła lub już się skończyła. Przeszłą dawkę nadal możesz dodać w historii leku.",
|
||||
"noneActionableCta": "Otwórz leki"
|
||||
},
|
||||
"quickEntryDiscard": {
|
||||
"title": "Odrzucić ten wpis?",
|
||||
|
||||
@@ -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