// Constant payment methods used across the application window.__PAYMENT_METHODS__ = [ { value: 'Cash', label: 'Cash' }, { value: 'UPI', label: 'UPI' }, { value: 'Card', label: 'Card' }, { value: 'UPI-H', label: 'UPI-H' }, { value: 'UPI-S', label: 'UPI-S' }, { value: 'Cash + Card', label: 'Cash + Card' }, { value: 'UPI H + CASH', label: 'UPI H + Cash' }, { value: 'UPI S + CASH', label: 'UPI S + Cash' }, { value: 'UPI H + CARD', label: 'UPI H + Card' }, { value: 'UPI S + CARD', label: 'UPI S + Card' } ]; window.__PAYMENT_METHOD_VALUES__ = window.__PAYMENT_METHODS__.map(function(m) { return m.value; }); 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 [customerName, setCustomerName] = React.useState(''); const [error, setError] = React.useState(''); const [showAlert, setShowAlert] = React.useState(false); const [loadingLoad, setLoadingLoad] = React.useState(false); 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]); // 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 ?? 0); const unit = Number(item.sellingPrice ?? item.unitSellingPrice ?? 0); return qty * unit; } const totalCount = sellerProducts.reduce((s, it) => s + Number(it.sellingQty ?? 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 [selectedBank, setSelectedBank] = React.useState(''); 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 ?? 0) > Number(it.qty ?? 0)); if (over) { setError('Your qty is low'); return; } // validate: selling qty must be > 0 const zeroQty = sellerProducts.find(it => Number(it.sellingQty ?? 0) <= 0); if (zeroQty) { setError('Selling quantity must be at least 1. For IMEI products, scan or select IMEIs first.'); 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 ?? 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'); const paymentMethod = selectedBank; const payload = { items: sellerProducts.map(it => ({ productId: it.productId || it._id || '', productNo: it.productNo || '', productName: it.productName || '', qty: Number(it.sellingQty ?? 0), sellingPrice: Number(it.sellingPrice || 0), lineTotal: Number(lineTotal(it),), imes: Array.isArray(it.selectedImes) ? it.selectedImes : [] })), 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) }; 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(''); setCustomerName(''); 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 */ } // 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); // Calculate tax per unit (GST percentage from sale data) const taxPercent = (Number(cgstPercent) + Number(sgstPercent) + Number(igstPercent)) || 0; const taxPerUnit = ((taxPercent / 100) * unit).toFixed(2); const line = (qty * unit).toFixed(2); return ` ${idx + 1} ${productDescription} ${qty} Rs. ${unit.toFixed(2)} Rs. ${taxPerUnit} (${taxPercent.toFixed(0)}%) Rs. ${line} `; }).join(''); const total = Number(sale.totalAmount || 0).toFixed(2); const date = new Date(sale.createdAt || Date.now()).toLocaleString(); // Build professional A4 TAX INVOICE HTML 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); // Format invoice number with date const invoiceNo = sale.invoiceNo || sale._id?.slice(-6)?.toUpperCase() || 'INV' + Date.now().toString().slice(-6); const invoiceDate = new Date(sale.createdAt || Date.now()).toLocaleDateString('en-IN', { day: '2-digit', month: 'long', year: 'numeric' }); const invoiceHtml = `TAX INVOICE

TAX INVOICE

${shopName || 'Your Shop Name'}
${shopAddress || 'Shop Address'}
Phone: ${shopContact || 'Contact Number'} | GSTIN: ${shopGst || 'N/A'}

Bill To

${sale.customerName || customerName || 'Walk-in Customer'}
Phone: ${sale.customerNo || customerNo || 'N/A'}

Invoice Details

Invoice No: ${invoiceNo}
Invoice Date: ${invoiceDate}
${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. ${outTotal}
`; 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)); } } // Small receipt format print function 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 */ } // 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 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 - 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(', ') : ''; // 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(); // Build the small receipt HTML 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 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)); } } // preview modal markup will be rendered below; Print fallback opens this modal return (
{showPreview ? (
setShowPreview(false)}>
e.stopPropagation()}>
Receipt Preview
) : null} {showAlert ? (
{error.toLowerCase().includes('success') || error.toLowerCase().includes('saved') ? 'โœ… Success' : 'โš ๏ธ Notice'}
{error}
) : null} {/* Page Header */}

๐Ÿ›’ Point of Sale

Scan barcode or enter product details to process customer sales

{/* Main Layout - Split Screen */}
{/* LEFT SIDE - Sales Cart */}
{/* Barcode Scanner Input - Prominent */}
๐Ÿ“ท

Scan Product

setProductNo(e.target.value)} onKeyPress={e => { if (e.key === 'Enter') { const needle = (productNo || '').toString().trim().toLowerCase(); if (!needle) return; // Step 1: Try to find by IMEI first (phones have individual IMEI barcodes) let found = null; let matchedImei = null; for (let pi = 0; pi < products.length; pi++) { const p = products[pi]; const pImes = Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.length ? p.centralOnlyImes : Array.isArray(p.centralImes) && p.centralImes.length ? p.centralImes : Array.isArray(p.imes) ? p.imes : []; const match = pImes.find(imei => String(imei || '').toLowerCase() === needle); if (match) { found = p; matchedImei = match; break; } } // Step 2: If not found by IMEI, try product number let foundByProductNo = false; if (!found) { found = products.find(p => String(p.productNo || '').toLowerCase() === needle); foundByProductNo = !!found; } 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; } // Determine if product is IMEI-tracked const availImes = (Array.isArray(found.centralOnlyImes) && found.centralOnlyImes.length) ? found.centralOnlyImes : (Array.isArray(found.centralImes) && found.centralImes.length) ? found.centralImes : (Array.isArray(found.imes) ? found.imes : []); const isImeiTracked = availImes.length > 0; const existingInCart = sellerProducts.find(x => (x.productId || x._id) === (found.productId || found._id)); // Validate before adding if (matchedImei && existingInCart && Array.isArray(existingInCart.selectedImes) && existingInCart.selectedImes.includes(matchedImei)) { setError('This IMEI is already in the cart'); return; } if (foundByProductNo && isImeiTracked && existingInCart) { setError('Product already in cart. Scan individual IMEI barcodes to add phones.'); return; } if (foundByProductNo && !isImeiTracked && existingInCart) { const currentQty = Number(existingInCart.sellingQty) || 0; if (currentQty + 1 > Number(existingInCart.qty || 0)) { setError('Your qty is low'); return; } } setError(''); setSellerProducts(sp => { const eid = (found.productId || found._id); const eIdx = sp.findIndex(x => (x.productId || x._id) === eid); if (matchedImei) { // IMEI scan: add this specific phone if (eIdx >= 0) { return sp.map((x, idx) => idx === eIdx ? { ...x, selectedImes: [...(x.selectedImes || []), matchedImei], sellingQty: (Number(x.sellingQty) || 0) + 1 } : x); } return [...sp, { ...found, sellingQty: 1, selectedImes: [matchedImei] }]; } if (foundByProductNo && isImeiTracked) { // IMEI product by productNo: add to cart, user must scan/select IMEIs return [...sp, { ...found, sellingQty: 0, selectedImes: [] }]; } if (foundByProductNo && !isImeiTracked) { // Accessory: increment qty or add new if (eIdx >= 0) { return sp.map((x, idx) => idx === eIdx ? { ...x, sellingQty: (Number(x.sellingQty) || 0) + 1 } : x); } return [...sp, { ...found, sellingQty: 1 }]; } return sp; }); setProductNo(''); } }} placeholder="Scan barcode or enter Product No / IMEI..." style={{ flex: 1, padding: '16px 20px', fontSize: '18px', border: '3px solid #3b82f6', borderRadius: '12px', outline: 'none', fontWeight: '500', backgroundColor: '#f8fafc' }} />
๐Ÿ’ก Scan with barcode scanner or type manually. Press Enter to add.
{/* Shopping Cart */}

๐Ÿ›๏ธ Cart Items ({sellerProducts.length})

{sellerProducts.length > 0 && ( )}
{sellerProducts.length === 0 ? (
๐Ÿ›’
Cart is Empty
Scan a product barcode to add items
) : (
{sellerProducts.filter(p => Number(p.qty) > 0).map((p, i) => { const availableImes = (Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.length) ? p.centralOnlyImes : (Array.isArray(p.centralImes) && p.centralImes.length) ? p.centralImes : (Array.isArray(p.imes) ? p.imes : []); const hasImes = availableImes.length > 0; const selectedCount = Array.isArray(p.selectedImes) ? p.selectedImes.length : 0; return (
{/* Product Info */}
{p.productName || 'Unnamed Product'}
{p.brand || '-'} {p.model || '-'} โ€ข #{p.productNo || '-'}
{hasImes && (
0 && selectedCount === Number(p.sellingQty ?? 0) ? '#16a34a' : '#f59e0b', fontWeight: '500'}}> {selectedCount > 0 ? 'โœ…' : '๐Ÿ“ฑ'} IMEI: {selectedCount} selected {Number(p.sellingQty ?? 0) === 0 ? '(scan IMEI barcodes)' : ''}
)}
{/* Quantity and Price Row */}
{ if (hasImes) return; 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)); }} /> {hasImes &&
Auto from IMEI
}
{ const v = Number(e.target.value) || 0; setSellerProducts(sp => sp.map((s, idx) => idx === i ? { ...s, sellingPrice: v } : s)); }} style={{ width: '100%', padding: '10px', fontSize: '15px', fontWeight: '600', backgroundColor: '#e0f2fe', borderRadius: '8px', textAlign: 'center', color: '#0284c7', border: '1px solid #bae6fd', outline: 'none' }} />
โ‚น{lineTotal(p).toFixed(2)}
{/* IMEI Selection */} {hasImes && (
{showImes[i] && (
{availableImes.map((val, idx2) => { const checked = Array.isArray(p.selectedImes) && p.selectedImes.includes(val); const isCentral = Array.isArray(p.centralOnlyImes) && p.centralOnlyImes.includes(val); return ( ); })}
)}
)}
); })}
)}
{/* RIGHT SIDE - Customer Info & Checkout */}
{/* Customer Information Card */}

๐Ÿ‘ค Customer Details

setCustomerName(e.target.value)} placeholder="Enter customer name" style={{ width: '100%', padding: '12px 16px', fontSize: '15px', border: '2px solid #e2e8f0', borderRadius: '10px', outline: 'none', transition: 'border-color 0.2s' }} onFocus={e => e.target.style.borderColor = '#3b82f6'} onBlur={e => e.target.style.borderColor = '#e2e8f0'} />
setCustomerNo(e.target.value)} placeholder="Enter mobile number" style={{ width: '100%', padding: '12px 16px', fontSize: '15px', border: '2px solid #e2e8f0', borderRadius: '10px', outline: 'none', transition: 'border-color 0.2s' }} onFocus={e => e.target.style.borderColor = '#3b82f6'} onBlur={e => e.target.style.borderColor = '#e2e8f0'} />
{/* Billing Summary Card */}

๐Ÿ’ฐ Bill Summary

Items ({totalCount}) โ‚น{subTotal.toFixed(2)}
{/* Discount */}
Discount (%) setDiscount(e.target.value)} style={{ width: '80px', padding: '6px 10px', fontSize: '14px', border: '2px solid rgba(255,255,255,0.3)', borderRadius: '8px', backgroundColor: 'rgba(255,255,255,0.15)', color: '#fff', fontWeight: '600', textAlign: 'center' }} />
{discountAmount > 0 && (
- โ‚น{discountAmount.toFixed(2)}
)}
{/* GST */}
CGST (%) setCgst(e.target.value)} style={{ padding: '6px 8px', fontSize: '13px', border: '2px solid rgba(255,255,255,0.3)', borderRadius: '6px', backgroundColor: 'rgba(255,255,255,0.15)', color: '#fff', fontWeight: '600', textAlign: 'center' }} /> โ‚น{cgstAmount.toFixed(2)}
SGST (%) setSgst(e.target.value)} style={{ padding: '6px 8px', fontSize: '13px', border: '2px solid rgba(255,255,255,0.3)', borderRadius: '6px', backgroundColor: 'rgba(255,255,255,0.15)', color: '#fff', fontWeight: '600', textAlign: 'center' }} /> โ‚น{sgstAmount.toFixed(2)}
IGST (%) setIgst(e.target.value)} style={{ padding: '6px 8px', fontSize: '13px', border: '2px solid rgba(255,255,255,0.3)', borderRadius: '6px', backgroundColor: 'rgba(255,255,255,0.15)', color: '#fff', fontWeight: '600', textAlign: 'center' }} /> โ‚น{igstAmount.toFixed(2)}
{/* Total */}
TOTAL AMOUNT โ‚น{totalAmount.toFixed(2)}
{/* Action Buttons */} {lastSale && (
)}
); } window.ProductSales = ProductSales;