AZ

Tenant Module

বিক্রি ও কিস্তি

Sales Ledger Installment Collection

বিক্রি, বকেয়া ও EMI এক জায়গায়

POS invoice, installment schedule, multi-payment breakdown এবং due collection দ্রুত manage করার screen।

Sales List

সব invoice

Invoice Customer Sale Paid/Due Payment Status Action
`; const win = window.open("", "_blank"); if (!win) { showToast("পপআপ ব্লকড, প্রিন্ট খুলতে পারেনি", "error"); return; } win.document.open(); win.document.write(html); win.document.close(); win.focus(); win.print(); } function hasCrudPermission(moduleKey, actionKey) { try { const role = JSON.parse(localStorage.getItem("az-current-user") || "{}")?.role || "superadmin"; if (role === "superadmin") return true; const matrix = JSON.parse(localStorage.getItem("az-role-crud-permissions") || "{}"); const mod = matrix?.[role]?.[moduleKey]; if (!mod) return true; return !!mod[actionKey]; } catch (error) { return true; } } function ensureCrudPermission(moduleKey, actionKey, label) { if (hasCrudPermission(moduleKey, actionKey)) return true; showToast(`${label || "এই কাজটি"} করার অনুমতি নেই`, "error"); return false; } function seedExpenseCategories() { return [ { id: "excat-rent", name: "ভাড়া", code: "RENT", budget: 30000, status: "Active", tag: "fixed", note: "", isDeleted: false }, { id: "excat-electric", name: "বিদ্যুৎ বিল", code: "ELEC", budget: 12000, status: "Active", tag: "utility", note: "", isDeleted: false }, { id: "excat-internet", name: "ইন্টারনেট", code: "NET", budget: 3000, status: "Active", tag: "utility", note: "", isDeleted: false }, { id: "excat-salary", name: "স্টাফ বেতন", code: "SAL", budget: 90000, status: "Active", tag: "fixed", note: "", isDeleted: false }, { id: "excat-transport", name: "পরিবহন", code: "TRANS", budget: 8000, status: "Active", tag: "variable", note: "", isDeleted: false }, { id: "excat-other", name: "অন্যান্য", code: "OTHER", budget: 5000, status: "Active", tag: "variable", note: "", isDeleted: false }, ]; } function loadExpenseCategories() { const saved = JSON.parse(localStorage.getItem("az-expense-categories") || "[]"); state.expenseCategories = (saved.length ? saved : seedExpenseCategories()).map((item) => normalizeSoftDeleteRow(item)); if (!saved.length) persistExpenseCategories(); } function persistExpenseCategories() { localStorage.setItem("az-expense-categories", JSON.stringify(state.expenseCategories)); } function activeExpenseCategories() { return state.expenseCategories.filter((item) => !item.isDeleted && item.status === "Active"); } function renderExpenseCategoryOptions(selected = "") { const categories = activeExpenseCategories(); const selectedValue = String(selected || "").trim(); els.expenseCategory.innerHTML = [``, ...categories.map((item) => ``)].join(""); if (selectedValue && categories.some((item) => item.name === selectedValue)) { els.expenseCategory.value = selectedValue; } } function monthlyCategorySpend(categoryName = "") { const monthKey = new Date().toISOString().slice(0, 7); return state.expenses .filter((expense) => !expense.isDeleted && expense.category === categoryName && String(expense.createdAt || "").slice(0, 7) === monthKey) .reduce((sum, expense) => sum + Number(expense.amount || 0), 0); } function expenseCategoryRows() { const query = state.expenseCategoryQuery.toLowerCase().trim(); return state.expenseCategories.filter((category) => { if (category.isDeleted) return false; const haystack = `${category.name} ${category.code} ${category.tag} ${category.note}`.toLowerCase(); const matchQuery = !query || haystack.includes(query); const matchStatus = state.expenseCategoryStatus === "all" || category.status === state.expenseCategoryStatus; return matchQuery && matchStatus; }); } function renderExpenseCategoryPage() { const rows = expenseCategoryRows(); const totalBudget = rows.reduce((sum, row) => sum + Number(row.budget || 0), 0); const activeCount = rows.filter((row) => row.status === "Active").length; els.expenseCategorySummary.textContent = `${rows.length.toLocaleString("bn-BD")}টি ক্যাটাগরি · Active ${activeCount.toLocaleString("bn-BD")} · বাজেট ${taka(totalBudget)}`; els.expenseCategoryTable.innerHTML = rows.length ? rows .map((category) => { const spent = monthlyCategorySpend(category.name); const budget = Number(category.budget || 0); const spentClass = budget > 0 && spent > budget ? "text-red-300" : "text-slate-700 dark:text-slate-200"; const badgeClass = category.status === "Active" ? "bg-emerald-500/15 text-emerald-300" : "bg-slate-500/20 text-slate-300"; return `

${escapeHtml(category.name)}

${escapeHtml(category.note || "-")}

${escapeHtml(category.code || "-")}

${taka(budget)}

এই মাসে: ${taka(spent)}

${escapeHtml(category.tag || "-")} ${escapeHtml(category.status)}
`; }) .join("") : `

কোনো ক্যাটাগরি পাওয়া যায়নি

`; lucide.createIcons(); } function saveExpenseCategory() { const editingId = state.editingExpenseCategoryId; if (editingId && !canManageRecords) { showToast("Expense category update করার পারমিশন নেই", "error"); return; } const existing = editingId ? state.expenseCategories.find((item) => item.id === editingId && !item.isDeleted) : null; const name = els.expenseCategoryName.value.trim(); const code = els.expenseCategoryCode.value.trim().toUpperCase(); if (!name) { showToast("ক্যাটাগরি নাম দিন", "error"); els.expenseCategoryName.focus(); return; } const duplicateName = state.expenseCategories.find((item) => !item.isDeleted && item.id !== existing?.id && item.name.toLowerCase() === name.toLowerCase()); if (duplicateName) { showToast("এই ক্যাটাগরি নাম আগেই আছে", "error"); els.expenseCategoryName.focus(); return; } const duplicateCode = code && state.expenseCategories.find((item) => !item.isDeleted && item.id !== existing?.id && String(item.code || "").toUpperCase() === code); if (duplicateCode) { showToast("এই ক্যাটাগরি কোড আগেই আছে", "error"); els.expenseCategoryCode.focus(); return; } const category = { id: existing?.id || `excat-${Date.now()}`, name, code, budget: Math.max(0, Number(els.expenseCategoryBudget.value || 0)), status: els.expenseCategoryStatus.value || "Active", tag: els.expenseCategoryTag.value.trim(), note: els.expenseCategoryNote.value.trim(), createdAt: existing?.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), isDeleted: false, }; if (existing) { const previousName = existing.name; const idx = state.expenseCategories.findIndex((item) => item.id === existing.id); if (idx >= 0) state.expenseCategories[idx] = category; if (previousName !== name) { state.expenses.forEach((expense) => { if (!expense.isDeleted && expense.category === previousName) expense.category = name; }); persistExpenses(); } } else { state.expenseCategories.unshift(category); } state.editingExpenseCategoryId = null; els.saveExpenseCategoryButtonText.textContent = "ক্যাটাগরি সেভ করুন"; els.expenseCategoryName.value = ""; els.expenseCategoryCode.value = ""; els.expenseCategoryBudget.value = 0; els.expenseCategoryStatus.value = "Active"; els.expenseCategoryTag.value = ""; els.expenseCategoryNote.value = ""; persistExpenseCategories(); renderExpenseCategoryOptions(); renderExpenseCategoryPage(); renderExpenseListPage(); showToast(existing ? "ক্যাটাগরি আপডেট হয়েছে" : "ক্যাটাগরি সেভ হয়েছে"); } function startEditExpenseCategory(id) { const category = state.expenseCategories.find((item) => item.id === id && !item.isDeleted); if (!category) { showToast("ক্যাটাগরি রেকর্ড পাওয়া যায়নি", "error"); return; } state.editingExpenseCategoryId = category.id; els.expenseCategoryName.value = category.name || ""; els.expenseCategoryCode.value = category.code || ""; els.expenseCategoryBudget.value = Number(category.budget || 0); els.expenseCategoryStatus.value = category.status || "Active"; els.expenseCategoryTag.value = category.tag || ""; els.expenseCategoryNote.value = category.note || ""; els.saveExpenseCategoryButtonText.textContent = "ক্যাটাগরি আপডেট করুন"; window.history.replaceState({}, "", "sales-installments.html?view=expense-category"); showPage("expense-category"); showToast("ক্যাটাগরি এডিট মোড চালু হয়েছে"); } function softDeleteExpenseCategory(id) { const category = state.expenseCategories.find((item) => item.id === id && !item.isDeleted); if (!category) { showToast("ক্যাটাগরি রেকর্ড পাওয়া যায়নি", "error"); return; } const hasUsage = state.expenses.some((expense) => !expense.isDeleted && expense.category === category.name); if (hasUsage) { showToast("এই ক্যাটাগরি ব্যবহার হয়েছে, আগে Inactive করুন", "error"); return; } category.isDeleted = true; category.deletedAt = new Date().toISOString(); category.updatedAt = new Date().toISOString(); if (state.editingExpenseCategoryId === id) state.editingExpenseCategoryId = null; persistExpenseCategories(); renderExpenseCategoryOptions(); renderExpenseCategoryPage(); showToast("ক্যাটাগরি soft delete হয়েছে"); } function seedIncomeCategories() { return [ { id: "incat-service", name: "সার্ভিস চার্জ", code: "SERV", target: 30000, status: "Active", tag: "regular", note: "", isDeleted: false }, { id: "incat-warranty", name: "ওয়ারেন্টি ক্লেইম রিফান্ড", code: "WAR", target: 12000, status: "Active", tag: "one-time", note: "", isDeleted: false }, { id: "incat-commission", name: "কমিশন", code: "COMM", target: 18000, status: "Active", tag: "regular", note: "", isDeleted: false }, { id: "incat-interest", name: "ব্যাংক ইন্টারেস্ট", code: "INT", target: 4000, status: "Active", tag: "regular", note: "", isDeleted: false }, { id: "incat-other", name: "অন্যান্য", code: "OTHER", target: 8000, status: "Active", tag: "misc", note: "", isDeleted: false }, ]; } function loadIncomeCategories() { const saved = JSON.parse(localStorage.getItem("az-income-categories") || "[]"); state.incomeCategories = (saved.length ? saved : seedIncomeCategories()).map((item) => normalizeSoftDeleteRow(item)); if (!saved.length) persistIncomeCategories(); } function persistIncomeCategories() { localStorage.setItem("az-income-categories", JSON.stringify(state.incomeCategories)); } function activeIncomeCategories() { return state.incomeCategories.filter((item) => !item.isDeleted && item.status === "Active"); } function renderIncomeCategoryOptions(selected = "") { const categories = activeIncomeCategories(); const selectedValue = String(selected || "").trim(); els.incomeSource.innerHTML = [``, ...categories.map((item) => ``)].join(""); if (selectedValue && categories.some((item) => item.name === selectedValue)) { els.incomeSource.value = selectedValue; } } function monthlyIncomeBySource(sourceName = "") { const monthKey = new Date().toISOString().slice(0, 7); return state.incomes .filter((income) => !income.isDeleted && income.source === sourceName && String(income.createdAt || "").slice(0, 7) === monthKey) .reduce((sum, income) => sum + Number(income.amount || 0), 0); } function incomeCategoryRows() { const query = state.incomeCategoryQuery.toLowerCase().trim(); return state.incomeCategories.filter((category) => { if (category.isDeleted) return false; const haystack = `${category.name} ${category.code} ${category.tag} ${category.note}`.toLowerCase(); const matchQuery = !query || haystack.includes(query); const matchStatus = state.incomeCategoryStatus === "all" || category.status === state.incomeCategoryStatus; return matchQuery && matchStatus; }); } function renderIncomeCategoryPage() { const rows = incomeCategoryRows(); const totalTarget = rows.reduce((sum, row) => sum + Number(row.target || 0), 0); const achieved = rows.reduce((sum, row) => sum + monthlyIncomeBySource(row.name), 0); els.incomeCategorySummary.textContent = `${rows.length.toLocaleString("bn-BD")}টি ক্যাটাগরি · টার্গেট ${taka(totalTarget)} · অর্জন ${taka(achieved)}`; els.incomeCategoryTable.innerHTML = rows.length ? rows .map((category) => { const achievedValue = monthlyIncomeBySource(category.name); const target = Number(category.target || 0); const pct = target > 0 ? Math.round((achievedValue / target) * 100) : 0; const achievedClass = target > 0 && achievedValue >= target ? "text-emerald-300" : "text-slate-700 dark:text-slate-200"; const badgeClass = category.status === "Active" ? "bg-emerald-500/15 text-emerald-300" : "bg-slate-500/20 text-slate-300"; return `

${escapeHtml(category.name)}

${escapeHtml(category.note || "-")}

${escapeHtml(category.code || "-")}

${taka(target)}

এই মাসে: ${taka(achievedValue)} (${pct.toLocaleString("bn-BD")}%)

${escapeHtml(category.tag || "-")} ${escapeHtml(category.status)}
`; }) .join("") : `

কোনো ইনকাম ক্যাটাগরি পাওয়া যায়নি

`; lucide.createIcons(); } function saveIncomeCategory() { const editingId = state.editingIncomeCategoryId; if (editingId && !canManageRecords) { showToast("Income category update করার পারমিশন নেই", "error"); return; } const existing = editingId ? state.incomeCategories.find((item) => item.id === editingId && !item.isDeleted) : null; const name = els.incomeCategoryName.value.trim(); const code = els.incomeCategoryCode.value.trim().toUpperCase(); if (!name) { showToast("ক্যাটাগরি নাম দিন", "error"); els.incomeCategoryName.focus(); return; } const duplicateName = state.incomeCategories.find((item) => !item.isDeleted && item.id !== existing?.id && item.name.toLowerCase() === name.toLowerCase()); if (duplicateName) { showToast("এই ক্যাটাগরি নাম আগেই আছে", "error"); els.incomeCategoryName.focus(); return; } const duplicateCode = code && state.incomeCategories.find((item) => !item.isDeleted && item.id !== existing?.id && String(item.code || "").toUpperCase() === code); if (duplicateCode) { showToast("এই ক্যাটাগরি কোড আগেই আছে", "error"); els.incomeCategoryCode.focus(); return; } const category = { id: existing?.id || `incat-${Date.now()}`, name, code, target: Math.max(0, Number(els.incomeCategoryTarget.value || 0)), status: els.incomeCategoryStatus.value || "Active", tag: els.incomeCategoryTag.value.trim(), note: els.incomeCategoryNote.value.trim(), createdAt: existing?.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), isDeleted: false, }; if (existing) { const previousName = existing.name; const idx = state.incomeCategories.findIndex((item) => item.id === existing.id); if (idx >= 0) state.incomeCategories[idx] = category; if (previousName !== name) { state.incomes.forEach((income) => { if (!income.isDeleted && income.source === previousName) income.source = name; }); persistIncomes(); } } else { state.incomeCategories.unshift(category); } state.editingIncomeCategoryId = null; els.saveIncomeCategoryButtonText.textContent = "ক্যাটাগরি সেভ করুন"; els.incomeCategoryName.value = ""; els.incomeCategoryCode.value = ""; els.incomeCategoryTarget.value = 0; els.incomeCategoryStatus.value = "Active"; els.incomeCategoryTag.value = ""; els.incomeCategoryNote.value = ""; persistIncomeCategories(); renderIncomeCategoryOptions(); renderIncomeCategoryPage(); renderIncomeListPage(); showToast(existing ? "ইনকাম ক্যাটাগরি আপডেট হয়েছে" : "ইনকাম ক্যাটাগরি সেভ হয়েছে"); } function startEditIncomeCategory(id) { const category = state.incomeCategories.find((item) => item.id === id && !item.isDeleted); if (!category) { showToast("ক্যাটাগরি রেকর্ড পাওয়া যায়নি", "error"); return; } state.editingIncomeCategoryId = category.id; els.incomeCategoryName.value = category.name || ""; els.incomeCategoryCode.value = category.code || ""; els.incomeCategoryTarget.value = Number(category.target || 0); els.incomeCategoryStatus.value = category.status || "Active"; els.incomeCategoryTag.value = category.tag || ""; els.incomeCategoryNote.value = category.note || ""; els.saveIncomeCategoryButtonText.textContent = "ক্যাটাগরি আপডেট করুন"; window.history.replaceState({}, "", "sales-installments.html?view=income-category"); showPage("income-category"); showToast("ইনকাম ক্যাটাগরি এডিট মোড চালু হয়েছে"); } function softDeleteIncomeCategory(id) { const category = state.incomeCategories.find((item) => item.id === id && !item.isDeleted); if (!category) { showToast("ক্যাটাগরি রেকর্ড পাওয়া যায়নি", "error"); return; } const hasUsage = state.incomes.some((income) => !income.isDeleted && income.source === category.name); if (hasUsage) { showToast("এই ক্যাটাগরি ব্যবহার হয়েছে, আগে Inactive করুন", "error"); return; } category.isDeleted = true; category.deletedAt = new Date().toISOString(); category.updatedAt = new Date().toISOString(); if (state.editingIncomeCategoryId === id) state.editingIncomeCategoryId = null; persistIncomeCategories(); renderIncomeCategoryOptions(); renderIncomeCategoryPage(); showToast("ইনকাম ক্যাটাগরি soft delete হয়েছে"); } function loadExpenses() { state.expenses = (JSON.parse(localStorage.getItem("az-expenses") || "[]") || []).map((expense) => normalizeSoftDeleteRow(expense)); } function persistExpenses() { localStorage.setItem("az-expenses", JSON.stringify(state.expenses)); } function loadIncomes() { state.incomes = (JSON.parse(localStorage.getItem("az-incomes") || "[]") || []).map((income) => normalizeSoftDeleteRow(income)); } function persistIncomes() { localStorage.setItem("az-incomes", JSON.stringify(state.incomes)); } function moneyStatusStyle(status = "") { if (status === "Paid" || status === "Received") return "bg-emerald-500/15 text-emerald-300"; return "bg-amber-500/15 text-amber-300"; } function expenseRows() { const query = state.expenseQuery.toLowerCase().trim(); return state.expenses.filter((expense) => { if (expense.isDeleted) return false; const haystack = `${expense.category} ${expense.reference} ${expense.method} ${expense.note}`.toLowerCase(); const matchQuery = !query || haystack.includes(query); const matchStatus = state.expenseStatus === "all" || expense.status === state.expenseStatus; return matchQuery && matchStatus; }); } function incomeRows() { const query = state.incomeQuery.toLowerCase().trim(); return state.incomes.filter((income) => { if (income.isDeleted) return false; const haystack = `${income.source} ${income.reference} ${income.method} ${income.note}`.toLowerCase(); const matchQuery = !query || haystack.includes(query); const matchStatus = state.incomeStatus === "all" || income.status === state.incomeStatus; const matchMethod = state.incomeMethod === "all" || income.method === state.incomeMethod; return matchQuery && matchStatus && matchMethod; }); } function renderExpenseListPage() { const rows = expenseRows(); const total = rows.reduce((sum, expense) => sum + Number(expense.amount || 0), 0); els.expenseListCount.textContent = `${rows.length.toLocaleString("bn-BD")}টি খরচ · মোট ${taka(total)}`; els.expenseListTable.innerHTML = rows.length ? rows .map( (expense) => `

${escapeHtml(expense.expenseNo)}

${escapeHtml(expense.reference || "-")}

${formatDate(expense.createdAt)} ${escapeHtml(expense.category)} ${escapeHtml(expense.method)} ${taka(expense.amount)} ${escapeHtml(expense.status)}
` ) .join("") : `

কোনো খরচ পাওয়া যায়নি

`; lucide.createIcons(); } function renderIncomeListPage() { const rows = incomeRows(); const total = rows.reduce((sum, income) => sum + Number(income.amount || 0), 0); const monthlyTarget = activeIncomeCategories().reduce((sum, category) => sum + Number(category.target || 0), 0); els.incomeListCount.textContent = `${rows.length.toLocaleString("bn-BD")}টি ইনকাম · মোট ${taka(total)} · টার্গেট ${taka(monthlyTarget)}`; els.incomeListTable.innerHTML = rows.length ? rows .map( (income) => `

${escapeHtml(income.incomeNo)}

${escapeHtml(income.reference || "-")}

${formatDate(income.createdAt)} ${escapeHtml(income.source)} ${escapeHtml(income.method)} ${taka(income.amount)} ${escapeHtml(income.status)}
` ) .join("") : `

কোনো ইনকাম পাওয়া যায়নি

`; lucide.createIcons(); } function saveExpense() { const editingId = state.editingExpenseId; if (editingId && !canManageRecords) { showToast("Expense update করার পারমিশন নেই", "error"); return; } const existingExpense = editingId ? state.expenses.find((item) => item.id === editingId && !item.isDeleted) : null; const category = els.expenseCategory.value.trim(); const amount = numberValue(els.expenseAmount); const categoryExists = state.expenseCategories.some((item) => !item.isDeleted && item.name === category); if (!category) { showToast("খরচ ক্যাটাগরি নির্বাচন করুন", "error"); els.expenseCategory.focus(); return; } if (!categoryExists) { showToast("ক্যাটাগরি লিস্ট থেকে নির্বাচন করুন", "error"); els.expenseCategory.focus(); return; } if (!amount) { showToast("খরচের পরিমাণ দিন", "error"); els.expenseAmount.focus(); return; } const expense = { id: existingExpense?.id || `exp-${Date.now()}`, expenseNo: existingExpense?.expenseNo || `EXP-${new Date().toISOString().slice(2, 10).replaceAll("-", "")}-${Math.floor(1000 + Math.random() * 9000)}`, category, amount, method: els.expenseMethod.value, status: els.expenseStatus.value, reference: els.expenseReference.value.trim(), note: els.expenseNote.value.trim(), createdAt: existingExpense?.createdAt || `${els.expenseDate.value || isoToday}T${new Date().toTimeString().slice(0, 8)}.000Z`, updatedAt: new Date().toISOString(), isDeleted: false, }; if (existingExpense) { const index = state.expenses.findIndex((item) => item.id === existingExpense.id); if (index >= 0) state.expenses[index] = expense; } else { state.expenses.unshift(expense); } state.editingExpenseId = null; els.saveExpenseButtonText.textContent = "খরচ সেভ করুন"; renderExpenseCategoryOptions(); els.expenseAmount.value = ""; els.expenseReference.value = ""; els.expenseNote.value = ""; persistExpenses(); renderExpenseListPage(); renderExpenseCategoryPage(); showToast(existingExpense ? "খরচ আপডেট হয়েছে" : "খরচ সেভ হয়েছে"); } function startEditExpense(id) { const expense = state.expenses.find((item) => item.id === id && !item.isDeleted); if (!expense) { showToast("খরচ রেকর্ড পাওয়া যায়নি", "error"); return; } state.editingExpenseId = expense.id; renderExpenseCategoryOptions(expense.category || ""); els.expenseDate.value = String(expense.createdAt || isoToday).slice(0, 10); els.expenseCategory.value = expense.category || ""; els.expenseAmount.value = Number(expense.amount || 0); els.expenseMethod.value = expense.method || "Cash"; els.expenseStatus.value = expense.status || "Paid"; els.expenseReference.value = expense.reference || ""; els.expenseNote.value = expense.note || ""; els.saveExpenseButtonText.textContent = "খরচ আপডেট করুন"; window.history.replaceState({}, "", "sales-installments.html?view=expense-add"); showPage("expense-add"); showToast("খরচ এডিট মোড চালু হয়েছে"); } function softDeleteExpense(id) { const expense = state.expenses.find((item) => item.id === id && !item.isDeleted); if (!expense) { showToast("খরচ রেকর্ড পাওয়া যায়নি", "error"); return; } expense.isDeleted = true; expense.deletedAt = new Date().toISOString(); expense.updatedAt = new Date().toISOString(); if (state.editingExpenseId === id) state.editingExpenseId = null; persistExpenses(); renderExpenseListPage(); renderExpenseCategoryPage(); showToast("খরচ soft delete হয়েছে"); } function saveIncome() { const editingId = state.editingIncomeId; if (editingId && !canManageRecords) { showToast("Income update করার পারমিশন নেই", "error"); return; } const existingIncome = editingId ? state.incomes.find((item) => item.id === editingId && !item.isDeleted) : null; const source = els.incomeSource.value.trim(); const amount = numberValue(els.incomeAmount); const sourceExists = state.incomeCategories.some((item) => !item.isDeleted && item.name === source); if (!source) { showToast("ইনকাম সোর্স নির্বাচন করুন", "error"); els.incomeSource.focus(); return; } if (!sourceExists) { showToast("ইনকাম ক্যাটাগরি লিস্ট থেকে নির্বাচন করুন", "error"); els.incomeSource.focus(); return; } if (!amount) { showToast("ইনকামের পরিমাণ দিন", "error"); els.incomeAmount.focus(); return; } const income = { id: existingIncome?.id || `inc-${Date.now()}`, incomeNo: existingIncome?.incomeNo || `INC-${new Date().toISOString().slice(2, 10).replaceAll("-", "")}-${Math.floor(1000 + Math.random() * 9000)}`, source, amount, method: els.incomeMethod.value, status: els.incomeStatus.value, reference: els.incomeReference.value.trim(), note: els.incomeNote.value.trim(), createdAt: existingIncome?.createdAt || `${els.incomeDate.value || isoToday}T${new Date().toTimeString().slice(0, 8)}.000Z`, updatedAt: new Date().toISOString(), isDeleted: false, }; if (existingIncome) { const index = state.incomes.findIndex((item) => item.id === existingIncome.id); if (index >= 0) state.incomes[index] = income; } else { state.incomes.unshift(income); } state.editingIncomeId = null; els.saveIncomeButtonText.textContent = "ইনকাম সেভ করুন"; renderIncomeCategoryOptions(); els.incomeAmount.value = ""; els.incomeReference.value = ""; els.incomeNote.value = ""; persistIncomes(); renderIncomeListPage(); renderIncomeCategoryPage(); showToast(existingIncome ? "ইনকাম আপডেট হয়েছে" : "ইনকাম সেভ হয়েছে"); } function startEditIncome(id) { const income = state.incomes.find((item) => item.id === id && !item.isDeleted); if (!income) { showToast("ইনকাম রেকর্ড পাওয়া যায়নি", "error"); return; } state.editingIncomeId = income.id; renderIncomeCategoryOptions(income.source || ""); els.incomeDate.value = String(income.createdAt || isoToday).slice(0, 10); els.incomeSource.value = income.source || ""; els.incomeAmount.value = Number(income.amount || 0); els.incomeMethod.value = income.method || "Cash"; els.incomeStatus.value = income.status || "Received"; els.incomeReference.value = income.reference || ""; els.incomeNote.value = income.note || ""; els.saveIncomeButtonText.textContent = "ইনকাম আপডেট করুন"; window.history.replaceState({}, "", "sales-installments.html?view=income-add"); showPage("income-add"); showToast("ইনকাম এডিট মোড চালু হয়েছে"); } function softDeleteIncome(id) { const income = state.incomes.find((item) => item.id === id && !item.isDeleted); if (!income) { showToast("ইনকাম রেকর্ড পাওয়া যায়নি", "error"); return; } income.isDeleted = true; income.deletedAt = new Date().toISOString(); income.updatedAt = new Date().toISOString(); if (state.editingIncomeId === id) state.editingIncomeId = null; persistIncomes(); renderIncomeListPage(); renderIncomeCategoryPage(); showToast("ইনকাম soft delete হয়েছে"); } function saveInvoiceSale() { const editingId = state.editingSaleId; if (editingId && !canManageRecords) { showToast("Sale update করার পারমিশন নেই", "error"); return; } const existingSale = editingId ? state.sales.find((item) => item.id === editingId && !item.isDeleted) : null; const customerName = els.invoiceCustomerName.value.trim(); const customerPhone = els.invoiceCustomerPhone.value.trim(); const items = state.invoiceItems.filter((item) => item.name.trim() && Number(item.qty) > 0 && Number(item.price) > 0); if (!customerName) { showToast("কাস্টমার নাম দিন", "error"); els.invoiceCustomerName.focus(); return; } if (!customerPhone) { showToast("মোবাইল নাম্বার দিন", "error"); els.invoiceCustomerPhone.focus(); return; } if (!items.length) { showToast("কমপক্ষে ১টি প্রোডাক্ট দিন", "error"); return; } const totals = invoiceTotals(); const paymentMode = state.invoicePaymentMode; let multiRemaining = totals.grand; const multiPayments = Array.from(document.querySelectorAll(".invoice-multi-input")) .map((input) => { const amount = Math.min(numberValue(input), multiRemaining); multiRemaining = Math.max(0, multiRemaining - amount); return { method: input.dataset.method, amount, date: new Date().toISOString(), note: "Invoice sale multi payment" }; }) .filter((payment) => payment.amount > 0); const payments = paymentMode === "multi" ? multiPayments : paymentMode === "installment" ? totals.paid > 0 ? [{ method: "Down Payment", amount: totals.paid, date: new Date().toISOString(), note: "Invoice EMI down payment" }] : [] : totals.paid > 0 ? [{ method: els.invoicePaymentMethod.value, amount: totals.paid, date: new Date().toISOString(), note: "Invoice sale payment" }] : []; const paymentMethod = paymentMode === "installment" ? "Installment" : paymentMode === "multi" ? multiPayments.map((payment) => payment.method).join(" + ") || "Multi" : els.invoicePaymentMethod.value; const firstDue = els.invoiceFirstDue.value || dateAdd(1).toISOString().slice(0, 10); const sale = updateSaleStatus({ id: existingSale?.id || `inv-${Date.now()}`, invoiceNo: existingSale?.invoiceNo || generateConfiguredInvoiceNo(), createdAt: existingSale?.createdAt || `${els.invoiceDate.value || isoToday}T${new Date().toTimeString().slice(0, 8)}.000Z`, updatedAt: new Date().toISOString(), customerName, phone: customerPhone, customerProfile: { email: els.invoiceCustomerEmail.value.trim(), nid: els.invoiceCustomerNid.value.trim(), dob: els.invoiceCustomerDob.value, address: els.invoiceCustomerAddress.value.trim(), occupation: els.invoiceCustomerOccupation.value, status: els.invoiceCustomerStatus.value || "Active", guarantor: { name: els.invoiceGuarantorName.value.trim(), address: els.invoiceGuarantorAddress.value.trim(), nid: els.invoiceGuarantorNid.value.trim(), occupation: els.invoiceGuarantorOccupation.value, }, }, cashier: resolveCashierName(), branch: "Main Branch", paymentMode, paymentMethod, itemSummary: items.map((item) => item.name).join(", "), items: items.map((item) => ({ productId: item.productId, name: item.name.trim(), variant: "", serial: String(item.serial || "").trim(), qty: Number(item.qty), price: Number(item.price) })), total: totals.grand, paid: totals.paid, due: totals.due, discountAmount: totals.discount, discounts: totals.discountDetails.rows.filter((discount) => discount.value > 0 || discount.amount > 0).map((discount) => ({ type: discount.type, value: discount.value, amount: discount.amount, })), status: totals.due > 0 ? "Due" : "Paid", payments, installment: paymentMode === "installment" ? { downPayment: totals.downPayment, profitRate: totals.profitRate, tenure: totals.tenure, lateFee: 250, firstDue, schedule: buildSchedule(totals.financeDue, totals.tenure, firstDue, 0, 0), } : null, isDeleted: false, }); persistInvoiceCustomerToCentral({ name: customerName, phone: customerPhone, email: els.invoiceCustomerEmail.value.trim(), nid: els.invoiceCustomerNid.value.trim(), dob: els.invoiceCustomerDob.value, address: els.invoiceCustomerAddress.value.trim(), occupation: els.invoiceCustomerOccupation.value, status: els.invoiceCustomerStatus.value || "Active", guarantor: { name: els.invoiceGuarantorName.value.trim(), address: els.invoiceGuarantorAddress.value.trim(), nid: els.invoiceGuarantorNid.value.trim(), occupation: els.invoiceGuarantorOccupation.value, }, }); if (existingSale) { const index = state.sales.findIndex((item) => item.id === existingSale.id); if (index >= 0) state.sales[index] = sale; } else { state.sales.unshift(sale); } persistManagedSales(); state.editingSaleId = null; resetInvoiceSaleForm(); renderAll(); const nextView = paymentMode === "installment" ? "emi-sales-list" : "invoice-sales-list"; showToast(existingSale ? "সেলস আপডেট হয়েছে" : paymentMode === "installment" ? "EMI সেলস সেভ হয়েছে" : "ইনভয়েস বিক্রি সেভ হয়েছে"); window.history.replaceState({}, "", `sales-installments.html?view=${nextView}`); showPage(nextView); } function resetInvoiceSaleForm() { state.editingSaleId = null; els.invoiceCustomerName.value = ""; els.invoiceCustomerPhone.value = ""; els.invoiceCustomerEmail.value = ""; els.invoiceCustomerNid.value = ""; els.invoiceCustomerDob.value = ""; els.invoiceCustomerAddress.value = ""; els.invoiceCustomerOccupation.value = ""; els.invoiceCustomerStatus.value = "Active"; els.invoiceGuarantorName.value = ""; els.invoiceGuarantorAddress.value = ""; els.invoiceGuarantorNid.value = ""; els.invoiceGuarantorOccupation.value = ""; els.invoiceDate.value = isoToday; els.invoicePaymentMethod.value = "Cash"; state.invoiceDiscounts = defaultInvoiceDiscounts(); els.invoiceTax.value = 0; els.invoicePaid.value = 0; els.invoiceDownPayment.value = 0; els.invoiceTenure.value = "12"; els.invoiceProfitRate.value = 12; els.invoiceFirstDue.value = dateAdd(1).toISOString().slice(0, 10); document.querySelectorAll(".invoice-multi-input").forEach((input) => { input.value = 0; }); state.invoicePaymentMode = getInvoicePrintSettings().defaultPaymentMode || "full"; state.invoiceItems = [emptyInvoiceItem()]; els.saveInvoiceSaleButtonText.textContent = "ইনভয়েস সেভ করুন"; renderInvoicePaymentMode(); renderInvoiceDiscounts(); renderInvoiceItems(); } function invoiceListRows() { const query = state.invoiceListQuery.toLowerCase().trim(); return state.sales.filter((sale) => { if (sale.isDeleted) return false; const haystack = `${sale.invoiceNo} ${sale.customerName} ${sale.phone} ${sale.itemSummary}`.toLowerCase(); const matchQuery = !query || haystack.includes(query); const matchStatus = state.invoiceListStatus === "all" || sale.status === state.invoiceListStatus; const matchMode = state.invoiceListPaymentMode === "all" || sale.paymentMode === state.invoiceListPaymentMode; const matchType = state.invoiceListType === "all" || saleType(sale) === state.invoiceListType; return matchQuery && matchStatus && matchMode && matchType; }); } function renderInvoiceSalesList() { const rows = invoiceListRows(); els.invoiceListCount.textContent = `${rows.length.toLocaleString("bn-BD")}টি ${saleTypeLabel(state.invoiceListType)} দেখানো হচ্ছে`; els.invoiceSalesListTable.innerHTML = rows.length ? rows .map( (sale) => `

${sale.itemSummary}

${saleTypeLabel(saleType(sale))}

${sale.customerName}

${sale.phone || "-"}

${formatDateTime(sale.createdAt)} ${taka(sale.total)}

Paid ${taka(sale.paid)}

Due ${taka(sale.due)}

${sale.status}
` ) .join("") : `

কোনো invoice পাওয়া যায়নি

`; } function showPage(view = pageView) { const changedPage = state.currentPageView !== view; const isEmiSale = view === "emi-sale"; const isEmiList = view === "emi-sales-list"; const isRefundOrders = view === "refund-orders"; const isAddPurchase = view === "add-purchase"; const isPurchaseList = view === "purchase-list" || view === "all-purchases"; const isExpenseAdd = view === "expense-add"; const isExpenseList = view === "expense-list"; const isExpenseCategory = view === "expense-category"; const isIncomeAdd = view === "income-add"; const isIncomeList = view === "income-list"; const isIncomeCategory = view === "income-category"; const isInvoiceSale = view === "invoice-sale" || isEmiSale; const isInvoiceList = view === "invoice-sales-list" || isEmiList; els.ledgerPage.classList.toggle("hidden", isInvoiceSale || isInvoiceList || isRefundOrders || isAddPurchase || isPurchaseList || isExpenseAdd || isExpenseList || isExpenseCategory || isIncomeAdd || isIncomeList || isIncomeCategory); els.invoiceSalePage.classList.toggle("hidden", !isInvoiceSale); els.invoiceSalesListPage.classList.toggle("hidden", !isInvoiceList); els.refundOrdersPage.classList.toggle("hidden", !isRefundOrders); els.addPurchasePage.classList.toggle("hidden", !isAddPurchase); els.purchaseListPage.classList.toggle("hidden", !isPurchaseList); els.expenseAddPage.classList.toggle("hidden", !isExpenseAdd); els.expenseListPage.classList.toggle("hidden", !isExpenseList); els.expenseCategoryPage.classList.toggle("hidden", !isExpenseCategory); els.incomeAddPage.classList.toggle("hidden", !isIncomeAdd); els.incomeListPage.classList.toggle("hidden", !isIncomeList); els.incomeCategoryPage.classList.toggle("hidden", !isIncomeCategory); if (isInvoiceSale && !els.invoiceDate.value) resetInvoiceSaleForm(); if (isInvoiceSale) configureInvoiceSalePage(isEmiSale); if (isInvoiceList) { if (changedPage) { state.invoiceListTouchedType = false; state.invoiceListType = isEmiList ? "emi" : "invoice"; } configureInvoiceListPage(isEmiList); renderInvoiceSalesList(); } if (isRefundOrders) renderRefundOrders(); if (isAddPurchase) renderAddPurchasePage(); if (isPurchaseList) renderPurchaseListPage(); if (isExpenseAdd) { if (!els.expenseDate.value) els.expenseDate.value = isoToday; renderExpenseCategoryOptions(els.expenseCategory.value || ""); } if (isExpenseList) renderExpenseListPage(); if (isExpenseCategory) renderExpenseCategoryPage(); if (isIncomeAdd) { if (!els.incomeDate.value) els.incomeDate.value = isoToday; renderIncomeCategoryOptions(els.incomeSource.value || ""); } if (isIncomeList) renderIncomeListPage(); if (isIncomeCategory) renderIncomeCategoryPage(); state.currentPageView = view; lucide.createIcons(); } function renderAll() { renderStats(); renderSalesTable(); renderDueList(); renderViewTabs(); renderCalculator(); renderInvoiceTotals(); renderInvoiceSalesList(); lucide.createIcons(); } function saleById(id) { return state.sales.find((sale) => sale.id === id && !sale.isDeleted); } function openInvoice(id) { const sale = saleById(id); if (!sale) return; state.selectedSaleId = id; els.drawerInvoiceNo.textContent = sale.invoiceNo; const invoiceConfig = getInvoicePrintSettings(); const companyLine = [invoiceConfig.companyAddress, invoiceConfig.companyPhone, invoiceConfig.companyEmail].filter(Boolean).join(" · "); const scheduleHtml = sale.installment ? `

EMI Schedule

${sale.installment.schedule.map((emi) => `

EMI ${emi.no}

${formatDate(emi.dueDate)}${Number(emi.lateFee || 0) ? ` · Late fine ${taka(emi.lateFee || 0)}` : ""}

${taka((emi.amount || 0) + (emi.lateFee || 0))} ${Number(emi.lateFee || 0) ? `EMI ${taka(emi.amount)} + Fine ${taka(emi.lateFee)}` : ""}
${emi.status}
`).join("")}
` : ""; const settlementHtml = sale.settlement ? `

Full Settlement

${formatDateTime(sale.settlement.date)}

Closed
Original Due${taka(sale.settlement.originalDue || 0)}
Discount / Waiver- ${taka(sale.settlement.waiverAmount || 0)}
Late Fee${taka(sale.settlement.lateFee || 0)}
Paid to Close${taka(sale.settlement.payable || 0)}
${sale.settlement.note ? `

${escapeHtml(sale.settlement.note)}

` : ""}
` : ""; els.drawerContent.innerHTML = `
${ invoiceConfig.showLogo && invoiceConfig.logoDataUrl ? `Logo` : "" }

${escapeHtml(invoiceConfig.companyName || "AZ PRO POS")}

${companyLine ? `

${escapeHtml(companyLine)}

` : ""} ${invoiceConfig.companyWebsite ? `

${escapeHtml(invoiceConfig.companyWebsite)}

` : ""}

${invoiceConfig.paperSize === "a4" ? "A4 Invoice" : "Thermal Invoice"}

${escapeHtml(sale.invoiceNo)}

${invoiceConfig.headerNote ? `

${escapeHtml(invoiceConfig.headerNote)}

` : ""}

Total

${taka(sale.total)}

Paid

${taka(sale.paid)}

Due

${taka(sale.due)}

${ invoiceConfig.showCustomerBlock ? `

${sale.customerName}

${sale.phone || "-"} · ${sale.branch}

${sale.status}
${invoiceConfig.showCashier ? `
Cashier${sale.cashier}
` : ""}
Sale Date & Time${formatDateTime(sale.createdAt)}
Payment${sale.paymentMethod}
` : "" }

Items

${sale.items.map((item) => `

${item.name}

${item.variant || ""}${invoiceConfig.showSerial && item.serial ? ` · ${item.serial}` : ""}

${taka((item.price || 0) * (item.qty || 1))}
`).join("")}
${ invoiceConfig.showPaymentHistory ? `

Payments

${(sale.payments || []).map((payment) => `

${payment.method}

${formatDateTime(payment.date)}${payment.note ? ` · ${payment.note}` : ""}

${taka(payment.amount)}
`).join("") || `
No payment yet
`}
` : "" } ${scheduleHtml} ${settlementHtml} ${ invoiceConfig.termsEnabled && invoiceConfig.termsText ? `

Terms

${escapeHtml(invoiceConfig.termsText)}

` : "" }

${escapeHtml(invoiceConfig.signatureLeft || "কাস্টমার স্বাক্ষর")}

${escapeHtml(invoiceConfig.signatureRight || "অথরাইজড স্বাক্ষর")}

${invoiceConfig.thermalFooter ? `

${escapeHtml(invoiceConfig.thermalFooter)}

` : ""}
`; els.invoiceDrawer.classList.remove("hidden"); lucide.createIcons(); } function printSaleInvoice(id) { const sale = saleById(id); if (!sale) return; const cfg = getInvoicePrintSettings(); const companyLine = [cfg.companyAddress, cfg.companyPhone, cfg.companyEmail].filter(Boolean).join(" | "); const itemsHtml = (sale.items || []) .map( (item) => ` ${escapeHtml(item.name)} ${escapeHtml(item.variant || "-")}${cfg.showSerial && item.serial ? ` / ${escapeHtml(item.serial)}` : ""} ${Number(item.qty || 1)} ${taka(item.price || 0)} ${taka((item.price || 0) * (item.qty || 1))} ` ) .join(""); const paymentsHtml = cfg.showPaymentHistory ? (sale.payments || []) .map( (payment) => ` ${escapeHtml(payment.method)} ${escapeHtml(formatDateTime(payment.date))} ${taka(payment.amount)} ` ) .join("") : ""; const settlementPrintHtml = sale.settlement ? `

Full Settlement

Date & Time${escapeHtml(formatDateTime(sale.settlement.date))}
Original Due${taka(sale.settlement.originalDue || 0)}
Discount / Waiver- ${taka(sale.settlement.waiverAmount || 0)}
Late Fee${taka(sale.settlement.lateFee || 0)}
Paid to Close${taka(sale.settlement.payable || 0)}
` : ""; const win = window.open("", "_blank", "width=980,height=820"); if (!win) return; win.document.write(` ${escapeHtml(sale.invoiceNo)} - Print
${cfg.showLogo && cfg.logoDataUrl ? `Logo` : ""}

${escapeHtml(cfg.companyName || "AZ PRO POS")}

${companyLine ? `

${escapeHtml(companyLine)}

` : ""} ${cfg.companyWebsite ? `

${escapeHtml(cfg.companyWebsite)}

` : ""}

${cfg.paperSize === "a4" ? "A4 Invoice" : "Thermal Invoice"}

${escapeHtml(sale.invoiceNo)}

Date & Time: ${escapeHtml(formatDateTime(sale.createdAt))}

${cfg.headerNote ? `

${escapeHtml(cfg.headerNote)}

` : ""} ${ cfg.showCustomerBlock ? `

${escapeHtml(sale.customerName)}

${escapeHtml(sale.phone || "-")}

${cfg.showCashier ? `

Cashier: ${escapeHtml(sale.cashier || "-")}

` : ""}

Status: ${escapeHtml(sale.status || "-")}

` : "" }
${itemsHtml}
ItemVariant/SerialQtyUnitTotal
${Number(sale.discountAmount || 0) > 0 ? `` : ""} ${cfg.showDueNote ? `` : ""}
Discount- ${taka(sale.discountAmount)}
Grand Total${taka(sale.total)}
Paid${taka(sale.paid)}
Due${taka(sale.due)}
${settlementPrintHtml} ${ cfg.showPaymentHistory ? `

Payment History

${paymentsHtml || ''}
MethodDate & TimeAmount
No payment yet
` : "" } ${ cfg.termsEnabled && cfg.termsText ? `

Terms: ${escapeHtml(cfg.termsText)}

` : "" }
${escapeHtml(cfg.signatureLeft || "কাস্টমার স্বাক্ষর")}
${escapeHtml(cfg.signatureRight || "অথরাইজড স্বাক্ষর")}
${cfg.thermalFooter ? `

${escapeHtml(cfg.thermalFooter)}

` : ""}