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,6 +1,6 @@
"use client";
import { Pill } from "lucide-react";
import { Pill, Trash2 } from "lucide-react";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
@@ -47,10 +47,12 @@ export function InventoryDetailDialog({
item,
open,
onOpenChange,
onDelete,
}: {
item: InventoryItem | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onDelete?: () => void;
}) {
const { t } = useTranslation();
if (!item) return null;
@@ -120,7 +122,18 @@ export function InventoryDetailDialog({
</div>
</DialogPanel>
<DialogFooter>
<DialogFooter variant={onDelete ? "bare" : undefined}>
{onDelete && (
<Button
className="me-auto"
onClick={onDelete}
type="button"
variant="destructive"
>
<Trash2 className="size-4" />
{t("inventory.detail.delete")}
</Button>
)}
<DialogClose render={<Button type="button" variant="outline" />}>
{t("inventory.detail.close")}
</DialogClose>
@@ -10,12 +10,14 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import {
type Availability,
type InventoryInput,
type InventoryItem,
availabilityOf,
createInventory,
deleteInventory,
listInventory,
} from "@/lib/inventory";
import { notify } from "@/lib/toast";
@@ -116,12 +118,31 @@ export function InventoryView() {
const [addOpen, setAddOpen] = useState(false);
const [selected, setSelected] = useState<InventoryItem | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const openItem = (item: InventoryItem) => {
setSelected(item);
setDetailOpen(true);
};
const removeItem = async () => {
if (!selected) return;
const id = selected.id;
try {
await deleteInventory(id);
setItems((prev) => prev.filter((it) => it.id !== id));
setDetailOpen(false);
notify.success(t("inventory.delete.doneTitle"), selected.name);
} catch {
notify.error(
t("inventory.delete.failedTitle"),
t("inventory.delete.failedBody"),
);
} finally {
setConfirmOpen(false);
}
};
// Persist a new item, then prepend the saved record so it appears immediately.
const addItem = async (input: InventoryInput) => {
try {
@@ -207,10 +228,25 @@ export function InventoryView() {
<InventoryDetailDialog
item={selected}
onDelete={() => setConfirmOpen(true)}
onOpenChange={setDetailOpen}
open={detailOpen}
/>
<ConfirmDialog
cancelLabel={t("inventory.delete.cancel")}
confirmLabel={t("inventory.delete.confirm")}
description={
selected
? t("inventory.delete.body", { name: selected.name })
: undefined
}
onConfirm={removeItem}
onOpenChange={setConfirmOpen}
open={confirmOpen}
title={t("inventory.delete.title")}
/>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{kpis.map((k) => (
<Kpi key={k.label} {...k} />
+59 -1
View File
@@ -1,6 +1,14 @@
"use client";
import { CircleCheck, Clock, PackageCheck, Pill, Search, Users } from "lucide-react";
import {
CircleCheck,
Clock,
PackageCheck,
Pill,
Search,
Trash2,
Users,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -9,10 +17,12 @@ 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 Dispense,
createDispense,
deleteDispense,
listDispenses,
} from "@/lib/dispenses";
import {
@@ -169,6 +179,8 @@ export function PharmacyView() {
const [selected, setSelected] = useState<Prescription | null>(null);
const [sheetOpen, setSheetOpen] = useState(false);
const [query, setQuery] = useState("");
// Dispense ledger entry staged for deletion (drives the confirm dialog).
const [toDelete, setToDelete] = useState<Dispense | null>(null);
useEffect(() => {
let active = true;
@@ -289,6 +301,25 @@ export function PharmacyView() {
}
};
// Remove a dispense ledger entry (a correction — the medication itself isn't
// un-dispensed, just the record).
const confirmDelete = async () => {
if (!toDelete) return;
const id = toDelete.id;
try {
await deleteDispense(id);
setDispenses((prev) => prev.filter((d) => d.id !== id));
notify.success(t("pharmacy.dispensed.deletedTitle"), toDelete.medication);
} catch {
notify.error(
t("pharmacy.dispensed.deleteFailedTitle"),
t("pharmacy.dispensed.deleteFailedBody"),
);
} finally {
setToDelete(null);
}
};
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-10 px-6 py-10">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
@@ -380,6 +411,14 @@ export function PharmacyView() {
{formatDispensedAt(d.dispensedAt)}
</span>
</div>
<button
aria-label={t("pharmacy.dispensed.delete")}
className="shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-destructive-foreground"
onClick={() => setToDelete(d)}
type="button"
>
<Trash2 className="size-4" />
</button>
</div>
))}
{dispenses.length === 0 && (
@@ -395,6 +434,25 @@ export function PharmacyView() {
open={sheetOpen}
rx={selected}
/>
<ConfirmDialog
cancelLabel={t("pharmacy.dispensed.deleteCancel")}
confirmLabel={t("pharmacy.dispensed.deleteConfirm")}
description={
toDelete
? t("pharmacy.dispensed.deleteBody", {
medication: toDelete.medication,
name: toDelete.name,
})
: undefined
}
onConfirm={confirmDelete}
onOpenChange={(o) => {
if (!o) setToDelete(null);
}}
open={toDelete !== null}
title={t("pharmacy.dispensed.deleteTitle")}
/>
</div>
);
}