'use client'; import { useEffect, useMemo, useState } from 'react'; import { Plus, Search, RefreshCw, Package, TrendingUp, AlertTriangle, } from 'lucide-react'; import { toast } from 'sonner'; import { catalogService, PartResponse, SellableSkuRow, StockMovementResponse } from '@/services/api/catalogService'; import { SlideOver } from '@/components/ui/SlideOver'; import { CustomSelect } from '@/components/ui/CustomSelect'; import { TablePagination, TABLE_PAGE_SIZE, useClientPagination } from '@/components/ui/TablePagination'; type LedgerTab = 'skus' | 'parts' | 'movements'; export default function InventoryPage() { const [activeTab, setActiveTab] = useState('skus'); const [parts, setParts] = useState([]); const [movements, setMovements] = useState([]); const [skus, setSkus] = useState([]); const [skuTotal, setSkuTotal] = useState(0); const [skuPage, setSkuPage] = useState(1); const skuLimit = TABLE_PAGE_SIZE; const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [debouncedSkuSearch, setDebouncedSkuSearch] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [showPartModal, setShowPartModal] = useState(false); const [partSku, setPartSku] = useState(''); const [partName, setPartName] = useState(''); const [partCost, setPartCost] = useState(''); const [partLowStock, setPartLowStock] = useState('3'); const [partSupplier, setPartSupplier] = useState(''); const [partBarcode, setPartBarcode] = useState(''); const [showAdjustModal, setShowAdjustModal] = useState(false); const [adjustPartId, setAdjustPartId] = useState(''); const [adjustQty, setAdjustQty] = useState(''); const [adjustType, setAdjustType] = useState<'Adjustment' | 'Damage'>('Adjustment'); const [adjustReason, setAdjustReason] = useState(''); const [showSkuAdjustModal, setShowSkuAdjustModal] = useState(false); const [skuAdjustRow, setSkuAdjustRow] = useState(null); const [skuAdjustQty, setSkuAdjustQty] = useState(''); const [skuAdjustType, setSkuAdjustType] = useState<'RECEIPT' | 'ADJUSTMENT'>('RECEIPT'); const [skuAdjustNotes, setSkuAdjustNotes] = useState(''); const fetchPartsData = async () => { setLoading(true); try { const [fetchedParts, fetchedMovements] = await Promise.all([ catalogService.getParts(), catalogService.getStockHistory(), ]); setParts(fetchedParts); setMovements(fetchedMovements); } catch (err: any) { toast.error(err?.message || 'Failed to load inventory ledger'); } finally { setLoading(false); } }; const fetchSkus = async () => { setLoading(true); try { const result = await catalogService.getSellableSkus({ page: skuPage, limit: skuLimit, q: debouncedSkuSearch || undefined, }); setSkus(result.items); setSkuTotal(result.total); } catch (err: any) { toast.error(err?.message || 'Failed to load sellable SKUs'); } finally { setLoading(false); } }; const fetchData = async () => { if (activeTab === 'skus') { await fetchSkus(); return; } await fetchPartsData(); }; useEffect(() => { const timer = setTimeout(() => { setDebouncedSkuSearch(searchQuery); setSkuPage(1); }, 400); return () => clearTimeout(timer); }, [searchQuery]); useEffect(() => { fetchData(); }, [activeTab, skuPage, debouncedSkuSearch]); const handleCreatePart = async (e: React.FormEvent) => { e.preventDefault(); if (!partSku.trim() || !partName.trim() || !partCost) { toast.error('SKU, name, and cost price are required'); return; } setIsSubmitting(true); try { const created = await catalogService.createPart({ sku: partSku.trim(), name: partName.trim(), cost_price: parseFloat(partCost), low_stock_alert: parseInt(partLowStock) || 3, supplier: partSupplier.trim() || undefined, barcode: partBarcode.trim() || undefined, }); setParts([...parts, created]); toast.success(`Part "${created.sku}" added to inventory`); setPartSku(''); setPartName(''); setPartCost(''); setPartLowStock('3'); setPartSupplier(''); setPartBarcode(''); setShowPartModal(false); } catch (err: any) { toast.error(err?.message || 'Failed to create inventory part'); } finally { setIsSubmitting(false); } }; const handleAdjustStock = async (e: React.FormEvent) => { e.preventDefault(); if (!adjustPartId || !adjustQty || !adjustReason.trim()) { toast.error('Part, quantity, and reason are required'); return; } setIsSubmitting(true); try { const qtyVal = parseInt(adjustQty); const payloadQty = adjustType === 'Damage' ? -Math.abs(qtyVal) : qtyVal; await catalogService.adjustStock({ entity_type: 'part', entity_id: adjustPartId, movement_type: adjustType, quantity: payloadQty, reference_type: 'ManualAdjustment', reference_id: adjustReason.trim(), }); toast.success('Stock movement recorded'); setAdjustPartId(''); setAdjustQty(''); setAdjustReason(''); setShowAdjustModal(false); fetchData(); } catch (err: any) { toast.error(err?.message || 'Failed to log stock movement'); } finally { setIsSubmitting(false); } }; const handleSkuAdjust = async (e: React.FormEvent) => { e.preventDefault(); if (!skuAdjustRow || !skuAdjustQty) { toast.error('Quantity is required'); return; } const qtyVal = parseInt(skuAdjustQty, 10); if (!Number.isFinite(qtyVal) || qtyVal === 0) { toast.error('Enter a non-zero quantity'); return; } setIsSubmitting(true); try { const qty = skuAdjustType === 'ADJUSTMENT' ? -Math.abs(qtyVal) : Math.abs(qtyVal); await catalogService.adjustSellableStock({ variant_id: skuAdjustRow.variant_id, event_type: skuAdjustType, qty, notes: skuAdjustNotes.trim() || undefined, }); toast.success(`Stock quantity updated for ${skuAdjustRow.sku}`); setShowSkuAdjustModal(false); setSkuAdjustRow(null); setSkuAdjustQty(''); setSkuAdjustNotes(''); fetchSkus(); } catch (err: any) { toast.error(err?.message || 'Failed to adjust sellable stock'); } finally { setIsSubmitting(false); } }; const openLogMovement = async () => { if (parts.length === 0) { try { const fetchedParts = await catalogService.getParts(); setParts(fetchedParts); } catch { toast.error('Failed to load parts'); } } setShowAdjustModal(true); }; const skuPages = Math.max(1, Math.ceil(skuTotal / skuLimit)); const filteredParts = useMemo(() => { const q = searchQuery.trim().toLowerCase(); if (!q) return parts; return parts.filter( (p) => p.sku.toLowerCase().includes(q) || p.name.toLowerCase().includes(q) || (p.supplier || '').toLowerCase().includes(q) || (p.barcode || '').toLowerCase().includes(q) ); }, [parts, searchQuery]); const filteredMovements = useMemo(() => { const q = searchQuery.trim().toLowerCase(); if (!q) return movements; return movements.filter((m) => { const partObj = parts.find((p) => p.part_id === m.entity_id); return ( (partObj?.name || '').toLowerCase().includes(q) || (partObj?.sku || '').toLowerCase().includes(q) || (m.movement_type || '').toLowerCase().includes(q) || (m.reference_id || '').toLowerCase().includes(q) ); }); }, [movements, parts, searchQuery]); const partsPager = useClientPagination(filteredParts); const movementsPager = useClientPagination(filteredMovements); const countBadge = activeTab === 'skus' ? skuTotal : activeTab === 'parts' ? parts.length : movements.length; 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 changeTab = (tab: LedgerTab) => { setActiveTab(tab); setSearchQuery(''); }; const renderStockBadge = (isLow: boolean, label?: string) => ( {isLow && } {label || (isLow ? 'Low stock' : 'In stock')} ); const renderMovementBadge = (type: string) => { const value = type.toLowerCase(); const cls = value === 'purchase' || value === 'receipt' ? 'bg-success text-white' : value === 'damage' ? 'bg-destructive text-white' : 'bg-muted text-muted-foreground'; return ( {type} ); }; return (

Stock Ledger

{countBadge}
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: 'skus', label: `SKUs (${skuTotal})` }, { id: 'parts', label: `Parts (${parts.length})` }, { id: 'movements', label: `History (${movements.length})` }, ] as { id: LedgerTab; label: string }[]).map((tab) => ( ))}
{loading ? (
Loading stock ledger...
) : activeTab === 'skus' ? ( <>
{skus.length === 0 ? ( ) : ( skus.map((row) => ( )) )}
Product SKU Price Pending Held Available Status Action
{searchQuery ? 'No SKUs match your search.' : 'No sellable SKUs found.'}
{row.product_name} {row.sku} ₹{row.price} {row.pending_confirmation_units || 0} {row.confirmed_units || 0} {row.available_stock} {renderStockBadge(row.is_low)}
) : activeTab === 'parts' ? ( <>
{filteredParts.length === 0 ? ( ) : ( partsPager.items.map((p) => { const isLow = p.stock <= p.low_stock_alert; return ( ); }) )}
Part SKU / Barcode Unit cost Stock Status
{searchQuery ? 'No parts match your search.' : 'No inventory parts registered.'}

{p.name}

{p.supplier || 'No supplier'}

{p.sku} {p.barcode && {p.barcode}} ₹{p.cost_price} {p.stock} {renderStockBadge(isLow, isLow ? 'Reorder' : 'In stock')}
) : ( <>
{filteredMovements.length === 0 ? ( ) : ( movementsPager.items.map((m) => { const partObj = parts.find((p) => p.part_id === m.entity_id); const isNeg = m.quantity < 0; return ( ); }) )}
Date Item Type Quantity Reference
{searchQuery ? 'No movements match your search.' : 'No stock movements recorded.'}
{new Date(m.created_at).toLocaleString()}

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

{partObj?.sku || '—'}

{renderMovementBadge(m.movement_type)} {isNeg ? '' : '+'} {m.quantity} {m.reference_id}
)}
{ if (isSubmitting) return; setShowSkuAdjustModal(false); }} title={skuAdjustRow ? `Adjust ${skuAdjustRow.sku}` : 'Adjust SKU'} icon={} > {skuAdjustRow && (

Current available: {skuAdjustRow.available_stock}

setSkuAdjustType(value as 'RECEIPT' | 'ADJUSTMENT')} options={[ { value: 'RECEIPT', label: 'Receive (add)' }, { value: 'ADJUSTMENT', label: 'Adjustment (remove)' }, ]} />
setSkuAdjustQty(e.target.value)} className={inputClass} />
setSkuAdjustNotes(e.target.value)} className={inputClass} />
)}
{ if (isSubmitting) return; setShowPartModal(false); }} title="Register part" icon={} >
setPartSku(e.target.value)} className={inputClass} />
setPartName(e.target.value)} className={inputClass} />
setPartCost(e.target.value)} className={inputClass} />
setPartLowStock(e.target.value)} className={inputClass} />
setPartSupplier(e.target.value)} className={inputClass} />
setPartBarcode(e.target.value)} className={inputClass} />
{ if (isSubmitting) return; setShowAdjustModal(false); }} title="Log stock movement" icon={} >
({ value: p.part_id, label: `${p.name} (${p.sku}) — ${p.stock}`, })), ]} placeholder="Select part" />
setAdjustType(value as 'Adjustment' | 'Damage')} options={[ { value: 'Adjustment', label: 'Stock adjustment' }, { value: 'Damage', label: 'Damage (reduces stock)' }, ]} />
setAdjustQty(e.target.value)} className={inputClass} />
setAdjustReason(e.target.value)} className={inputClass} />
); }