import { apiFetch } from '../api'; import React, { useState, useEffect, useRef } from 'react'; import { Plus, X, Search, Filter, MoreVertical, RefreshCw, Edit2, Power, PowerOff, Tag, Package, Image as ImageIcon, Barcode, DollarSign, ChevronDown, Clock, Check, Trash2, Database } from 'lucide-react'; import Pagination from '../components/Pagination'; interface ProductSession { id?: number; dayOfWeek: string; active: boolean; startTime: string; endTime: string; } interface Product { id?: number; productId: string; name: string; category: string; description: string; basePrice: number; price: number; offerPrice: number; discountPercent: number; discountAmount: number; counter: string; tag: string; parcelCharges: number; barcode: string; attributesOptional: boolean; veg: boolean; hasAllergy: boolean; parcelNotAllowed: boolean; sessionOptional: boolean; sessions: ProductSession[]; imageData: string; active: boolean; stock: number; stalls?: { id: number; name: string }[]; parcellable?: boolean; } interface Stall { id: number; name: string; baseItems?: { id: number; name: string }[]; } interface BaseItem { id: number; name: string; } const DAYS = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; const getDefaultSessions = (): ProductSession[] => DAYS.map(day => ({ dayOfWeek: day, active: true, startTime: '00:00', endTime: '23:59' })); const emptyProduct: Product = { productId: '', name: '', category: '', description: '', basePrice: 0, price: 0, offerPrice: 0, discountPercent: 0, discountAmount: 0, counter: '', tag: '', parcelCharges: 0, barcode: '', attributesOptional: false, veg: true, hasAllergy: false, parcelNotAllowed: false, sessionOptional: false, sessions: getDefaultSessions(), imageData: '', active: true, stock: 0, parcellable: false }; const Products = () => { const [products, setProducts] = useState([]); const [baseItems, setBaseItems] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [showSessionModal, setShowSessionModal] = useState(false); const [editingProduct, setEditingProduct] = useState(null); const [formData, setFormData] = useState(emptyProduct); const [allStalls, setAllStalls] = useState([]); const [openMenuId, setOpenMenuId] = useState(null); const [searchTerm, setSearchTerm] = useState(''); // Pagination States const [currentPage, setCurrentPage] = useState(0); const [pageSize, setPageSize] = useState(10); const [totalElements, setTotalElements] = useState(0); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); const menuRef = useRef(null); const fileInputRef = useRef(null); // Debounce search term useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 500); return () => clearTimeout(timer); }, [searchTerm]); // Reset to first page when search changes useEffect(() => { setCurrentPage(0); }, [debouncedSearchTerm]); useEffect(() => { fetchProducts(); // SSE Real-time stock updates const host = window.location.hostname; const eventSource = new EventSource(`http://${host}:8080/api/stock/stream`); eventSource.addEventListener('stockUpdate', (event: any) => { try { const update = JSON.parse(event.data); setProducts(prevProducts => prevProducts.map(p => p.id === update.productId ? { ...p, stock: update.stock } : p ) ); } catch (err) { console.error('Error processing stock update:', err); } }); return () => { eventSource.close(); }; }, [currentPage, pageSize, debouncedSearchTerm]); useEffect(() => { fetchBaseItems(); fetchAllStalls(); const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setOpenMenuId(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const fetchProducts = async () => { setLoading(true); try { const host = window.location.hostname; const params = new URLSearchParams(); params.append('page', currentPage.toString()); params.append('size', pageSize.toString()); if (debouncedSearchTerm) { params.append('search', debouncedSearchTerm); } const response = await apiFetch(`http://${host}:8080/api/products?${params.toString()}`); const data = await response.json(); if (data && data.content) { setProducts(data.content); setTotalElements(data.totalElements); } else { setProducts([]); setTotalElements(0); } } catch (error) { console.error('Error fetching products:', error); } finally { setLoading(false); } }; const fetchBaseItems = async () => { try { const response = await apiFetch('http://localhost:8080/api/base-items?size=100'); const data = await response.json(); setBaseItems(data.content || data); } catch (error) { console.error('Error fetching base items:', error); } }; const fetchAllStalls = async () => { try { const response = await apiFetch('http://localhost:8080/api/stalls'); const data = await response.json(); setAllStalls(data); } catch (error) { console.error('Error fetching stalls:', error); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const url = editingProduct ? `http://localhost:8080/api/products/${editingProduct.id}` : 'http://localhost:8080/api/products'; const method = editingProduct ? 'PUT' : 'POST'; try { const response = await apiFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), }); if (response.ok) { setShowModal(false); setEditingProduct(null); setFormData(emptyProduct); fetchProducts(); } } catch (error) { console.error('Error saving product:', error); } }; const handleEdit = (product: Product) => { setEditingProduct(product); setFormData({ ...product, sessions: product.sessions && product.sessions.length > 0 ? product.sessions : getDefaultSessions() }); setShowModal(true); setOpenMenuId(null); }; const handleToggleActive = async (product: Product) => { try { const response = await apiFetch(`http://localhost:8080/api/products/${product.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...product, active: !product.active }), }); if (response.ok) { fetchProducts(); setOpenMenuId(null); } } catch (error) { console.error('Error toggling active status:', error); } }; const handleToggleStock = async (product: Product) => { try { const response = await apiFetch(`http://localhost:8080/api/products/${product.id}/toggle-stock`, { method: 'PATCH', }); if (response.ok) { fetchProducts(); setOpenMenuId(null); } } catch (error) { console.error('Error toggling stock status:', error); } }; const handleDelete = async (product: Product) => { if (!window.confirm(`Are you sure you want to delete ${product.name}?`)) return; try { const response = await apiFetch(`http://localhost:8080/api/products/${product.id}`, { method: 'DELETE', }); if (response.ok) { fetchProducts(); setOpenMenuId(null); } } catch (error) { console.error('Error deleting product:', error); } }; const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { setFormData({ ...formData, imageData: reader.result as string }); }; reader.readAsDataURL(file); } }; const updateSession = (index: number, updates: Partial) => { const newSessions = [...formData.sessions]; newSessions[index] = { ...newSessions[index], ...updates }; setFormData({ ...formData, sessions: newSessions }); }; return (

Products

Manage your product catalog and inventory

{/* Filters Bar */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-gray-50 border border-[#e2e8f0] rounded-lg text-sm focus:outline-none focus:border-[#001828]/30 transition-all" />
{/* Table */}
{loading ? ( ) : products.length === 0 ? ( ) : products.map((product) => ( ))}
Product Info Category Selling at Price Details Status Actions
Loading...
No products found
{product.imageData ? ( {product.name} ) : (
)}

{product.name}

ID: {product.productId || 'N/A'}

{product.category}
{(() => { // Combine direct stalls with stalls that have this product's category in their baseItems const directStalls = product.stalls || []; const indirectStalls = allStalls.filter(s => s.baseItems?.some(bi => bi.name.toLowerCase() === product.category?.toLowerCase()) ).map(s => ({ id: s.id, name: s.name })); // Unique stalls by ID const combinedStalls = Array.from( new Map([...directStalls, ...indirectStalls].map(s => [s.id, s])).values() ); return combinedStalls.length > 0 ? ( combinedStalls.map(stall => ( {stall.name} )) ) : ( Not Assigned ); })()}
₹{product.price}
{product.active ? 'Active' : 'Inactive'} 0 ? 'bg-orange-100 text-orange-700' : 'bg-red-100 text-red-700'}`}> {product.stock > 0 ? `In Stock (${product.stock})` : 'Out of Stock'}
{openMenuId === product.id && (
e.stopPropagation()} className="absolute right-6 top-12 w-48 bg-white rounded-xl shadow-2xl border border-[#e2e8f0] z-[110] overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200" >
)}
{ setPageSize(newSize); setCurrentPage(0); }} />
{/* Product Modal */} {showModal && (

{editingProduct ? 'Edit Product' : 'Add New Product'}

setFormData({ ...formData, name: e.target.value })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm font-semibold" />