import { useState, useRef, useCallback } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { usePOSStore } from '@/stores/pos.store' import { productSearchOptions, posMutations, posKeys, type Transaction, type Product } from '@/api/pos' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { Search, ScanBarcode, Wrench, PenLine } from 'lucide-react' import { toast } from 'sonner' interface POSItemPanelProps { transaction: Transaction | null } export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) { const queryClient = useQueryClient() const { currentTransactionId, setTransaction, locationId, accountId } = usePOSStore() const [search, setSearch] = useState('') const [customOpen, setCustomOpen] = useState(false) const [customDesc, setCustomDesc] = useState('') const [customPrice, setCustomPrice] = useState('') const [customQty, setCustomQty] = useState('1') const searchRef = useRef(null) // Debounced product search const { data: productsData, isLoading: searchLoading } = useQuery({ ...productSearchOptions(search), enabled: search.length >= 1, }) const products = productsData?.data ?? [] // Add line item mutation const addItemMutation = useMutation({ mutationFn: async (product: Product) => { let txnId = currentTransactionId // Auto-create transaction if none exists if (!txnId) { const txn = await posMutations.createTransaction({ transactionType: 'sale', locationId: locationId ?? undefined, accountId: accountId ?? undefined, }) txnId = txn.id setTransaction(txnId) } return posMutations.addLineItem(txnId, { productId: product.id, description: product.name, qty: 1, unitPrice: parseFloat(product.price ?? '0'), }) }, onSuccess: () => { const txnId = usePOSStore.getState().currentTransactionId queryClient.invalidateQueries({ queryKey: posKeys.transaction(txnId ?? '') }) }, onError: (err) => toast.error(err.message), }) // Custom item mutation const addCustomMutation = useMutation({ mutationFn: async () => { let txnId = currentTransactionId if (!txnId) { const txn = await posMutations.createTransaction({ transactionType: 'sale', locationId: locationId ?? undefined, accountId: accountId ?? undefined, }) txnId = txn.id setTransaction(txnId) } return posMutations.addLineItem(txnId, { description: customDesc, qty: parseInt(customQty) || 1, unitPrice: parseFloat(customPrice) || 0, }) }, onSuccess: () => { const txnId = usePOSStore.getState().currentTransactionId queryClient.invalidateQueries({ queryKey: posKeys.transaction(txnId ?? '') }) setCustomOpen(false) setCustomDesc('') setCustomPrice('') setCustomQty('1') }, onError: (err) => toast.error(err.message), }) // UPC scan const scanMutation = useMutation({ mutationFn: async (upc: string) => { const product = await posMutations.lookupUpc(upc) let txnId = currentTransactionId if (!txnId) { const txn = await posMutations.createTransaction({ transactionType: 'sale', locationId: locationId ?? undefined, accountId: accountId ?? undefined, }) txnId = txn.id setTransaction(txnId) } return posMutations.addLineItem(txnId, { productId: product.id, description: product.name, qty: 1, unitPrice: parseFloat(product.price ?? '0'), }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') }) setSearch('') toast.success('Item scanned') }, onError: (err) => toast.error(err.message), }) const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => { // Barcode scanners typically send Enter after the code if (e.key === 'Enter' && search.length >= 6) { // Looks like a UPC — try scanning scanMutation.mutate(search) } }, [search, scanMutation]) return (
{/* Search bar */}
setSearch(e.target.value)} onKeyDown={handleSearchKeyDown} placeholder="Search products or scan barcode..." className="pl-10 h-11 text-base" autoFocus />
{/* Product grid */}
{searchLoading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : products.length > 0 ? (
{products.map((product) => ( ))}
) : search.length >= 1 ? (
No products found for "{search}"
) : (
Search for products to add to the sale
)}
{/* Quick action buttons */}
{/* Custom item dialog */} Add Custom Item
{ e.preventDefault(); addCustomMutation.mutate() }} className="space-y-4" >
setCustomDesc(e.target.value)} placeholder="Item description" required className="h-11" />
setCustomPrice(e.target.value)} placeholder="0.00" required className="h-11" />
setCustomQty(e.target.value)} className="h-11" />
) }