function ProductSell({ 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 [banks, setBanks] = React.useState([]); const [whatsappStock, setWhatsappStock] = 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); React.useEffect(() => { async function loadProducts() { try { setError(''); // Sales server branch stock endpoint 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 branch stock'); setProducts(Array.isArray(data.rows) ? data.rows : []); } catch (e) { setError(e.message || 'Failed to load products'); } } loadProducts(); }, [salesUrl, token]); React.useEffect(() => { if (!error) { setShowAlert(false); return; } setShowAlert(true); const t = setTimeout(() => setShowAlert(false), 4000); return () => clearTimeout(t); }, [error]); 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(); }, [token]); React.useEffect(() => { async function loadWhatsappStock() { try { const url = new URL((salesUrl || '') + '/api/whatsapp-stock'); const res = await fetch(url, { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); if (res.ok && Array.isArray(data.rows)) setWhatsappStock(data.rows); } catch (e) { /* ignore */ } } loadWhatsappStock(); }, [salesUrl, token]); function lineTotal(item) { const qty = Number(item.sellingQty ?? item.qty ?? 0); const unit = Number(item.sellingPrice ?? item.sellingPrice ?? 0); return qty * unit; } const totalCount = sellerProducts.reduce((s, it) => s + Number(it.sellingQty ?? it.qty ?? 0), 0); const totalAmount = sellerProducts.reduce((s, it) => s + lineTotal(it), 0); async function doSell() { try { if (sellerProducts.length === 0) { setError('No products to sell'); 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 payload = { items: sellerProducts.map(it => ({ productId: it._id || it.productId, productNo: it.productNo || '', productName: it.productName || it.name || '', qty: Number(it.sellingQty ?? it.qty ?? 0), sellingPrice: Number(it.sellingPrice || 0), lineTotal: Number(lineTotal(it)) })), customerNo, paymentMethod: 'online', amountPaid: Number(totalAmount || 0), bank_id: selectedBank }; // Decide endpoint: if any item is from whatsapp stock, use whatsapp-sales endpoint const whatsappIds = new Set((whatsappStock || []).map(w => String(w._id))); const hasWhatsappItem = sellerProducts.some(sp => whatsappIds.has(String(sp._id || sp.productId))); const url = new URL((salesUrl || '') + (hasWhatsappItem ? '/api/whatsapp-sales' : '/api/sales')); 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 || data.error || 'Sell failed'); setLastSale(data.sale || data || null); setSellerProducts([]); setCustomerNo(''); setSelectedBank('select'); setError('Sale saved'); // refresh product lists to reflect updated stock try { const bsUrl = new URL((salesUrl || '') + '/api/branch-stock'); bsUrl.searchParams.set('only_branch','1'); const r2 = await fetch(bsUrl, { headers: { Authorization: 'Bearer ' + token } }); const d2 = await r2.json(); if (r2.ok && Array.isArray(d2.rows)) setProducts(d2.rows); const wsUrl = new URL((salesUrl || '') + '/api/whatsapp-stock'); const r3 = await fetch(wsUrl, { headers: { Authorization: 'Bearer ' + token } }); const d3 = await r3.json(); if (r3.ok && Array.isArray(d3.rows)) setWhatsappStock(d3.rows); } catch (__) { } } catch (e) { setError(e.message || 'Sell failed'); } finally { setSellingBusy(false); } } async function printSale() { try { const sale = lastSale || { items: sellerProducts, totalAmount, customerNo, createdAt: new Date().toISOString() }; // fetch branch info let shopName = ''; let shopContact = ''; 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 || ''; } } 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 || ''; } // 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 => { 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 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); return `${name}${qty}${unit}${line}`; }).join(''); const total = Number(sale.totalAmount || 0).toFixed(2); const date = new Date(sale.createdAt || Date.now()).toLocaleString(); const html = `Receipt` + `

CASH RECEIPT

${shopName || 'Shop'}
${shopContact || ''}
` + `
Date: ${date}
` + `${items}
ItemQtyUnitLine
` + ``; const w = window.open('', '_blank'); if (!w) { // fallback to in-page preview when popups are blocked setPreviewHtml(html); setShowPreview(true); setError('Popup blocked: showing preview. Allow popups to print directly.'); return; } w.document.open(); w.document.write(html); w.document.close(); w.focus(); setTimeout(() => { try { w.print(); } catch (e) { /* ignore */ } }, 300); } catch (e) { setError('Failed to open printer: ' + (e.message || e)); } } function whatsappShare() { (async () => { const sale = lastSale || { items: sellerProducts, totalAmount, customerNo }; // attempt to fetch authoritative branch info let shopName = ''; let shopContact = ''; try { // Prefer authoritative MySQL user info (username/contact) from SalesServer proxy to AUTH const mu = await fetch(new URL((salesUrl || '') + '/api/mysql-user'), { method: 'POST', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' } }); const muData = await mu.json().catch(() => ({})); if (mu.ok && muData && muData.user) { shopName = muData.user.username || muData.user.name || muData.user.shopName || ''; shopContact = muData.user.contact || muData.user.phone || muData.user.mobile || ''; } } catch (e) { /* ignore */ } // Fallback to branches endpoint / JWT if mysql-user not present if (!shopName || !shopContact) { try { const url = new URL((salesUrl || '') + '/api/branches'); const res = await fetch(url, { 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 foundBranch = null; if (branchId) foundBranch = data.branches.find(b => String(b._id) === String(branchId)); if (!foundBranch) foundBranch = data.branches[0]; shopName = foundBranch?.name || ''; shopContact = foundBranch?.phoneNumber || foundBranch?.phone || ''; } } 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 || ''; } } const itemsText = (sale.items || []).map(i => { const qty = i.qty || i.sellingQty || 0; const unit = Number(i.sellingPrice || i.unitSellingPrice || 0).toFixed(2); return `${i.productName || i.productNo || 'item'} x${qty} @ ${unit}`; }).join('\n\n'); // normalize customer number let digits = (sale.customerNo || '').toString().replace(/[^0-9]/g, ''); if (digits.length === 11 && digits.startsWith('0')) digits = digits.slice(1); if (digits.length === 10) digits = '91' + digits; const cust = digits; const message = `Shop: ${shopName}\nContact: ${shopContact}\n\nItems:\n${itemsText}\n\nTotal: ${Number(sale.totalAmount || totalAmount || 0).toFixed(2)}`; const whatsappUrl = window.ENV_CONFIG?.WHATSAPP_WEB_URL || 'https://web.whatsapp.com'; window.open(`${whatsappUrl}/send?phone=${cust}&text=${encodeURIComponent(message)}`, '_blank'); })(); } // 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 {}; } } return (
{showPreview ? (
setShowPreview(false)}>
e.stopPropagation()}>
Receipt Preview
) : null} {showAlert && (
Message
{error}
)}

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

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

My Products
{sellerProducts.length === 0 ? (
🧾
No Products Added
Enter a product no and click Add to populate your list.
) : (
{/* */} {sellerProducts.map((p, i) => ( {/* */} ))}
Product No Product Name QtyCost PriceSelling Price Selling Qty Line Total Action
{p.productNo || p._id || '-'} {p.productName || p.name || '-'} {p.quantity ?? p.qty ?? '-'}{p.costPrice ?? p.totalCost ?? '-'}{p.sellingPrice ?? p.sellingPrice ?? '-'} { const v = Number(e.target.value) || 0; setSellerProducts(sp => sp.map((s, idx) => idx === i ? { ...s, sellingQty: v } : s)); }} /> {lineTotal(p).toFixed(2)}
Totals: {totalCount} {totalAmount.toFixed(2)}
)}
); } window.ProductSell = ProductSell;