function ProductSales({ salesUrl, token }) { const [products, setProducts] = React.useState([]); const [sellerProducts, setSellerProducts] = React.useState([]); const [productNo, setProductNo] = React.useState(''); const [customerNo, setCustomerNo] = React.useState(''); const [error, setError] = React.useState(''); const [showAlert, setShowAlert] = React.useState(false); const [loadingLoad, setLoadingLoad] = 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); // 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]); // Filter products by productNo const filtered = products.filter(p => (!productNo || (p.productNo && p.productNo.toLowerCase().includes(productNo.toLowerCase()))) ); 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); // state to toggle IME list per product index const [showImes, setShowImes] = React.useState({}); // Discount state (percentage) const [discount, setDiscount] = React.useState(0); const discountAmount = ((Number(discount) || 0) / 100) * subTotal; const taxableAmount = Math.max(0, subTotal - discountAmount); // GST state const [cgst, setCgst] = React.useState(0); const [sgst, setSgst] = React.useState(0); const [igst, setIgst] = React.useState(0); // 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)); 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, 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'); // on success save returned sale for printing / whatsapp and clear the cart 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) { // create a copy for updates 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); // remove selected IMEs from product IME arrays if present 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)); } }); } // decrement qty but not below 0 product.qty = Math.max(0, Number(product.qty ?? 0) - soldQty); next[foundIdx] = product; } return next; }); } } catch (e) { /* non-fatal local update failure */ } // clear sellerProducts (cart) setSellerProducts([]); setCustomerNo(''); setSelectedBank(''); setCgst(0); setSgst(0); setIgst(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 */ } 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 || ''; // Extract IMEI numbers from the item const imes = Array.isArray(i.imes) ? i.imes : (Array.isArray(i.selectedImes) ? i.selectedImes : []); const imeiText = imes.length > 0 ? imes.map(imei => `IMEI: ${imei}`).join(', ') : ''; // Combine product name with IMEI information 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 total = Number(sale.totalAmount || 0).toFixed(2); const date = new Date(sale.createdAt || Date.now()).toLocaleString(); // GST details 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 printedSubTotal = sale.subTotal ?? subTotal; let gstLines = ''; if (cgstPercent > 0) gstLines += `
CGST ${cgstPercent}%: ${cgstAmt.toFixed(2)}
`; if (sgstPercent > 0) gstLines += `
SGST ${sgstPercent}%: ${sgstAmt.toFixed(2)}
`; if (igstPercent > 0) gstLines += `
IGST ${igstPercent}%: ${igstAmt.toFixed(2)}
`; if (gstLines) gstLines += `
`; // Build the bordered invoice HTML per requested layout 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 ?? total).toFixed(2); const invoiceHtml = `Invoice` + `
๐Ÿ“‹ GSTIN: ${shopGst || 'N/A'}
๐Ÿ“ž ${shopContact || 'Contact N/A'}
` + `
๐Ÿ’ฐ PRODUCT SALES RECEIPT ๐Ÿ’ฐ
` + `
${shopName || 'Branch Name'}
` + `
๐Ÿ“ ${shopAddress || 'Branch Address'}
` + `
๐Ÿ‘ค Customer: ${sale.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(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)); } } // preview modal markup will be rendered below; Print fallback opens this modal return (
{showPreview ? (
setShowPreview(false)}>
e.stopPropagation()}>
Receipt Preview
) : null} {showAlert ? (
Message
{error}
) : null}

setProductNo(e.target.value)} placeholder="Enter product no to filter" />

setCustomerNo(e.target.value)} placeholder="Enter mobile number" />

{/* Payment section */}
Branch Product Sell
{sellerProducts.length === 0 ? (
๐Ÿงพ
No Products Added
Enter a product no and click Add to populate your list.
) : (
{sellerProducts.filter(p => Number(p.qty) > 0).map((p, i) => ( ))} {/* GST Inputs */} {/* Total Amount */}
Product No Product Name Brand Model Qty Selling Price Selling Qty Line Total Validity IMEs Action
{p.productNo || '-'} {p.productName || '-'} {p.brand || '-'} {p.model || '-'} {p.qty ?? '-'} {p.sellingPrice ?? '-'} { const inputVal = Number(e.target.value) || 0; const available = Number(p.qty ?? 0); let v = inputVal; if (inputVal > available) { // Prevent setting selling quantity more than available setError('Your qty is low'); v = available; } setSellerProducts(sp => sp.map((s, idx) => idx === i ? { ...s, sellingQty: v } : s)); }} /> {lineTotal(p).toFixed(2)} {p.validity ? new Date(p.validity).toLocaleDateString() : '-'}
{showImes[i] ? (
Select IMEs
{(((Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.length) ? p.centralOnlyImes : (Array.isArray(p.centralImes) && p.centralImes.length) ? p.centralImes : (Array.isArray(p.imes) ? p.imes : [])) || []).map((val, idx2) => { const checked = Array.isArray(p.selectedImes) && p.selectedImes.includes(val); const isCentral = Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.includes(val); return (
{val}
{isCentral ?
(central)
: null}
{ setSellerProducts(sp => sp.map((s, idxS) => idxS === i ? { ...s, selectedImes: checked ? (s.selectedImes || []).filter(x => x !== val) : ((s.selectedImes || []).concat([val])) } : s)); }} />
); })}
{Array.isArray(p.selectedImes) && p.selectedImes.length ? `${p.selectedImes.length} selected` : '0 selected'}
) : null}
Sub Total: {totalCount} {subTotal.toFixed(2)}
Discount (%) setDiscount(e.target.value)} /> โ‚น {discountAmount.toFixed(2)}
CGST (%) setCgst(e.target.value)} /> โ‚น {cgstAmount.toFixed(1)}
SGST (%) setSgst(e.target.value)} /> โ‚น {sgstAmount.toFixed(1)}
IGST (%) setIgst(e.target.value)} /> โ‚น {igstAmount.toFixed(1)}
Total Amount: {totalAmount.toFixed(1)}
)}
); } window.ProductSales = ProductSales;