// 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
Bill To
${sale.customerName || customerName || 'Walk-in Customer'}
Phone: ${sale.customerNo || customerNo || 'N/A'}
Invoice Details
Invoice No: ${invoiceNo}
Invoice Date: ${invoiceDate}
| Sr. No. |
Items |
Quantity |
Price / Unit |
Tax / Unit |
Amount |
${items}
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}
` +
`
| # |
๐ฆ Product Details |
HSN |
Qty |
Rate |
Amount |
${items}
` +
`` +
`| ๐ Sub Total: | โน ${outSubTotal.toFixed(2)} |
` +
(sale.discount ? `| ๐ท๏ธ Discount (${sale.discount}%): | - โน ${outDiscount.toFixed(2)} |
` : '') +
`| ๐ต Taxable Amount: | โน ${outTaxable.toFixed(2)} |
` +
(cgstPercent > 0 ? `| ๐๏ธ CGST ${cgstPercent}%: | โน ${Number(cgstAmt).toFixed(2)} |
` : '') +
(sgstPercent > 0 ? `| ๐๏ธ SGST ${sgstPercent}%: | โน ${Number(sgstAmt).toFixed(2)} |
` : '') +
(igstPercent > 0 ? `| ๐๏ธ 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 */}
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;