fix(records): offer Undo after deleting a vaccination, visit or entry

Deleting a vaccination, a doctor visit or an address-book entry closed
the sheet with no way back, although all three are soft deletes with a
documented restore route. The vaccination hook had a restore mutation
nobody called; visits and the address book had none.

The two hooks gain a restore mutation, and each delete now confirms
with an Undo toast, the same pattern the illness episode uses. The Undo
runs after the sheet has closed, so it awaits the restore itself and
the hook's own onSuccess refreshes the lists, instead of relying on
callbacks a closed sheet never receives. Restoring an address-book
entry brings the entry back, not the links the delete cleared on the
visits that named it. The three new success toasts are pinned in the
success-affordance guard.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 20:06:02 +01:00
1 parent f761500247
commit d204b4344b
18 files changed
+259 -46

No files matched your search

+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Eigenen Impfstoff entfernen?",
"sectionDescription": "Selbst angelegte Impfstoffe, die der Katalog nicht kennt.",
"catalogGroup": "Katalog"
}
},
"deleted": "Impfung gelöscht",
"restoreFailed": "Impfung konnte nicht wiederhergestellt werden."
},
"illness": {
"title": "Krankheit",
@@ -13492,7 +13494,9 @@
"defaultReason": "Kein früherer Termin bei derselben Praxis, in derselben Fachrichtung oder derselben Art.",
"capped": "Die letzten zwei Jahre",
"cappedReason": "Der letzte passende Termin liegt mehr als zwei Jahre zurück, und ein Bericht umfasst höchstens zwei Jahre."
}
},
"deleted": "Termin gelöscht",
"restoreFailed": "Termin konnte nicht wiederhergestellt werden."
},
"checkups": {
"viewSwitchLabel": "Vorsorge, Termine, Eingriffe oder Körperstellen anzeigen",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Eintrag löschen?",
"deleteDescription": "Deine bisherigen Termine bleiben erhalten und behalten diesen Namen.",
"deleteFailed": "Eintrag konnte nicht gelöscht werden.",
"noSpecialtyGroup": "Ohne Fachrichtung"
"noSpecialtyGroup": "Ohne Fachrichtung",
"deleted": "Eintrag gelöscht",
"restoreFailed": "Eintrag konnte nicht wiederhergestellt werden."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Remove own vaccine?",
"sectionDescription": "Vaccines you added yourself because the catalogue does not list them.",
"catalogGroup": "Catalogue"
}
},
"deleted": "Vaccination deleted",
"restoreFailed": "The vaccination could not be restored."
},
"illness": {
"title": "Illness",
@@ -13492,7 +13494,9 @@
"defaultReason": "No earlier visit with the same practice, specialty or kind.",
"capped": "The last two years",
"cappedReason": "The last matching visit is more than two years ago, and a report covers two years at most."
}
},
"deleted": "Visit deleted",
"restoreFailed": "The visit could not be restored."
},
"checkups": {
"viewSwitchLabel": "Show checkups, visits, procedures or body sites",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Delete this entry?",
"deleteDescription": "Your existing visits stay, and they keep this name.",
"deleteFailed": "The entry could not be deleted.",
"noSpecialtyGroup": "No specialty"
"noSpecialtyGroup": "No specialty",
"deleted": "Entry deleted",
"restoreFailed": "The entry could not be restored."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "¿Quitar la vacuna propia?",
"sectionDescription": "Vacunas que añadiste tú porque el catálogo no las recoge.",
"catalogGroup": "Catálogo"
}
},
"deleted": "Vacuna eliminada",
"restoreFailed": "No se pudo restaurar la vacuna."
},
"illness": {
"title": "Enfermedad",
@@ -13492,7 +13494,9 @@
"defaultReason": "No hay ninguna visita anterior con la misma consulta, especialidad o tipo.",
"capped": "Los últimos dos años",
"cappedReason": "La última visita coincidente fue hace más de dos años, y un informe abarca como máximo dos años."
}
},
"deleted": "Cita eliminada",
"restoreFailed": "No se pudo restaurar la cita."
},
"checkups": {
"viewSwitchLabel": "Mostrar revisiones, citas, procedimientos o zonas del cuerpo",
@@ -13525,7 +13529,9 @@
"deleteTitle": "¿Eliminar esta entrada?",
"deleteDescription": "Tus citas anteriores se mantienen y conservan este nombre.",
"deleteFailed": "No se pudo eliminar la entrada.",
"noSpecialtyGroup": "Sin especialidad"
"noSpecialtyGroup": "Sin especialidad",
"deleted": "Entrada eliminada",
"restoreFailed": "No se pudo restaurar la entrada."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Retirer ce vaccin personnel ?",
"sectionDescription": "Les vaccins que vous avez ajoutés parce que le catalogue ne les connaît pas.",
"catalogGroup": "Catalogue"
}
},
"deleted": "Vaccination supprimée",
"restoreFailed": "La vaccination n'a pas pu être restaurée."
},
"illness": {
"title": "Maladie",
@@ -13492,7 +13494,9 @@
"defaultReason": "Aucune visite antérieure au même cabinet, dans la même spécialité ou du même type.",
"capped": "Les deux dernières années",
"cappedReason": "La dernière consultation correspondante date de plus de deux ans, et un rapport couvre au plus deux ans."
}
},
"deleted": "Rendez-vous supprimé",
"restoreFailed": "Le rendez-vous n'a pas pu être restauré."
},
"checkups": {
"viewSwitchLabel": "Afficher les dépistages, les rendez-vous, les interventions ou les parties du corps",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Supprimer cette entrée ?",
"deleteDescription": "Vos rendez-vous existants restent et conservent ce nom.",
"deleteFailed": "L'entrée n'a pas pu être supprimée.",
"noSpecialtyGroup": "Sans spécialité"
"noSpecialtyGroup": "Sans spécialité",
"deleted": "Entrée supprimée",
"restoreFailed": "L'entrée n'a pas pu être restaurée."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Rimuovere il vaccino personale?",
"sectionDescription": "Vaccini aggiunti da te perché il catalogo non li elenca.",
"catalogGroup": "Catalogo"
}
},
"deleted": "Vaccinazione eliminata",
"restoreFailed": "Impossibile ripristinare la vaccinazione."
},
"illness": {
"title": "Malattia",
@@ -13492,7 +13494,9 @@
"defaultReason": "Nessuna visita precedente presso lo stesso studio, nella stessa specialità o dello stesso tipo.",
"capped": "Gli ultimi due anni",
"cappedReason": "L'ultima visita corrispondente risale a più di due anni fa, e un report copre al massimo due anni."
}
},
"deleted": "Appuntamento eliminato",
"restoreFailed": "Non è stato possibile ripristinare l'appuntamento."
},
"checkups": {
"viewSwitchLabel": "Mostra controlli, appuntamenti, interventi o parti del corpo",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Eliminare questa voce?",
"deleteDescription": "I tuoi appuntamenti esistenti restano e conservano questo nome.",
"deleteFailed": "Non è stato possibile eliminare la voce.",
"noSpecialtyGroup": "Senza specializzazione"
"noSpecialtyGroup": "Senza specializzazione",
"deleted": "Voce eliminata",
"restoreFailed": "Non è stato possibile ripristinare la voce."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "직접 추가한 백신을 삭제할까요?",
"sectionDescription": "목록에 없어서 직접 추가한 백신이에요.",
"catalogGroup": "목록"
}
},
"deleted": "예방접종 기록을 삭제했어요",
"restoreFailed": "예방접종 기록을 복원하지 못했어요."
},
"illness": {
"title": "아픈 날",
@@ -13492,7 +13494,9 @@
"defaultReason": "같은 병원, 진료과 또는 유형의 이전 방문이 없어요.",
"capped": "최근 2년",
"cappedReason": "마지막으로 일치하는 진료가 2년 이상 지났으며, 보고서는 최대 2년까지 다룹니다."
}
},
"deleted": "진료를 삭제했어요",
"restoreFailed": "진료를 복원하지 못했어요."
},
"checkups": {
"viewSwitchLabel": "건강검진, 진료, 시술 또는 신체 부위 보기",
@@ -13525,7 +13529,9 @@
"deleteTitle": "이 항목을 삭제할까요?",
"deleteDescription": "기존 진료 기록은 그대로 남고, 이 이름도 유지돼요.",
"deleteFailed": "항목을 삭제하지 못했어요.",
"noSpecialtyGroup": "진료과 없음"
"noSpecialtyGroup": "진료과 없음",
"deleted": "항목을 삭제했어요",
"restoreFailed": "항목을 복원하지 못했어요."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Eigen vaccin verwijderen?",
"sectionDescription": "Vaccins die je zelf hebt toegevoegd omdat ze niet in de catalogus staan.",
"catalogGroup": "Catalogus"
}
},
"deleted": "Vaccinatie verwijderd",
"restoreFailed": "De vaccinatie kon niet worden teruggezet."
},
"illness": {
"title": "Ziekte",
@@ -13492,7 +13494,9 @@
"defaultReason": "Geen eerder bezoek bij dezelfde praktijk, in hetzelfde specialisme of van hetzelfde soort.",
"capped": "De afgelopen twee jaar",
"cappedReason": "Het laatste passende bezoek is meer dan twee jaar geleden, en een rapport beslaat hooguit twee jaar."
}
},
"deleted": "Bezoek verwijderd",
"restoreFailed": "Het bezoek kon niet worden teruggezet."
},
"checkups": {
"viewSwitchLabel": "Controles, bezoeken, ingrepen of lichaamsdelen tonen",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Deze vermelding verwijderen?",
"deleteDescription": "Je bestaande bezoeken blijven bestaan en houden deze naam.",
"deleteFailed": "De vermelding kon niet worden verwijderd.",
"noSpecialtyGroup": "Geen specialisme"
"noSpecialtyGroup": "Geen specialisme",
"deleted": "Vermelding verwijderd",
"restoreFailed": "De vermelding kon niet worden teruggezet."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Usunąć własną szczepionkę?",
"sectionDescription": "Szczepionki dodane przez Ciebie, bo nie ma ich w katalogu.",
"catalogGroup": "Katalog"
}
},
"deleted": "Szczepienie usunięte",
"restoreFailed": "Nie udało się przywrócić szczepienia."
},
"illness": {
"title": "Choroba",
@@ -13492,7 +13494,9 @@
"defaultReason": "Brak wcześniejszej wizyty w tym samym gabinecie, tej samej specjalizacji lub tego samego rodzaju.",
"capped": "Ostatnie dwa lata",
"cappedReason": "Ostatnia pasująca wizyta była ponad dwa lata temu, a raport obejmuje najwyżej dwa lata."
}
},
"deleted": "Wizyta usunięta",
"restoreFailed": "Nie udało się przywrócić wizyty."
},
"checkups": {
"viewSwitchLabel": "Pokaż badania profilaktyczne, wizyty, zabiegi lub miejsca na ciele",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Usunąć ten wpis?",
"deleteDescription": "Twoje dotychczasowe wizyty pozostają i zachowują tę nazwę.",
"deleteFailed": "Nie udało się usunąć wpisu.",
"noSpecialtyGroup": "Bez specjalizacji"
"noSpecialtyGroup": "Bez specjalizacji",
"deleted": "Wpis usunięty",
"restoreFailed": "Nie udało się przywrócić wpisu."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Remover vacina própria?",
"sectionDescription": "Vacinas que você adicionou porque o catálogo não as lista.",
"catalogGroup": "Catálogo"
}
},
"deleted": "Vacina excluída",
"restoreFailed": "Não foi possível restaurar a vacina."
},
"illness": {
"title": "Doenças",
@@ -13492,7 +13494,9 @@
"defaultReason": "Nenhuma consulta anterior com o mesmo consultório, especialidade ou tipo.",
"capped": "Os últimos dois anos",
"cappedReason": "A última consulta correspondente foi há mais de dois anos, e um relatório cobre no máximo dois anos."
}
},
"deleted": "Consulta excluída",
"restoreFailed": "Não foi possível restaurar a consulta."
},
"checkups": {
"viewSwitchLabel": "Mostrar check-ups, consultas, procedimentos ou locais do corpo",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Excluir esta entrada?",
"deleteDescription": "Suas consultas existentes permanecem e mantêm este nome.",
"deleteFailed": "Não foi possível excluir a entrada.",
"noSpecialtyGroup": "Sem especialidade"
"noSpecialtyGroup": "Sem especialidade",
"deleted": "Entrada excluída",
"restoreFailed": "Não foi possível restaurar a entrada."
},
"links": {
"picker": {
+9 -3
View File
@@ -10980,7 +10980,9 @@
"deleteTitle": "Убрать свою вакцину?",
"sectionDescription": "Вакцины, которые вы добавили сами, потому что их нет в каталоге.",
"catalogGroup": "Каталог"
}
},
"deleted": "Вакцинация удалена",
"restoreFailed": "Не удалось восстановить вакцинацию."
},
"illness": {
"title": "Заболевание",
@@ -13492,7 +13494,9 @@
"defaultReason": "Нет более раннего визита с той же клиникой, специальностью или видом.",
"capped": "Последние два года",
"cappedReason": "Последний подходящий визит был более двух лет назад, а отчёт охватывает не больше двух лет."
}
},
"deleted": "Визит удалён",
"restoreFailed": "Визит не удалось восстановить."
},
"checkups": {
"viewSwitchLabel": "Показать осмотры, визиты, процедуры или области тела",
@@ -13525,7 +13529,9 @@
"deleteTitle": "Удалить эту запись?",
"deleteDescription": "Ваши существующие визиты остаются, и они сохраняют это имя.",
"deleteFailed": "Запись не удалось удалить.",
"noSpecialtyGroup": "Без специальности"
"noSpecialtyGroup": "Без специальности",
"deleted": "Запись удалена",
"restoreFailed": "Запись не удалось восстановить."
},
"links": {
"picker": {
@@ -216,6 +216,7 @@ const PINNED_AFFORDANCES: Record<
"src/components/documents/sources/document-source-picker.tsx": {
"text-success": 1,
},
"src/components/encounters/encounter-sheet.tsx": { "toast.success": 1 },
"src/components/error-details.tsx": { CheckCircle2: 2, "text-success": 1 },
"src/components/illness/episode-menu.tsx": {
CheckCircle2: 2,
@@ -347,8 +348,10 @@ const PINNED_AFFORDANCES: Record<
"src/components/symptoms/symptom-entry-form.tsx": { "toast.success": 1 },
"src/components/mood/mood-list.tsx": { "toast.success": 3 },
"src/components/onboarding/done-screen.tsx": { CheckCircle2: 2 },
"src/components/practitioners/practitioner-list.tsx": { "toast.success": 1 },
"src/components/records/allergy-form.tsx": { "toast.success": 1 },
"src/components/records/allergy-manager.tsx": { "toast.success": 1 },
"src/components/vaccinations/vaccination-sheet.tsx": { "toast.success": 1 },
"src/components/records/conditions-manager.tsx": { "toast.success": 1 },
"src/components/records/family-history-form.tsx": { "toast.success": 1 },
"src/components/records/family-history-manager.tsx": { "toast.success": 1 },
+21 -3
View File
@@ -33,6 +33,7 @@
*/
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/errors/error-toast";
import {
@@ -85,7 +86,7 @@ export function EncounterSheet({
onSaved?: (saved: Encounter) => void;
}) {
const { t } = useTranslations();
const { create, update, remove } = useEncounterMutations();
const { create, update, remove, restore } = useEncounterMutations();
const [confirmingDelete, setConfirmingDelete] = useState(false);
// Seeded once, at mount. The caller remounts this component on every open by
// varying its `key`, so a second edit never shows the first one's values and
@@ -210,8 +211,25 @@ export function EncounterSheet({
<AlertDialogAction
variant="destructive"
onClick={() => {
remove.mutate(encounter.id, {
onSuccess: () => onOpenChange(false),
const id = encounter.id;
remove.mutate(id, {
onSuccess: () => {
onOpenChange(false);
// The delete is soft. The Undo runs after this sheet
// closed, so it awaits the restore itself rather than
// passing callbacks a closed sheet never receives.
toast.success(t("encounters.deleted"), {
action: {
label: t("common.undo"),
onClick: () =>
void restore.mutateAsync(id).catch((failure) =>
showErrorToast(t("encounters.restoreFailed"), {
cause: failure,
}),
),
},
});
},
onError: (failure) =>
showErrorToast(t("encounters.deleteFailed"), {
cause: failure,
@@ -0,0 +1,63 @@
/**
* Deleting an address-book entry is a soft delete with a restore route, and
* nothing on the web offered it: a mistaken delete could not be taken back.
* The delete now confirms with an Undo that calls the restore.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const toastSuccess = vi.hoisted(() => vi.fn());
const errorToast = vi.hoisted(() => vi.fn());
vi.mock("sonner", () => ({ toast: { success: toastSuccess } }));
vi.mock("@/lib/errors/error-toast", () => ({ showErrorToast: errorToast }));
import { deleteWithUndo } from "../practitioner-list";
type Mutations = Parameters<typeof deleteWithUndo>[0];
const t = (key: string) => key;
function mutations(restoreResult: Promise<unknown>) {
const remove = {
mutate: vi.fn((_id: string, options: { onSuccess?: () => void }) =>
options.onSuccess?.(),
),
};
const restore = { mutateAsync: vi.fn(() => restoreResult) };
return { remove, restore, all: { remove, restore } as unknown as Mutations };
}
beforeEach(() => {
toastSuccess.mockReset();
errorToast.mockReset();
});
describe("deleteWithUndo", () => {
it("confirms the delete with an Undo that restores the entry", async () => {
const m = mutations(Promise.resolve({ id: "p1" }));
deleteWithUndo(m.all, "p1", t);
expect(m.remove.mutate).toHaveBeenCalledWith("p1", expect.anything());
const [message, options] = toastSuccess.mock.calls[0]!;
expect(message).toBe("practitioners.deleted");
expect(options.action.label).toBe("common.undo");
options.action.onClick();
expect(m.restore.mutateAsync).toHaveBeenCalledWith("p1");
await Promise.resolve();
expect(errorToast).not.toHaveBeenCalled();
});
it("says so when the restore fails", async () => {
const m = mutations(Promise.reject(new Error("offline")));
deleteWithUndo(m.all, "p1", t);
toastSuccess.mock.calls[0]![1].action.onClick();
await vi.waitFor(() =>
expect(errorToast).toHaveBeenCalledWith(
"practitioners.restoreFailed",
expect.anything(),
),
);
});
});
@@ -13,6 +13,7 @@
*/
import { ContactRound, Pencil, Plus, Search, Trash2 } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/errors/error-toast";
import {
@@ -43,6 +44,33 @@ import { VISITS_PATH } from "@/lib/encounters/visit-links";
import { useTranslations } from "@/lib/i18n/context";
import { PractitionerSheet } from "./practitioner-sheet";
/**
* Delete an entry, with an Undo: the delete is soft, and the restore brings
* the entry back.
*/
export function deleteWithUndo(
{ remove, restore }: ReturnType<typeof usePractitionerMutations>,
id: string,
t: (key: string) => string,
) {
remove.mutate(id, {
onSuccess: () =>
toast.success(t("practitioners.deleted"), {
action: {
label: t("common.undo"),
onClick: () =>
void restore.mutateAsync(id).catch((failure) =>
showErrorToast(t("practitioners.restoreFailed"), {
cause: failure,
}),
),
},
}),
onError: (failure) =>
showErrorToast(t("practitioners.deleteFailed"), { cause: failure }),
});
}
export function PractitionerList({ enabled = true }: { enabled?: boolean }) {
const { t } = useTranslations();
const { canWriteDomain, canManageDomain } = useRecordCapabilities();
@@ -52,7 +80,7 @@ export function PractitionerList({ enabled = true }: { enabled?: boolean }) {
const [term, setTerm] = useState("");
const debounced = useDebouncedValue(term, 250);
const list = usePractitioners(debounced, enabled);
const { remove } = usePractitionerMutations();
const mutations = usePractitionerMutations();
const [editing, setEditing] = useState<Practitioner | "new" | null>(null);
const [deleting, setDeleting] = useState<Practitioner | null>(null);
@@ -257,12 +285,7 @@ export function PractitionerList({ enabled = true }: { enabled?: boolean }) {
if (!deleting) return;
const id = deleting.id;
setDeleting(null);
remove.mutate(id, {
onError: (failure) =>
showErrorToast(t("practitioners.deleteFailed"), {
cause: failure,
}),
});
deleteWithUndo(mutations, id, t);
}}
>
{t("common.delete")}
@@ -20,6 +20,7 @@
*/
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/errors/error-toast";
import {
@@ -69,7 +70,7 @@ export function VaccinationSheet({
onCreated?: (created: Vaccination) => void;
}) {
const { t } = useTranslations();
const { create, update, remove } = useVaccinationMutations();
const { create, update, remove, restore } = useVaccinationMutations();
// The list row carries no document links; the detail read does. Until it
// answers, the draft's links stay "untouched" and Save leaves them alone.
const detail = useVaccination(vaccination?.id ?? null);
@@ -198,8 +199,28 @@ export function VaccinationSheet({
<AlertDialogAction
variant="destructive"
onClick={() => {
remove.mutate(vaccination.id, {
onSuccess: () => onOpenChange(false),
const id = vaccination.id;
remove.mutate(id, {
onSuccess: () => {
onOpenChange(false);
// The delete is soft. The Undo runs after this sheet
// closed, so it awaits the restore itself rather than
// passing callbacks a closed sheet never receives.
toast.success(t("vaccinations.deleted"), {
action: {
label: t("common.undo"),
onClick: () =>
void restore.mutateAsync(id).catch((failure) =>
showErrorToast(
t("vaccinations.restoreFailed"),
{
cause: failure,
},
),
),
},
});
},
onError: (failure) =>
showErrorToast(t("vaccinations.deleteFailed"), {
cause: failure,
+16 -2
View File
@@ -19,7 +19,13 @@ import {
useQueryClient,
} from "@tanstack/react-query";
import { apiDelete, apiGet, apiGetList, apiPatch } from "@/lib/api/api-fetch";
import {
apiDelete,
apiGet,
apiGetList,
apiPatch,
apiPost,
} from "@/lib/api/api-fetch";
import { useIdempotentPost } from "@/lib/api/use-idempotent-post";
import { invalidateReminderReads } from "@/hooks/use-measurement-reminders";
import {
@@ -192,5 +198,13 @@ export function useEncounterMutations() {
onSuccess: invalidate,
});
return { create, update, remove };
// The Undo of a delete. A restored visit re-anchors its reminder only
// while its date is still ahead.
const restore = useMutation({
mutationKey: queryKeys.encounterRestore(),
mutationFn: (id: string) => apiPost<Encounter>(`${BASE}/${id}/restore`, {}),
onSuccess: invalidate,
});
return { create, update, remove, restore };
}
+10 -1
View File
@@ -91,5 +91,14 @@ export function usePractitionerMutations() {
onSuccess: invalidate,
});
return { create, update, remove };
// The Undo of a delete. It brings the entry back, not the links the
// delete cleared on the visits that named it.
const restore = useMutation({
mutationKey: queryKeys.practitionerRestore(),
mutationFn: (id: string) =>
apiPost<Practitioner>(`${BASE}/${id}/restore`, {}),
onSuccess: invalidate,
});
return { create, update, remove, restore };
}
+2
View File
@@ -95,7 +95,9 @@ export const encounterKeys = {
encounterCreate: () => ["encounters", "create"] as const,
encounterUpdate: () => ["encounters", "update"] as const,
encounterDelete: () => ["encounters", "delete"] as const,
encounterRestore: () => ["encounters", "restore"] as const,
practitionerCreate: () => ["practitioners", "create"] as const,
practitionerUpdate: () => ["practitioners", "update"] as const,
practitionerDelete: () => ["practitioners", "delete"] as const,
practitionerRestore: () => ["practitioners", "restore"] as const,
};