// 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 (
Add supplier bills (Excel — one sheet per bill — or JSON) directly to this branch. Each bill is recorded in the admin Supply History.
| {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} |
| {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 && ( )} |