function MobileProductSales({ salesUrl, token }) { const [products, setProducts] = React.useState([]); const [sellerProducts, setSellerProducts] = React.useState([]); const [productNo, setProductNo] = React.useState(''); const [customerNo, setCustomerNo] = React.useState(''); const [customerName, setCustomerName] = React.useState(''); const [error, setError] = React.useState(''); const [showAlert, setShowAlert] = React.useState(false); const [banks, setBanks] = React.useState([]); const [selectedBank, setSelectedBank] = React.useState('select'); const [sellingBusy, setSellingBusy] = React.useState(false); const [lastSale, setLastSale] = React.useState(null); const [previewHtml, setPreviewHtml] = React.useState(''); const [showPreview, setShowPreview] = React.useState(false); // state to toggle IME list per product index const [showImes, setShowImes] = React.useState({}); // Discount state (percentage) const [discount, setDiscount] = React.useState(0); // GST state const [cgst, setCgst] = React.useState(0); const [sgst, setSgst] = React.useState(0); const [igst, setIgst] = React.useState(0); // Fetch branch stock products React.useEffect(() => { async function fetchProducts() { try { setError(''); const url = new URL(salesUrl + '/api/branch-stock'); url.searchParams.set('only_branch', '1'); 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'); setProducts(Array.isArray(data.rows) ? data.rows : []); } catch (e) { setError(e.message); } } fetchProducts(); }, [salesUrl, token]); // show popup when error or message is set React.useEffect(() => { if (!error) { setShowAlert(false); return; } setShowAlert(true); const t = setTimeout(() => { setShowAlert(false); }, 4000); return () => clearTimeout(t); }, [error]); // Fetch banks for online payment dropdown React.useEffect(() => { async function loadBanks() { try { const url = new URL(salesUrl + '/api/banks'); const res = await fetch(url, { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); if (res.ok && Array.isArray(data.banks)) setBanks(data.banks); } catch (e) { /* ignore */ } } loadBanks(); }, [salesUrl, token]); function lineTotal(item) { const qty = Number(item.sellingQty ?? item.qty ?? 0); const unit = Number(item.sellingPrice ?? item.unitSellingPrice ?? 0); return qty * unit; } const totalCount = sellerProducts.reduce((s, it) => s + Number(it.sellingQty ?? it.qty ?? 0), 0); const subTotal = sellerProducts.reduce((s, it) => s + lineTotal(it), 0); const discountAmount = ((Number(discount) || 0) / 100) * subTotal; const taxableAmount = Math.max(0, subTotal - discountAmount); // GST calculation (apply on taxableAmount i.e. after discount) const cgstAmount = ((Number(cgst) || 0) / 100) * taxableAmount; const sgstAmount = ((Number(sgst) || 0) / 100) * taxableAmount; const igstAmount = ((Number(igst) || 0) / 100) * taxableAmount; // Total calculation logic let totalAmount = taxableAmount; if (igst > 0) { totalAmount += igstAmount; } else { totalAmount += cgstAmount + sgstAmount; } totalAmount = Number(totalAmount.toFixed(1)); function addByProductNo() { const needle = (productNo || '').toString().trim().toLowerCase(); if (!needle) return; const found = products.find((p) => String(p.productNo || '').toLowerCase() === needle); if (!found) { setError('Product not found'); return; } if (Number(found.qty) === 0) { setError('This product has zero quantity and cannot be added to sales.'); return; } setError(''); setSellerProducts((sp) => { if (sp.some((x) => (x.productId || x._id) === (found.productId || found._id))) return sp; return [...sp, found]; }); setProductNo(''); } async function doSell() { try { if (sellerProducts.length === 0) { setError('No products to sell'); return; } // validate quantities before sending const over = sellerProducts.find((it) => Number(it.sellingQty ?? it.qty ?? 0) > Number(it.qty ?? 0)); if (over) { setError('Your qty is low'); return; } // validate IME selections: for products that track IMEs, selected IMEs must match selling qty const imeMismatch = sellerProducts.find((it) => { const sellingQty = Number(it.sellingQty ?? it.qty ?? 0); const availableImes = Array.isArray(it.centralOnlyImes) && it.centralOnlyImes.length ? it.centralOnlyImes : Array.isArray(it.centralImes) && it.centralImes.length ? it.centralImes : Array.isArray(it.imes) ? it.imes : []; if (!availableImes || availableImes.length === 0) return false; // not IME-tracked const selected = Array.isArray(it.selectedImes) ? it.selectedImes.length : 0; return selected !== sellingQty; }); if (imeMismatch) { setError('Selected IMEs must match selling quantity for IME-tracked products'); return; } if (!(customerNo || '').toString().replace(/[^0-9]/g, '')) { setError('Customer mobile number is required'); return; } if (!selectedBank || selectedBank === 'select') { setError('Select a payment method'); return; } setSellingBusy(true); setError(''); const url = new URL(salesUrl + '/api/sales'); // cash option removed: payments are online via selected bank const paymentMethod = 'online'; const payload = { items: sellerProducts.map((it) => ({ productId: it.productId || it._id || '', productNo: it.productNo || '', productName: it.productName || '', qty: Number(it.sellingQty ?? it.qty ?? 0), sellingPrice: Number(it.sellingPrice || 0), lineTotal: Number(lineTotal(it)), imes: Array.isArray(it.selectedImes) && it.selectedImes.length ? it.selectedImes : Array.isArray(it.imes) ? it.imes : [] })), customerNo, customerName: customerName || 'Walk-in Customer', subTotal, cgst: Number(cgst), sgst: Number(sgst), igst: Number(igst), discount: Number(discount) || 0, discountAmount: Number(discountAmount.toFixed(2)), cgstAmount: Number(cgstAmount.toFixed(2)), sgstAmount: Number(sgstAmount.toFixed(2)), igstAmount: Number(igstAmount.toFixed(2)), totalAmount: Number(totalAmount.toFixed(2)), paymentMethod, amountPaid: Number(totalAmount || 0), bank_id: selectedBank && selectedBank !== 'select' ? selectedBank : '' }; const res = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Sell failed'); const savedSale = data.sale || data || null; setLastSale(savedSale); // update local products: remove sold IMEs and decrement qty try { if (Array.isArray(savedSale?.items) && savedSale.items.length) { setProducts((prev) => { const next = (prev || []).map((prod) => ({ ...prod })); for (const sold of savedSale.items) { const idCandidates = [sold.productId, sold._id, sold.productNo].filter(Boolean).map(String); const foundIdx = next.findIndex((p) => idCandidates.includes(String(p.productId || p._id || p.productNo))); if (foundIdx === -1) continue; const product = next[foundIdx]; const soldQty = Number(sold.qty ?? sold.sellingQty ?? 0); const soldImes = Array.isArray(sold.imes) ? sold.imes : Array.isArray(sold.selectedImes) ? sold.selectedImes : []; if (soldImes && soldImes.length) { ['centralOnlyImes', 'centralImes', 'imes'].forEach((key) => { if (Array.isArray(product[key]) && product[key].length) { product[key] = product[key].filter((v) => !soldImes.includes(v)); } }); } product.qty = Math.max(0, Number(product.qty ?? 0) - soldQty); next[foundIdx] = product; } return next; }); } } catch (e) { /* ignore */ } setSellerProducts([]); setCustomerNo(''); setCustomerName(''); setSelectedBank('select'); setCgst(0); setSgst(0); setIgst(0); setDiscount(0); setError('Sale saved'); } catch (e) { setError(e.message || 'Sell failed'); } finally { setSellingBusy(false); } } // decode JWT payload safely (no verification) to read branch/shop info function decodeJwt(tk) { try { const theToken = tk || token || localStorage.getItem('branch_token') || localStorage.getItem('sales_token') || ''; const parts = theToken.split('.'); if (parts.length < 2) return {}; const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); const json = decodeURIComponent( atob(base64) .split('') .map(function (c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }) .join('') ); return JSON.parse(json); } catch (e) { return {}; } } async function printSale() { try { const sale = lastSale || { items: sellerProducts, totalAmount, customerNo, createdAt: new Date().toISOString() }; // fetch branch info let shopName = ''; let shopContact = ''; let shopGst = ''; let shopAddress = ''; try { const res = await fetch(new URL(salesUrl + '/api/branches'), { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); if (res.ok && Array.isArray(data.branches) && data.branches.length > 0) { const payload = decodeJwt(); const branchId = payload?.branch_id || payload?._id || ''; let found = null; if (branchId) found = data.branches.find((b) => String(b._id) === String(branchId)); if (!found) found = data.branches[0]; shopName = found?.name || ''; shopContact = found?.phoneNumber || found?.phone || ''; shopGst = found?.gstNo || found?.gst || ''; shopAddress = found?.address || found?.branchAddress || ''; } } catch (e) { /* ignore */ } if (!shopName || !shopContact) { const payload = decodeJwt(); shopName = shopName || payload.shopName || payload.name || payload.branchName || ''; shopContact = shopContact || payload.phone || payload.phoneNumber || payload.branchPhone || ''; shopGst = shopGst || payload.gstNo || payload.gst || ''; shopAddress = shopAddress || payload.address || payload.branchAddress || ''; } // fetch branch stock to resolve prices let stock = []; try { const sres = await fetch(new URL(salesUrl + '/api/branch-stock?only_branch=1'), { headers: { Authorization: 'Bearer ' + token } }); const sdata = await sres.json(); if (sres.ok && Array.isArray(sdata.rows)) stock = sdata.rows; } catch (e) { /* ignore */ } // GST details - declare before items mapping const cgstPercent = sale.cgst || cgst; const sgstPercent = sale.sgst || sgst; const igstPercent = sale.igst || igst; const cgstAmt = sale.cgstAmount ?? cgstAmount; const sgstAmt = sale.sgstAmount ?? sgstAmount; const igstAmt = sale.igstAmount ?? igstAmount; const items = (sale.items || []) .map((i, idx) => { const found = (stock || []).find( (p) => (String(p._id) && String(p._id) === String(i.productId || i._id)) || (p.productId && String(p.productId) === String(i.productId)) || (p.productNo && i.productNo && String(p.productNo) === String(i.productNo)) ); const name = found?.productName || found?.name || i.productName || i.productNo || ''; const brand = found?.brand || ''; const model = found?.model || ''; // Extract IMEI numbers from the item - prioritize selectedImes (actually sold) const imes = Array.isArray(i.selectedImes) && i.selectedImes.length > 0 ? i.selectedImes : Array.isArray(i.imes) ? i.imes : []; const imeiText = imes.length > 0 ? imes.map((imei) => `IMEI: ${imei}`).join(', ') : ''; // Build product description with brand/model let productDescription = `${name}`; if (brand || model) { productDescription += `
${brand} ${model}`.trim() + ``; } if (imeiText) { productDescription += `
${imeiText}`; } const qty = Number(i.qty || i.sellingQty || 0); const unit = Number(found?.sellingPrice ?? found?.unitSellingPrice ?? i.sellingPrice ?? 0).toFixed(2); const line = (qty * Number(unit)).toFixed(2); const taxPerUnit = igstPercent > 0 ? (Number(igstAmt) / Math.max(1, qty)).toFixed(2) : ((Number(cgstAmt) + Number(sgstAmt)) / Math.max(1, qty)).toFixed(2); return ` ${idx + 1} ${productDescription} ${qty} Rs. ${unit} Rs. ${taxPerUnit} Rs. ${line} `; }) .join(''); const outSubTotal = Number((sale.subTotal ?? subTotal) || 0); const outDiscount = Number((sale.discountAmount ?? discountAmount) || 0); const outTotal = Number(sale.totalAmount ?? totalAmount).toFixed(2); const invoiceHtml = `TAX INVOICE
TAX INVOICE
Invoice Date: ${new Date(sale.createdAt || Date.now()).toLocaleString()}
${shopName || 'Shop Name'}
${shopAddress || ''}
Contact: ${shopContact || ''}
GSTIN: ${shopGst || ''}
Billed To
Name: ${(sale.customerName || customerName || 'Walk-in Customer')}
Mobile: ${(sale.customerNo || customerNo || '')}
Payment
Method: Online
Amount Paid: Rs. ${Number(sale.totalAmount || outTotal || 0).toFixed(2)}
${items}
Sr. No. Items Quantity Price / Unit Tax / Unit Amount

Notes

  • No return deal
  • Warranty as per manufacturer terms

Terms & Conditions

  • Customer will pay the GST
  • Payment due within 15 days
Authorised Signatory For
${shopName || 'Shop Name'}
Signature
Total
Rs. ${outSubTotal.toFixed(2)}
${sale.discount > 0 ? `
Discount (${sale.discount}%)
Rs. ${outDiscount.toFixed(2)}
` : ''} ${cgstPercent > 0 ? `
CGST (${cgstPercent}%)
Rs. ${Number(cgstAmt).toFixed(2)}
` : ''} ${sgstPercent > 0 ? `
SGST (${sgstPercent}%)
Rs. ${Number(sgstAmt).toFixed(2)}
` : ''} ${igstPercent > 0 ? `
IGST (${igstPercent}%)
Rs. ${Number(igstAmt).toFixed(2)}
` : ''}
GRAND TOTAL
Rs. ${Number(outTotal).toFixed(2)}
`; const w = window.open('', '_blank'); if (!w) { setPreviewHtml(invoiceHtml); setShowPreview(true); setError('Popup blocked: showing preview. Allow popups to print directly.'); return; } w.document.open(); w.document.write(invoiceHtml); w.document.close(); w.focus(); setTimeout(() => { try { w.print(); } catch (e) { /* ignore */ } }, 300); } catch (e) { setError('Failed to open printer: ' + (e.message || e)); } } async function printSmallReceipt() { try { const sale = lastSale || { items: sellerProducts, totalAmount, customerNo, customerName, createdAt: new Date().toISOString() }; // fetch branch info let shopName = ''; let shopContact = ''; let shopGst = ''; let shopAddress = ''; try { const res = await fetch(new URL(salesUrl + '/api/branches'), { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); if (res.ok && Array.isArray(data.branches) && data.branches.length > 0) { const payload = decodeJwt(); const branchId = payload?.branch_id || payload?._id || ''; let found = null; if (branchId) found = data.branches.find((b) => String(b._id) === String(branchId)); if (!found) found = data.branches[0]; shopName = found?.name || ''; shopContact = found?.phoneNumber || found?.phone || ''; shopGst = found?.gstNo || found?.gst || ''; shopAddress = found?.address || found?.branchAddress || ''; } } catch (e) { /* ignore */ } if (!shopName || !shopContact) { const payload = decodeJwt(); shopName = shopName || payload.shopName || payload.name || payload.branchName || ''; shopContact = shopContact || payload.phone || payload.phoneNumber || payload.branchPhone || ''; shopGst = shopGst || payload.gstNo || payload.gst || ''; shopAddress = shopAddress || payload.address || payload.branchAddress || ''; } // fetch branch stock to resolve prices let stock = []; try { const sres = await fetch(new URL(salesUrl + '/api/branch-stock?only_branch=1'), { headers: { Authorization: 'Bearer ' + token } }); const sdata = await sres.json(); if (sres.ok && Array.isArray(sdata.rows)) stock = sdata.rows; } catch (e) { /* ignore */ } const cgstPercent = sale.cgst || cgst; const sgstPercent = sale.sgst || sgst; const igstPercent = sale.igst || igst; const cgstAmt = sale.cgstAmount ?? cgstAmount; const sgstAmt = sale.sgstAmount ?? sgstAmount; const igstAmt = sale.igstAmount ?? igstAmount; const items = (sale.items || []) .map((i, idx) => { const found = (stock || []).find( (p) => (String(p._id) && String(p._id) === String(i.productId || i._id)) || (p.productId && String(p.productId) === String(i.productId)) || (p.productNo && i.productNo && String(p.productNo) === String(i.productNo)) ); const name = found?.productName || found?.name || i.productName || i.productNo || ''; const imes = Array.isArray(i.selectedImes) && i.selectedImes.length > 0 ? i.selectedImes : Array.isArray(i.imes) ? i.imes : []; const imeiText = imes.length > 0 ? imes.map((imei) => `IMEI: ${imei}`).join(', ') : ''; const productDescription = imeiText ? `${name}
${imeiText}` : name; const qty = Number(i.qty || i.sellingQty || 0); const unit = Number(found?.sellingPrice ?? found?.unitSellingPrice ?? i.sellingPrice ?? 0).toFixed(2); const line = (qty * Number(unit)).toFixed(2); const hasImei = imes.length > 0; return ` ${idx + 1} ${productDescription}   ${qty} โ‚น${unit} โ‚น${line} `; }) .join(''); const date = new Date(sale.createdAt || Date.now()).toLocaleString(); const outSubTotal = Number((sale.subTotal ?? subTotal) || 0); const outDiscount = Number((sale.discountAmount ?? discountAmount) || 0); const outTaxable = Number((sale.taxableAmount ?? Math.max(0, outSubTotal - outDiscount)) || 0); const outTotal = Number(sale.totalAmount ?? totalAmount).toFixed(2); const receiptHtml = `Receipt` + `
๐Ÿ“‹ GSTIN: ${shopGst || 'N/A'}
๐Ÿ“ž ${shopContact || 'Contact N/A'}
` + `
๐Ÿ’ฐ PRODUCT SALES RECEIPT ๐Ÿ’ฐ
` + `
${shopName || 'Branch Name'}
` + `
๐Ÿ“ ${shopAddress || 'Branch Address'}
` + `
๐Ÿ‘ค Customer: ${sale.customerName || customerName || 'Walk-in Customer'}
` + `
๐Ÿ“ฑ Phone: ${sale.customerNo || customerNo || 'N/A'}     ๐Ÿ“… Date: ${date}
` + `${items}
# ๐Ÿ“ฆ Product Details HSN Qty Rate Amount
` + `` + `` + (sale.discount ? `` : '') + `` + (cgstPercent > 0 ? `` : '') + (sgstPercent > 0 ? `` : '') + (igstPercent > 0 ? `` : '') + `` + `
๐Ÿ“Š Sub Total:โ‚น ${outSubTotal.toFixed(2)}
๐Ÿท๏ธ Discount (${sale.discount}%):- โ‚น ${outDiscount.toFixed(2)}
๐Ÿ’ต Taxable Amount:โ‚น ${outTaxable.toFixed(2)}
๐Ÿ›๏ธ CGST ${cgstPercent}%:โ‚น ${Number(cgstAmt).toFixed(2)}
๐Ÿ›๏ธ SGST ${sgstPercent}%:โ‚น ${Number(sgstAmt).toFixed(2)}
๐Ÿ›๏ธ IGST ${igstPercent}%:โ‚น ${Number(igstAmt).toFixed(2)}
๐Ÿ’ฐ GRAND TOTAL:โ‚น ${outTotal}
` + `
๐Ÿ™ Thank you for your business! ๐Ÿ™
Visit again soon!
`; const w = window.open('', '_blank'); if (!w) { setPreviewHtml(receiptHtml); setShowPreview(true); setError('Popup blocked: showing preview. Allow popups to print directly.'); return; } w.document.open(); w.document.write(receiptHtml); w.document.close(); w.focus(); setTimeout(() => { try { w.print(); } catch (e) { /* ignore */ } }, 300); } catch (e) { setError('Failed to open small receipt printer: ' + (e.message || e)); } } const currency = (n) => new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 2 }).format(n || 0); const getAvailableImes = (p) => (Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.length ? p.centralOnlyImes : Array.isArray(p.centralImes) && p.centralImes.length ? p.centralImes : Array.isArray(p.imes) ? p.imes : []) || []; return (
{showPreview ? (
setShowPreview(false)} >
e.stopPropagation()} >
Receipt Preview
) : null} {showAlert ? (
Message
{error}
) : null}
Add Product
setProductNo(e.target.value)} placeholder="Enter product no" style={{ flex: 1 }} />
Tip: enter exact product number.
Customer & Payment
setCustomerName(e.target.value)} placeholder="Enter customer name" style={{ width: '100%', marginTop: 6, marginBottom: 10 }} /> setCustomerNo(e.target.value)} placeholder="Enter mobile number" style={{ width: '100%', marginTop: 6, marginBottom: 10 }} />
Cart
{totalCount} item(s)
{sellerProducts.length === 0 ? (
๐Ÿงพ
No Products Added
Add using product number above.
) : (
{sellerProducts .filter((p) => Number(p.qty) > 0) .map((p, i) => { const availableImes = getAvailableImes(p); const selected = Array.isArray(p.selectedImes) ? p.selectedImes : []; const isImeiTracked = availableImes.length > 0; return (
{p.productName || p.productNo || 'Product'}
#{p.productNo || '-'} {(p.brand || p.model) ? โ€ข {(p.brand || '').trim()} {(p.model || '').trim()} : null}
Available: {p.qty ?? '-'}  |  Unit: {currency(p.sellingPrice ?? '-')}
{p.validity ? (
Validity: {new Date(p.validity).toLocaleDateString()}
) : null}
{ const inputVal = Number(e.target.value) || 0; const available = Number(p.qty ?? 0); let v = inputVal; if (inputVal > available) { setError('Your qty is low'); v = available; } setSellerProducts((sp) => sp.map((s, idx) => (idx === i ? { ...s, sellingQty: v } : s))); }} style={{ width: '100%', marginTop: 6 }} />
{currency(lineTotal(p))}
{isImeiTracked ? (
{showImes[i] ? (
{availableImes.map((val, idx2) => { const checked = selected.includes(val); const isCentral = Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.includes(val); return ( ); })}
{selected.length ? `${selected.length} selected` : '0 selected'}
) : null}
) : null}
); })}
)}
Totals
Sub Total
{currency(subTotal)}
setDiscount(e.target.value)} />
- {currency(discountAmount)}
setCgst(e.target.value)} />
{currency(cgstAmount)}
setSgst(e.target.value)} />
{currency(sgstAmount)}
setIgst(e.target.value)} />
{currency(igstAmount)}
Total Amount
{currency(totalAmount)}
Actions
); } window.MobileProductSales = MobileProductSales;