v3.0.1
BD POS
Point of Sale System
Secure
Always On
Verified
Register New Shop
Loading...
Please wait
Initializing...
BD POS
Point of Sale System
−
⊿
Normal
Fast Mode
Barcode Scanner ON — Scan now
Invoices
Today
7 Days
30 Days
Custom
All
All
Due
Paid
Partial
By Category (This Month)
Expense List
+ Add Product
Adjust Stock
‹ Back
Add Product
Basic Info
Product Name *
Generate Barcode
Print / Size
Save Product
Delete Product
‹ Back
Purchase / Stock-In
‹ Back
Branch Management
+ Add Branch
‹ Back
Table Management
+ Add Table
‹ Back
Repair Job Card
+ New Ticket
All
Pending
In Progress
Completed
Delivered
‹ Back
Subscription
Renew / Extend Subscription
‹ Back
Sales Report
You can only view sales data. Profit/loss details are visible to the Owner only.
‹ Back
Profit / Loss Reports
Today
7 Days
30 Days
This Month
Custom
Profit by Product
Product
Sales / Profit
Invoice / Customer
Sales / Profit
Home
Bill
Invoices
৳ Expenses
Stock
Settings
Close BD-POS?
আপনি কি সত্যিই বন্ধ করতে চান? সকল অসংরক্ষিত তথ্য মুছে যাবে।
না, থাকুন
হ্যাঁ, বন্ধ করুন
Table Order
Add / Edit Items
Edit Table
Order Total
৳ 0
Cart
Table (optional)
— No Table / Takeaway —
Barcode Scanner
Scan using your USB / Bluetooth scanner in the field below.
Tap the field below, then scan...
Press Enter after scan or the scanner will auto-submit.
Select Business Type
Select your business type — all features stay the same, only categories and units will be customized.
Shop Name *
Currency
৳ BDT (Bangladesh) ₹ INR (India)
$ USD ₦ NGN (Nigeria)
₱ PHP (Philippines)
Common expenses for this business:
Create Shop
Switch Business Type
Smooth reload + full module switch
// ═══════════════════════════════════════════════════════════════════════
// 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 += '
';
}
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 = '
— Select Product — ';
for (var i = 0; i < PRODUCTS.length; i++) {
sel.innerHTML += '
' + xss(PRODUCTS[i].name) + ' ';
}
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 = '
No products — add products first ';
}
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 = '
';
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();
}