fix(medications): give backdated/manual intakes the same Undo the card has

Logging a dose via Medications → Add → "Log intake" (incl. backdated)
or the dashboard's medication-intake quick-add toasted success with no
Undo, unlike the card's own take/skip path — a dose logged against the
wrong medication (easy on the quick-add, which auto-picks) was only
correctable via detail page → history → find row → delete. Both paths
now capture the created event id and attach the same Undo action.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 03:52:32 +02:00
1 parent 8c385b9ac2
commit 7ac9e7833e
3 files changed
+81 -16

No files matched your search

@@ -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(
@@ -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 {
@@ -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<void>;
}): Promise<boolean> {
const {
medication,
@@ -224,22 +230,42 @@ export async function runLogIntake(deps: {
doseTaken,
t,
queryClient,
undoIntake,
} = deps;
try {
const body: Record<string, unknown> = { 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 }));