diff --git a/src/components/dashboard/medication-intake-quick-add.tsx b/src/components/dashboard/medication-intake-quick-add.tsx index b3d0cd6fb..e6d5b7ee7 100644 --- a/src/components/dashboard/medication-intake-quick-add.tsx +++ b/src/components/dashboard/medication-intake-quick-add.tsx @@ -30,6 +30,7 @@ import { } from "@/lib/medications/window-status"; import { useAuth } from "@/hooks/use-auth"; import { ApiError, apiGet, apiPost } from "@/lib/api/api-fetch"; +import { runUndoIntake } from "@/components/medications/use-medication-intake"; /** * v1.4.37 W7b — dashboard "Hinzufügen" → "Medikamenteneinnahme" quick-add. @@ -271,11 +272,18 @@ export function MedicationIntakeQuickAdd({ const doseDeviates = trimmedDose.length > 0 && trimmedDose !== (selectedMedication?.dose ?? "").trim(); - await apiPost(`/api/medications/${medicationId}/intake`, { - takenAt: timestamp, - skipped: false, - ...(doseDeviates && { doseTaken: trimmedDose }), - }); + // v1.30.1 M7 — the route returns the created event + // (`apiSuccess(event, 201)`), same shape the card's take/skip path + // already reads; the id drives the new Undo action below. + const created = await apiPost<{ id?: string } | undefined>( + `/api/medications/${medicationId}/intake`, + { + takenAt: timestamp, + skipped: false, + ...(doseDeviates && { doseTaken: trimmedDose }), + }, + ); + const eventId = created?.id; await invalidateKeys(queryClient, medicationDependentKeys); // Fan-out to every inline compliance chart key (one per medication) @@ -284,7 +292,26 @@ export function MedicationIntakeQuickAdd({ queryKey: queryKeys.complianceChartInline(), }); - toast.success(t("common.saved")); + toast.success( + t("common.saved"), + eventId && selectedMedication + ? { + action: { + label: t("medications.intakeUndo"), + onClick: () => + void runUndoIntake({ + medication: { + id: medicationId, + name: selectedMedication.name, + }, + eventId, + t, + queryClient, + }), + }, + } + : undefined, + ); onSuccess?.(); } catch (err) { setError( diff --git a/src/components/medications/log-intake-dialog.tsx b/src/components/medications/log-intake-dialog.tsx index 47604cbee..db4a1e12b 100644 --- a/src/components/medications/log-intake-dialog.tsx +++ b/src/components/medications/log-intake-dialog.tsx @@ -33,7 +33,10 @@ import { NativeSelect } from "@/components/ui/native-select"; import { Switch } from "@/components/ui/switch"; import { useTranslations } from "@/lib/i18n/context"; import { formatDose } from "@/lib/medications/format-dose"; -import { runLogIntake } from "@/components/medications/use-medication-intake"; +import { + runLogIntake, + runUndoIntake, +} from "@/components/medications/use-medication-intake"; interface LogIntakeSchedule { windowStart: string; @@ -194,6 +197,15 @@ export function LogIntakeDialog({ ...(doseDeviates && { doseTaken: trimmedDose }), t, queryClient, + // v1.30.1 M7 — a backdated/manual intake now gets the same + // Undo affordance the card's take/skip path already has. + undoIntake: (eventId) => + runUndoIntake({ + medication: { id: med.id, name: med.name }, + eventId, + t, + queryClient, + }), }); if (ok) onOpenChange(false); } finally { diff --git a/src/components/medications/use-medication-intake.ts b/src/components/medications/use-medication-intake.ts index 33f92ef0a..f3f96a110 100644 --- a/src/components/medications/use-medication-intake.ts +++ b/src/components/medications/use-medication-intake.ts @@ -215,6 +215,12 @@ export async function runLogIntake(deps: { doseTaken?: string; t: Translator; queryClient: QueryClient; + /** + * v1.30.1 M7 — reverse the just-logged intake. Optional so existing + * unit tests that don't care about Undo can omit it; the toast simply + * carries no action when it's absent. + */ + undoIntake?: (eventId: string) => void | Promise; }): Promise { const { medication, @@ -224,22 +230,42 @@ export async function runLogIntake(deps: { doseTaken, t, queryClient, + undoIntake, } = deps; try { const body: Record = { skipped }; if (!skipped) body.takenAt = takenAt; if (scheduledFor) body.scheduledFor = scheduledFor; if (!skipped && doseTaken) body.doseTaken = doseTaken; - await apiPost(`/api/medications/${medication.id}/intake`, body); - await invalidateMedicationReads(queryClient); - toast.success( - t( - skipped - ? "medications.intakeToastSkipped" - : "medications.intakeToastTaken", - { name: medication.name }, - ), + // v1.30.1 M7 — the route returns the created event + // (`apiSuccess(event, 201)`, same as the card's `runRecordIntake`); + // its id drives the Undo action below. + const created = await apiPost<{ id?: string } | undefined>( + `/api/medications/${medication.id}/intake`, + body, ); + const eventId = created?.id; + await invalidateMedicationReads(queryClient); + const message = t( + skipped + ? "medications.intakeToastSkipped" + : "medications.intakeToastTaken", + { name: medication.name }, + ); + // v1.30.1 M7 — only pass a second `toast.success` argument when there's + // a real Undo action to attach; keeps the no-undo call signature + // identical to the pre-fix behaviour (existing unit tests assert the + // single-argument call). + if (eventId && undoIntake) { + toast.success(message, { + action: { + label: t("medications.intakeUndo"), + onClick: () => void undoIntake(eventId), + }, + }); + } else { + toast.success(message); + } return true; } catch { toast.error(t("medications.intakeToastFailed", { name: medication.name }));