'use client'; import { useEffect, useMemo, useState } from 'react'; import { Plus, RefreshCw, Search, ClipboardCheck, CheckCircle2, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { catalogService, PartResponse, PurchaseOrderResponse } from '@/services/api/catalogService'; import { SlideOver } from '@/components/ui/SlideOver'; import { CustomSelect } from '@/components/ui/CustomSelect'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; type StatusFilter = 'all' | 'ordered' | 'received'; const EMPTY_ITEM = { part_id: '', quantity_ordered: 1, unit_price: '' }; export default function PurchasesPage() { const [purchaseOrders, setPurchaseOrders] = useState([]); const [parts, setParts] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [isSubmitting, setIsSubmitting] = useState(false); const [showPOModal, setShowPOModal] = useState(false); const [supplierName, setSupplierName] = useState(''); const [poItems, setPoItems] = useState>([]); const [showReceiveModal, setShowReceiveModal] = useState(false); const [selectedPO, setSelectedPO] = useState(null); const [receiveQuantities, setReceiveQuantities] = useState>({}); const fetchData = async () => { setLoading(true); try { const [fetchedPOs, fetchedParts] = await Promise.all([ catalogService.getPurchaseOrders(), catalogService.getParts(), ]); setPurchaseOrders(fetchedPOs); setParts(fetchedParts); } catch (err: any) { toast.error(err?.message || 'Failed to load purchase orders'); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, []); const handleCreatePO = async (e: React.FormEvent) => { e.preventDefault(); if (!supplierName.trim() || poItems.length === 0) { toast.error('Supplier name and at least one item are required'); return; } const payloadItems = poItems .filter((item) => item.part_id !== '' && parseInt(String(item.quantity_ordered), 10) > 0) .map((item) => ({ part_id: item.part_id, quantity_ordered: parseInt(String(item.quantity_ordered), 10), unit_price: parseFloat(item.unit_price) || 0, })); if (payloadItems.length === 0) { toast.error('Please specify valid items and quantities'); return; } setIsSubmitting(true); try { const created = await catalogService.createPurchaseOrder({ supplier_name: supplierName.trim(), items: payloadItems, }); setPurchaseOrders([...purchaseOrders, created]); toast.success(`Purchase order ${created.po_number} created`); setSupplierName(''); setPoItems([]); setShowPOModal(false); } catch (err: any) { toast.error(err?.message || 'Failed to create purchase order'); } finally { setIsSubmitting(false); } }; const handleReceivePO = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedPO) return; setIsSubmitting(true); try { const itemsReceivedPayload = Object.entries(receiveQuantities).map(([itemId, qty]) => ({ id: itemId, quantity_received: qty, })); const updated = await catalogService.receivePurchaseOrder( selectedPO.purchase_order_id, itemsReceivedPayload ); toast.success(`Purchase order ${updated.po_number} marked as received`); setShowReceiveModal(false); setSelectedPO(null); setReceiveQuantities({}); fetchData(); } catch (err: any) { toast.error(err?.message || 'Failed to receive purchase order items'); } finally { setIsSubmitting(false); } }; const addPOItemRow = () => { setPoItems((prev) => [...prev, { ...EMPTY_ITEM }]); }; const updatePOItemRow = (index: number, field: 'part_id' | 'quantity_ordered' | 'unit_price', value: string) => { const updated = [...poItems]; if (field === 'part_id') { updated[index].part_id = value; const partObj = parts.find((p) => p.part_id === value); if (partObj) updated[index].unit_price = partObj.cost_price.toString(); } else if (field === 'quantity_ordered') { updated[index].quantity_ordered = parseInt(value, 10) || 1; } else { updated[index].unit_price = value; } setPoItems(updated); }; const removePOItemRow = (index: number) => { setPoItems(poItems.filter((_, i) => i !== index)); }; const openCreateModal = () => { setSupplierName(''); setPoItems([{ ...EMPTY_ITEM }]); setShowPOModal(true); }; const openReceiveModal = (po: PurchaseOrderResponse) => { setSelectedPO(po); const initialQtys: Record = {}; po.items.forEach((item) => { initialQtys[item.id] = Math.max(0, item.quantity_ordered - item.quantity_received); }); setReceiveQuantities(initialQtys); setShowReceiveModal(true); }; const closeReceive = () => { if (isSubmitting) return; setShowReceiveModal(false); setSelectedPO(null); }; const isReceived = (status?: string) => (status || '').toLowerCase() === 'received'; const isOrdered = (status?: string) => (status || '').toLowerCase() === 'ordered'; const orderedCount = purchaseOrders.filter((po) => isOrdered(po.status)).length; const receivedCount = purchaseOrders.filter((po) => isReceived(po.status)).length; const filteredOrders = useMemo(() => { const q = searchQuery.trim().toLowerCase(); return purchaseOrders.filter((po) => { const statusMatch = statusFilter === 'all' || (statusFilter === 'ordered' && isOrdered(po.status)) || (statusFilter === 'received' && isReceived(po.status)); const searchMatch = !q || (po.po_number || '').toLowerCase().includes(q) || (po.supplier_name || '').toLowerCase().includes(q) || (po.status || '').toLowerCase().includes(q); return statusMatch && searchMatch; }); }, [purchaseOrders, searchQuery, statusFilter]); const pager = useClientPagination(filteredOrders); const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const inputClass = 'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary'; const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5'; const primaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50'; const secondaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50'; const renderStatusBadge = (status: string) => { const received = isReceived(status); const ordered = isOrdered(status); return ( {status || 'Draft'} ); }; return (

Supplier Purchase Orders

{purchaseOrders.length}
setSearchQuery(e.target.value)} className="w-full h-9 pl-9 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" />
{([ { id: 'all', label: `All (${purchaseOrders.length})` }, { id: 'ordered', label: `Ordered (${orderedCount})` }, { id: 'received', label: `Received (${receivedCount})` }, ] as { id: StatusFilter; label: string }[]).map((tab) => ( ))}
{loading ? (
Loading purchase orders...
) : (
{filteredOrders.length === 0 ? ( ) : ( pager.items.map((po) => ( )) )}
PO number Supplier Status Total Created Action
{searchQuery || statusFilter !== 'all' ? 'No purchase orders match your search.' : 'No purchase orders found.'}
{po.po_number} {po.supplier_name} {renderStatusBadge(po.status)} ₹{po.total_amount} {po.created_at ? new Date(po.created_at).toLocaleDateString() : '—'}
{!isReceived(po.status) ? ( ) : ( Received )}
)}
{ if (isSubmitting) return; setShowPOModal(false); }} title="Create purchase order" icon={} >
setSupplierName(e.target.value)} className={inputClass} />
{poItems.map((row, idx) => (
updatePOItemRow(idx, 'part_id', value)} options={[ { value: '', label: 'Select part' }, ...parts.map((p) => ({ value: p.part_id, label: `${p.name} (${p.sku})`, })), ]} placeholder="Select part" className="flex-1 min-w-[160px]" /> updatePOItemRow(idx, 'quantity_ordered', e.target.value)} className={`${inputClass} w-20`} /> updatePOItemRow(idx, 'unit_price', e.target.value)} className={`${inputClass} w-24`} />
))} {poItems.length === 0 && (

No parts added yet.

)}
} > {selectedPO && (

Log quantities received from{' '} {selectedPO.supplier_name}.

{selectedPO.items.map((item) => { const partObj = parts.find((p) => p.part_id === item.part_id); return (

{partObj?.name || 'Unknown part'}

Ordered {item.quantity_ordered} · Received {item.quantity_received}

setReceiveQuantities({ ...receiveQuantities, [item.id]: parseInt(e.target.value, 10) || 0, }) } className={inputClass} />
); })}
)}
); }