mirror of
https://github.com/temetro/temetro.git
synced 2026-08-25 18:07:44 +00:00
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:
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user