From da7752272e1e0d9d3fcaa378faf258f8ec3ac6b1 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Sat, 13 Jun 2026 23:49:27 +0300 Subject: [PATCH] frontend: Inventory "Add item" button + dialog Add an Add-item button to the Inventory page that opens a COSS form dialog (name required; form, strength, stock, reorder point, location, expiry). On submit it persists via createInventory and prepends the saved row so it appears immediately. Mirrors the appointments add-dialog pattern. Co-Authored-By: Claude Opus 4.8 --- .../pharmacy/add-inventory-dialog.tsx | 191 ++++++++++++++++++ .../components/pharmacy/inventory-view.tsx | 49 ++++- 2 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 frontend/components/pharmacy/add-inventory-dialog.tsx diff --git a/frontend/components/pharmacy/add-inventory-dialog.tsx b/frontend/components/pharmacy/add-inventory-dialog.tsx new file mode 100644 index 0000000..b26768f --- /dev/null +++ b/frontend/components/pharmacy/add-inventory-dialog.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { type FormEvent, type ReactNode, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogDescription, + DialogFooter, + DialogHeader, + DialogPanel, + DialogPopup, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import type { InventoryInput } from "@/lib/inventory"; +import { notify } from "@/lib/toast"; + +function Field({ label, children }: { label: string; children: ReactNode }) { + return ( + + ); +} + +// "Add item" dialog for the pharmacy inventory. Only the medication name is +// required (matches the backend schema); the rest is optional descriptive + +// stock detail. The new item is handed back via onAdd, which persists it +// through the inventory API and prepends it to the page list. +export function AddInventoryDialog({ + open, + onOpenChange, + onAdd, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onAdd: (input: InventoryInput) => void; +}) { + const { t } = useTranslation(); + const [name, setName] = useState(""); + const [form, setForm] = useState(""); + const [strength, setStrength] = useState(""); + const [unit, setUnit] = useState(""); + const [stockQuantity, setStockQuantity] = useState("0"); + const [reorderThreshold, setReorderThreshold] = useState("0"); + const [location, setLocation] = useState(""); + const [expiresAt, setExpiresAt] = useState(""); + + const reset = () => { + setName(""); + setForm(""); + setStrength(""); + setUnit(""); + setStockQuantity("0"); + setReorderThreshold("0"); + setLocation(""); + setExpiresAt(""); + }; + + const submit = (event: FormEvent) => { + event.preventDefault(); + const trimmed = name.trim(); + if (!trimmed) { + notify.error( + t("inventory.dialog.nameRequiredTitle"), + t("inventory.dialog.nameRequiredBody"), + ); + return; + } + onAdd({ + name: trimmed, + form: form.trim(), + strength: strength.trim(), + unit: unit.trim(), + stockQuantity: Number.parseInt(stockQuantity, 10) || 0, + reorderThreshold: Number.parseInt(reorderThreshold, 10) || 0, + location: location.trim(), + expiresAt: expiresAt || null, + }); + notify.success(t("inventory.dialog.addedTitle"), trimmed); + reset(); + onOpenChange(false); + }; + + return ( + { + onOpenChange(o); + if (!o) reset(); + }} + open={open} + > + + + {t("inventory.dialog.title")} + + {t("inventory.dialog.description")} + + + +
+ + + setName(event.target.value)} + placeholder={t("inventory.dialog.namePlaceholder")} + value={name} + /> + + +
+ + setForm(event.target.value)} + placeholder={t("inventory.dialog.formPlaceholder")} + value={form} + /> + + + setStrength(event.target.value)} + placeholder={t("inventory.dialog.strengthPlaceholder")} + value={strength} + /> + +
+ +
+ + setStockQuantity(event.target.value)} + type="number" + value={stockQuantity} + /> + + + setUnit(event.target.value)} + placeholder={t("inventory.dialog.unitPlaceholder")} + value={unit} + /> + + + setReorderThreshold(event.target.value)} + type="number" + value={reorderThreshold} + /> + +
+ +
+ + setLocation(event.target.value)} + placeholder={t("inventory.dialog.locationPlaceholder")} + value={location} + /> + + + setExpiresAt(event.target.value)} + type="date" + value={expiresAt} + /> + +
+
+ + + }> + {t("inventory.dialog.cancel")} + + + +
+
+
+ ); +} diff --git a/frontend/components/pharmacy/inventory-view.tsx b/frontend/components/pharmacy/inventory-view.tsx index 736ed99..54c0fa4 100644 --- a/frontend/components/pharmacy/inventory-view.tsx +++ b/frontend/components/pharmacy/inventory-view.tsx @@ -1,18 +1,23 @@ "use client"; -import { AlertTriangle, Boxes, PackageX, Pill, Search } from "lucide-react"; +import { AlertTriangle, Boxes, PackageX, Pill, Plus, Search } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { AddInventoryDialog } from "@/components/pharmacy/add-inventory-dialog"; 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 { type Availability, + type InventoryInput, type InventoryItem, availabilityOf, + createInventory, listInventory, } from "@/lib/inventory"; +import { notify } from "@/lib/toast"; const availabilityVariant: Record< Availability, @@ -90,6 +95,20 @@ export function InventoryView() { const { t } = useTranslation(); const [items, setItems] = useState([]); const [query, setQuery] = useState(""); + const [addOpen, setAddOpen] = useState(false); + + // Persist a new item, then prepend the saved record so it appears immediately. + const addItem = async (input: InventoryInput) => { + try { + const created = await createInventory(input); + setItems((prev) => [created, ...prev]); + } catch { + notify.error( + t("inventory.dialog.failedTitle"), + t("inventory.dialog.failedBody"), + ); + } + }; useEffect(() => { let active = true; @@ -138,17 +157,29 @@ export function InventoryView() { {t("inventory.subtitle")}

-
- - setQuery(event.target.value)} - placeholder={t("inventory.searchPlaceholder")} - value={query} - /> +
+
+ + setQuery(event.target.value)} + placeholder={t("inventory.searchPlaceholder")} + value={query} + /> +
+
+ +
{kpis.map((k) => (