2import { useState, useEffect, useRef } from "react";
3import Link from "next/link";
13interface CartItem extends Product {
18export default function SalesPage() {
19 const [loading, setLoading] = useState(false);
20 const [products, setProducts] = useState<Product[]>([]);
21 const [searchTerm, setSearchTerm] = useState("");
22 const [cart, setCart] = useState<CartItem[]>([]);
23 const [isDropdownOpen, setIsDropdownOpen] = useState(false);
24 const searchInputRef = useRef<HTMLInputElement>(null);
25 const dropdownRef = useRef<HTMLDivElement>(null);
27 const loadProducts = async (search: string) => {
28 if (!search || search.trim().length < 2) {
30 setIsDropdownOpen(false);
35 const response = await fetch(`/api/v1/openapi/products?limit=10000&search=${encodeURIComponent(search)}`);
36 const data = await response.json();
38 if (data.success && data.data) {
39 const productsArray = data.data.value || data.data;
40 setProducts(Array.isArray(productsArray) ? productsArray : []);
41 setIsDropdownOpen(true);
44 console.error('Failed to load products:', error);
51 const timer = setTimeout(() => {
52 loadProducts(searchTerm);
54 return () => clearTimeout(timer);
57 // Close dropdown when clicking outside
59 const handleClickOutside = (event: MouseEvent) => {
60 if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
61 searchInputRef.current && !searchInputRef.current.contains(event.target as Node)) {
62 setIsDropdownOpen(false);
66 document.addEventListener('mousedown', handleClickOutside);
67 return () => document.removeEventListener('mousedown', handleClickOutside);
70 const addToCart = (product: Product) => {
71 const existingItem = cart.find(item => item.Pid === product.Pid);
73 setCart(cart.map(item =>
74 item.Pid === product.Pid
75 ? { ...item, quantity: item.quantity + 1, subtotal: (item.quantity + 1) * (item.Price || 0) }
82 subtotal: product.Price || 0
87 setIsDropdownOpen(false);
88 searchInputRef.current?.focus();
91 const removeFromCart = (pid: number) => {
92 setCart(cart.filter(item => item.Pid !== pid));
95 const updateQuantity = (pid: number, quantity: number) => {
100 setCart(cart.map(item =>
102 ? { ...item, quantity, subtotal: quantity * (item.Price || 0) }
107 const cartTotal = cart.reduce((sum, item) => sum + item.subtotal, 0);
109 const clearCart = () => {
114 <div className="min-h-screen bg-gray-50">
115 <div className="max-w-7xl mx-auto p-6">
116 <div className="mb-6">
117 <h1 className="text-3xl font-bold text-gray-900 mb-2">Point of Sale</h1>
118 <p className="text-gray-600">Scan or search for products to add to the sale</p>
121 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
122 {/* Product Search Combobox */}
123 <div className="lg:col-span-2">
124 <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
125 <h2 className="text-xl font-bold text-gray-900 mb-4">Search Products</h2>
127 {/* Search Combobox */}
128 <div className="relative">
133 onChange={(e) => setSearchTerm(e.target.value)}
134 onFocus={() => products.length > 0 && setIsDropdownOpen(true)}
135 placeholder="Type to search by name, PLU, or barcode..."
136 className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-lg"
140 {/* Dropdown Results */}
141 {isDropdownOpen && searchTerm.length >= 2 && (
144 className="absolute z-50 w-full mt-2 bg-white border border-gray-300 rounded-lg shadow-lg max-h-96 overflow-y-auto"
147 <div className="p-4 text-center">
148 <div className="inline-block w-6 h-6 border-3 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
149 <p className="text-gray-600 text-sm mt-2">Searching...</p>
151 ) : products.length > 0 ? (
153 <div className="px-4 py-2 bg-gray-50 border-b border-gray-200 text-sm text-gray-600">
154 {products.length} product{products.length !== 1 ? 's' : ''} found
156 {products.slice(0, 50).map((product) => (
159 onClick={() => addToCart(product)}
160 className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0 transition"
162 <div className="flex justify-between items-start">
163 <div className="flex-1 pr-4">
164 <div className="font-semibold text-gray-900">{product.Name}</div>
165 <div className="text-sm text-gray-600 mt-1">
166 PLU: {product.PLU || 'N/A'} • Stock: {product.OnHand || 0}
169 <div className="text-lg font-bold text-green-600 whitespace-nowrap">
170 ${(product.Price || 0).toFixed(2)}
177 <div className="p-4 text-center text-gray-500">
178 <div className="text-3xl mb-2">🔍</div>
179 <p className="text-sm">No products found</p>
187 <p className="mt-3 text-sm text-gray-500">
188 💡 Type at least 2 characters to search. Click a product to add it to the cart.
192 {/* Quick Actions */}
193 <div className="mt-4 grid grid-cols-3 gap-3">
195 href="/pages/invoices"
196 className="p-3 bg-white rounded-lg border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition text-center"
198 <div className="text-2xl mb-1">📋</div>
199 <div className="text-xs font-medium text-gray-700">Recent Sales</div>
202 href="/pages/products"
203 className="p-3 bg-white rounded-lg border border-gray-200 hover:border-green-300 hover:bg-green-50 transition text-center"
205 <div className="text-2xl mb-1">📦</div>
206 <div className="text-xs font-medium text-gray-700">Products</div>
209 href="/pages/customers"
210 className="p-3 bg-white rounded-lg border border-gray-200 hover:border-purple-300 hover:bg-purple-50 transition text-center"
212 <div className="text-2xl mb-1">👥</div>
213 <div className="text-xs font-medium text-gray-700">Customers</div>
218 {/* Cart / Current Sale */}
219 <div className="lg:col-span-1">
220 <div className="bg-white rounded-lg shadow-sm border border-gray-200 sticky top-6">
221 <div className="p-4 border-b border-gray-200 bg-blue-50">
222 <h2 className="text-lg font-bold text-gray-900">Current Sale</h2>
223 <p className="text-sm text-gray-600">{cart.length} item(s)</p>
226 <div className="p-4 max-h-[400px] overflow-y-auto">
227 {cart.length === 0 ? (
228 <div className="text-center py-8 text-gray-500">
229 <div className="text-4xl mb-2">🛒</div>
230 <p className="text-sm">Cart is empty</p>
233 <div className="space-y-3">
234 {cart.map((item) => (
235 <div key={item.Pid} className="border-b border-gray-200 pb-3">
236 <div className="flex justify-between items-start mb-2">
237 <div className="flex-1 pr-2">
238 <div className="font-semibold text-sm text-gray-900">{item.Name}</div>
239 <div className="text-xs text-gray-600">${(item.Price || 0).toFixed(2)} each</div>
242 onClick={() => removeFromCart(item.Pid)}
243 className="text-red-600 hover:text-red-800 text-xs"
248 <div className="flex items-center justify-between">
249 <div className="flex items-center space-x-2">
251 onClick={() => updateQuantity(item.Pid, item.quantity - 1)}
252 className="w-6 h-6 bg-gray-200 rounded hover:bg-gray-300 text-sm font-bold"
256 <span className="font-semibold text-gray-900 w-8 text-center">{item.quantity}</span>
258 onClick={() => updateQuantity(item.Pid, item.quantity + 1)}
259 className="w-6 h-6 bg-gray-200 rounded hover:bg-gray-300 text-sm font-bold"
264 <div className="font-bold text-gray-900">${item.subtotal.toFixed(2)}</div>
272 {cart.length > 0 && (
274 <div className="p-4 border-t border-gray-200 bg-gray-50">
275 <div className="flex justify-between items-center mb-4">
276 <span className="text-lg font-bold text-gray-900">Total:</span>
277 <span className="text-2xl font-bold text-green-600">${cartTotal.toFixed(2)}</span>
279 <button className="w-full py-3 bg-green-600 text-white rounded-lg font-bold hover:bg-green-700 transition mb-2">
284 className="w-full py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 transition"