import React, { useState, useEffect } from 'react'; import { Search, ShoppingCart, Eye, Plus, X, Loader2, CheckCircle, Trash2, Edit2 } from 'lucide-react'; interface PurchaseOrderItem { productName: string; quantity: number; rate: number; total: number; } interface PurchaseOrder { id: number; purchaseId: string; date: string; vendor: { id: number; name: string }; amount: number; paidTotal: number; status: 'OPEN' | 'TRANSIT' | 'CANCELLED' | 'BILLED' | 'RECEIVED' | 'PARTIALLY PAID' | 'CLOSE'; referenceId: string; items: PurchaseOrderItem[]; } const Purchases: React.FC = () => { const [orders, setOrders] = useState([]); const [vendors, setVendors] = useState<{id: number, name: string}[]>([]); const [availableProducts, setAvailableProducts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [activeSearchIdx, setActiveSearchIdx] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [showAddModal, setShowAddModal] = useState(false); const [isSaving, setIsSaving] = useState(false); const [editingOrder, setEditingOrder] = useState(null); const [showHistoryModal, setShowHistoryModal] = useState(false); const [orderHistory, setOrderHistory] = useState([]); const [activeHistoryOrder, setActiveHistoryOrder] = useState(null); // New Order Form State const initialOrderState = { purchaseId: `PO_${Math.floor(1000 + Math.random() * 9000)}`, date: new Date().toISOString().split('T')[0], vendorId: '', status: 'OPEN', discount: 0, referenceId: '', instruction: '', items: [{ productName: '', quantity: 1, rate: 0, total: 0 }] }; const [newOrder, setNewOrder] = useState(initialOrderState); useEffect(() => { fetchOrders(); fetchVendors(); fetchProducts(); const handleClickOutside = (e: MouseEvent) => { if (!(e.target as HTMLElement).closest('.product-search-container')) { setActiveSearchIdx(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const fetchOrders = async () => { try { setIsLoading(true); const response = await fetch('/api/purchases/orders'); if (response.ok) { const data = await response.json(); setOrders(data); } } catch (error) { console.error('Error fetching orders:', error); } finally { setIsLoading(false); } }; const fetchVendors = async () => { try { const response = await fetch('/api/purchases/vendors'); if (response.ok) { const data = await response.json(); setVendors(data); } } catch (error) { console.error('Error fetching vendors:', error); } }; const fetchProducts = async () => { try { const response = await fetch('/api/products?size=1000'); if (response.ok) { const data = await response.json(); setAvailableProducts(data.content || data); } } catch (error) { console.error('Error fetching products:', error); } }; const handleAddItem = () => { setNewOrder({ ...newOrder, items: [...newOrder.items, { productName: '', quantity: 1, rate: 0, total: 0 }] }); }; const handleItemChange = (index: number, field: keyof PurchaseOrderItem, value: string | number) => { setNewOrder(prev => { const updatedItems = [...prev.items]; const item = { ...updatedItems[index], [field]: value }; if (field === 'quantity' || field === 'rate' || field === 'productName') { item.total = Number(item.quantity) * Number(item.rate); } updatedItems[index] = item; return { ...prev, items: updatedItems }; }); }; const handleProductSelect = (index: number, product: any) => { setNewOrder(prev => { const updatedItems = [...prev.items]; updatedItems[index] = { ...updatedItems[index], productName: product.name, rate: product.basePrice || 0, total: Number(updatedItems[index].quantity) * Number(product.basePrice || 0) }; return { ...prev, items: updatedItems }; }); setActiveSearchIdx(null); }; const calculateTotal = () => { return newOrder.items.reduce((sum, item) => sum + item.total, 0) - newOrder.discount; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); try { const payload = { ...newOrder, id: editingOrder?.id, vendor: { id: parseInt(newOrder.vendorId) }, amount: calculateTotal(), date: new Date(newOrder.date).toISOString() }; const response = await fetch('/api/purchases/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { setShowAddModal(false); setEditingOrder(null); setNewOrder(initialOrderState); fetchOrders(); } } catch (error) { console.error('Error saving order:', error); } finally { setIsSaving(false); } }; const deleteOrder = async (id: number) => { if (!window.confirm('Delete this purchase order?')) return; try { await fetch(`/api/purchases/orders/${id}`, { method: 'DELETE' }); fetchOrders(); } catch (error) { console.error('Error deleting order:', error); } } const getStatusColor = (status: string) => { switch (status) { case 'OPEN': return 'bg-blue-100 text-blue-700'; case 'TRANSIT': return 'bg-indigo-100 text-indigo-700'; case 'BILLED': return 'bg-amber-100 text-amber-700'; case 'RECEIVED': return 'bg-emerald-100 text-emerald-700'; case 'PARTIALLY PAID': return 'bg-violet-100 text-violet-700'; case 'CLOSE': return 'bg-gray-100 text-gray-700'; case 'CANCELLED': return 'bg-rose-100 text-rose-700'; default: return 'bg-gray-100 text-gray-700'; } }; const updateOrderStatus = async (orderId: number, newStatus: string) => { try { const order = orders.find(o => o.id === orderId); if (!order) return; const response = await fetch(`/api/purchases/orders`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...order, status: newStatus }) }); if (response.ok) { fetchOrders(); } } catch (error) { console.error('Error updating status:', error); } }; const handleEditOrder = (order: PurchaseOrder) => { setEditingOrder(order); setNewOrder({ purchaseId: order.purchaseId, date: order.date.split('T')[0], vendorId: order.vendor.id.toString(), status: order.status, discount: 0, // We should ideally store discount in DB too, but for now 0 referenceId: order.referenceId || '', instruction: '', // Same for instruction items: order.items.map(item => ({...item})) }); setShowAddModal(true); }; const fetchOrderHistory = async (order: PurchaseOrder) => { try { setActiveHistoryOrder(order); const response = await fetch(`/api/purchases/orders/${order.id}/history`); if (response.ok) { const data = await response.json(); setOrderHistory(data); setShowHistoryModal(true); } } catch (error) { console.error('Error fetching history:', error); } }; return (

Purchase Orders

Track and manage your inventory procurement

setSearchTerm(e.target.value)} />
{isLoading ? ( ) : orders.filter(o => o.purchaseId.toLowerCase().includes(searchTerm.toLowerCase())).map((order) => ( ))}
Purchase ID Date Vendor Amount Status Reference ID Actions
Loading records...
{order.purchaseId} {new Date(order.date).toLocaleDateString()}
{order.vendor.name.charAt(0)}
{order.vendor.name}
₹{order.amount.toLocaleString()} {order.referenceId || `Pur_${Math.random().toString(36).substring(2, 10).toUpperCase()}`}
{showAddModal && (
{editingOrder ? : }

{editingOrder ? 'Edit Purchase Order' : 'Create New Purchase'}

{editingOrder ? 'Update terms or quantities' : 'Draft a new inventory procurement order'}

setNewOrder({...newOrder, date: e.target.value})} className="w-full px-4 py-3 bg-white border border-[#e2e8f0] rounded-xl text-sm font-bold outline-none focus:border-[#231651]" />

Order Items

{newOrder.items.map((item, idx) => ( ))}
Product Name Quantity Rate Total
handleItemChange(idx, 'productName', e.target.value)} onFocus={() => setActiveSearchIdx(idx)} className="w-full bg-transparent text-sm font-bold outline-none" /> {activeSearchIdx === idx && (
{availableProducts .filter(p => { const name = p.name || ''; const query = (item.productName || '').toLowerCase(); return name.toLowerCase().includes(query); }) .slice(0, 10) .map(p => ( )) } {item.productName && !availableProducts.some(p => p.name.toLowerCase() === item.productName.toLowerCase()) && ( )} {availableProducts.length === 0 && (
No existing products found
)}
)}
handleItemChange(idx, 'quantity', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-bold outline-none" />
handleItemChange(idx, 'rate', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-bold outline-none" />
₹{item.total.toLocaleString()}
setNewOrder({...newOrder, referenceId: e.target.value})} className="w-full px-4 py-3 bg-white border border-[#e2e8f0] rounded-xl text-sm font-bold outline-none focus:border-[#231651]" placeholder="Optional ref #" />