feat: enhance inventory tabs with statistical cards for better insights

This commit is contained in:
2026-10-09 14:51:12 +05:30
parent 0d2b0d553d
commit 6c86c30766
6 changed files with 104 additions and 6 deletions
@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { EyeIcon, PlusIcon, TrashIcon, TruckIcon } from "lucide-react" import { CalendarClockIcon, DollarSignIcon, EyeIcon, PackageIcon, PlusIcon, TrashIcon, TruckIcon } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { createGrn, getGrn, getGrns, getProducts, getSuppliers } from "@/lib/api/inventory" import { createGrn, getGrn, getGrns, getProducts, getSuppliers } from "@/lib/api/inventory"
@@ -26,6 +26,7 @@ import { Textarea } from "@/components/ui/textarea"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" import { DataTable, type DataTableColumn } from "@/components/hexa/data-table"
import { PageSkeleton } from "@/components/hexa/page-skeleton" import { PageSkeleton } from "@/components/hexa/page-skeleton"
import { StatCard } from "@/components/hexa/stat-card"
function formatDate(iso: string) { function formatDate(iso: string) {
return new Date(iso).toLocaleString(undefined, { return new Date(iso).toLocaleString(undefined, {
@@ -316,6 +317,15 @@ function InventoryGrnTab() {
const [dialogOpen, setDialogOpen] = React.useState(false) const [dialogOpen, setDialogOpen] = React.useState(false)
const [detailGrnId, setDetailGrnId] = React.useState<string | null>(null) const [detailGrnId, setDetailGrnId] = React.useState<string | null>(null)
const totalValue = grns.reduce((sum, g) => sum + g.totalAmount, 0)
const now = new Date()
const thisMonthGrns = grns.filter((g) => {
const d = new Date(g.receivedAt)
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth()
})
const thisMonthValue = thisMonthGrns.reduce((sum, g) => sum + g.totalAmount, 0)
const avgValue = grns.length ? totalValue / grns.length : 0
const columns: DataTableColumn<GoodsReceivedNote>[] = [ const columns: DataTableColumn<GoodsReceivedNote>[] = [
{ {
key: "grnNumber", key: "grnNumber",
@@ -371,6 +381,13 @@ function InventoryGrnTab() {
</Button> </Button>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard icon={PackageIcon} tone="blue" label="Total GRNs" value={grns.length} />
<StatCard icon={DollarSignIcon} tone="purple" label="Total Received Value" value={formatCurrency(totalValue)} />
<StatCard icon={CalendarClockIcon} tone="amber" label="This Month" value={formatCurrency(thisMonthValue)} hint={`${thisMonthGrns.length} GRNs`} />
<StatCard icon={TruckIcon} tone="green" label="Avg. GRN Value" value={formatCurrency(avgValue)} />
</div>
<DataTable <DataTable
columns={columns} columns={columns}
rows={grns} rows={grns}
@@ -1,7 +1,18 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { CheckIcon, EyeIcon, ImagePlusIcon, PackageIcon, PencilIcon, PlusIcon, XIcon } from "lucide-react" import {
CheckCircle2Icon,
CheckIcon,
DollarSignIcon,
EyeIcon,
ImagePlusIcon,
PackageIcon,
PencilIcon,
PlusIcon,
TriangleAlertIcon,
XIcon,
} from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { createProduct, getProduct, getProducts, updateProduct, uploadProductImage } from "@/lib/api/inventory" import { createProduct, getProduct, getProducts, updateProduct, uploadProductImage } from "@/lib/api/inventory"
@@ -25,6 +36,7 @@ import { Spinner } from "@/components/ui/spinner"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { InfoBanner } from "@/components/hexa/info-banner" import { InfoBanner } from "@/components/hexa/info-banner"
import { PageSkeleton } from "@/components/hexa/page-skeleton" import { PageSkeleton } from "@/components/hexa/page-skeleton"
import { StatCard } from "@/components/hexa/stat-card"
import { StatusBadge } from "@/components/hexa/status-badge" import { StatusBadge } from "@/components/hexa/status-badge"
interface ProductFormValues { interface ProductFormValues {
@@ -324,6 +336,10 @@ function InventoryProductsTab() {
const [statusPendingId, setStatusPendingId] = React.useState<string | null>(null) const [statusPendingId, setStatusPendingId] = React.useState<string | null>(null)
const [detailProductId, setDetailProductId] = React.useState<string | null>(null) const [detailProductId, setDetailProductId] = React.useState<string | null>(null)
const activeProducts = products.filter((p) => p.isActive).length
const outOfStock = products.filter((p) => p.quantityOnHand <= 0).length
const inventoryValue = products.reduce((sum, p) => sum + p.costPrice * p.quantityOnHand, 0)
async function handleCreate(values: ProductFormValues, imageFile: File | null) { async function handleCreate(values: ProductFormValues, imageFile: File | null) {
const input: ProductInput = { const input: ProductInput = {
name: values.name.trim(), name: values.name.trim(),
@@ -393,6 +409,13 @@ function InventoryProductsTab() {
</Button> </Button>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard icon={PackageIcon} tone="blue" label="Total Products" value={products.length} />
<StatCard icon={CheckCircle2Icon} tone="green" label="Active" value={activeProducts} />
<StatCard icon={TriangleAlertIcon} tone="amber" label="Out of Stock" value={outOfStock} />
<StatCard icon={DollarSignIcon} tone="purple" label="Inventory Value" value={formatCurrency(inventoryValue)} hint="At cost price" />
</div>
{products.length === 0 ? ( {products.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 rounded-xl border bg-card py-16 text-center"> <div className="flex flex-col items-center justify-center gap-2 rounded-xl border bg-card py-16 text-center">
<PackageIcon className="size-8 text-muted-foreground" /> <PackageIcon className="size-8 text-muted-foreground" />
@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { EyeIcon, PlusIcon, RotateCcwIcon } from "lucide-react" import { CalendarClockIcon, EyeIcon, PackagePlusIcon, PlusIcon, RotateCcwIcon, TriangleAlertIcon } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { createStockReturn, getGrns, getProducts, getSales, getStockReturn, getStockReturns } from "@/lib/api/inventory" import { createStockReturn, getGrns, getProducts, getSales, getStockReturn, getStockReturns } from "@/lib/api/inventory"
@@ -30,6 +30,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" import { DataTable, type DataTableColumn } from "@/components/hexa/data-table"
import { InfoBanner } from "@/components/hexa/info-banner" import { InfoBanner } from "@/components/hexa/info-banner"
import { PageSkeleton } from "@/components/hexa/page-skeleton" import { PageSkeleton } from "@/components/hexa/page-skeleton"
import { StatCard } from "@/components/hexa/stat-card"
const RETURN_CATEGORY_OPTIONS: { label: string; value: ReturnCategory }[] = [ const RETURN_CATEGORY_OPTIONS: { label: string; value: ReturnCategory }[] = [
{ label: "Expired", value: "Expired" }, { label: "Expired", value: "Expired" },
@@ -367,6 +368,17 @@ function InventoryReturnsTab() {
const [dialogOpen, setDialogOpen] = React.useState(false) const [dialogOpen, setDialogOpen] = React.useState(false)
const [detailReturnId, setDetailReturnId] = React.useState<string | null>(null) const [detailReturnId, setDetailReturnId] = React.useState<string | null>(null)
const writeOffs = returns.filter((r) => !addsBackToStock(r.category))
const customerReturns = returns.filter((r) => addsBackToStock(r.category))
const writeOffValue = writeOffs.reduce((sum, r) => sum + r.amount, 0)
const now = new Date()
const thisMonthWriteOffValue = writeOffs
.filter((r) => {
const d = new Date(r.returnedAt)
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth()
})
.reduce((sum, r) => sum + r.amount, 0)
const columns: DataTableColumn<StockReturn>[] = [ const columns: DataTableColumn<StockReturn>[] = [
{ {
key: "product", key: "product",
@@ -429,6 +441,13 @@ function InventoryReturnsTab() {
</Button> </Button>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard icon={RotateCcwIcon} tone="blue" label="Total Returns" value={returns.length} />
<StatCard icon={TriangleAlertIcon} tone="amber" label="Write-off Value" value={formatCurrency(writeOffValue)} />
<StatCard icon={PackagePlusIcon} tone="green" label="Customer Returns" value={customerReturns.length} hint="Added back to stock" />
<StatCard icon={CalendarClockIcon} tone="purple" label="This Month Write-offs" value={formatCurrency(thisMonthWriteOffValue)} />
</div>
<DataTable <DataTable
columns={columns} columns={columns}
rows={returns} rows={returns}
@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { EyeIcon, PlusIcon, ReceiptIcon, TrashIcon } from "lucide-react" import { CalendarClockIcon, DollarSignIcon, EyeIcon, PlusIcon, ReceiptIcon, TrashIcon, TrendingUpIcon } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { createSale, getProducts, getSale, getSales } from "@/lib/api/inventory" import { createSale, getProducts, getSale, getSales } from "@/lib/api/inventory"
@@ -28,6 +28,7 @@ import { Spinner } from "@/components/ui/spinner"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" import { DataTable, type DataTableColumn } from "@/components/hexa/data-table"
import { PageSkeleton } from "@/components/hexa/page-skeleton" import { PageSkeleton } from "@/components/hexa/page-skeleton"
import { StatCard } from "@/components/hexa/stat-card"
const PAYMENT_METHOD_OPTIONS: { label: string; value: PaymentMethod }[] = [ const PAYMENT_METHOD_OPTIONS: { label: string; value: PaymentMethod }[] = [
{ label: "Cash", value: "Cash" }, { label: "Cash", value: "Cash" },
@@ -358,6 +359,15 @@ function InventorySalesTab() {
const [dialogOpen, setDialogOpen] = React.useState(false) const [dialogOpen, setDialogOpen] = React.useState(false)
const [detailSaleId, setDetailSaleId] = React.useState<string | null>(null) const [detailSaleId, setDetailSaleId] = React.useState<string | null>(null)
const totalRevenue = sales.reduce((sum, s) => sum + s.totalAmount, 0)
const today = new Date()
const todaySales = sales.filter((s) => {
const d = new Date(s.soldAt)
return d.toDateString() === today.toDateString()
})
const todayRevenue = todaySales.reduce((sum, s) => sum + s.totalAmount, 0)
const avgSaleValue = sales.length ? totalRevenue / sales.length : 0
const columns: DataTableColumn<Sale>[] = [ const columns: DataTableColumn<Sale>[] = [
{ {
key: "saleNumber", key: "saleNumber",
@@ -414,6 +424,13 @@ function InventorySalesTab() {
</Button> </Button>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard icon={ReceiptIcon} tone="blue" label="Total Sales" value={sales.length} />
<StatCard icon={DollarSignIcon} tone="purple" label="Total Revenue" value={formatCurrency(totalRevenue)} />
<StatCard icon={CalendarClockIcon} tone="amber" label="Today" value={formatCurrency(todayRevenue)} hint={`${todaySales.length} sales`} />
<StatCard icon={TrendingUpIcon} tone="green" label="Avg. Sale Value" value={formatCurrency(avgSaleValue)} />
</div>
<DataTable <DataTable
columns={columns} columns={columns}
rows={sales} rows={sales}
@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { ArrowDownCircleIcon, ArrowUpCircleIcon, ListIcon, RotateCcwIcon } from "lucide-react" import { ArrowDownCircleIcon, ArrowUpCircleIcon, ListIcon, ListOrderedIcon, RotateCcwIcon } from "lucide-react"
import { getProducts, getStockMovements } from "@/lib/api/inventory" import { getProducts, getStockMovements } from "@/lib/api/inventory"
import type { ReturnCategory, StockMovement } from "@/lib/models/inventory" import type { ReturnCategory, StockMovement } from "@/lib/models/inventory"
@@ -15,6 +15,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" import { DataTable, type DataTableColumn } from "@/components/hexa/data-table"
import { InfoBanner } from "@/components/hexa/info-banner" import { InfoBanner } from "@/components/hexa/info-banner"
import { PageSkeleton } from "@/components/hexa/page-skeleton" import { PageSkeleton } from "@/components/hexa/page-skeleton"
import { StatCard } from "@/components/hexa/stat-card"
const TYPE_OPTIONS: { label: string; value: "all" | StockMovement["type"] }[] = [ const TYPE_OPTIONS: { label: string; value: "all" | StockMovement["type"] }[] = [
{ label: "All types", value: "all" }, { label: "All types", value: "all" },
@@ -92,6 +93,10 @@ function InventoryStockTrackingTab() {
const productOptions = [{ label: "All products", value: "all" }, ...products.map((p) => ({ label: p.name, value: p.productId }))] const productOptions = [{ label: "All products", value: "all" }, ...products.map((p) => ({ label: p.name, value: p.productId }))]
const stockInValue = filtered.filter((m) => m.type === "GRN").reduce((sum, m) => sum + m.amount, 0)
const stockOutValue = filtered.filter((m) => m.type === "Sale").reduce((sum, m) => sum + m.amount, 0)
const writeOffValue = filtered.filter((m) => m.type === "Return" && m.quantityChange < 0).reduce((sum, m) => sum + m.amount, 0)
const columns: DataTableColumn<StockMovement>[] = [ const columns: DataTableColumn<StockMovement>[] = [
{ key: "date", header: "Date", cell: (row) => formatDate(row.date) }, { key: "date", header: "Date", cell: (row) => formatDate(row.date) },
{ {
@@ -138,6 +143,13 @@ function InventoryStockTrackingTab() {
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard icon={ListOrderedIcon} tone="blue" label="Movements Shown" value={filtered.length} />
<StatCard icon={ArrowDownCircleIcon} tone="green" label="Stock In" value={formatCurrency(stockInValue)} hint="GRN value" />
<StatCard icon={ArrowUpCircleIcon} tone="purple" label="Stock Out" value={formatCurrency(stockOutValue)} hint="Sales value" />
<StatCard icon={RotateCcwIcon} tone="amber" label="Write-offs" value={formatCurrency(writeOffValue)} hint="Returns value" />
</div>
<div className="flex flex-col gap-3 rounded-xl border bg-card p-3 sm:flex-row sm:items-end sm:flex-wrap"> <div className="flex flex-col gap-3 rounded-xl border bg-card p-3 sm:flex-row sm:items-end sm:flex-wrap">
<div className="flex min-w-36 flex-col gap-1.5"> <div className="flex min-w-36 flex-col gap-1.5">
<Label>Type</Label> <Label>Type</Label>
@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { PlusIcon, TruckIcon } from "lucide-react" import { CheckCircle2Icon, PlusIcon, TruckIcon, XCircleIcon } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { createSupplier, getSuppliers } from "@/lib/api/inventory" import { createSupplier, getSuppliers } from "@/lib/api/inventory"
@@ -22,6 +22,7 @@ import { Label } from "@/components/ui/label"
import { Spinner } from "@/components/ui/spinner" import { Spinner } from "@/components/ui/spinner"
import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" import { DataTable, type DataTableColumn } from "@/components/hexa/data-table"
import { PageSkeleton } from "@/components/hexa/page-skeleton" import { PageSkeleton } from "@/components/hexa/page-skeleton"
import { StatCard } from "@/components/hexa/stat-card"
import { StatusBadge } from "@/components/hexa/status-badge" import { StatusBadge } from "@/components/hexa/status-badge"
import type { Supplier } from "@/lib/models/inventory" import type { Supplier } from "@/lib/models/inventory"
@@ -115,6 +116,9 @@ function InventorySuppliersTab() {
const suppliers = suppliersFetch.data ?? [] const suppliers = suppliersFetch.data ?? []
const [dialogOpen, setDialogOpen] = React.useState(false) const [dialogOpen, setDialogOpen] = React.useState(false)
const activeSuppliers = suppliers.filter((s) => s.isActive).length
const inactiveSuppliers = suppliers.length - activeSuppliers
async function handleCreate(values: SupplierFormValues) { async function handleCreate(values: SupplierFormValues) {
const input: SupplierInput = { const input: SupplierInput = {
name: values.name.trim(), name: values.name.trim(),
@@ -171,6 +175,12 @@ function InventorySuppliersTab() {
</Button> </Button>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<StatCard icon={TruckIcon} tone="blue" label="Total Suppliers" value={suppliers.length} />
<StatCard icon={CheckCircle2Icon} tone="green" label="Active" value={activeSuppliers} />
<StatCard icon={XCircleIcon} tone="amber" label="Inactive" value={inactiveSuppliers} />
</div>
<DataTable <DataTable
columns={columns} columns={columns}
rows={suppliers} rows={suppliers}