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)} +
+ +
+ +