From 4a9ebe07b8389a507487277e9c8884a030e40228 Mon Sep 17 00:00:00 2001 From: Haritha Randunu Date: Wed, 7 Oct 2026 15:43:17 +0530 Subject: [PATCH] feat: add inventory management components and API integration - Implemented InventorySalesTab for recording and viewing sales. - Created InventorySuppliersTab for managing suppliers. - Developed InventoryView to consolidate inventory-related tabs. - Added API functions for handling products, suppliers, sales, and GRNs. - Defined necessary TypeScript models for inventory entities. - Updated navigation to include an Inventory section. --- src/app/dashboard/inventory/page.tsx | 5 + .../hexa/views/inventory-grn-tab.tsx | 405 ++++++++++++++ .../hexa/views/inventory-products-tab.tsx | 516 ++++++++++++++++++ .../hexa/views/inventory-returns-tab.tsx | 474 ++++++++++++++++ .../hexa/views/inventory-sales-tab.tsx | 448 +++++++++++++++ .../hexa/views/inventory-suppliers-tab.tsx | 204 +++++++ src/components/hexa/views/inventory-view.tsx | 40 ++ src/hooks/use-nav-items.ts | 2 + src/lib/api/inventory.ts | 99 ++++ src/lib/models/inventory.ts | 159 ++++++ src/lib/nav.ts | 2 + 11 files changed, 2354 insertions(+) create mode 100644 src/app/dashboard/inventory/page.tsx create mode 100644 src/components/hexa/views/inventory-grn-tab.tsx create mode 100644 src/components/hexa/views/inventory-products-tab.tsx create mode 100644 src/components/hexa/views/inventory-returns-tab.tsx create mode 100644 src/components/hexa/views/inventory-sales-tab.tsx create mode 100644 src/components/hexa/views/inventory-suppliers-tab.tsx create mode 100644 src/components/hexa/views/inventory-view.tsx create mode 100644 src/lib/api/inventory.ts create mode 100644 src/lib/models/inventory.ts diff --git a/src/app/dashboard/inventory/page.tsx b/src/app/dashboard/inventory/page.tsx new file mode 100644 index 0000000..e427922 --- /dev/null +++ b/src/app/dashboard/inventory/page.tsx @@ -0,0 +1,5 @@ +import { InventoryView } from "@/components/hexa/views/inventory-view" + +export default function InventoryPage() { + return +} diff --git a/src/components/hexa/views/inventory-grn-tab.tsx b/src/components/hexa/views/inventory-grn-tab.tsx new file mode 100644 index 0000000..45ad31b --- /dev/null +++ b/src/components/hexa/views/inventory-grn-tab.tsx @@ -0,0 +1,405 @@ +"use client" + +import * as React from "react" +import { EyeIcon, PlusIcon, TrashIcon, TruckIcon } from "lucide-react" +import { toast } from "sonner" + +import { createGrn, getGrn, getGrns, getProducts, getSuppliers } from "@/lib/api/inventory" +import type { CreateGrnInput, GoodsReceivedNote, GrnItemInput } from "@/lib/models/inventory" +import { formatCurrency } from "@/lib/format" +import { useFetch } from "@/lib/use-fetch" +import { useMutation } from "@/lib/use-mutation" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Spinner } from "@/components/ui/spinner" +import { Textarea } from "@/components/ui/textarea" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" +import { PageSkeleton } from "@/components/hexa/page-skeleton" + +function formatDate(iso: string) { + return new Date(iso).toLocaleString(undefined, { + day: "numeric", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }) +} + +interface GrnLine { + productId: string + quantity: string + unitCost: string +} + +interface CreateGrnDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onCreated: () => void +} + +function CreateGrnDialog({ open, onOpenChange, onCreated }: CreateGrnDialogProps) { + const suppliersFetch = useFetch(() => getSuppliers(false)) + const suppliers = suppliersFetch.data ?? [] + const productsFetch = useFetch(() => getProducts(false)) + const products = productsFetch.data ?? [] + + const [supplierId, setSupplierId] = React.useState("") + const [notes, setNotes] = React.useState("") + const [lines, setLines] = React.useState([{ productId: "", quantity: "1", unitCost: "0" }]) + const { busy, run } = useMutation() + const [prevOpen, setPrevOpen] = React.useState(open) + + if (open !== prevOpen) { + setPrevOpen(open) + if (open) { + setSupplierId("") + setNotes("") + setLines([{ productId: "", quantity: "1", unitCost: "0" }]) + } + } + + function updateLine(idx: number, patch: Partial) { + setLines((prev) => prev.map((l, i) => (i === idx ? { ...l, ...patch } : l))) + } + + function addLine() { + setLines((prev) => [...prev, { productId: "", quantity: "1", unitCost: "0" }]) + } + + function removeLine(idx: number) { + setLines((prev) => prev.filter((_, i) => i !== idx)) + } + + const total = lines.reduce((sum, l) => sum + (Number(l.quantity) || 0) * (Number(l.unitCost) || 0), 0) + const linesValid = lines.length > 0 && lines.every((l) => !!l.productId && Number(l.quantity) > 0 && Number(l.unitCost) >= 0) + const canSubmit = linesValid && !busy + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!canSubmit) return + const input: CreateGrnInput = { + supplierId: supplierId || undefined, + notes: notes.trim() || undefined, + items: lines.map((l) => ({ + productId: l.productId, + quantity: Number(l.quantity), + unitCost: Number(l.unitCost), + })), + } + try { + await run(async () => { + await createGrn(input) + toast.success("GRN recorded", { description: "Stock levels were updated." }) + onCreated() + }) + onOpenChange(false) + } catch { + // run() already toasted the real error; keep the dialog open for retry. + } + } + + return ( + + +
+ + Record Goods Received + + Stock coming in from a supplier — increases each product's quantity on hand. + + + +
+ + +
+ +
+ +
+ Product + Quantity + Unit Cost (Rs.) + +
+ {lines.map((line, idx) => ( +
+ + updateLine(idx, { quantity: e.target.value })} + required + /> + updateLine(idx, { unitCost: e.target.value })} + required + /> + +
+ ))} + +
+ +
+ Total + {formatCurrency(total)} +
+ +
+ +