function BranchSupply({ salesUrl, token }) { const [branches, setBranches] = React.useState([]); const [selectedBranch, setSelectedBranch] = React.useState(''); const [stock, setStock] = React.useState([]); const [selectedRows, setSelectedRows] = React.useState({}); const [totalValue, setTotalValue] = React.useState(0); const [error, setError] = React.useState(''); const [loading, setLoading] = React.useState(false); const [productFilter, setProductFilter] = React.useState(''); const [imesOpen, setImesOpen] = React.useState({}); const loadBranches = async () => { try { const res = await fetch(salesUrl + '/api/branches', { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Failed to load branches'); setBranches(Array.isArray(data.branches) ? data.branches : []); } catch (e) { setError(e.message); } }; const loadStock = async (branchId) => { try { setError(''); const url = new URL(salesUrl + '/api/branch-stock'); if (branchId) url.searchParams.set('branch_id', branchId); const res = await fetch(url, { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Failed to load branch stock'); // For admin view, hide branch-only stock rows (those created by branches) so admin // sees only admin/central-created stock. Branch-only productIds are generated // with prefix 'branch_' in the backend. const rows = Array.isArray(data.rows) ? data.rows : []; const filtered = rows.filter(r => { try { const pid = String(r.productId || r._id || ''); // Exclude branch-only items if (pid.startsWith('branch_')) return false; return true; } catch (e) { return true; } }); setStock(filtered); setSelectedRows({}); setTotalValue(0); } catch (e) { setError(e.message); } }; React.useEffect(() => { loadBranches(); }, [token]); React.useEffect(() => { if (selectedBranch) loadStock(selectedBranch); }, [selectedBranch]); // close IME dropdowns on outside click React.useEffect(() => { function onDocClick(e) { // if click happened inside a dropdown, ignore; otherwise close all imes dropdowns try { if (e && e.target && e.target.closest && e.target.closest('.imes-dropdown')) return; } catch (__) {} setImesOpen({}); } document.addEventListener('click', onDocClick); return () => document.removeEventListener('click', onDocClick); }, []); const getAvailableQty = (row) => Number(row?.centralQty != null ? row.centralQty : (row?.qty ?? 0)) || 0; const onQtyChange = (productId, qty) => { const row = stock.find(s => (s.productId || s._id) === productId) || {}; const entered = Number(qty) || 0; const available = getAvailableQty(row); let q = entered; if (entered > available) { // cap to available and inform user setError(`Requested qty (${entered}) exceeds available stock (${available}). Using ${available} instead.`); q = available; // clear the message after a short while setTimeout(() => { setError(''); }, 5000); } const pct = Number(selectedRows[productId]?.pct || 0); const cost = Number(row?.costPrice || 0); const sellingPrice = pct ? (cost * (1 + pct / 100)) : Number(row?.sellingPrice || 0); const value = sellingPrice * q; // ensure selected imes array length does not exceed qty setSelectedRows(prev => { const prevImes = Array.isArray(prev[productId]?.imes) ? prev[productId].imes.slice(0, q) : []; const next = { ...prev, [productId]: { qty: q, value, productId, pct, sellingPrice, imes: prevImes } }; const total = Object.values(next).reduce((s, it) => s + (Number(it.value) || 0), 0); setTotalValue(total); return next; }); }; const onImesChange = (productId, imesArray) => { const row = stock.find(s => (s.productId || s._id) === productId) || {}; setSelectedRows(prev => { const qty = Number(prev[productId]?.qty || 0); const pct = Number(prev[productId]?.pct || 0); const cost = Number(row?.costPrice || 0); const sellingPrice = pct ? (cost * (1 + pct / 100)) : Number(row?.sellingPrice || 0); const imes = Array.isArray(imesArray) ? imesArray.slice() : []; const value = sellingPrice * qty; const next = { ...prev, [productId]: { qty, value, productId, pct, sellingPrice, imes } }; const total = Object.values(next).reduce((s, it) => s + (Number(it.value) || 0), 0); setTotalValue(total); return next; }); }; // Toggle a single ime for a product using functional update to avoid stale closures const toggleIme = (productId, ime) => { setSelectedRows(prev => { const curr = Array.isArray(prev[productId]?.imes) ? prev[productId].imes.slice() : []; const idx = curr.indexOf(ime); if (idx >= 0) curr.splice(idx, 1); else curr.push(ime); // reuse onImesChange logic: compute derived fields const qty = Number(prev[productId]?.qty || 0); const pct = Number(prev[productId]?.pct || 0); const row = stock.find(s => (s.productId || s._id) === productId) || {}; const cost = Number(row?.costPrice || 0); const sellingPrice = pct ? (cost * (1 + pct / 100)) : Number(row?.sellingPrice || 0); const value = sellingPrice * qty; const next = { ...prev, [productId]: { qty, value, productId, pct, sellingPrice, imes: curr } }; const total = Object.values(next).reduce((s, it) => s + (Number(it.value) || 0), 0); setTotalValue(total); return next; }); }; const onPctChange = (productId, pctVal) => { const row = stock.find(s => (s.productId || s._id) === productId) || {}; const pct = Number(pctVal) || 0; const q = Number(selectedRows[productId]?.qty || 0); const cost = Number(row?.costPrice || 0); const sellingPrice = cost * (1 + pct / 100); const value = sellingPrice * q; const next = { ...selectedRows, [productId]: { qty: q, value, productId, pct, sellingPrice } }; setSelectedRows(next); const total = Object.values(next).reduce((s, it) => s + (Number(it.value) || 0), 0); setTotalValue(total); }; const onSupply = async () => { try { setError(''); if (!selectedBranch) return setError('Select a branch'); setLoading(true); // Build items, but ensure we never send more than available stock. const selected = Object.values(selectedRows); if (selected.length === 0) return setError('Select at least one product and enter qty'); let adjusted = false; const items = selected.map(r => { const row = stock.find(s => (s.productId || s._id) === r.productId) || {}; const sellingPrice = r.sellingPrice ?? row.sellingPrice ?? 0; const available = getAvailableQty(row); const qtyToSend = Math.min(Number(r.qty) || 0, available); if (qtyToSend !== (Number(r.qty) || 0)) adjusted = true; const itemObj = { productId: r.productId, productName: row.productName || row.name || '', brand: row.brand || '', model: row.model || '', validity: row.validity || null, qty: qtyToSend, sellingPrice: sellingPrice, costPrice: row.costPrice, pct: r.pct || 0 }; if (Array.isArray(r.imes) && r.imes.length) itemObj.imes = r.imes.slice(0, qtyToSend); return itemObj; }).filter(i => i.qty > 0); // debug: print exactly what we are about to send try { console.log('BranchSupply: sending items', items); } catch (__) {} // Validate selected imes match qty for items that require IME selection for (const it of items) { if (Array.isArray(it.imes) && it.imes.length > 0) { if (Number(it.imes.length) !== Number(it.qty)) { setLoading(false); return setError(`Selected IMEs (${it.imes.length}) do not match the quantity (${it.qty}) for product ${it.productName || it.productId}`); } } } if (items.length === 0) return setError('Select at least one product and enter qty'); // If adjustments were made (user requested more than available), update UI and inform if (adjusted) { // reflect adjusted qtys back into selectedRows and totalValue const next = { ...selectedRows }; items.forEach(it => { const row = stock.find(s => (s.productId || s._id) === it.productId) || {}; const sellingPrice = it.sellingPrice ?? row.sellingPrice ?? 0; const value = sellingPrice * it.qty; next[it.productId] = { qty: it.qty, value, productId: it.productId, pct: it.pct, sellingPrice }; }); setSelectedRows(next); const total = Object.values(next).reduce((s, it) => s + (Number(it.value) || 0), 0); setTotalValue(total); setError('Some requested quantities exceeded available stock and were adjusted to available amounts.'); setTimeout(() => { setError(''); }, 5000); } const res = await fetch(salesUrl + '/api/branch-supply', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ branch_id: selectedBranch, items }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Supply failed'); // Always reload branch stock after a successful supply to keep UI consistent // If the API returns updated rows, we'll still refresh from server to ensure canonical state await loadStock(selectedBranch); // notify other parts of the app that branch stock changed try { const ev = new CustomEvent('branch-stock-updated', { detail: { branchId: selectedBranch } }); window.dispatchEvent(ev); } catch (__) {} setSelectedRows({}); setTotalValue(0); } catch (e) { setError(e.message); } finally { setLoading(false); } }; const currency = (n) => new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 2 }).format(n || 0); const filteredStock = React.useMemo(() => { // Use available quantity (centralQty or qty) and exclude items with available <= 0 const availableFilter = (item) => getAvailableQty(item) > 0; if (!productFilter) { return stock.filter(availableFilter); } return stock .filter(availableFilter) .filter(item => item.productNo?.toLowerCase().includes(productFilter.toLowerCase())); }, [stock, productFilter]); return (
| Product No | Product Name | Brand | Model | Qty | IME / IME Count | Total Cost | Selling Price | Supply Qty | Value | Validity |
|---|---|---|---|---|---|---|---|---|---|---|
| {s.productNo || '-'} | {s.productName || '-'} | {s.brand || '-'} | {s.model || '-'} | {(s.centralQty != null ? s.centralQty : (s.qty ?? '-'))} |
{((Array.isArray(s.imes) && s.imes.length) || (Array.isArray(s.centralImes) && s.centralImes.length)) ? (
{ ev.stopPropagation(); setImesOpen(prev => ({ ...(prev || {}), [pid]: !prev[pid] })); }}
style={{
border: '1px solid #d1d5db',
padding: '6px 8px',
minWidth: 160,
borderRadius: 6,
background: '#fff',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
{imesOpen && imesOpen[pid] ? (
{(sel.imes || []).length ? `${(sel.imes || []).length} selected` : (availableImesCount ? `${availableImesCount} IMEs available` : 'Select IMEs')}
â–¾
e.stopPropagation()} style={{
position: 'absolute',
zIndex: 40,
background: '#fff',
border: '1px solid #e5e7eb',
boxShadow: '0 6px 18px rgba(0,0,0,0.06)',
marginTop: 6,
padding: 8,
borderRadius: 6,
maxHeight: 180,
overflow: 'auto',
minWidth: 220
}}>
{imeList.map(iObj => {
const i = iObj.val;
const checked = Array.isArray(sel.imes) ? sel.imes.includes(i) : false;
return (
) : null}
{
const prev = Array.isArray(sel.imes) ? sel.imes.slice() : [];
const idx = prev.indexOf(i);
if (idx >= 0) prev.splice(idx, 1); else prev.push(i);
onImesChange(pid, prev);
}}
style={{
padding: '6px 8px',
borderRadius: 4,
marginBottom: 4,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: checked ? '#eef2ff' : 'transparent',
cursor: 'pointer'
}}>
);
})}
{i}
{iObj.origin === 'central' ? (central) : null}
{checked ? '✓' : ''}
{(sel.imes || []).length} selected
|
{s.totalCostPrice != null ? currency(s.totalCostPrice) : (s.costPrice != null ? currency(s.costPrice) : '-')} |
onPctChange(pid, e.target.value)} /> %
{sel.sellingPrice != null ? currency(sel.sellingPrice) : (s.sellingPrice != null ? currency(s.sellingPrice) : '-')}
|
onQtyChange(pid, e.target.value)} /> | {currency(sel.value)} | {s.validity ? new Date(s.validity).toLocaleDateString() : '-'} |