v3.0.2
Loading...
Please wait
Initializing...
BD POS
Point of Sale System
Barcode Scanner ON — Scan now
Invoices
Expenses
Today's Expense
Monthly Expense
Monthly Sales
Monthly Profit
By Category (This Month)
Expense List
Stock Management
Total Products
Out of Stock
Total Cost Value
Total Sell Value
Expected Profit
Low Stock
Loading...
Add Product
Upload image (optional)
Optional — tap to select
Stock In / Out
Current Stock
Stock History
Select a product to see history
Vendor Management
Add New Vendor
Saved Vendors
Loading...
Purchase / Stock-In
This Month Purchase
Total Entries
NEW PURCHASE ENTRY
Total Cost ৳ 0
Purchase History
Loading...
My Account
Loading...
Customer Management
Table Management
Repair Job Card
Staff Management
Branch Management
Change Business Type
Shop Settings
Reports
Subscription
Light Mode
Change Password
Sign Out
Staff
Branch Management
Customers
Total Customers
Total Due
Table Management
Free
Occupied
Reserved
Loading...
Repair Job Card
Pending
In Progress
Completed
Delivered
Subscription
Sales Report
7-Day Sales
🏆 Top Products
Profit / Loss Reports
0 items
৳ 0
Sell
// ═══════════════════════════════════════════════════════════════════════ // CUSTOMER MANAGEMENT // ═══════════════════════════════════════════════════════════════════════ var CUSTOMERS = []; var CUST_CURRENT_KEY = ''; // "name|phone" var currentcustomerId = ''; // legacy alias // ── storage helpers ────────────────────────────────────────────────── function custGetManual() { try { return JSON.parse(storageGet('bd_manual_customers') || '[]'); } catch(e) { return []; } } function custSaveManual(list) { try { storageSet('bd_manual_customers', JSON.stringify(list)); } catch(e) {} } // ── event wiring (called once on DOMContentLoaded) ─────────────────── var _custEventsInitialized = false; function custInitEvents() { if (_custEventsInitialized) return; // prevent duplicate binding _custEventsInitialized = true; // Back button var backBtn = document.getElementById('cust-back-btn'); if (backBtn) backBtn.addEventListener('click', function() { closeSubpage(); }); // Add Customer button var addBtn = document.getElementById('cust-add-btn'); if (addBtn) addBtn.addEventListener('click', function() { custOpenForm(null); }); // Search var searchEl = document.getElementById('cust-search'); if (searchEl) searchEl.addEventListener('input', function() { custRenderList(); }); // Customer list - event delegation (no inline onclick) var listEl = document.getElementById('cust-list'); if (listEl) { listEl.addEventListener('click', function(e) { var card = e.target.closest('[data-cust-key]'); if (card) { var key = card.getAttribute('data-cust-key'); custViewDetail(key); } // retry button if (e.target.closest('[data-cust-retry]')) { loadCustomers(); } }); } // Form modal buttons var formCloseBtn = document.getElementById('cust-form-close-btn'); if (formCloseBtn) formCloseBtn.addEventListener('click', function() { closeModal('customer-form'); }); var formCancelBtn = document.getElementById('cust-form-cancel-btn'); if (formCancelBtn) formCancelBtn.addEventListener('click', function() { closeModal('customer-form'); }); var formSaveBtn = document.getElementById('cust-form-save-btn'); if (formSaveBtn) formSaveBtn.addEventListener('click', function() { custSave(); }); // Detail modal buttons var detailCloseBtn = document.getElementById('cust-detail-close-btn'); if (detailCloseBtn) detailCloseBtn.addEventListener('click', function() { closeModal('customer-detail'); }); var detailClose2Btn = document.getElementById('cust-detail-close2-btn'); if (detailClose2Btn) detailClose2Btn.addEventListener('click', function() { closeModal('customer-detail'); }); var detailEditBtn = document.getElementById('cust-detail-edit-btn'); if (detailEditBtn) detailEditBtn.addEventListener('click', function() { custEditCurrent(); }); } // ── load from server ───────────────────────────────────────────────── function loadCustomers() { var el = document.getElementById('cust-list'); if (!el) return; el.innerHTML = '
'; http('GET', 'invoices?limit=1000', null, function(err, res) { if (err || !res) { el.innerHTML = '
Network error.
Tap to retry
'; return; } if (!res.ok) { var msg = (res.msg || res.message || 'Error'); el.innerHTML = '
Failed: ' + xss(msg) + '
Tap to retry
'; return; } // Build customer map from invoices var invoices = (res.data && res.data.invoices) ? res.data.invoices : []; var custMap = {}; for (var i = 0; i < invoices.length; i++) { var inv = invoices[i]; if (!inv.cust_name) continue; var key = inv.cust_name + '|' + (inv.cust_phone || ''); if (!custMap[key]) { custMap[key] = { name: inv.cust_name, phone: inv.cust_phone || '', due: 0, total_bills: 0, total_spent: 0, manual: false }; } custMap[key].total_bills++; custMap[key].total_spent += parseFloat(inv.total || 0); if (inv.pay_status === 'due' || inv.pay_status === 'partial') { var dueAmt = (inv.due_amount != null) ? parseFloat(inv.due_amount) : (parseFloat(inv.total || 0) - parseFloat(inv.paid_amount || 0)); custMap[key].due += (dueAmt > 0 ? dueAmt : 0); } } // Merge manually added customers var manual = custGetManual(); for (var mi = 0; mi < manual.length; mi++) { var m = manual[mi]; if (!m.name) continue; var mkey = m.name + '|' + (m.phone || ''); if (!custMap[mkey]) { custMap[mkey] = { name: m.name, phone: m.phone || '', address: m.address || '', note: m.note || '', due: 0, total_bills: 0, total_spent: 0, manual: true }; } else { // enrich with manual metadata custMap[mkey].address = m.address || custMap[mkey].address || ''; custMap[mkey].note = m.note || custMap[mkey].note || ''; } } CUSTOMERS = []; for (var k in custMap) CUSTOMERS.push(custMap[k]); CUSTOMERS.sort(function(a, b) { return b.total_spent - a.total_spent; }); // Update stats var tot = document.getElementById('cust-total'); var dueT = document.getElementById('cust-due-total'); if (tot) tot.textContent = CUSTOMERS.length; var totalDue = 0; for (var ci = 0; ci < CUSTOMERS.length; ci++) totalDue += CUSTOMERS[ci].due; if (dueT) dueT.textContent = cur(totalDue); custRenderList(); }); } // ── render list ────────────────────────────────────────────────────── function custRenderList() { var el = document.getElementById('cust-list'); if (!el) return; var q = ''; var searchEl = document.getElementById('cust-search'); if (searchEl) q = searchEl.value.toLowerCase().trim(); var list = CUSTOMERS; if (q) { list = []; for (var i = 0; i < CUSTOMERS.length; i++) { var c = CUSTOMERS[i]; if (c.name.toLowerCase().indexOf(q) !== -1 || c.phone.indexOf(q) !== -1) { list.push(c); } } } if (list.length === 0) { el.innerHTML = '
' + '
' + '
No customers yet
' + '
Invoice-এ customer name দিলে এখানে দেখাবে
অথবা "+ Add Customer" বাটনে ক্লিক করুন
' + '
'; return; } var html = ''; for (var j = 0; j < list.length; j++) { var c = list[j]; var ckey = c.name + '|' + c.phone; var initial = (c.name && c.name.length > 0) ? c.name[0].toUpperCase() : '?'; html += '
' + '
' + xss(initial) + '
' + '
' + '
' + xss(c.name) + '
' + (c.phone ? '
' + xss(c.phone) + '
' : '') + '
' + c.total_bills + ' bills · Spent ' + cur(c.total_spent) + '
' + '
' + (c.due > 0 ? '
' + '
DUE
' + '
' + cur(c.due) + '
' + '
' : '') + '
'; } el.innerHTML = html; } // legacy alias used elsewhere in the file function filterCustomers() { custRenderList(); } function rendercustomerList() { custRenderList(); } // ── view customer detail ────────────────────────────────────────────── function custViewDetail(key) { var parts = key.split('|'); var name = parts[0] || ''; var phone = parts.slice(1).join('|'); // phone may be empty CUST_CURRENT_KEY = key; // legacy alias currentcustomerId = key; var titleEl = document.getElementById('cust-detail-name'); var bodyEl = document.getElementById('cust-detail-body'); if (titleEl) titleEl.textContent = name; if (bodyEl) bodyEl.innerHTML = '
Loading...
'; openModal('customer-detail'); http('GET', 'invoices?limit=1000', null, function(err, res) { if (!res || !res.ok) { if (bodyEl) bodyEl.innerHTML = '
Failed to load data.
'; return; } var allInv = (res.data && res.data.invoices) ? res.data.invoices : []; var invs = []; for (var i = 0; i < allInv.length; i++) { var inv = allInv[i]; // BUG FIX: this used to match on `phone === '' || inv.cust_phone === phone`, // which meant that for any customer with no phone number, EVERY invoice // with the same name matched regardless of its own phone — merging the // purchase history/due totals of unrelated customers who share a name // (e.g. multiple walk-in "Rahim" customers with different numbers). // Match on the exact same "name|phone" key used to build the customer // list instead, so only invoices belonging to this specific customer show. var invKey = inv.cust_name + '|' + (inv.cust_phone || ''); if (invKey === key) { invs.push(inv); } } var totalSpent = 0, totalDue = 0; for (var j = 0; j < invs.length; j++) { totalSpent += parseFloat(invs[j].total || 0); if (invs[j].pay_status === 'due' || invs[j].pay_status === 'partial') { var dAmt = (invs[j].due_amount != null) ? parseFloat(invs[j].due_amount) : (parseFloat(invs[j].total || 0) - parseFloat(invs[j].paid_amount || 0)); totalDue += (dAmt > 0 ? dAmt : 0); } } var html = '
' + '
Total Bills
' + invs.length + '
' + '
Total Spent
' + cur(totalSpent) + '
' + (totalDue > 0 ? '
Total Due
' + cur(totalDue) + '
' : '') + '
'; if (phone) { html += '
' + ' Phone' + '' + xss(phone) + '' + '
'; } html += '
Purchase History
'; if (invs.length === 0) { html += '
No invoices found
'; } else { for (var k = 0; k < invs.length; k++) { var inv2 = invs[k]; var sc = inv2.pay_status === 'paid' ? 'var(--g)' : (inv2.pay_status === 'due' ? 'var(--r)' : 'var(--y)'); html += '
' + '
' + '
INV #' + xss(String(inv2.invoice_no || inv2.id)) + '
' + '
' + xss(inv2.created_at || '') + '
' + '
' + '
' + '
' + cur(inv2.total) + '
' + '
' + xss(inv2.pay_status) + '
' + '
' + '
'; } } if (bodyEl) bodyEl.innerHTML = html; }); } // legacy alias function viewCustomer(name, phone) { var key = name + '|' + (phone || ''); custViewDetail(key); } // ── open Add/Edit form ──────────────────────────────────────────────── function custOpenForm(editKey) { var editIdEl = document.getElementById('cust-edit-id'); var nameEl = document.getElementById('cust-name'); var phoneEl = document.getElementById('cust-phone'); var addressEl = document.getElementById('cust-address'); var noteEl = document.getElementById('cust-note'); var titleEl = document.getElementById('cust-form-title'); if (!editIdEl || !nameEl) { showToast('Form not ready'); return; } if (editKey) { // Edit mode var found = null; for (var i = 0; i < CUSTOMERS.length; i++) { var ck = CUSTOMERS[i].name + '|' + (CUSTOMERS[i].phone || ''); if (ck === editKey) { found = CUSTOMERS[i]; break; } } var parts = editKey.split('|'); editIdEl.value = editKey; nameEl.value = parts[0] || ''; phoneEl.value = parts.slice(1).join('|') || ''; addressEl.value = found ? (found.address || '') : ''; noteEl.value = found ? (found.note || '') : ''; if (titleEl) titleEl.textContent = 'Edit Customer'; if (found && !found.manual) { showToast('Note: came from invoice — will be saved as a contact card'); } } else { // Add mode editIdEl.value = ''; nameEl.value = ''; phoneEl.value = ''; addressEl.value = ''; noteEl.value = ''; if (titleEl) titleEl.textContent = 'Add Customer'; } openModal('customer-form'); // Focus name field after modal opens setTimeout(function() { if (nameEl) nameEl.focus(); }, 300); } // legacy aliases function openCustomerForm(id) { custOpenForm(null); } // ── save customer ───────────────────────────────────────────────────── function custSave() { var nameEl = document.getElementById('cust-name'); var phoneEl = document.getElementById('cust-phone'); var addressEl = document.getElementById('cust-address'); var noteEl = document.getElementById('cust-note'); var editIdEl = document.getElementById('cust-edit-id'); if (!nameEl) return; var name = nameEl.value.trim(); var phone = phoneEl ? phoneEl.value.trim() : ''; var address = addressEl ? addressEl.value.trim() : ''; var note = noteEl ? noteEl.value.trim() : ''; var editKey = editIdEl ? editIdEl.value.trim() : ''; if (!name) { showToast('Customer name লিখুন'); nameEl.focus(); return; } var manual = custGetManual(); if (editKey) { // Edit: find by key and update var found = false; for (var i = 0; i < manual.length; i++) { var mk = manual[i].name + '|' + (manual[i].phone || ''); if (mk === editKey) { manual[i] = {name: name, phone: phone, address: address, note: note}; found = true; break; } } if (!found) { // not in manual list yet (invoice-based customer) — add as new manual entry manual.push({name: name, phone: phone, address: address, note: note}); } } else { // Add: check duplicate var dup = false; for (var j = 0; j < manual.length; j++) { if (manual[j].name.toLowerCase() === name.toLowerCase() && (manual[j].phone || '') === phone) { dup = true; break; } } if (dup) { showToast('এই customer ইতিমধ্যে আছে'); return; } manual.push({name: name, phone: phone, address: address, note: note}); } custSaveManual(manual); showToast(' Customer saved'); closeModal('customer-form'); loadCustomers(); } // legacy alias function saveCustomer() { custSave(); } // ── edit current customer (from detail modal) ───────────────────────── function custEditCurrent() { var key = CUST_CURRENT_KEY || currentcustomerId; closeModal('customer-detail'); setTimeout(function() { custOpenForm(String(key)); }, 200); } // legacy alias function editCurrentCustomer() { custEditCurrent(); } // ── init ────────────────────────────────────────────────────────────── (function() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', custInitEvents); } else { custInitEvents(); } })(); // ═══════════════════════════════════════════════════════════════════════ // PURCHASE MANAGEMENT // ═══════════════════════════════════════════════════════════════════════ var PURCHASE_HISTORY = []; function openPurchasePage() { try { openStockSubpage('sub-stock-purchase'); // Always refresh from server so product list is current http('GET', 'products', null, function(err, res) { if (res && res.ok) { PRODUCTS = res.data.products || []; STOCK_PRODUCTS = res.data.products || []; } loadPurchasePage(); }); } catch (e) { showToast('Could not open Purchase page: ' + (e && e.message ? e.message : 'error')); } } function loadPurchasePage() { // Populate product dropdown var sel = document.getElementById('pur-product-sel'); if (sel && PRODUCTS.length > 0) { sel.innerHTML = ''; for (var i = 0; i < PRODUCTS.length; i++) { sel.innerHTML += ''; } sel.onchange = function() { var opt = sel.options[sel.selectedIndex]; var cost = opt ? opt.getAttribute('data-cost') : 0; var costEl = document.getElementById('pur-unit-cost'); if (costEl && cost) costEl.value = cost; calcPurchaseTotal(); }; } else if (sel) { sel.innerHTML = ''; } var qtyEl = document.getElementById('pur-qty'); var costEl2 = document.getElementById('pur-unit-cost'); if (qtyEl) qtyEl.oninput = calcPurchaseTotal; if (costEl2) costEl2.oninput = calcPurchaseTotal; loadPurchaseHistory(); } function calcPurchaseTotal() { var qty = parseFloat((document.getElementById('pur-qty') || {}).value || 0) || 0; var cost = parseFloat((document.getElementById('pur-unit-cost') || {}).value || 0) || 0; var tot = document.getElementById('pur-total-cost'); if (tot) tot.textContent = cur(qty * cost); } function loadPurchaseHistory() { // Load from stored purchase log (via expenses API using purchase category) var el = document.getElementById('purchase-history-list'); if (el) el.innerHTML = '
Loading...
'; http('GET', 'expenses', null, function(err, res) { var purchases = []; if (res && res.ok) { var exps = res.data.expenses || []; purchases = exps.filter(function(e) { return e.category === 'Stock Purchase' || e.note && e.note.indexOf('[PURCHASE]') !== -1; }); } PURCHASE_HISTORY = purchases; // Update stats var now = new Date(); var monthTotal = 0; var count = 0; for (var i = 0; i < purchases.length; i++) { var d = new Date(purchases[i].created_at || ''); if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth()) { monthTotal += parseFloat(purchases[i].amount || 0); } count++; } var mt = document.getElementById('purchase-month-total'); var tc = document.getElementById('purchase-total-count'); if (mt) mt.textContent = cur(monthTotal); if (tc) tc.textContent = count; var listEl = document.getElementById('purchase-history-list'); if (!listEl) return; if (purchases.length === 0) { listEl.innerHTML = '
No purchase entries yet
'; return; } var html = ''; for (var j = 0; j < purchases.length; j++) { var p = purchases[j]; html += '
' + '
' + '
' + xss(p.note || p.category) + '
' + '
' + xss(p.created_at || '') + '
' + '
' + cur(p.amount) + '
' + '
'; } listEl.innerHTML = html; }); } function savePurchaseEntry() { var prodSel = document.getElementById('pur-product-sel'); var prodId = prodSel ? prodSel.value : ''; var qty = parseFloat((document.getElementById('pur-qty') || {}).value || 0) || 0; var unitCost = parseFloat((document.getElementById('pur-unit-cost') || {}).value || 0) || 0; var supplier = (document.getElementById('pur-supplier') || {}).value || ''; var note = (document.getElementById('pur-note') || {}).value || ''; if (!prodId) { showToast('Select a product'); return; } if (qty <= 0) { showToast('Enter quantity'); return; } var prodName = ''; for (var i = 0; i < PRODUCTS.length; i++) { if (PRODUCTS[i].id == prodId) { prodName = PRODUCTS[i].name; break; } } var totalCost = qty * unitCost; var entryNote = '[PURCHASE] ' + prodName + ' x' + qty + (supplier ? ' from ' + supplier : '') + (note ? ' — ' + note : ''); function doStockIn() { var adjBody = { qty: Math.abs(qty), reason: 'stock_in', note: 'Purchase: ' + (supplier || 'unknown supplier') + (note ? ' — ' + note : ''), cost_price: unitCost }; http('POST', 'products/' + prodId + '/stock', adjBody, function(err2, res2) { if (err2 || !res2 || !res2.ok) { showToast('Error: ' + (res2 ? res2.msg : 'network error')); loadPurchaseHistory(); loadProducts(); loadStockPage(); return; } showToast('Purchase saved! Stock updated +' + qty); // Reset form if (document.getElementById('pur-qty')) document.getElementById('pur-qty').value = ''; if (document.getElementById('pur-unit-cost')) document.getElementById('pur-unit-cost').value = ''; if (document.getElementById('pur-supplier')) document.getElementById('pur-supplier').value = ''; if (document.getElementById('pur-note')) document.getElementById('pur-note').value = ''; if (document.getElementById('pur-total-cost')) document.getElementById('pur-total-cost').textContent = cur(0); loadPurchaseHistory(); // Reload products to update stock count loadProducts(); loadStockPage(); }); } // Only record an expense entry when a purchase price was actually given — // the expense API rejects a 0 amount, and price is intentionally optional here. if (totalCost > 0) { var expBody = { category: 'Stock Purchase', amount: totalCost, note: entryNote, expense_date: new Date().toISOString().split('T')[0] }; http('POST', 'expenses', expBody, function(err1, res1) { doStockIn(); }); } else { doStockIn(); } } // ═══════════════════════════════════════════════════════════════════════ // BUSINESS TYPE: CATEGORY AUTO-POPULATE // ═══════════════════════════════════════════════════════════════════════ // Override spe-category and spe-unit dropdowns based on business type function populateBizDropdowns() { // Sync BB_BIZ_CONFIG then call populateStockProductSelects (which is the canonical populate fn) var bizKey = S ? getBizKey(S.type) : 'general'; for (var i = 0; i < BIZ_TYPES.length; i++) { if (BIZ_TYPES[i].key === bizKey) { BB_BIZ_CONFIG = BIZ_TYPES[i]; break; } } populateStockProductSelects(); }