feat: delete controls across add-pages (lab, dispense, rx, inventory, tasks)

Backend:
- DELETE /api/patients/:fileNumber/labs (remove one lab result, lab:write)
- DELETE /api/dispenses/:id (void a ledger entry, inventory:write)

Frontend (all guarded by ConfirmDialog):
- lab "Recent results" rows: delete result
- pharmacy "Recently dispensed" rows: delete record
- prescriptions/tasks detail sheets + inventory detail dialog gain a delete
  action (prescription delete stays off the shared Pharmacy sheet)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-17 19:23:41 +03:00
parent 96bda1b902
commit d37a4e9425
15 changed files with 467 additions and 10 deletions
@@ -1,11 +1,14 @@
"use client";
import { Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetFooter,
SheetHeader,
SheetPanel,
SheetPopup,
@@ -30,10 +33,14 @@ export function PrescriptionDetailSheet({
rx,
open,
onOpenChange,
onDelete,
}: {
rx: Prescription | null;
open: boolean;
onOpenChange: (open: boolean) => void;
// Optional: only the Prescriptions page (full clinician) passes this, so the
// shared sheet stays read-only when opened from Pharmacy.
onDelete?: () => void;
}) {
const { t } = useTranslation();
return (
@@ -135,6 +142,14 @@ export function PrescriptionDetailSheet({
</div>
)}
</SheetPanel>
{rx && onDelete && (
<SheetFooter>
<Button onClick={onDelete} type="button" variant="destructive">
<Trash2 className="size-4" />
{t("prescriptions.detail.delete")}
</Button>
</SheetFooter>
)}
</SheetPopup>
</Sheet>
);
@@ -14,11 +14,13 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Input } from "@/components/ui/input";
import {
type Prescription,
type RxStatus,
createPrescription,
deletePrescription,
formatPrescribedAt,
listPrescriptions,
} from "@/lib/prescriptions";
@@ -130,6 +132,7 @@ export function PrescriptionsView() {
const [list, setList] = useState<Prescription[]>([]);
const [selected, setSelected] = useState<Prescription | null>(null);
const [sheetOpen, setSheetOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
@@ -151,6 +154,24 @@ export function PrescriptionsView() {
setSheetOpen(true);
};
const removeRx = async () => {
if (!selected) return;
const id = selected.id;
try {
await deletePrescription(id);
setList((prev) => prev.filter((r) => r.id !== id));
setSheetOpen(false);
notify.success(t("prescriptions.delete.doneTitle"), selected.medication);
} catch {
notify.error(
t("prescriptions.delete.failedTitle"),
t("prescriptions.delete.failedBody"),
);
} finally {
setConfirmOpen(false);
}
};
// Persist a new prescription, then add the saved record to the top of the list.
const addPrescription = async (rx: NewPrescription) => {
try {
@@ -273,10 +294,28 @@ export function PrescriptionsView() {
/>
<PrescriptionDetailSheet
onDelete={() => setConfirmOpen(true)}
onOpenChange={setSheetOpen}
open={sheetOpen}
rx={selected}
/>
<ConfirmDialog
cancelLabel={t("prescriptions.delete.cancel")}
confirmLabel={t("prescriptions.delete.confirm")}
description={
selected
? t("prescriptions.delete.body", {
medication: selected.medication,
name: selected.name,
})
: undefined
}
onConfirm={removeRx}
onOpenChange={setConfirmOpen}
open={confirmOpen}
title={t("prescriptions.delete.title")}
/>
</div>
);
}