function InStockView({ salesUrl, token }) { const [suppliers, setSuppliers] = React.useState([]); const [entries, setEntries] = React.useState([]); const [open, setOpen] = React.useState(false); const [supplierId, setSupplierId] = React.useState(''); const [supplierAmount, setSupplierAmount] = React.useState(''); const [gstAmount, setGstAmount] = React.useState(''); const [category, setCategory] = React.useState(''); const [items, setItems] = React.useState([ { productNo: '', productName: '', brand: '', model: '', quantity: 1, costPrice: '', validity: '', imes: [] } ]); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(''); const [purchaseType, setPurchaseType] = React.useState('normal'); const [creditAmount, setCreditAmount] = React.useState(''); const [filter, setFilter] = React.useState({ productNo: '', productName: '', brand: '', model: '', productDays: '', quantity: '' }); const [validityPopup, setValidityPopup] = React.useState(false); const [validityData, setValidityData] = React.useState([]); const [showRedDot, setShowRedDot] = React.useState(false); const [showBarcodeSheet, setShowBarcodeSheet] = React.useState(false); // Re-stock / delete (master inventory actions) const [lowStockThreshold, setLowStockThreshold] = React.useState(5); const [restockTarget, setRestockTarget] = React.useState(null); // { entryId, item } const [restockQty, setRestockQty] = React.useState(''); const [restockCostPrice, setRestockCostPrice] = React.useState(''); const [restockImes, setRestockImes] = React.useState(''); const [restockSaving, setRestockSaving] = React.useState(false); const [restockError, setRestockError] = React.useState(''); const [deleteTarget, setDeleteTarget] = React.useState(null); // { entryId, productNo, productName } const [deleteBusy, setDeleteBusy] = React.useState(false); // Detect admin vs branch user from localStorage (branch login stores branch_token) const isBranchUser = typeof window !== 'undefined' && !!localStorage.getItem('branch_token'); const isAdminUser = !isBranchUser; // Helper: get the best token to use for authenticated requests const getAuthToken = React.useCallback(() => { const storedBranchToken = typeof window !== 'undefined' ? (localStorage.getItem('branch_token') || '') : ''; return token || storedBranchToken || ''; }, [token]); const loadSuppliers = async () => { try { const storedBranchToken = typeof window !== 'undefined' ? (localStorage.getItem('branch_token') || '') : ''; const effectiveToken = token || storedBranchToken || ''; let res = await fetch(salesUrl + '/api/suppliers', { headers: { Authorization: 'Bearer ' + effectiveToken } }); if (res.status === 401 && storedBranchToken && storedBranchToken !== effectiveToken) { res = await fetch(salesUrl + '/api/suppliers', { headers: { Authorization: 'Bearer ' + storedBranchToken } }); } const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Failed to load suppliers'); setSuppliers(Array.isArray(data.suppliers) ? data.suppliers : []); } catch (e) { setError(e.message); } }; const loadEntries = async () => { try { const storedBranchToken = typeof window !== 'undefined' ? (localStorage.getItem('branch_token') || '') : ''; const effectiveToken = token || storedBranchToken || ''; let res = await fetch(salesUrl + '/api/in-stock', { headers: { Authorization: 'Bearer ' + effectiveToken } }); if (res.status === 401 && storedBranchToken && storedBranchToken !== effectiveToken) { res = await fetch(salesUrl + '/api/in-stock', { headers: { Authorization: 'Bearer ' + storedBranchToken } }); } const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Failed to load'); setEntries(Array.isArray(data.entries) ? data.entries : []); } catch (e) { setError(e.message); } }; React.useEffect(() => { loadSuppliers(); loadEntries(); }, []); React.useEffect(() => { const hasExpiringProducts = entries.some(entry => { return entry.items.some(item => { if (!item.validity) return false; const validityDate = new Date(item.validity); const today = new Date(); const diffInDays = Math.ceil((validityDate - today) / (1000 * 60 * 60 * 24)); return diffInDays > 0 && diffInDays <= 7; // Check for products expiring within 7 days }); }); setShowRedDot(hasExpiringProducts); }, [entries]); const handleFilterChange = (field, value) => { setFilter(prev => ({ ...prev, [field]: value })); }; const filteredEntries = React.useMemo(() => { return entries.filter(entry => { return entry.items.some(item => { const productDays = filter.productDays ? parseInt(filter.productDays, 10) : null; const createdDate = new Date(entry.createdAt); const daysInStock = Math.floor((Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24)) + 1; return ( (!filter.productNo || item.productNo?.includes(filter.productNo)) && (!filter.productName || item.productName?.toLowerCase().includes(filter.productName.toLowerCase())) && (!filter.brand || item.brand?.toLowerCase().includes(filter.brand.toLowerCase())) && (!filter.model || item.model?.toLowerCase().includes(filter.model.toLowerCase())) && (!productDays || daysInStock === productDays) && (filter.quantity === '' || item.quantity === parseInt(filter.quantity, 10)) ); }); }); }, [entries, filter]); // Flat list of all items across all entries that are at or below the low-stock threshold. // Each element carries the parent entry reference so we can identify it for restock/delete. const lowStockItems = React.useMemo(() => { const out = []; const t = Number(lowStockThreshold) || 0; (entries || []).forEach(e => { (Array.isArray(e.items) ? e.items : []).forEach(it => { const q = Number(it.quantity || 0); if (q <= t) { out.push({ entryId: e._id, supplierName: e.supplier_id?.supplierName || 'Unknown Supplier', productNo: it.productNo, productName: it.productName, quantity: q, }); } }); }); return out; }, [entries, lowStockThreshold]); // Open the re-stock modal for a given entry/item const openRestockModal = (entryId, item) => { setRestockTarget({ entryId, item }); setRestockQty(''); setRestockCostPrice(String(item?.costPrice ?? '')); setRestockImes(''); setRestockError(''); }; const closeRestockModal = () => { setRestockTarget(null); setRestockQty(''); setRestockCostPrice(''); setRestockImes(''); setRestockError(''); setRestockSaving(false); }; const submitRestock = async () => { if (!restockTarget) return; const inc = Number(restockQty); if (!inc || inc <= 0 || !Number.isFinite(inc)) { setRestockError('Enter a quantity greater than 0'); return; } setRestockSaving(true); setRestockError(''); try { const { entryId, item } = restockTarget; const payload = { addQty: inc }; if (restockCostPrice !== '' && !isNaN(Number(restockCostPrice))) { payload.costPrice = Number(restockCostPrice); } const imesArr = (restockImes || '') .split(/[\s,;\n]+/) .map(s => s.trim()) .filter(Boolean); if (imesArr.length > 0) payload.imes = imesArr; const url = `${salesUrl}/api/in-stock/${encodeURIComponent(entryId)}/items/${encodeURIComponent(item.productNo)}/restock`; const res = await fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getAuthToken(), }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (!res.ok || data.success === false) { throw new Error(data.message || 'Failed to restock'); } await loadEntries(); closeRestockModal(); } catch (e) { setRestockError(e.message || 'Failed to restock'); } finally { setRestockSaving(false); } }; const submitDelete = async () => { if (!deleteTarget) return; setDeleteBusy(true); try { const { entryId, productNo } = deleteTarget; const url = `${salesUrl}/api/in-stock/${encodeURIComponent(entryId)}/items/${encodeURIComponent(productNo)}`; const res = await fetch(url, { method: 'DELETE', headers: { Authorization: 'Bearer ' + getAuthToken() }, }); const data = await res.json().catch(() => ({})); if (!res.ok || data.success === false) { throw new Error(data.message || 'Failed to delete'); } await loadEntries(); setDeleteTarget(null); } catch (e) { setError(e.message || 'Failed to delete'); } finally { setDeleteBusy(false); } }; // Removed sumCost calculation and Supplier Amount check const canSubmit = supplierId && items.every(it => it.productName); // Calculate product amount (qty x cost price) for each item const productAmounts = items.map(it => (Number(it.quantity) || 0) * (Number(it.costPrice) || 0)); const totalProductAmount = productAmounts.reduce((sum, amt) => sum + amt, 0); // Calculate total bill amount const totalBillAmount = (Number(supplierAmount) || 0) + (Number(gstAmount) || 0); const addRow = () => setItems(it => [...it, { productNo: '', productName: '', brand: '', model: '', quantity: 1, costPrice: '', validity: '' }]); const addRowWithImes = () => setItems(it => [...it, { productNo: '', productName: '', brand: '', model: '', quantity: 1, costPrice: '', validity: '', imes: [] }]); const updateItem = (idx, field, value) => setItems(list => list.map((it, i) => { if (i !== idx) return it; // if quantity changed and category is Mobile, ensure imes array length matches quantity if (field === 'quantity') { const qty = Number(value) || 0; const prevImes = Array.isArray(it.imes) ? it.imes.slice(0, qty) : []; while (prevImes.length < qty) prevImes.push(''); return { ...it, [field]: value, imes: prevImes }; } return { ...it, [field]: value }; })); const removeRow = (idx) => setItems(list => list.filter((_, i) => i !== idx)); const resetModal = () => { setSupplierId(''); setSupplierAmount(''); setItems([{ productNo: '', productName: '', brand: '', model: '', quantity: 1, costPrice: '', validity: '', imes: [] }]); setError(''); setGstAmount(''); setCategory(''); setPurchaseType('normal'); setCreditAmount(''); }; // Ensure IMES inputs are present when category is Mobile: keep imes array length == quantity React.useEffect(() => { setItems(prev => prev.map(item => { const qty = Number(item.quantity) || 0; if (category === 'Mobile') { const imes = Array.isArray(item.imes) ? item.imes.slice(0, qty) : []; while (imes.length < qty) imes.push(''); return { ...item, imes }; } // clear imes for non-mobile categories to avoid showing inputs return { ...item, imes: [] }; })); }, [category]); // Helper to generate 16-digit alphanumeric product number function generateProductNo() { // Generate exactly 16 random alphanumeric characters const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let productNo = ''; for (let i = 0; i < 16; i++) { productNo += chars.charAt(Math.floor(Math.random() * chars.length)); } return productNo; } // Validate product number - should be 16 alphanumeric characters function isValidProductNo(productNo) { if (!productNo || productNo.trim() === '') return false; // Must be exactly 16 characters, alphanumeric only const cleaned = productNo.trim(); const pattern = /^[A-Z0-9]{16}$/i; return pattern.test(cleaned); } const submit = async () => { setSaving(true); setError(''); try { const res = await fetch(salesUrl + '/api/in-stock', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ supplier_id: supplierId, supplierAmount: Number(supplierAmount) || 0, gstAmount: Number(gstAmount) || 0, purchaseType, creditAmount: purchaseType === 'credit' ? (Number(creditAmount) || 0) : 0, category: category || null, items: items.map(it => { // Generate or validate product number let finalProductNo = it.productNo && it.productNo.trim(); // If empty or invalid, generate a 16-digit alphanumeric product number if (!isValidProductNo(finalProductNo)) { finalProductNo = generateProductNo(); } return { productNo: finalProductNo, productName: it.productName, brand: it.brand, model: it.model, quantity: Number(it.quantity) || 1, costPrice: Number(it.costPrice) || 0, validity: it.validity, imes: Array.isArray(it.imes) ? it.imes.filter(x => x && x.trim()) : [] }; }) }) }); const data = await res.json(); if (!res.ok || !data.success) throw new Error(data.message || 'Save failed'); setOpen(false); resetModal(); await loadEntries(); } catch (e) { setError(e.message); } finally { setSaving(false); } }; const selectedSupplierTotal = React.useMemo(() => { if (!supplierId) return 0; const fromEntries = entries.reduce((sum, e) => { const sid = e.supplier_id?._id || e.supplier_id; if (sid !== supplierId) return sum; return sum + (Number(e.supplierAmount) || 0); }, 0); return fromEntries; }, [entries, supplierId, open]); const handleProductValidity = () => { const today = new Date(); const data = entries.flatMap(entry => { return entry.items .filter(item => { if (!item.validity) return false; const validityDate = new Date(item.validity); const diffInDays = Math.ceil((validityDate - today) / (1000 * 60 * 60 * 24)); return diffInDays > 0 && diffInDays <= 7; }) .map(item => ({ productNo: item.productNo || 'N/A', validityDate: new Date(item.validity).toLocaleDateString() })); }); setValidityData(data); setValidityPopup(true); setShowRedDot(false); // Hide red dot when popup is opened }; return (
Manage your product inventory with ease
Manage your inventory operations
Complete list of all products in your inventory
| Supplier | Product No | Product Name | Brand | Model | Qty | Total Qty | IMEI Count | Cost Price | Validity | Days in Stock | Created | {isAdminUser && (Actions | )}
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
{e.supplier_id?.supplierName || 'Unknown Supplier'}
{e.supplier_id?.agencyName || '-'}
|
{it.productNo || '-'} |
{it.productName || '-'}
|
{it.brand || '-'} | {it.model || '-'} | {it.quantity ?? 0} | {it.totalQuantity ?? it.quantity ?? 0} | {Array.isArray(it.imes) && it.imes.length ? ( {it.imes.length} IMEIs ) : ( - )} | {new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 2 }).format(it.costPrice || 0)} |
{it.validity ? new Date(it.validity).toLocaleDateString() : '-'}
|
{(() => { try { const created = new Date(e.createdAt); if (isNaN(created.getTime())) return '-'; const msPerDay = 1000 * 60 * 60 * 24; const days = Math.floor((Date.now() - created.getTime()) / msPerDay) + 1; return `${days} day${days !== 1 ? 's' : ''}`; } catch (err) { return '-'; } })()} |
{new Date(e.createdAt).toLocaleDateString()}
{new Date(e.createdAt).toLocaleTimeString()}
|
{isAdminUser && (
|
)}
Add products to your inventory
| Product No | Product Name * | Brand | Model | Qty | Cost Price | Validity | {category === 'Mobile' && (IMEI Numbers | )}|
|---|---|---|---|---|---|---|---|---|
| updateItem(idx,'productNo',e.target.value)} placeholder="Auto-generated" style={{ width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '13px', outline: 'none' }} /> | updateItem(idx,'productName',e.target.value)} placeholder="Enter product name" style={{ width: '100%', padding: '8px 12px', border: '2px solid #d1d5db', borderRadius: '6px', fontSize: '13px', outline: 'none' }} /> | updateItem(idx,'brand',e.target.value)} placeholder="Brand" style={{ width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '13px', outline: 'none' }} /> | updateItem(idx,'model',e.target.value)} placeholder="Model" style={{ width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '13px', outline: 'none' }} /> | updateItem(idx,'quantity',e.target.value)} placeholder="1" min="1" style={{ width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '13px', textAlign: 'center', outline: 'none' }} /> | updateItem(idx,'costPrice',e.target.value)} placeholder="0.00" step="0.01" style={{ width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '13px', textAlign: 'right', outline: 'none' }} /> | updateItem(idx,'validity',e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '13px', outline: 'none' }} /> | {category === 'Mobile' && (
{(() => {
const qty = Number(it.quantity) || 1;
const imesArr = Array.isArray(it.imes) && it.imes.length ? it.imes.slice(0, qty) : Array.from({ length: qty }, () => '');
return imesArr.map((im, iim) => (
{
const val = e.target.value;
setItems(list => list.map((row, rIdx) => {
if (rIdx !== idx) return row;
const qtyLocal = Number(row.quantity) || 1;
const newImes = Array.isArray(row.imes) ? row.imes.slice(0, qtyLocal) : Array.from({ length: qtyLocal }, () => '');
while (newImes.length < qtyLocal) newImes.push('');
newImes[iim] = val;
return { ...row, imes: newImes };
}));
}}
placeholder={`IMEI ${iim+1}`}
style={{
width: '140px',
padding: '6px 8px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '12px',
outline: 'none'
}}
/>
));
})()}
|
)}
Products expiring within 7 days