mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* v1.18.1 — shared illness-episode overflow menu (mirrors the medication
|
|
* card menu): one kebab folding Edit / Mark recovered / Delete. Delete is
|
|
* gated behind an AlertDialog (every destructive peer guards this; an episode
|
|
* delete cascades its day-logs + encrypted notes, so a single unguarded tap
|
|
* was the Medium finding). Neutral throughout.
|
|
*/
|
|
import { useState } from "react";
|
|
import { MoreVertical, Pencil, CheckCircle2, Trash2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/errors/error-toast";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
|
|
import { useDeleteEpisode, useRestoreEpisode } from "./use-illness";
|
|
import type { IllnessEpisodeDTO } from "./types";
|
|
|
|
export interface EpisodeMenuProps {
|
|
episode: IllnessEpisodeDTO;
|
|
onEdit: () => void;
|
|
/** Provided only when the episode is active + non-chronic. */
|
|
onResolve?: () => void;
|
|
resolving?: boolean;
|
|
/** Called after a confirmed delete (e.g. navigate back from a detail page). */
|
|
onDeleted?: () => void;
|
|
}
|
|
|
|
export function EpisodeMenu({
|
|
episode,
|
|
onEdit,
|
|
onResolve,
|
|
resolving,
|
|
onDeleted,
|
|
}: EpisodeMenuProps) {
|
|
const { t } = useTranslations();
|
|
const del = useDeleteEpisode();
|
|
const restore = useRestoreEpisode();
|
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
|
|
async function handleConfirmDelete() {
|
|
try {
|
|
await del.mutateAsync(episode.id);
|
|
setConfirmOpen(false);
|
|
onDeleted?.();
|
|
// Soft-delete keeps the episode + its day-logs intact, so the delete is
|
|
// reversible. Surface an Undo toast wired to the restore route (the labs
|
|
// delete+undo UX).
|
|
toast.success(t("illness.deletedToast"), {
|
|
action: {
|
|
label: t("common.undo"),
|
|
onClick: () => {
|
|
restore.mutate(episode.id, {
|
|
onError: (failure) =>
|
|
showErrorToast(t("illness.restoreError"), { cause: failure }),
|
|
});
|
|
},
|
|
},
|
|
});
|
|
} catch {
|
|
// Keep the dialog open; the list will surface any error on next read.
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-11 sm:size-9"
|
|
aria-label={t("illness.menu.label")}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onSelect={onEdit}>
|
|
<Pencil className="h-4 w-4" />
|
|
{t("common.edit")}
|
|
</DropdownMenuItem>
|
|
{onResolve ? (
|
|
<DropdownMenuItem onSelect={onResolve} disabled={resolving}>
|
|
<CheckCircle2 className="h-4 w-4" />
|
|
{t("illness.markRecovered")}
|
|
</DropdownMenuItem>
|
|
) : null}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onSelect={(e) => {
|
|
e.preventDefault();
|
|
setConfirmOpen(true);
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
{t("common.delete")}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
{t("illness.deleteConfirm.title")}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{t("illness.deleteConfirm.body", { label: episode.label })}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleConfirmDelete}
|
|
disabled={del.isPending}
|
|
>
|
|
{t("common.delete")}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|