mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(medications): surface failed dose logging and add an undo affordance
A failed take/skip POST cleared the spinner without any feedback, so the user believed the dose was logged when it was not. Show an error toast on a non-ok response or a thrown request, and never show the success confirmation in that case. On a successful take/skip, attach an Undo action to the toast that soft-deletes the just-recorded intake via the per-event delete route, so a misclick no longer needs a trip through the intake history to correct.
This commit is contained in:
1 parent
0cb8070a22
commit
8434af40cb
7 files changed
+88
-21
No files matched your search
@@ -622,6 +622,10 @@
|
||||
"skipped": "Übersprungen",
|
||||
"intakeToastTaken": "{name} als genommen erfasst",
|
||||
"intakeToastSkipped": "{name} übersprungen",
|
||||
"intakeToastFailed": "{name} konnte nicht erfasst werden – bitte erneut versuchen",
|
||||
"intakeUndo": "Rückgängig",
|
||||
"intakeUndone": "Eintrag zurückgenommen",
|
||||
"intakeUndoFailed": "Rückgängig fehlgeschlagen – im Verlauf korrigieren",
|
||||
"weekdaySunday": "Sonntag",
|
||||
"weekdayMonday": "Montag",
|
||||
"weekdayTuesday": "Dienstag",
|
||||
|
||||
@@ -622,6 +622,10 @@
|
||||
"skipped": "Skipped",
|
||||
"intakeToastTaken": "{name} recorded as taken",
|
||||
"intakeToastSkipped": "{name} skipped",
|
||||
"intakeToastFailed": "Could not record {name} — try again",
|
||||
"intakeUndo": "Undo",
|
||||
"intakeUndone": "Entry reverted",
|
||||
"intakeUndoFailed": "Could not undo — open the history to fix it",
|
||||
"weekdaySunday": "Sunday",
|
||||
"weekdayMonday": "Monday",
|
||||
"weekdayTuesday": "Tuesday",
|
||||
|
||||
@@ -622,6 +622,10 @@
|
||||
"skipped": "Omitida",
|
||||
"intakeToastTaken": "{name} registrada como tomada",
|
||||
"intakeToastSkipped": "{name} omitida",
|
||||
"intakeToastFailed": "No se pudo registrar {name}; inténtalo de nuevo",
|
||||
"intakeUndo": "Deshacer",
|
||||
"intakeUndone": "Entrada revertida",
|
||||
"intakeUndoFailed": "No se pudo deshacer; corrígelo en el historial",
|
||||
"weekdaySunday": "Domingo",
|
||||
"weekdayMonday": "Lunes",
|
||||
"weekdayTuesday": "Martes",
|
||||
|
||||
@@ -622,6 +622,10 @@
|
||||
"skipped": "Sauté",
|
||||
"intakeToastTaken": "{name} enregistré comme pris",
|
||||
"intakeToastSkipped": "{name} sauté",
|
||||
"intakeToastFailed": "Impossible d'enregistrer {name} — réessayez",
|
||||
"intakeUndo": "Annuler",
|
||||
"intakeUndone": "Entrée annulée",
|
||||
"intakeUndoFailed": "Annulation impossible — corrigez dans l'historique",
|
||||
"weekdaySunday": "Dimanche",
|
||||
"weekdayMonday": "Lundi",
|
||||
"weekdayTuesday": "Mardi",
|
||||
|
||||
@@ -622,6 +622,10 @@
|
||||
"skipped": "Saltata",
|
||||
"intakeToastTaken": "{name} registrata come assunta",
|
||||
"intakeToastSkipped": "{name} saltata",
|
||||
"intakeToastFailed": "Impossibile registrare {name}; riprova",
|
||||
"intakeUndo": "Annulla",
|
||||
"intakeUndone": "Voce annullata",
|
||||
"intakeUndoFailed": "Impossibile annullare; correggi nella cronologia",
|
||||
"weekdaySunday": "Domenica",
|
||||
"weekdayMonday": "Lunedì",
|
||||
"weekdayTuesday": "Martedì",
|
||||
|
||||
@@ -622,6 +622,10 @@
|
||||
"skipped": "Pominięte",
|
||||
"intakeToastTaken": "{name} oznaczone jako przyjęte",
|
||||
"intakeToastSkipped": "{name} pominięte",
|
||||
"intakeToastFailed": "Nie udało się zapisać {name} — spróbuj ponownie",
|
||||
"intakeUndo": "Cofnij",
|
||||
"intakeUndone": "Wpis cofnięty",
|
||||
"intakeUndoFailed": "Nie udało się cofnąć — popraw w historii",
|
||||
"weekdaySunday": "Niedziela",
|
||||
"weekdayMonday": "Poniedziałek",
|
||||
"weekdayTuesday": "Wtorek",
|
||||
|
||||
@@ -168,6 +168,26 @@ export function MedicationCard({
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// v1.11.3 C2 — reverse the just-recorded intake via the soft-delete
|
||||
// route. Surfaced from the success toast's Undo action so a misclicked
|
||||
// take / skip no longer needs a history dive to correct.
|
||||
async function undoIntake(eventId: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/medications/${medication.id}/intake/${eventId}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
toast.error(t("medications.intakeUndoFailed"));
|
||||
return;
|
||||
}
|
||||
await invalidateKeys(queryClient, medicationDependentKeys);
|
||||
toast.success(t("medications.intakeUndone"));
|
||||
} catch {
|
||||
toast.error(t("medications.intakeUndoFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function recordIntake(skipped: boolean) {
|
||||
const key = skipped ? "skip" : "take";
|
||||
setIntakeLoading(key);
|
||||
@@ -177,29 +197,52 @@ export function MedicationCard({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skipped }),
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success(
|
||||
t(
|
||||
skipped
|
||||
? "medications.intakeToastSkipped"
|
||||
: "medications.intakeToastTaken",
|
||||
{ name: medication.name },
|
||||
),
|
||||
// v1.11.3 C1 — a failed POST used to clear the spinner silently, so
|
||||
// the user believed the dose was logged when it was not. Surface the
|
||||
// failure and never show the success confirmation in that case.
|
||||
if (!res.ok) {
|
||||
toast.error(
|
||||
t("medications.intakeToastFailed", { name: medication.name }),
|
||||
);
|
||||
await invalidateKeys(queryClient, medicationDependentKeys);
|
||||
// v1.8.5 — after a TAKEN dose on a tracking-enabled injection,
|
||||
// prompt (skippably) for the site. The dialog PATCHes it onto
|
||||
// the just-created event via the status-toggle route.
|
||||
if (!skipped && tracksInjection) {
|
||||
try {
|
||||
const json = await res.json();
|
||||
const eventId = json?.data?.id as string | undefined;
|
||||
if (eventId) setSiteIntakeId(eventId);
|
||||
} catch {
|
||||
/* dose recorded; the site prompt is best-effort */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The POST returns the created event (`apiSuccess(event, 201)`); its
|
||||
// id drives both the Undo affordance and the optional injection-site
|
||||
// prompt below.
|
||||
let eventId: string | undefined;
|
||||
try {
|
||||
const json = await res.json();
|
||||
eventId = json?.data?.id as string | undefined;
|
||||
} catch {
|
||||
/* dose recorded; the body is best-effort for the id */
|
||||
}
|
||||
toast.success(
|
||||
t(
|
||||
skipped
|
||||
? "medications.intakeToastSkipped"
|
||||
: "medications.intakeToastTaken",
|
||||
{ name: medication.name },
|
||||
),
|
||||
eventId
|
||||
? {
|
||||
action: {
|
||||
label: t("medications.intakeUndo"),
|
||||
onClick: () => void undoIntake(eventId),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
await invalidateKeys(queryClient, medicationDependentKeys);
|
||||
// v1.8.5 — after a TAKEN dose on a tracking-enabled injection,
|
||||
// prompt (skippably) for the site. The dialog PATCHes it onto
|
||||
// the just-created event via the status-toggle route.
|
||||
if (!skipped && tracksInjection && eventId) {
|
||||
setSiteIntakeId(eventId);
|
||||
}
|
||||
} catch {
|
||||
toast.error(
|
||||
t("medications.intakeToastFailed", { name: medication.name }),
|
||||
);
|
||||
} finally {
|
||||
setIntakeLoading(null);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user