"use client" import * as React from "react" import { EyeIcon, PlusIcon, ReceiptIcon, TrashIcon } from "lucide-react" import { toast } from "sonner" import { createSale, getProducts, getSale, getSales } from "@/lib/api/inventory" import { getMembers } from "@/lib/api/customers" import type { CreateSaleInput, Sale, SaleItemInput } from "@/lib/models/inventory" import type { PaymentMethod } from "@/lib/models/booking" import { formatCurrency } from "@/lib/format" import { useFetch } from "@/lib/use-fetch" import { useMutation } from "@/lib/use-mutation" import { Badge } from "@/components/ui/badge" 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 { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { DataTable, type DataTableColumn } from "@/components/hexa/data-table" import { PageSkeleton } from "@/components/hexa/page-skeleton" const PAYMENT_METHOD_OPTIONS: { label: string; value: PaymentMethod }[] = [ { label: "Cash", value: "Cash" }, { label: "Bank", value: "Bank" }, ] function formatDate(iso: string) { return new Date(iso).toLocaleString(undefined, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }) } interface SaleLine { productId: string quantity: string unitPrice: string } interface CreateSaleDialogProps { open: boolean onOpenChange: (open: boolean) => void onCreated: () => void } function CreateSaleDialog({ open, onOpenChange, onCreated }: CreateSaleDialogProps) { const productsFetch = useFetch(() => getProducts(false)) const products = productsFetch.data ?? [] const membersFetch = useFetch(() => getMembers()) const members = (membersFetch.data ?? []).filter((m) => !!m.customer) const memberOptions = [ { label: "Walk-in (not registered)", value: "none" }, ...members.map((m) => ({ label: m.customer!.cusName ?? m.customer!.primaryPH ?? "Member", value: m.fkCustomerId })), ] const [customerId, setCustomerId] = React.useState("") const [customerName, setCustomerName] = React.useState("") const [paymentMethod, setPaymentMethod] = React.useState("Cash") const [lines, setLines] = React.useState([{ productId: "", quantity: "1", unitPrice: "0" }]) const { busy, run } = useMutation() const [prevOpen, setPrevOpen] = React.useState(open) if (open !== prevOpen) { setPrevOpen(open) if (open) { setCustomerId("") setCustomerName("") setPaymentMethod("Cash") setLines([{ productId: "", quantity: "1", unitPrice: "0" }]) } } function selectMember(id: string) { setCustomerId(id === "none" ? "" : id) const member = id !== "none" ? members.find((m) => m.fkCustomerId === id) : undefined if (member?.customer) setCustomerName(member.customer.cusName ?? "") } function updateLine(idx: number, patch: Partial) { setLines((prev) => prev.map((l, i) => { if (i !== idx) return l const next = { ...l, ...patch } if (patch.productId) { const product = products.find((p) => p.productId === patch.productId) if (product) next.unitPrice = String(product.sellingPrice) } return next }) ) } function addLine() { setLines((prev) => [...prev, { productId: "", quantity: "1", unitPrice: "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.unitPrice) || 0), 0) const linesValid = lines.length > 0 && lines.every((l) => !!l.productId && Number(l.quantity) > 0 && Number(l.unitPrice) >= 0) const canSubmit = linesValid && !busy async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!canSubmit) return const input: CreateSaleInput = { paymentMethod, customerId: customerId || undefined, customerName: customerName.trim() || undefined, items: lines.map((l) => ({ productId: l.productId, quantity: Number(l.quantity), unitPrice: Number(l.unitPrice), })), } try { await run(async () => { await createSale(input) toast.success("Sale recorded", { description: "Stock levels were updated." }) onCreated() }) onOpenChange(false) } catch { // run() already toasted the real error; keep the dialog open for retry. } } return (
Record Sale Stock going out to a customer — decreases quantity on hand.
setCustomerName(e.target.value)} />
Product Quantity Unit Price (Rs.)
{lines.map((line, idx) => (
updateLine(idx, { quantity: e.target.value })} required /> updateLine(idx, { unitPrice: e.target.value })} required />
))}
Total {formatCurrency(total)}
) } interface SaleDetailDialogProps { saleId: string | null onOpenChange: (open: boolean) => void } function SaleDetailDialog({ saleId, onOpenChange }: SaleDetailDialogProps) { const detailFetch = useFetch(() => (saleId ? getSale(saleId) : Promise.resolve(null))) const sale = detailFetch.data // This dialog stays mounted across opens (only `saleId` changes) -- useFetch only runs its // fetcher on mount and on an explicit reload(), so a fresh id needs an explicit reload too. React.useEffect(() => { if (saleId) detailFetch.reload() // eslint-disable-next-line react-hooks/exhaustive-deps -- only react to the id changing }, [saleId, detailFetch.reload]) return ( !open && onOpenChange(false)}> Sale Details {detailFetch.loading || !sale ? (
) : (

Sale Number

{sale.saleNumber}

Sold

{formatDate(sale.soldAt)}

Customer

{sale.customerName || "Walk-in"}

Payment Method

{sale.paymentMethod}

Total

{formatCurrency(sale.totalAmount)}

Items

{sale.items.map((item, idx) => (

{item.productName}

{item.quantity} × {formatCurrency(item.unitPrice)}

{formatCurrency(item.lineTotal)}
))}
)}
) } function InventorySalesTab() { const salesFetch = useFetch(() => getSales()) const sales = salesFetch.data ?? [] const [dialogOpen, setDialogOpen] = React.useState(false) const [detailSaleId, setDetailSaleId] = React.useState(null) const columns: DataTableColumn[] = [ { key: "saleNumber", header: "Sale", cell: (row) => (

{row.saleNumber}

{row.customerName || "Walk-in"}

), }, { key: "sold", header: "Sold", cell: (row) => formatDate(row.soldAt) }, { key: "items", header: "Items", cell: (row) => row.items.length }, { key: "method", header: "Payment", cell: (row) => {row.paymentMethod} }, { key: "total", header: "Total", cell: (row) => formatCurrency(row.totalAmount) }, { key: "actions", header: "Actions", className: "text-center", cell: (row) => ( setDetailSaleId(row.saleId)}> } /> View details ), }, ] if (salesFetch.loading) { return } return (

Sales

Stock going out to a customer.

row.saleId} emptyMessage="No sales yet." onRowClick={(row) => setDetailSaleId(row.saleId)} renderMobileCard={(row) => (

{row.saleNumber}

{row.customerName || "Walk-in"} · {formatDate(row.soldAt)}

{row.paymentMethod} {formatCurrency(row.totalAmount)}
)} /> setDetailSaleId(null)} />
) } export { InventorySalesTab }