diff --git a/messages/de.json b/messages/de.json index fbd360987..e753b8a60 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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", diff --git a/messages/en.json b/messages/en.json index f82aea721..02f68a71f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/es.json b/messages/es.json index ddba1ab5d..d42255e5c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -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", diff --git a/messages/fr.json b/messages/fr.json index f7aa21243..a88341839 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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", diff --git a/messages/it.json b/messages/it.json index 32d387f2e..536e0b18c 100644 --- a/messages/it.json +++ b/messages/it.json @@ -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ì", diff --git a/messages/pl.json b/messages/pl.json index fc8995f31..f5b13eb51 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -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", diff --git a/src/components/medications/medication-card.tsx b/src/components/medications/medication-card.tsx index a3f66f186..b3bbdb8bf 100644 --- a/src/components/medications/medication-card.tsx +++ b/src/components/medications/medication-card.tsx @@ -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); }