-------------------------------------------------------------------------------- /views/sales/_collect-form.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/expenses/_collect-form.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /controllers/ExpenseController.js: -------------------------------------------------------------------------------- 1 | const Expense = require('../models/Expense'); 2 | const { ExpenseValidator } = require('../middlewares/Validator'); 3 | 4 | const ExpenseController = {}; 5 | 6 | ExpenseController.create = async (req, res) => { 7 | let { 8 | purpose, 9 | equipments, 10 | transports, 11 | courierCommission, 12 | retailHoldings, 13 | stationeryTools, 14 | salaryUtilities, 15 | marketing, 16 | others, 17 | expenseDate, 18 | } = req.body; 19 | equipments = equipments ? equipments : 0; 20 | transports = transports ? transports : 0; 21 | courierCommission = courierCommission ? courierCommission : 0; 22 | retailHoldings = retailHoldings ? retailHoldings : 0; 23 | stationeryTools = stationeryTools ? stationeryTools : 0; 24 | salaryUtilities = salaryUtilities ? salaryUtilities : 0; 25 | marketing = marketing ? marketing : 0; 26 | others = others ? others : 0; 27 | 28 | const validator = ExpenseValidator({ 29 | purpose, 30 | equipments, 31 | transports, 32 | courierCommission, 33 | retailHoldings, 34 | stationeryTools, 35 | salaryUtilities, 36 | marketing, 37 | others, 38 | }); 39 | if (validator.error) { 40 | req.flash('error', validator.error); 41 | return res.redirect('/expenses'); 42 | } 43 | 44 | try { 45 | const { 46 | purpose, 47 | equipments, 48 | transports, 49 | courierCommission, 50 | retailHoldings, 51 | stationeryTools, 52 | salaryUtilities, 53 | marketing, 54 | others, 55 | } = validator.value; 56 | const amount = 57 | equipments + 58 | transports + 59 | courierCommission + 60 | retailHoldings + 61 | stationeryTools + 62 | salaryUtilities + 63 | marketing + 64 | others; 65 | expense = new Expense({ 66 | purpose, 67 | equipments, 68 | transports, 69 | courierCommission, 70 | retailHoldings, 71 | stationeryTools, 72 | salaryUtilities, 73 | marketing, 74 | others, 75 | amount, 76 | expenseDate, 77 | }); 78 | await expense.save(); 79 | req.flash('success', `New expense has been successfully added!`); 80 | return res.redirect('/expenses'); 81 | } catch (e) { 82 | req.flash('error', `Error While Saving Data - ${e}`); 83 | return res.redirect('/expenses'); 84 | } 85 | }; 86 | 87 | ExpenseController.read = async (req, res) => { 88 | const perPage = 30; 89 | const page = req.params.page || 1; 90 | let expenses = Expense.find({}); 91 | let count = await Expense.countDocuments(); 92 | 93 | let queryString = {}, 94 | countDocs; 95 | if (req.query.startDate || req.query.endDate) { 96 | expenses = Expense.aggregate(); 97 | countDocs = Expense.aggregate(); 98 | queryString.searchQuery = ''; 99 | } 100 | if (req.query.startDate) { 101 | expenses = expenses.match({ 102 | expenseDate: { $gte: new Date(req.query.startDate) }, 103 | }); 104 | countDocs = countDocs.match({ 105 | expenseDate: { $gte: new Date(req.query.startDate) }, 106 | }); 107 | queryString.startDate = req.query.startDate; 108 | } 109 | if (req.query.endDate) { 110 | expenses = expenses.match({ 111 | expenseDate: { $lt: new Date(req.query.endDate) }, 112 | }); 113 | countDocs = countDocs.match({ 114 | expenseDate: { $lt: new Date(req.query.endDate) }, 115 | }); 116 | queryString.endDate = req.query.endDate; 117 | } 118 | if (countDocs) { 119 | countDocs = await countDocs.exec(); 120 | count = countDocs.length; 121 | } 122 | 123 | expenses = await expenses 124 | .skip(perPage * page - perPage) 125 | .limit(perPage) 126 | .sort({ expenseDate: -1 }) 127 | .exec(); 128 | res.render('expenses/index', { 129 | expenses, 130 | queryString, 131 | current: page, 132 | pages: Math.ceil(count / perPage), 133 | }); 134 | }; 135 | 136 | ExpenseController.delete = async (req, res) => { 137 | await Expense.findByIdAndDelete(req.params.id); 138 | req.flash('success', `Expense has been deleted successfully!`); 139 | res.redirect('/expenses'); 140 | }; 141 | 142 | ExpenseController.update = async (req, res) => { 143 | const { 144 | purpose, 145 | equipments, 146 | transports, 147 | courierCommission, 148 | retailHoldings, 149 | stationeryTools, 150 | salaryUtilities, 151 | marketing, 152 | others, 153 | expenseDate, 154 | } = req.body; 155 | const validator = ExpenseValidator({ 156 | purpose, 157 | equipments, 158 | transports, 159 | courierCommission, 160 | retailHoldings, 161 | stationeryTools, 162 | salaryUtilities, 163 | marketing, 164 | others, 165 | }); 166 | if (validator.error) { 167 | req.flash('error', validator.error); 168 | return res.redirect('/expenses'); 169 | } else { 170 | const { 171 | purpose, 172 | equipments, 173 | transports, 174 | courierCommission, 175 | retailHoldings, 176 | stationeryTools, 177 | salaryUtilities, 178 | marketing, 179 | others, 180 | } = validator.value; 181 | const amount = 182 | equipments + 183 | transports + 184 | courierCommission + 185 | retailHoldings + 186 | stationeryTools + 187 | salaryUtilities + 188 | marketing + 189 | others; 190 | const newExpense = await Expense.findByIdAndUpdate( 191 | req.params.id, 192 | { 193 | $set: { 194 | purpose, 195 | equipments, 196 | transports, 197 | courierCommission, 198 | retailHoldings, 199 | stationeryTools, 200 | salaryUtilities, 201 | marketing, 202 | others, 203 | amount, 204 | expenseDate, 205 | }, 206 | }, 207 | { new: true } 208 | ); 209 | req.flash( 210 | 'success', 211 | `Expense for ${newExpense.purpose} has been updated successfully!` 212 | ); 213 | res.redirect('/expenses'); 214 | } 215 | }; 216 | 217 | // API 218 | ExpenseController.getExpense = async (req, res) => { 219 | try { 220 | const { 221 | purpose, 222 | equipments, 223 | transports, 224 | courierCommission, 225 | retailHoldings, 226 | stationeryTools, 227 | salaryUtilities, 228 | marketing, 229 | others, 230 | expenseDate, 231 | createdAt, 232 | } = await Expense.findById(req.params.id); 233 | if (purpose) { 234 | return res.send({ 235 | purpose, 236 | equipments, 237 | transports, 238 | courierCommission, 239 | retailHoldings, 240 | stationeryTools, 241 | salaryUtilities, 242 | marketing, 243 | others, 244 | expenseDate, 245 | createdAt, 246 | }); 247 | } 248 | return res.send("Expense Doesn't Exist"); 249 | } catch (e) { 250 | return ''; 251 | } 252 | }; 253 | 254 | module.exports = ExpenseController; 255 | -------------------------------------------------------------------------------- /middlewares/ServicingGenerator.js: -------------------------------------------------------------------------------- 1 | const PdfPrinter = require('pdfmake'); 2 | const Servicing = require('../models/Servicing'); 3 | const dateFormat = require('dateformat'); 4 | const fs = require('fs'); 5 | 6 | const fonts = { 7 | Roboto: { 8 | normal: 'public/fonts/Roboto-Regular.ttf', 9 | bold: 'public/fonts/Roboto-Medium.ttf', 10 | italics: 'public/fonts/Roboto-Italic.ttf', 11 | bolditalics: 'public/fonts/Roboto-Italic.ttf', 12 | }, 13 | }; 14 | 15 | const printer = new PdfPrinter(fonts); 16 | 17 | const docDefinition = (data) => { 18 | return { 19 | content: [ 20 | { 21 | columns: [ 22 | [ 23 | { 24 | image: './public/images/logo.png', 25 | width: 120, 26 | }, 27 | { 28 | text: 'Maktro Electronics Ltd.', 29 | style: 'title', 30 | }, 31 | { 32 | text: `2/23 Razia Sultana Road,\n Mohammadpur, Dhaka.\nwww.maktro.com\n01714-178875, 01714-178876`, 33 | style: 'generalText', 34 | }, 35 | ], 36 | [ 37 | { 38 | text: 'SERVICING', 39 | style: 'header', 40 | alignment: 'right', 41 | }, 42 | { 43 | text: `Service ID: ${data.id.slice(-8)}`, 44 | alignment: 'right', 45 | style: 'lessFocused', 46 | }, 47 | { 48 | text: `Status: ${data.status.toUpperCase()}`, 49 | alignment: 'right', 50 | bold: true, 51 | style: 'primaryText', 52 | }, 53 | { 54 | text: '', 55 | style: 'mb3', 56 | }, 57 | ], 58 | ], 59 | }, 60 | 61 | { 62 | columns: [ 63 | [ 64 | { 65 | text: `${data.name}`, 66 | style: 'h2', 67 | alignment: 'right', 68 | }, 69 | { 70 | text: `${data.address}`, 71 | style: 'lessFocused', 72 | alignment: 'right', 73 | }, 74 | { 75 | text: `Contact: ${data.phone}`, 76 | style: 'generalText', 77 | alignment: 'right', 78 | }, 79 | { 80 | text: '', 81 | style: 'mb3', 82 | }, 83 | ], 84 | ], 85 | }, 86 | { 87 | text: '_______', 88 | alignment: 'center', 89 | style: 'table', 90 | }, 91 | { 92 | text: `Received Date: ${dateFormat(data.createdAt, 'mmmm d, yyyy')}`, 93 | alignment: 'center', 94 | style: 'lessFocused', 95 | }, 96 | { 97 | table: { 98 | widths: [300, '*'], 99 | body: [ 100 | [ 101 | { 102 | text: 'Description', 103 | style: 'tableHeader', 104 | alignment: 'center', 105 | margin: 6, 106 | }, 107 | { 108 | text: 'Amount', 109 | style: 'tableHeader', 110 | alignment: 'center', 111 | margin: 6, 112 | }, 113 | ], 114 | [ 115 | { 116 | text: `Product Name: \n${data.product.name}`, 117 | style: 'tableText', 118 | margin: 5, 119 | }, 120 | { 121 | text: `${data.quantity.toLocaleString()} Unit(s)`, 122 | style: 'tableText', 123 | alignment: 'center', 124 | margin: 5, 125 | }, 126 | ], 127 | [ 128 | { 129 | text: `Servicing Charge`, 130 | style: 'tableText', 131 | margin: 5, 132 | }, 133 | { 134 | text: `${ 135 | data.serviceCharge 136 | ? data.serviceCharge.toLocaleString() + ' Tk/-' 137 | : '-' 138 | }`, 139 | style: 'tableText', 140 | alignment: 'center', 141 | margin: 5, 142 | }, 143 | ], 144 | 145 | [ 146 | { 147 | text: `Expected Delivery Date:\n${dateFormat( 148 | data.deliverDate, 149 | 'mmmm d, yyyy' 150 | )}`, 151 | style: 'tableText', 152 | margin: 5, 153 | colSpan: 2, 154 | }, 155 | ], 156 | ], 157 | }, 158 | }, 159 | { 160 | text: '', 161 | style: 'mt30', 162 | }, 163 | { 164 | text: '...', 165 | alignment: 'center', 166 | style: 'mb30', 167 | }, 168 | { 169 | columns: [ 170 | { 171 | text: `Authorized By\n\n\n___________________________\nMaktro Electronics Ltd.`, 172 | style: 'generalText', 173 | }, 174 | { 175 | text: `Customer Sign\n\n\n___________________________\n${data.name}`, 176 | style: 'generalText', 177 | alignment: 'right', 178 | }, 179 | ], 180 | }, 181 | ], 182 | styles: { 183 | header: { 184 | fontSize: 26, 185 | bold: true, 186 | color: '#6a60a9', 187 | }, 188 | lessFocused: { 189 | fontSize: 11, 190 | color: '#888', 191 | lineHeight: 1.4, 192 | }, 193 | generalText: { 194 | fontSize: 10, 195 | color: '#444', 196 | lineHeight: 1.2, 197 | }, 198 | primaryText: { 199 | fontSize: 10, 200 | color: '#6a60a9', 201 | lineHeight: 1.2, 202 | }, 203 | title: { 204 | fontSize: 15, 205 | bold: 'true', 206 | color: '#6a60a9', 207 | lineHeight: 1.4, 208 | }, 209 | h2: { 210 | fontSize: 13, 211 | bold: 'true', 212 | color: '#555', 213 | lineHeight: 1.3, 214 | }, 215 | mb30: { 216 | marginBottom: 30, 217 | }, 218 | mt30: { 219 | marginTop: 30, 220 | }, 221 | table: { 222 | marginTop: 30, 223 | marginBottom: 30, 224 | }, 225 | tableHeader: { 226 | fontSize: 13, 227 | bold: 'true', 228 | color: '#6a60a9', 229 | }, 230 | tableText: { 231 | fontSize: 12, 232 | color: '#333', 233 | }, 234 | }, 235 | }; 236 | }; 237 | 238 | const GeneratePdf = async (req, res, next) => { 239 | const servicing = await Servicing.findById(req.params.id).populate('product'); 240 | const pdfDoc = await printer.createPdfKitDocument(docDefinition(servicing)); 241 | if (fs.existsSync(`files/servicing/${req.params.id}.pdf`)) { 242 | fs.unlinkSync(`files/servicing/${req.params.id}.pdf`); 243 | } 244 | pdfDoc.pipe(fs.createWriteStream(`files/servicing/${req.params.id}.pdf`)); 245 | pdfDoc.end(); 246 | return next(); 247 | }; 248 | 249 | module.exports = GeneratePdf; 250 | -------------------------------------------------------------------------------- /controllers/ReturnController.js: -------------------------------------------------------------------------------- 1 | const ReturnModel = require('../models/Return'); 2 | const Product = require('../models/Product'); 3 | const Customer = require('../models/Customer'); 4 | const Entry = require('../models/Entry'); 5 | const Inventory = require('../models/Inventory'); 6 | const { ReturnValidator } = require('../middlewares/Validator'); 7 | 8 | const updateCustomerInfo = async (customer) => { 9 | const getReturns = await ReturnModel.find({ customer }); 10 | let returnAmount = 0; 11 | 12 | if (getReturns) { 13 | getReturns.forEach((returnRecord) => { 14 | returnAmount += returnRecord.amount; 15 | }); 16 | } 17 | 18 | await Customer.findByIdAndUpdate(customer, { $set: { returnAmount } }); 19 | }; 20 | 21 | const ReturnController = {}; 22 | ReturnController.create = async (req, res) => { 23 | const { customer, product, quantity, amount, returnDate } = req.body; 24 | const validator = ReturnValidator({ customer, product, quantity, amount }); 25 | if (validator.error) { 26 | req.flash('error', validator.error); 27 | return res.redirect('/returns'); 28 | } 29 | const getProduct = await Product.findOne({ code: validator.value.product }); 30 | if (!getProduct) { 31 | req.flash('error', "Product code doesn't match. Try again!"); 32 | return res.redirect('/returns'); 33 | } 34 | const getCustomer = await Customer.findOne({ 35 | phone: validator.value.customer, 36 | }); 37 | if (!getCustomer) { 38 | req.flash('error', "User doesn't exist with this ID. Please try again!"); 39 | return res.redirect('/returns'); 40 | } 41 | const getInventory = await Inventory.findOne({ product: getProduct._id }); 42 | if (getInventory.sales < quantity) { 43 | req.flash('error', `Oops! You can't return more products than sales.`); 44 | return res.redirect('/returns'); 45 | } 46 | try { 47 | const { quantity, amount } = validator.value; 48 | const newEntry = await new Entry({ 49 | product: getProduct._id, 50 | quantity, 51 | type: 'return', 52 | }).save(); 53 | let setReturnDate = returnDate || new Date(); 54 | 55 | const { customer } = await new ReturnModel({ 56 | entry: newEntry._id, 57 | customer: getCustomer._id, 58 | product: getProduct._id, 59 | quantity, 60 | amount, 61 | returnDate: setReturnDate, 62 | }).save(); 63 | await updateCustomerInfo(customer); 64 | 65 | req.flash('success', `A new Returns record has been added successfully!`); 66 | return res.redirect('/returns'); 67 | } catch (e) { 68 | req.flash('error', `Error While Saving Data - ${e}`); 69 | return res.redirect('/returns'); 70 | } 71 | }; 72 | 73 | ReturnController.read = async (req, res) => { 74 | const perPage = 30; 75 | const page = req.params.page || 1; 76 | 77 | let returnRecords = ReturnModel.find({}) 78 | .populate('product') 79 | .populate('customer'); 80 | let count = await ReturnModel.countDocuments(); 81 | 82 | let queryString = {}, 83 | countDocs; 84 | let lookUpProduct = { 85 | from: 'products', 86 | localField: 'product', 87 | foreignField: '_id', 88 | as: 'product', 89 | }; 90 | let lookUpCustomer = { 91 | from: 'customers', 92 | localField: 'customer', 93 | foreignField: '_id', 94 | as: 'customer', 95 | }; 96 | let matchObj = { 97 | 'customer.phone': { $regex: req.query.searchQuery, $options: 'i' }, 98 | }; 99 | 100 | if (req.query.searchQuery) { 101 | returnRecords = ReturnModel.aggregate() 102 | .lookup(lookUpProduct) 103 | .lookup(lookUpCustomer) 104 | .match(matchObj) 105 | .unwind({ 106 | preserveNullAndEmptyArrays: true, 107 | path: '$customer', 108 | }) 109 | .unwind({ 110 | preserveNullAndEmptyArrays: true, 111 | path: '$product', 112 | }); 113 | countDocs = ReturnModel.aggregate().lookup(lookUpProduct).match(matchObj); 114 | queryString.query = req.query.searchQuery; 115 | } 116 | if (req.query.startDate) { 117 | returnRecords = returnRecords.match({ 118 | returnDate: { $gte: new Date(req.query.startDate) }, 119 | }); 120 | countDocs = countDocs.match({ 121 | returnDate: { $gte: new Date(req.query.startDate) }, 122 | }); 123 | queryString.startDate = req.query.startDate; 124 | } 125 | if (req.query.endDate) { 126 | returnRecords = returnRecords.match({ 127 | returnDate: { $lt: new Date(req.query.endDate) }, 128 | }); 129 | countDocs = countDocs.match({ 130 | returnDate: { $lt: new Date(req.query.endDate) }, 131 | }); 132 | queryString.endDate = req.query.endDate; 133 | } 134 | if (countDocs) { 135 | countDocs = await countDocs.exec(); 136 | count = countDocs.length; 137 | } 138 | 139 | returnRecords = await returnRecords 140 | .skip(perPage * page - perPage) 141 | .limit(perPage) 142 | .sort({ returnDate: -1 }) 143 | .exec(); 144 | returnRecords = returnRecords.filter( 145 | (returnRecord) => returnRecord.customer !== null 146 | ); 147 | returnRecords = returnRecords.filter( 148 | (returnRecord) => returnRecord.product !== null 149 | ); 150 | res.render('returns/index', { 151 | returnRecords, 152 | queryString, 153 | current: page, 154 | pages: Math.ceil(count / perPage), 155 | }); 156 | }; 157 | 158 | ReturnController.delete = async (req, res) => { 159 | const { customer, entry } = await ReturnModel.findById(req.params.id); 160 | await ReturnModel.findByIdAndDelete(req.params.id); 161 | await Entry.findByIdAndDelete(entry); 162 | await updateCustomerInfo(customer); 163 | 164 | req.flash('success', `Return has been deleted successfully!`); 165 | res.redirect('/returns'); 166 | }; 167 | 168 | ReturnController.update = async (req, res) => { 169 | const { entry, product, quantity, amount, returnDate } = req.body; 170 | const getProduct = await Product.findOne({ code: product }); 171 | const getInventory = await Inventory.findOne({ product: getProduct._id }); 172 | if (getInventory.sales < quantity) { 173 | req.flash('error', `Oops! You can't return more product than you sales.`); 174 | return res.redirect('/return'); 175 | } 176 | await Entry.findByIdAndUpdate(entry, { $set: { quantity } }); 177 | const updatedReturn = await ReturnModel.findByIdAndUpdate(req.params.id, { 178 | $set: { quantity, amount, returnDate }, 179 | }); 180 | await updateCustomerInfo(updatedReturn.customer); 181 | req.flash('success', `Returns information has been updated successfully!`); 182 | res.redirect('/returns'); 183 | }; 184 | 185 | ReturnController.getReturn = async (req, res) => { 186 | try { 187 | const { entry, customer, product, quantity, amount, returnDate } = 188 | await ReturnModel.findById(req.params.id) 189 | .populate('product') 190 | .populate('customer'); 191 | const getProduct = await Product.findById(product); 192 | const getCustomer = await Customer.findById(customer); 193 | if (entry) { 194 | return res.send({ 195 | entry, 196 | customer: getCustomer.phone, 197 | product: getProduct.code, 198 | quantity, 199 | amount, 200 | returnDate, 201 | }); 202 | } 203 | return res.send("Return Doesn't Exist"); 204 | } catch (e) { 205 | return ''; 206 | } 207 | }; 208 | 209 | module.exports = ReturnController; 210 | --------------------------------------------------------------------------------