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 (
+
+ );
+}
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) => (