// Branch portal: import supplier bills (Excel / CSV / JSON) straight into this branch's stock. // Each bill becomes one branch supply record (source "import"), so it shows in the admin Supply History, // and credit bills are recorded as supplier credits. Uses window.InventoryImport (src/utils/inventoryImport.js). function BranchStockImport({ salesUrl, getToken, suppliers, branchEntries, onClose, onImported, onSuppliersChanged }) { const II = window.InventoryImport; const currency = (n) => new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 2 }).format(n || 0); const squash = (v) => String(v || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const toInputDate = (s) => { if (!s) return ''; if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; const m = String(s).match(/^(\d{1,2})[-/.](\d{1,2}|[A-Za-z]{3})[-/.](\d{2,4})$/); if (m) { const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const mo = /^\d+$/.test(m[2]) ? Number(m[2]) : months.indexOf(m[2].toLowerCase()) + 1; const y = m[3].length === 2 ? 2000 + Number(m[3]) : Number(m[3]); if (mo >= 1) return `${y}-${String(mo).padStart(2, '0')}-${String(m[1]).padStart(2, '0')}`; } const d = new Date(s); return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10); }; const matchSupplier = (name) => { const n = squash(name); if (!n) return ''; const hit = (suppliers || []).find(s => { const a = squash(s.supplierName); const b = squash(s.agencyName); return (a && (a === n || a.includes(n) || n.includes(a))) || (b && (b === n || b.includes(n) || n.includes(b))); }); return hit ? hit._id : ''; }; const [fileName, setFileName] = React.useState(''); const [parsing, setParsing] = React.useState(false); const [parseError, setParseError] = React.useState(''); const [bills, setBills] = React.useState([]); const [activeKey, setActiveKey] = React.useState(''); const [markup, setMarkup] = React.useState(''); const [codeNotes, setCodeNotes] = React.useState({ renamed: 0, generated: 0 }); const [saving, setSaving] = React.useState(false); const [saveError, setSaveError] = React.useState(''); const [creatingSupplier, setCreatingSupplier] = React.useState(false); const fileRef = React.useRef(null); // Stock rows this branch bought itself can be topped up; rows sent from head office stay separate const branchRows = React.useMemo(() => { const own = new Map(); const fromHeadOffice = new Set(); (branchEntries || []).forEach(r => { const code = String(r.productNo || '').trim().toLowerCase(); if (!code) return; if (String(r.productId || '').startsWith('branch_')) own.set(code, r); else fromHeadOffice.add(code); }); return { own, fromHeadOffice }; }, [branchEntries]); const sellingFor = (item) => { if (item.sellingPrice > 0) return item.sellingPrice; const existing = branchRows.own.get(String(item.productNo || '').toLowerCase()); if (existing && Number(existing.sellingPrice) > 0) return Number(existing.sellingPrice); if (markup !== '' && !Number.isNaN(Number(markup))) return Math.round(item.costPrice * (1 + Number(markup) / 100) * 100) / 100; return 0; }; const makeBill = (invoice, index) => { const s = II.summarize(invoice.items); const isCredit = /credit/i.test(invoice.meta?.billType || ''); return { key: `${index}-${invoice.name}`, name: invoice.name, meta: invoice.meta || {}, items: invoice.items, include: true, status: 'pending', error: '', supplierId: matchSupplier(invoice.meta?.supplierName), billNo: invoice.meta?.billNo || '', billDate: toInputDate(invoice.meta?.billDate), purchaseType: isCredit ? 'credit' : 'normal', supplierAmount: String(s.supplierAmount), gstAmount: String(s.gstAmount), creditAmount: isCredit ? String(s.supplierAmount) : '', }; }; const billProblem = (bill) => { const s = II.summarize(bill.items); if (!s.valid.length) return 'No valid products'; if (!bill.supplierId) return 'Select a supplier'; if (bill.purchaseType === 'credit' && !(II.toNumber(bill.creditAmount) > 0)) return 'Enter the credit amount'; return null; }; const active = bills.find(b => b.key === activeKey) || bills[0] || null; const summary = React.useMemo(() => (active ? II.summarize(active.items) : null), [active]); const multi = bills.length > 1; const pendingBills = bills.filter(b => b.include && b.status !== 'done'); const readyBills = pendingBills.filter(b => !billProblem(b)); const totals = React.useMemo(() => { let products = 0, units = 0, amount = 0, unpriced = 0; pendingBills.forEach(b => { const s = II.summarize(b.items); products += s.valid.length; units += s.totalQty; amount += s.supplierAmount; unpriced += s.valid.filter(i => !(sellingFor(i) > 0)).length; }); return { products, units, amount, unpriced }; }, [pendingBills, markup, branchRows]); const activeStats = React.useMemo(() => { if (!summary) return { topUp: 0, separate: 0 }; let topUp = 0, separate = 0; summary.valid.forEach(i => { const code = String(i.productNo || '').toLowerCase(); if (branchRows.own.has(code)) topUp++; else if (branchRows.fromHeadOffice.has(code)) separate++; }); return { topUp, separate }; }, [summary, branchRows]); const updateBill = (key, patch) => setBills(list => list.map(b => (b.key === key ? { ...b, ...patch } : b))); const handleFile = async (file) => { if (!file) return; setParseError(''); setSaveError(''); setBills([]); setActiveKey(''); setParsing(true); setFileName(file.name); try { if (!II || !window.XLSX) throw new Error('The file reader did not load. Check your internet connection and reload the page.'); if (file.size > 5 * 1024 * 1024) throw new Error('File is larger than 5 MB.'); const { invoices } = await II.parseInventoryFile(file); setCodeNotes(II.normalizeProductCodes(invoices, { fillBlank: true })); const next = invoices.map((inv, i) => makeBill(inv, i)); if (!next.some(b => b.items.length)) throw new Error('No product rows were found in the file.'); setBills(next); setActiveKey(next[0].key); } catch (e) { setParseError(e.message || 'Could not read the file'); } finally { setParsing(false); if (fileRef.current) fileRef.current.value = ''; } }; const removeRow = (rowNumber) => { if (!active) return; const items = active.items.filter(i => i.rowNumber !== rowNumber); const s = II.summarize(items); updateBill(active.key, { items, supplierAmount: String(s.supplierAmount), gstAmount: String(s.gstAmount), creditAmount: active.purchaseType === 'credit' ? String(s.supplierAmount) : active.creditAmount, }); }; const applySupplierToAll = () => { if (!active?.supplierId) return; setBills(list => list.map(b => (b.status === 'done' ? b : { ...b, supplierId: active.supplierId }))); }; const authFetch = (path, opts = {}) => fetch((salesUrl || '') + path, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken(), ...(opts.headers || {}) } }); const createSupplierFromFile = async () => { const name = active?.meta?.supplierName; if (!name) return; setCreatingSupplier(true); setSaveError(''); try { const res = await authFetch('/api/suppliers', { method: 'POST', body: JSON.stringify({ supplierName: name, agencyName: name, gstNumber: active.meta.supplierGstin || '' }) }); const data = await res.json(); if (!res.ok || !data.success) throw new Error(data.message || 'Could not create supplier'); await onSuppliersChanged?.(); const newId = data.supplier?._id || ''; setBills(list => list.map(b => (!b.supplierId && squash(b.meta?.supplierName) === squash(name) ? { ...b, supplierId: newId } : b))); } catch (e) { setSaveError(e.message); } finally { setCreatingSupplier(false); } }; const saveBill = async (bill) => { const s = II.summarize(bill.items); const body = { source: 'import', supplier_id: bill.supplierId, supplierAmount: II.toNumber(bill.supplierAmount), gstAmount: II.toNumber(bill.gstAmount), purchaseType: bill.purchaseType, creditAmount: bill.purchaseType === 'credit' ? II.toNumber(bill.creditAmount) : 0, billNo: bill.billNo, billDate: bill.billDate || null, items: s.valid.map(i => ({ productNo: i.productNo, productName: i.productName, brand: i.brand, model: i.model, qty: i.quantity, costPrice: i.costPrice, sellingPrice: sellingFor(i), validity: i.validity || null, warrantyMonths: i.warrantyMonths, warrantyDetails: i.warrantyDetails, imes: i.imes, hsn: i.hsn, mrp: i.mrp, gstPercent: i.gstPercent, })), }; const res = await authFetch('/api/branch-supply', { method: 'POST', body: JSON.stringify(body) }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.success) throw new Error(data.message || 'Import failed'); return s.valid.length; }; // One branch supply record per bill keeps bill no/date and the supplier credit separate const submit = async () => { if (!readyBills.length || saving) return; const blocked = pendingBills.find(b => billProblem(b)); if (blocked) { setActiveKey(blocked.key); setSaveError(`${blocked.billNo || blocked.name}: ${billProblem(blocked)}. Fix it or untick the bill to skip it.`); return; } if (totals.unpriced > 0) { setSaveError(`${totals.unpriced} product(s) have no selling price or MRP. Enter a markup % so they can be sold at the POS (use 0 to sell at cost).`); return; } setSaving(true); setSaveError(''); let imported = 0; let failed = null; for (const bill of readyBills) { updateBill(bill.key, { status: 'saving', error: '' }); try { imported += await saveBill(bill); updateBill(bill.key, { status: 'done' }); } catch (e) { updateBill(bill.key, { status: 'error', error: e.message }); failed = { bill, message: e.message }; break; } } setSaving(false); if (imported) await onImported?.(imported); if (failed) { setActiveKey(failed.bill.key); setSaveError(`${failed.bill.billNo || failed.bill.name} was not imported: ${failed.message}${imported ? ` (${imported} products from earlier bills were added)` : ''}`); return; } onClose(); }; const statusBadge = (b) => { if (b.status === 'done') return ✓ Added; if (b.status === 'saving') return Saving…; if (b.status === 'error') return Failed; if (!b.include) return Skipped; const p = billProblem(b); return p ? ⚠ {p} : Ready; }; const label = { display: 'block', fontSize: '13px', fontWeight: '600', color: '#374151', marginBottom: '6px' }; const input = { width: '100%', padding: '10px 12px', border: '1px solid #d1d5db', borderRadius: '8px', fontSize: '14px', outline: 'none', background: 'white', boxSizing: 'border-box' }; const th = { padding: '10px', textAlign: 'left', fontWeight: '600', color: '#374151', borderBottom: '1px solid #e5e7eb', whiteSpace: 'nowrap', fontSize: '12px' }; const td = { padding: '8px 10px', borderBottom: '1px solid #f1f5f9', fontSize: '13px', verticalAlign: 'top' }; const btn = (bg, color = 'white') => ({ background: bg, color, border: 'none', borderRadius: '8px', padding: '10px 18px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }); const pill = (bg, color) => ({ background: bg, color, padding: '6px 10px', borderRadius: '999px', fontWeight: '600' }); const locked = !active || active.status === 'done' || saving; return (

📥 Import Stock to Branch

Add supplier bills (Excel — one sheet per bill — or JSON) directly to this branch. Each bill is recorded in the admin Supply History.

e.preventDefault()} onDrop={(e) => { e.preventDefault(); handleFile(e.dataTransfer.files?.[0]); }} style={{ border: '2px dashed #c7d2fe', background: '#eef2ff', borderRadius: '12px', padding: '20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', flexWrap: 'wrap' }} >
{fileName ? `📄 ${fileName}` : 'Drop the bill file here or choose a file'}
Columns: S.No, Code, Product, HSN, MRP, Rate, Qty, T.Value, CGST %, CGST Amt, SGST %, SGST Amt, Total (optional: Brand, Model, IMEI, Selling Price, Warranty, Validity)
handleFile(e.target.files?.[0])} />
{parseError && (
{parseError}
)} {bills.length > 0 && (codeNotes.renamed > 0 || codeNotes.generated > 0) && (
{codeNotes.generated > 0 &&
{codeNotes.generated} product(s) had no code — a code was created from the product name (starts with “P”) so they can be billed at the POS.
} {codeNotes.renamed > 0 &&
{codeNotes.renamed} product(s) share a supplier code with a different product (e.g. “TWS”) — a short suffix was added so each keeps its own stock.
}
)} {multi && (
{bills.length} bills found in this file — each is added as a separate supply {active?.supplierId && bills.some(b => b.status !== 'done' && b.supplierId !== active.supplierId) && ( )}
{['', 'Bill', 'Date', 'Products', 'Units', 'Bill total', 'Type', 'Status'].map(h => )} {bills.map(b => { const s = II.summarize(b.items); const isActive = active && b.key === active.key; return ( setActiveKey(b.key)} style={{ cursor: 'pointer', background: isActive ? '#eef2ff' : 'white' }}> ); })}
{h}
e.stopPropagation()}> updateBill(b.key, { include: e.target.checked })} /> {b.billNo || b.name} {b.billDate || '-'} {s.valid.length}{s.invalid.length ? (+{s.invalid.length} skipped) : ''} {s.totalQty} {currency(s.supplierAmount)} {b.purchaseType === 'credit' ? 'Credit' : 'Paid'} {statusBadge(b)}{b.error ?
{b.error}
: null}
)} {active && summary && ( <> {multi && (
Bill {active.billNo || active.name}{active.status === 'done' ? ' — added' : ''}
)}
{!active.supplierId && active.meta.supplierName && (
File says “{active.meta.supplierName}”.{' '}
)}
updateBill(active.key, { billNo: e.target.value })} placeholder="e.g. GST-R-373" />
updateBill(active.key, { billDate: e.target.value })} />
updateBill(active.key, { supplierAmount: e.target.value })} />
updateBill(active.key, { gstAmount: e.target.value })} />
{active.purchaseType === 'credit' && (
updateBill(active.key, { creditAmount: e.target.value })} />
)}
0 && markup === '' ? '#f59e0b' : '#d1d5db' }} type="number" min="0" value={markup} disabled={saving} onChange={(e) => setMarkup(e.target.value)} placeholder="e.g. 30" />
Selling price = file price → MRP → current branch price → cost + markup
{summary.valid.length} products ready {summary.totalQty} units Total {currency(summary.supplierAmount)} · GST {currency(summary.gstAmount)} {summary.invalid.length > 0 && {summary.invalid.length} rows will be skipped} {activeStats.topUp > 0 && {activeStats.topUp} already in this branch — quantity will be added} {activeStats.separate > 0 && {activeStats.separate} also supplied by head office — kept as a separate stock line}
{['Row', 'Code', 'Product', 'HSN', 'Qty', 'Cost', 'Selling price', 'GST', 'Total', 'Extras', ''].map(h => )} {active.items.map(i => { const sell = sellingFor(i); return ( ); })}
{h}
{i.rowNumber} {i.productNo || '-'} {i.originalCode ?
was {i.originalCode}
: null} {i.autoCode ?
auto
: null}
{i.productName || '-'}
{i.errors.map(err =>
⚠ {err}
)}
{i.hsn || '-'} {i.quantity} {currency(i.costPrice)} {sell > 0 ? currency(sell) : needs markup} {i.mrp ?
MRP {currency(i.mrp)}
: null}
{i.gstPercent ? `${i.gstPercent}% · ${currency(i.gstAmount)}` : '-'} {currency(i.lineTotal)} {[i.brand, i.model].filter(Boolean).join(' ')} {i.imes.length ?
{i.imes.length} IMEI
: null} {i.warrantyMonths ?
{i.warrantyMonths}m warranty
: null}
{!locked && ( )}
)} {saveError && (
{saveError}
)}
{bills.length > 0 && `${pendingBills.length} bill(s) · ${totals.products} products · ${totals.units} units · ${currency(totals.amount)}`} {totals.unpriced > 0 && · {totals.unpriced} need a selling price}
); } window.BranchStockImport = BranchStockImport;